Architecture
VM Architecture Overview
Lambda C uses a compact, three-layer architecture optimized for embedded systems:
System Layers
1. Host Application (C)
- FFI function registration
- VM initialization
- Script execution loop
- Hardware interface
2. Lambda C VM (Compact)
- Bytecode loader & linker
- 4-byte instruction interpreter
- FFI dispatch table (64 slots)
- Global memory manager
3. Script Layer (C subset)
- User logic
- Algorithm implementation
- FFI calls to host functions
4-Byte Instruction Format
Why 4 Bytes?
Lambda C's 4-byte instruction format is optimized for embedded systems with strict size and performance constraints.
What This Achieves:
- Ultra-compact bytecode - Typical programs fit in 5-10KB, enabling Flash/EEPROM deployment
- Single fetch per instruction - Cache-friendly design for consistent performance
- High instruction density - ~1000 instructions in just 4KB of ROM
- Predictable memory footprint - Easy capacity planning for resource-constrained devices
Instruction Encoding
Each instruction is packed into 4 bytes:
[31:24] Opcode (8 bits) - Instruction type
[23:16] Operand 1 (8 bits) - Register/immediate
[15:8] Operand 2 (8 bits) - Register/immediate
[7:0] Operand 3 (8 bits) - Register/immediate
Example: Integer Addition
ADD r1, r2, r3 → [0x01][r1][r2][r3]
Example: FFI Call
CALL func_id, argc → [0x20][func_id][argc][0x00]Comparison: Bytecode Density
| Program | Lambda C (4-byte) | Lua 5.4 | MicroPython |
|---|---|---|---|
| Fibonacci (25 iter) | ~200 bytes | ~450 bytes | ~800 bytes |
| 1225 instructions | ~5 KB | ~12 KB | ~20 KB |
Memory Architecture
Three Stack Model
Lambda C uses three independent stacks for safety and efficiency:
1. VM Stack (vm->stack)
Purpose: Script variables and temporary values
Structure:
typedef union {
int i;
double d;
void *p;
char bytes[8];
} vm_stack_cell_t;
Characteristics:
- Managed by
sp(stack pointer) - Overflow detection built-in
- 8-byte aligned cells for ARM compatibility
2. C Stack
Purpose: VM internal function calls, intrinsics
Characteristics:
- OS-managed (not monitored by VM)
- Risk: Deep recursion can overflow
- Mitigation: Parse depth limits, recursion counters
3. Type Stack (vm->type_stack)
Purpose: Runtime type tracking for Compact VM
Characteristics:
- Parallel to VM stack
- Tracks int/double/pointer types
- Enables type-safe FFI marshalling
Memory Layout Example (Cortex-M0+, 32KB RAM)
0x20000000 ┌─────────────────────┐
│ C Stack (4KB) │ OS, interrupts, VM calls
0x20001000 ├─────────────────────┤
│ VM Stack (8KB) │ Script variables
0x20003000 ├─────────────────────┤
│ Global Area (8KB) │ Global variables
0x20005000 ├─────────────────────┤
│ Heap (12KB) │ malloc (arena)
0x20008000 └─────────────────────┘
FFI (Foreign Function Interface)
Load-Time Linking
Lambda C resolves function symbols once at load time, enabling O(1) constant-time dispatch during execution. This eliminates repeated symbol lookups and provides predictable FFI call overhead.
Linking Process
Step 1: Registration (Host)
lcvm_register_function_ex("get_time", ffi_get_time, 0, "");
lcvm_register_function_ex("gpio_write", ffi_gpio_write, 2, "ii");
Step 2: Load Bytecode
lcvm_compact_load("script.lcbc", &prog, 0);
Step 3: Link (Symbol → ID conversion)
lcvm_link_program(&prog); // "get_time" → function ID 0
Step 4: Runtime Dispatch
CALL 0, 0 → Direct jump to ffi_get_time (no lookup!)FFI Dispatch Performance
| Operation | Time | Mechanism |
|---|---|---|
| Lambda C | ~100ns | O(1) table lookup |
| Lua 5.4 | ~200ns | Hash table lookup |
| MicroPython | ~500ns | String comparison |
Type Marshalling
Lambda C automatically converts between C types and VM types:
From Script to Host (FFI call)
// Script
gpio_write(13, 1);
// Host FFI implementation
void ffi_gpio_write(LcvmState *vm) {
int value, pin;
lcvm_pop_v(&value, sizeof(int)); // Pop in reverse order
lcvm_pop_v(&pin, sizeof(int));
HAL_GPIO_WritePin(GPIO_PORT, pin, value);
}
From Host to Script (return value)
// Host FFI
void ffi_get_time(LcvmState *vm) {
double t = system_time_ms() / 1000.0;
lcvm_push_double(vm, t); // Return to script
}
// Script
double t = get_time();
Sandbox Mode
Security Isolation
Sandbox mode restricts pointer access to prevent memory corruption and unauthorized hardware access. It is enabled by default — every memory access through the VM is range-checked unless you explicitly turn it off:
lcvm_set_sandbox_mode(0); // disable (or set LCVM_SANDBOX=0)
The default-on state means scripts cannot stray outside the VM-managed regions without a deliberate opt-out.
Allowed Memory Regions
While the sandbox is on (the default), pointer access is permitted to:
- VM stack (
vm->stack) - Global area (
vm->garea) - Heap (allocated via
lcvm_malloc())
Forbidden:
- OS memory
- MMIO (Memory-Mapped I/O) registers
- Other threads' memory
Hardware Access Pattern
// WRONG: Direct hardware access (blocked in sandbox)
int *GPIO_REGISTER = (int *)0x40020000;
*GPIO_REGISTER = 1; // Runtime error!
// CORRECT: Via FFI
gpio_write(13, 1); // Calls validated FFI functionSandbox Use Cases
- Safety-Critical: Prevent accidental hardware damage and memory corruption from script bugs
- Fault containment: A buggy script can corrupt VM data but cannot reach OS memory, MMIO, or other tasks
The sandbox is a memory-safety mechanism, not a defense against hostile bytecode: there is no instruction-level verifier, and .lcbc is trusted to come from the operator's own toolchain over a trusted channel. Lambda C does not target multi-tenant execution or untrusted-code isolation — see the homepage trust model.
Hot-Reload Mechanism
Zero-Downtime Update
Lambda C supports reloading scripts without restarting the VM or losing state.
Hot-Reload Sequence
void vm_hot_reload(LcvmState *vm, compact_program_t *old_prog,
const char *new_script_path) {
compact_program_t new_prog;
// 1. Load new bytecode
lcvm_compact_load(new_script_path, &new_prog, 0);
// 2. Re-link FFI
lcvm_link_program(&new_prog);
// 3. Reset VM state
lcvm_reset(vm);
lcvm_compact_init_garea(vm, &new_prog);
// 4. Free old program, swap in new
lcvm_compact_free(old_prog);
*old_prog = new_prog;
// 5. Continue execution
lcvm_run_compact(vm, old_prog, 0);
}Use Case: Game Development
while (game_running) {
if (key_pressed('R')) {
// Recompile script
system("lcvmc -oc gameplay.lcbc gameplay.c");
// Hot-reload
vm_hot_reload(&vm, &prog, "gameplay.lcbc");
}
// Run game logic
lcvm_run_compact(&vm, &prog, 0);
render_frame();
}
Watchdog System
Dual Protection
Lambda C provides two watchdog mechanisms:
1. Time-Based Watchdog
Prevents infinite loops from hanging the system:
lcvm_set_watchdog_ms(100); // Max 100ms execution time
Example:
// Script with infinite loop
while (1) {
// ... work ...
}
// VM aborts after 100ms with LCVM_ERR_WATCHDOG_TIME2. Instruction Count Watchdog
CPU-speed independent protection:
lcvm_set_watchdog_ic(100000); // Max 100,000 instructions
Example:
for (i = 0; i < 1000000; i++) {
// ... work ...
}
// VM aborts after 100,000 instructionsRecommended Settings
| Application Type | Time Limit | Instruction Limit |
|---|---|---|
| Real-time control (10ms cycle) | 8ms | 50,000 |
| Periodic task (1s cycle) | 800ms | 500,000 |
| Batch processing | 5000ms | 1,000,000+ |
| Development/Debug | 0 (disabled) | 0 (disabled) |
Error Recovery
Graceful Degradation
Lambda C uses setjmp/longjmp for non-local error handling:
int lcvm_run_compact(LcvmState *vm, compact_program_t *prog, int trace) {
if (setjmp(vm->error_jmp) != 0) {
// Error occurred - safely return
return -1;
}
// Normal execution
// ...
}Error Handling Pattern
int ret = lcvm_run_compact(&vm, &prog, 0);
if (ret != 0) {
// Get detailed error info
LcvmErrorCode code = lcvm_get_error_code(&vm);
const char *msg = lcvm_get_error_message(&vm);
int pc = lcvm_get_error_pc(&vm);
int line = lcvm_get_error_line(&vm);
fprintf(stderr, "Error: %s at PC=%d, line=%d\n", msg, pc, line);
// Take corrective action
switch (code) {
case LCVM_ERR_DIV_ZERO:
// Log and reset
break;
case LCVM_ERR_HEAP_EXHAUSTED:
lcvm_heap_reset();
break;
}
}
Static Type Mode
~2x Speedup
Compile with -Os option to omit type information on the stack (12 bytes/entry), significantly improving execution speed:
lcvmc -Os -oc output.lcbc source.cBenchmark (fib(25))
| Mode | Execution Time |
|---|---|
| Normal mode | 27ms |
| Static type mode | 13ms |
Technical Background
In normal mode, the VM tracks type information for each value on the stack. Static type mode skips this type tracking for integer-focused programs where types are determined at compile time.
Limitations:
- Optimized for integer operations (normal mode recommended for floating-point)
- printf/sprintf supported; configure
LCVM_PRINTF_BUFSIZEfor embedded (default: 1024 bytes)
Function Pointer Support
Callback Pattern
Lambda C fully supports function pointers in Compact VM:
typedef int (*operation_t)(int, int);
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int apply(operation_t op, int x, int y) {
return op(x, y);
}
int main() {
int r1 = apply(add, 10, 5); // 15
int r2 = apply(sub, 10, 5); // 5
return 0;
}State Transition Table
Efficient state management using function pointer arrays:
typedef void (*state_handler_t)();
void state_idle() { /* ... */ }
void state_run() { /* ... */ }
void state_stop() { /* ... */ }
state_handler_t handlers[3] = { state_idle, state_run, state_stop };
void execute_state(int state) {
handlers[state]();
}
Performance Characteristics
Benchmark Results
Fibonacci(25) Calculation
- Normal mode: 27ms
- Static type mode: 13ms (~2x speedup)
- Total iterations: 121,393
- Test platform: AMD Radeon 780M
Instruction Throughput
- ~1.2 million instructions/second
- Consistent performance across different workloads
Memory Efficiency
Typical Runtime Memory Usage
- Minimum RAM: 8-16KB for basic applications
- Bytecode density: 4 bytes per instruction
- Example program (1225 instructions): ~5KB bytecode size
Scalability
- Low-end MCUs: Runs on 8KB RAM (Cortex-M0+)
- Mid-range: 16-32KB RAM provides comfortable margin
- High-end: 64KB+ RAM enables large applications
Design Decisions
Why 4 Bytes?
Alternatives Considered:
- 1-byte opcodes: Too limited (256 opcodes max)
- 8-byte instructions: Wastes space, cache unfriendly
- Variable-length: Complex decoder, unpredictable size
4-byte Advantages:
- Fits in single 32-bit fetch
- Enough space for 3 operands
- Aligned for ARM/RISC-V
- Predictable bytecode size
Why Arena Allocator?
No free() by Design:
- Eliminates fragmentation
- O(1) allocation
- Deterministic performance
- Suitable for periodic reset scenarios (control loops)
Trade-off: Must call lcvm_heap_reset() periodically
Why C Subset?
Advantages over Lua/Python:
- Zero learning curve for embedded developers
- Natural FFI integration (C types → C types)
- Predictable performance (no GC pauses)
- Easy algorithm porting from C
Designed for Orchestration: Lambda C is intentionally a subset, not a general-purpose language. For embedded orchestration scenarios, simplicity is a strength:
- Predictable control flow - no hidden metaprogramming or dynamic surprises
- Deterministic performance - what you see is what you execute
- Easy verification - simpler language means easier code review and testing
- Focused purpose - coordinate hardware, not complex data transformations