The f00 scripting VM
std/f00 embeds a small Rebol/Red-like interpreter
inside a compiled Recoil program. You load script text at runtime, evaluate it
against a VM you own, and read back a typed result — without recompiling.
std/f00 is package-owned. In a project with a recoil.project, run
r3 -s recoil.r3 install f00 then r3 -s recoil.r3 fetch; the project
records recoil-f00 and its pinned recoil-zlib dependency.
The VM holds no globals; all of its state lives in a heap arena you size at construction, which keeps it viable on low-memory targets.
Try it in the browser: the f00 REPL runs this VM as WebAssembly.
Running a script
Recoil [
Title: "f00 host" Type: module Module: examples/f00-host
Imports: [
std/f00 [
vm-new vm-free f00-do-string
f00-result-status f00-result-value-string result-ok-code f00-result-error-message
]
]
]
; arena bytes, symbol-table capacity
vm: make c-pointer! vm-new 65536 128
f00-do-string :vm make string! {x: 10 either x > 5 ["big"] ["small"]}
either (f00-result-status :vm) = result-ok-code [
print f00-result-value-string :vm ; => big
] [
print f00-result-error-message :vm
]
vm-free vm
Bindings persist across f00-do-string calls on the same VM, so a
REPL can keep state. Check f00-result-status against
result-ok-code (and the result-load-error-code /
result-bind-error-code / result-eval-error-code
variants).
Exposing compiled functions to scripts
List the functions you want reachable from f00 in a F00-Natives:
header field, then call f00-register-natives :vm once. Each listed
function is bound as a bare word in the VM's root context.
Recoil [
Title: "f00 natives" Type: module Module: examples/f00-natives
Imports: [
std/f00 [ vm-new vm-free f00-do-string f00-result-value-string f00-register-natives ]
]
F00-Natives: [ add2 shout ]
]
add2: func [a [i64!] b [i64!] return: [i64!]] [return a + b]
shout: func [s [string!] return: [string!]] [return rejoin [s "!"]]
vm: make c-pointer! vm-new 65536 128
f00-register-natives :vm
f00-do-string :vm make string! {add2 3 4} print f00-result-value-string :vm ; => 7
f00-do-string :vm make string! {shout "hi"} print f00-result-value-string :vm ; => hi!
The compiler reads each signature and generates a uniform adapter plus a
single registration routine — nothing is generated unless the program imports
std/f00. Boundary parameter/return types are limited to
i64!, f64!, logic!, and
string!; a runtime type/arity mismatch is reported as a structured
f00 eval error, not a crash. An exposed function may take at most 8 parameters.
Host-native boundary types
A function exposed through F00-Natives may use only these parameter
and return types. Anything else is a compile error.
| Recoil type | f00 value | Notes |
|---|---|---|
i64! | integer | |
f64! | decimal | an integer argument promotes to decimal |
logic! | logic | |
string! | string | copied across the boundary in both directions |
String lifetimes are handled for you: arguments are borrowed copies released after the call, and returned strings are copied into the VM arena, including the case where a function returns one of its own string arguments unchanged. Heap and aggregate value types (blocks, objects) are not yet marshalled across the boundary.
compress, decompress and zlib
The VM provides compress data 'method and
decompress data 'method natives. They operate on binary! or
string! bytes and return a binary!; the method word is
accepted but not branched on (zlib round-trips regardless).
They are backed by real zlib from the pinned recoil-zlib package,
which recoil-f00 depends on, so the project lockfile records it
transitively. Native builds use that vendored archive: no system
zlib.h, -lz, or CMake installation is assumed. Recoil
compiles the pinned upstream C sources with the active target compiler and archiver,
including MSVC, Emscripten, ARM cross-toolchains, and ESP-IDF targets.
ESP32 / ESP-IDF builds
Recoil can target the ESP32 through ESP-IDF. You need Rebol 3 (with
-s), an installed ESP-IDF for your chip, and the IDF environment
exported in the shell so idf.py and IDF_PATH are
available.
# export the ESP-IDF environment first
. "$HOME/esp/esp-idf/export.sh"
Building for ESP32
r3 -s recoil.r3 --target esp32 -o cache/esp32/simple.elf tests/simple-test.rcl
The output path is the copied ELF; when ESP-IDF also emits a .bin
it is copied beside the ELF. The generated ESP-IDF project (with
CMakeLists.txt, sdkconfig.defaults, and the generated
main/ C) is kept under cache/<host-platform>/esp-idf/<stem>/ (for example cache/linux/esp-idf/tests-simple-test-rcl/) for flashing and
monitoring with idf.py.
Calling ESP-IDF APIs
Beyond print, programs call ESP-IDF / FreeRTOS C functions
directly through the foreign FFI. FFI is not capability-gated, so
peripheral access (GPIO, timers, FreeRTOS delays) needs no special capability.
Recoil derives the ESP-IDF component REQUIRES from the headers you
include:
| foreign header | ESP-IDF component |
|---|---|
driver/* | driver |
freertos/* | freertos |
esp_timer.h | esp_timer |
esp_log.h | log |
nvs_flash.h | nvs_flash |
Capability surface
The ESP32 target enables the wired backend slices and leaves only environment and JS-host access out:
- Enabled:
file(SD-card-backed FATFS),console,tcp,dns,tls,http,https. - Disabled:
env,js-host.
Imports that need an unsupported capability fail before C compilation (for
example std/io, which requires env and
console). Peripheral access remains available through the
foreign FFI. The repository's
ESP32 guide
has the flashing procedure and the code-size baseline.