Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e8d2bcb
Aarch64: add tests for better coverage
pguyot Oct 19, 2025
d34bdf8
JIT: factorize tail calls to reduce binary size
pguyot Sep 29, 2025
9108215
armv6m: use literal pool to reduce binary size
pguyot Sep 29, 2025
ef59f9d
JIT: Add flushing of stream and backend
pguyot Oct 17, 2025
b15f68b
armv6m: fix all replacements placeholders to FF
pguyot Oct 17, 2025
2c2e37f
Update avmpack API to return end section
pguyot Oct 17, 2025
60cdaa0
pico: add support for flash-based JIT stream
pguyot Oct 17, 2025
e459cee
Implement support for private_append
pguyot Oct 25, 2025
6bdb0d6
private_append / aarch64 additional coverage fixup
pguyot Oct 25, 2025
4d7c18c
riscv32: initial commit of asm module
pguyot Oct 4, 2025
24333d6
riscv32: initial backend implementation
pguyot Oct 5, 2025
1880d22
riscv32: add to workflow using a cross toolchain and libraries
pguyot Oct 15, 2025
c26a9f2
riscv32: add to precompiled targets and test infrastructure
pguyot Oct 18, 2025
201a77d
riscv32: remove unused literal pool logic
pguyot Oct 19, 2025
5b187e8
riscv32: fix several backend bugs
pguyot Oct 19, 2025
f99642d
riscv32: fix compilation with JIT disabled on esp32
pguyot Oct 19, 2025
98c51a2
riscv32: enable JIT for pico2, add it to CI
pguyot Oct 21, 2025
918d4c9
riscv32: Implement and use C extension
pguyot Oct 20, 2025
0bc8f9f
riscv32: add it to the documentation
pguyot Oct 23, 2025
bb48102
riscv32: Convert placeholders to FFFFFFFF for embedded JIT
pguyot Oct 23, 2025
d74dfb9
riscv32: Implement support for private_append
pguyot Oct 25, 2025
f0e36c2
Add missing define for HAVE_GETCWD for rp2
pguyot Oct 21, 2025
8041a85
JIT on esp32: fix CMakeLists
pguyot Oct 23, 2025
3b4ba25
JIT: Optimize memory usage by patching jump table asap
pguyot Oct 24, 2025
6da7621
riscv32: Optimize memory usage by patching jump table asap
pguyot Oct 24, 2025
b9992b2
JIT: directly emit code if label address is known
pguyot Oct 24, 2025
1dba3e0
riscv32: embedded JIT on esp32
pguyot Oct 23, 2025
29a62fc
Dump largest refc binaries on oom crashes
pguyot Oct 25, 2025
1db55ab
Improve support for floats in bitstrings
pguyot Oct 25, 2025
d1ad931
JIT: make imported functions available to compiler
pguyot Sep 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 118 additions & 1 deletion .github/workflows/build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,19 @@ jobs:
arch: "s390x"
library-arch: s390x-linux-gnu

# riscv32-ilp32 build
- os: "ubuntu-24.04"
cc: "riscv32-unknown-linux-gnu-gcc"
cxx: "riscv32-unknown-linux-gnu-g++"
cflags: "-O2"
otp: "28"
elixir_version: "1.17"
rebar3_version: "3.24.0"
cmake_opts_other: "-DAVM_WARNINGS_ARE_ERRORS=ON -DCMAKE_TOOLCHAIN_FILE=${RUNNER_TEMP}/riscv32_ilp32_toolchain.cmake"
compiler_pkgs: "qemu-user qemu-user-binfmt binfmt-support"
arch: "riscv32"
library-arch: riscv32-linux-gnu-ilp32

env:
ImageOS: ${{ matrix.container == 'ubuntu:20.04' && 'ubuntu20' || matrix.os == 'ubuntu-20.04' && 'ubuntu20' || matrix.os == 'ubuntu-22.04' && 'ubuntu22' || matrix.os == 'ubuntu-24.04' && 'ubuntu24' || 'ubuntu24' }}
CC: ${{ matrix.cc }}
Expand All @@ -386,7 +399,7 @@ jobs:
run: sudo dpkg --add-architecture i386

- name: "Setup cross compilation architecture"
if: matrix.library-arch != ''
if: matrix.library-arch != '' && matrix.library-arch != 'riscv32-linux-gnu-ilp32'
run: |
sudo dpkg --add-architecture ${{ matrix.arch }}
cat > ${RUNNER_TEMP}/cross-compile-sources.list <<EOF
Expand All @@ -411,6 +424,97 @@ jobs:
set(MBEDTLS_LIBRARIES_DIR /usr/lib/${{ matrix.library-arch }})
EOF

- name: "Setup cross compilation architecture (riscv32)"
if: matrix.library-arch == 'riscv32-linux-gnu-ilp32'
run: |
sudo dpkg --add-architecture ${{ matrix.arch }}

# Download toolchain and libraries from release
gh release download riscv-toolchain-2025.10.18 \
-R pguyot/crossbuild-essential-riscv32 \
--pattern 'riscv32-gnu-toolchain-ilp32_2025.10.18_amd64.deb' \
--pattern 'libc6-ilp32_2.39-0ubuntu1_riscv32.deb' \
--pattern 'libc6-dev-ilp32_2.39-0ubuntu1_riscv32.deb' \
--pattern 'libc6-dbg-ilp32_2.39-0ubuntu1_riscv32.deb' \
--pattern 'zlib1g-ilp32_1.3.1-0ubuntu1_riscv32.deb' \
--pattern 'zlib1g-dev-ilp32_1.3.1-0ubuntu1_riscv32.deb' \
--pattern 'libmbedcrypto7-ilp32_2.28.8-0ubuntu1_riscv32.deb' \
--pattern 'libmbedtls-dev-ilp32_2.28.8-0ubuntu1_riscv32.deb' \
--pattern 'libmbedtls14-ilp32_2.28.8-0ubuntu1_riscv32.deb' \
--pattern 'libmbedx509-1-ilp32_2.28.8-0ubuntu1_riscv32.deb'

# Install the toolchain
sudo dpkg -i riscv32-gnu-toolchain-ilp32_2025.10.18_amd64.deb

# Add to PATH for all subsequent steps
echo "/opt/riscv32-ilp32/bin" >> $GITHUB_PATH

# Install the libs
sudo dpkg -i libc6-ilp32_2.39-0ubuntu1_riscv32.deb
sudo dpkg -i libc6-dev-ilp32_2.39-0ubuntu1_riscv32.deb
sudo dpkg -i libc6-dbg-ilp32_2.39-0ubuntu1_riscv32.deb

sudo dpkg -i zlib1g-ilp32_1.3.1-0ubuntu1_riscv32.deb
sudo dpkg -i zlib1g-dev-ilp32_1.3.1-0ubuntu1_riscv32.deb

# Install mbedtls runtime packages first (in dependency order)
sudo dpkg -i libmbedcrypto7-ilp32_2.28.8-0ubuntu1_riscv32.deb
sudo dpkg -i libmbedx509-1-ilp32_2.28.8-0ubuntu1_riscv32.deb
sudo dpkg -i libmbedtls14-ilp32_2.28.8-0ubuntu1_riscv32.deb
# Then install the dev package
sudo dpkg -i libmbedtls-dev-ilp32_2.28.8-0ubuntu1_riscv32.deb

sudo sed -i '/Types: deb/a Architectures: amd64' /etc/apt/sources.list.d/ubuntu.sources

cat > ${RUNNER_TEMP}/riscv32_ilp32_toolchain.cmake <<'EOF'
# Toolchain file for RISC-V32 ILP32 (RV32-IMAC) cross-compilation
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR riscv32)
set(CMAKE_C_LIBRARY_ARCHITECTURE riscv32-linux-gnu-ilp32)

# Specify the cross compiler
set(CMAKE_C_COMPILER riscv32-unknown-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER riscv32-unknown-linux-gnu-g++)

# Specify the target architecture
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=rv32imac -mabi=ilp32" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=rv32imac -mabi=ilp32" CACHE STRING "" FORCE)

# Set up paths for cross-compiled libraries
set(ZLIB_LIBRARY /usr/lib/riscv32-linux-gnu-ilp32/libz.so CACHE FILEPATH "")
set(ZLIB_INCLUDE_DIR /usr/include/riscv32-linux-gnu CACHE PATH "")
set(ZLIB_FOUND TRUE CACHE BOOL "")

# MbedTLS configuration
set(MBEDTLS_ROOT_DIR /usr)
set(MBEDTLS_LIBRARIES_DIR /usr/lib/riscv32-linux-gnu-ilp32)

# Add cross-compilation include path to compiler flags
include_directories(SYSTEM /usr/include/riscv32-linux-gnu)

# Search for programs in the build host directories
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

# Search for libraries and headers in the target directories
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
EOF

# Set up qemu-user binfmt to find libraries
sudo ln -s /opt/riscv32-ilp32/sysroot/lib/ld-linux-riscv32-ilp32.so.1 /lib/ld-linux-riscv32-ilp32.so.1
sudo mkdir -p /usr/gnemul
sudo ln -s /opt/riscv32-ilp32/sysroot /usr/gnemul/qemu-riscv32

# Copy cross-compiled libraries to sysroot for qemu-user
sudo cp /usr/lib/${{ matrix.library-arch }}/libz.so.1* /opt/riscv32-ilp32/sysroot/lib/
sudo cp /usr/lib/${{ matrix.library-arch }}/libmbedtls.so.14 /opt/riscv32-ilp32/sysroot/lib/
sudo cp /usr/lib/${{ matrix.library-arch }}/libmbedcrypto.so.7 /opt/riscv32-ilp32/sysroot/lib/
sudo cp /usr/lib/${{ matrix.library-arch }}/libmbedx509.so.1 /opt/riscv32-ilp32/sysroot/lib/

env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: "APT update"
run: sudo apt update -y

Expand Down Expand Up @@ -526,6 +630,19 @@ jobs:
ulimit -c unlimited
./tests/test-heap

- name: "Test: test-jit_stream_flash with valgrind"
if: matrix.library-arch == ''
working-directory: build
run: |
ulimit -c unlimited
valgrind --error-exitcode=1 ./tests/test-jit_stream_flash

- name: "Test: test-jit_stream_flash"
working-directory: build
run: |
ulimit -c unlimited
./tests/test-jit_stream_flash

- name: "Test: test-mailbox with valgrind"
if: matrix.library-arch == ''
working-directory: build
Expand Down
34 changes: 27 additions & 7 deletions .github/workflows/pico-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,17 @@ jobs:
strategy:
matrix:
board: ["pico", "pico_w", "pico2"]
platform: [""]
language: ["cpp"]
jit: ["", "-DAVM_DISABLE_JIT=OFF"]
include:
- board: "pico2"
platform: "-DPICO_PLATFORM=rp2350-riscv"
jit: ""

- board: "pico2"
platform: "-DPICO_PLATFORM=rp2350-riscv"
jit: "-DAVM_DISABLE_JIT=OFF"

steps:
- name: Checkout repo
Expand All @@ -57,6 +67,16 @@ jobs:
libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib \
erlang-base erlang-dev erlang-dialyzer erlang-eunit rebar3

- name: Install riscv32 toolchain
if: matrix.platform == '-DPICO_PLATFORM=rp2350-riscv'
run: |
sudo mkdir -p /opt
cd /opt
sudo wget https://github.com/raspberrypi/pico-sdk-tools/releases/download/v2.2.0-3/riscv-toolchain-15-x86_64-lin.tar.gz
sudo tar xzf riscv-toolchain-15-x86_64-lin.tar.gz
ls /opt
echo "/opt/riscv-toolchain-15-x86_64-lin/bin" >> $GITHUB_PATH

- name: "Git config safe.directory for codeql"
run: git config --global --add safe.directory /__w/AtomVM/AtomVM

Expand All @@ -74,7 +94,7 @@ jobs:
set -euo pipefail
mkdir build
cd build
cmake .. -G Ninja -DPICO_BOARD=${{ matrix.board }}
cmake .. -G Ninja -DPICO_BOARD=${{ matrix.board }} ${{ matrix.platform }} ${{ matrix.jit }}
ninja

- name: "Perform CodeQL Analysis"
Expand All @@ -97,7 +117,7 @@ jobs:
mkdir build.nosmp
cd build.nosmp
# TODO: fix all warnings and enable -DAVM_WARNINGS_ARE_ERRORS=ON
cmake .. -G Ninja -DPICO_BOARD=${{ matrix.board }} -DAVM_DISABLE_SMP=1
cmake .. -G Ninja -DPICO_BOARD=${{ matrix.board }} ${{ matrix.jit }} -DAVM_DISABLE_SMP=1
cmake --build . --target=rp2_tests

- name: Run tests with rp2040js
Expand All @@ -112,7 +132,7 @@ jobs:
npx tsx run-tests.ts ../build.nosmp/tests/rp2_tests.uf2 ../build.nosmp/tests/test_erl_sources/rp2_test_modules.uf2

- name: Build atomvmlib.uf2
if: startsWith(github.ref, 'refs/tags/') && matrix.board != 'pico_w'
if: startsWith(github.ref, 'refs/tags/') && matrix.board != 'pico_w' && matrix.platform == '' && matrix.jit == ''
shell: bash
run: |
set -euo pipefail
Expand All @@ -122,7 +142,7 @@ jobs:
make atomvmlib-${{ matrix.board }}.uf2

- name: Rename AtomVM and write sha256sum
if: startsWith(github.ref, 'refs/tags/')
if: startsWith(github.ref, 'refs/tags/') && matrix.platform == '' && matrix.jit == ''
shell: bash
run: |
pushd src/platforms/rp2/build
Expand All @@ -137,7 +157,7 @@ jobs:
popd

- name: Rename atomvmlib and write sha256sum
if: startsWith(github.ref, 'refs/tags/') && matrix.board != 'pico_w'
if: startsWith(github.ref, 'refs/tags/') && matrix.board != 'pico_w' && matrix.platform == '' && matrix.jit == ''
shell: bash
run: |
pushd build/libs
Expand All @@ -148,7 +168,7 @@ jobs:

- name: Release (Pico & Pico2)
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/') && matrix.board != 'pico_w'
if: startsWith(github.ref, 'refs/tags/') && matrix.board != 'pico_w' && matrix.platform == '' && matrix.jit == ''
with:
draft: true
fail_on_unmatched_files: true
Expand All @@ -160,7 +180,7 @@ jobs:

- name: Release (PicoW)
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/') && matrix.board == 'pico_w'
if: startsWith(github.ref, 'refs/tags/') && matrix.board == 'pico_w' && matrix.platform == '' && matrix.jit == ''
with:
draft: true
fail_on_unmatched_files: true
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ if (NOT AVM_DISABLE_JIT AND NOT DEFINED AVM_JIT_TARGET_ARCH)
endif()
endif()

set(AVM_PRECOMPILED_TARGETS "x86_64;aarch64;armv6m;armv6m+float32" CACHE STRING "Targets to precompile code to if AVM_DISABLE_JIT is OFF or AVM_ENABLE_PRECOMPILED is ON")
set(AVM_PRECOMPILED_TARGETS "x86_64;aarch64;armv6m;armv6m+float32;riscv32" CACHE STRING "Targets to precompile code to if AVM_DISABLE_JIT is OFF or AVM_ENABLE_PRECOMPILED is ON")

if((${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") OR
(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") OR
Expand Down
34 changes: 31 additions & 3 deletions doc/src/atomvm-internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Following BEAM, there are two flavors of the emulator: jit and emu, but eventual
- Native: the VM only runs native code and all code must be precompiled on the desktop using the JIT compiler (which effectively is a AOT or Ahead-of-Time compiler). In this mode, it is not necessary to bundle the jit compiler on the embedded target.
- Hybrid: the VM can run native code as well as emulated BEAM code and some code is precompiled on the desktop.

JIT is available on some platforms (currently only x86_64 and aarch64) and compiles Erlang bytecode at runtime. Erlang bytecode is never interpreted. EMU is available on all platforms and Erlang bytecode is interpreted.
JIT is available on some platforms (currently x86_64, aarch64, armv6m and riscv32) and compiles Erlang bytecode at runtime. Erlang bytecode is never interpreted. EMU is available on all platforms and Erlang bytecode is interpreted.

Modules can include precompiled code in a dedicated beam chunk with name 'avmN'. The chunk can contain native code for several architectures, however it may only contain native code for a given version of the native interface. Current version is 1. This native code is executed by the jit-flavor of the emulator as well as the emu flavor if execution of precompiled is enabled.

Expand All @@ -154,9 +154,37 @@ The JIT compiler is written in Erlang and is therefore precompiled. When a proce

JIT compiler is composed of two main interfaces : backend and stream.

A backend implementation is required for each architecture. The backend is called by jit module as it translates bytecodes to machine code. The current implementations are `jit_x86_64` and `jit_aarch64` which are suitable for systems with System V X86 64 ABI or AArch64 ABI.
A backend implementation is required for each architecture. The backend is called by jit module as it translates bytecodes to machine code. The current implementations are :
- `jit_x86_64` for System V X86 64 ABI
- `jit_aarch64` for AArch64 ABI
- `jit_armv6m` for AArch32 ABI
- `jit_riscv32` for rv32imc ilp32 ABI.

A stream implementation is responsible for streaming the machine code, especially in the context of low memory. Two implementations currently exist: `jit_stream_binary` that streams assembly code to an Erlang binary, suitable for tests and precompilation on the desktop, and `jit_stream_mmap` that streams assembly code in an `mmap(2)` allocated page, suitable for JIT compilation on Unix.
A stream implementation is responsible for streaming the machine code, especially in the context of low memory. Three implementations currently exist:
- `jit_stream_binary` that streams assembly code to an Erlang binary, suitable for tests and precompilation on the desktop
- `jit_stream_mmap` that streams assembly code in an `mmap(2)` allocated page, suitable for JIT compilation on Unix
- `jit_stream_flash` available on Pico that allows for embedded JIT.

### Embedded JIT and Native

On embedded devices, Native mode means the code is precompiled on the desktop and executed natively on the device. This currently works on all ARMv6M devices (Pico and STM32).

The default partition scheme on all platforms is optimized for the Emulated VM which is larger than the JIT or Native VM, and for the Emulated atomvmlib (with no native code for estdlib and no jit library) which is smaller than the JIT atomvmlib (that includes native code for estdlib and jit library).

JIT mode means the Erlang bytecode is compiled to native code directly on the device. This actually is possible on Raspberry Pi Pico by using the flash to store the native code. The first time the code is executed, it is compiled and streamed to flash, and for next runs (including at a future boot), the native code is directly executed.

To achive embedded JIT, it is required to flash the device with the JIT compiler for armv6m which is part of the jit library. This library is quite large, so for Pico boards that come with 2MB of flash, it is required to remove jit modules for other backends. It is also required to change the way code is partitioned.

For example, it is possible to have the following offsets defined in `src/platforms/rp2/src/main.c`:

```
#define LIB_AVM ((void *) 0x10060000)
#define MAIN_AVM ((void *) 0x101B0000)
```

To fit in the lib partition, all networking modules should also be removed (the Pico doesn't have any networking capacity).

After the first run, compiled modules in flash are used unless there is a version mismatch or the application avm or the library avm have been updated on the device. AVM packages end with a section called "end" (0x656E64). When the JIT compiler flashes native code, it changes this name to "END" (0x454E44), by effectively clearing 3 bits in the flash, which is possible without erasing any flash block. Any rewrite of these avm packages will overwrite the section names to "end".

## The Scheduler

Expand Down
22 changes: 18 additions & 4 deletions libs/estdlib/src/code_server.erl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
atom_resolver/2,
literal_resolver/2,
type_resolver/2,
import_resolver/2,
set_native_code/3
]).

Expand Down Expand Up @@ -135,6 +136,14 @@ literal_resolver(_Module, _Index) ->
type_resolver(_Module, _Index) ->
erlang:nif_error(undefined).

%% @doc Get an imported function triplet from its index
%% @return The imported function as {Module, Function, Arity}
%% @param Module module to get the imported function from
%% @param Index imported function index in the module
-spec import_resolver(Module :: module(), Index :: non_neg_integer()) -> {atom(), atom(), non_neg_integer()}.
import_resolver(_Module, _Index) ->
erlang:nif_error(undefined).

%% @doc Associate a native code stream with a module
%% @return ok
%% @param Module module to set the native code of
Expand All @@ -152,7 +161,7 @@ set_native_code(_Module, _LabelsCount, _Stream) ->
load(Module) ->
case erlang:system_info(emu_flavor) of
jit ->
% atomvm_heap_growth, fibonacci divides compilation time by two
% atomvm_heap_growth, fibonacci reduces compilation time
{Pid, Ref} = spawn_opt(
fun() ->
try
Expand All @@ -164,18 +173,23 @@ load(Module) ->
code_server:literal_resolver(Module, Index)
end,
TypeResolver = fun(Index) -> code_server:type_resolver(Module, Index) end,
Stream0 = jit:stream(jit_mmap_size(byte_size(Code))),
{BackendModule, BackendState0} = jit:backend(Stream0),
ImportResolver = fun(Index) ->
code_server:import_resolver(Module, Index)
end,
{StreamModule, Stream0} = jit:stream(jit_mmap_size(byte_size(Code))),
{BackendModule, BackendState0} = jit:backend(StreamModule, Stream0),
{LabelsCount, BackendState1} = jit:compile(
Code,
AtomResolver,
LiteralResolver,
TypeResolver,
ImportResolver,
BackendModule,
BackendState0
),
Stream1 = BackendModule:stream(BackendState1),
code_server:set_native_code(Module, LabelsCount, Stream1),
Stream2 = StreamModule:flush(Stream1),
code_server:set_native_code(Module, LabelsCount, Stream2),
End = erlang:system_time(millisecond),
io:format("~B ms (bytecode: ~B bytes, native code: ~B bytes)\n", [
End - Start, byte_size(Code), BackendModule:offset(BackendState1)
Expand Down
1 change: 1 addition & 0 deletions libs/jit/include/jit.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
-define(JIT_ARCH_X86_64, 1).
-define(JIT_ARCH_AARCH64, 2).
-define(JIT_ARCH_ARMV6M, 3).
-define(JIT_ARCH_RISCV32, 4).

-define(JIT_VARIANT_PIC, 1).
-define(JIT_VARIANT_FLOAT32, 2).
Expand Down
2 changes: 2 additions & 0 deletions libs/jit/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ set(ERLANG_MODULES
jit_aarch64_asm
jit_armv6m
jit_armv6m_asm
jit_riscv32
jit_riscv32_asm
jit_x86_64
jit_x86_64_asm
)
Expand Down
Loading
Loading