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.
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.
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
]
]
]
; 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.
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/esp-idf/ 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 is intentionally conservative today:
- Enabled:
console(core language features andprint). - Not yet enabled:
file,env,tcp,dns,tls,http,https,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.