N.I.S.S.E. (Naive Interpreter for Small Shell Erlang) implements a bytecode compilation and execution model, mirroring the architecture of the real BEAM VM. This moves the system from a tree-walking interpreter to a true virtual machine with separate compilation and runtime phases.
Interactive REPL: Press ~ to open the CLI, then type erl and press Enter to launch the browser-based REPL.
The REPL provides an interactive environment for:
counter(N)send(Pid, Value)processes()10 + 20nisse-compiler.js)Transforms human-readable Erlang-like source code into bytecode instructions.
Opcodes with Reduction Costs:
MOVE (1) - Move value to register (1 reduction)CALL (2) - Normal procedure call with stack frame (2 reductions)RET (3) - Return from function (1 reduction)JUMP (4) - Tail call / unconditional jump (1 reduction - no stack growth)JUMP_IF (5) - Conditional jump (1 reduction)SEND (6) - Send message to process (2 reductions)RECV (7) - Receive message from mailbox (suspends if empty)SPAWN (8) - Spawn new process (3 reductions)HALT (9) - Terminate process (0 reductions)PRINT (10) - I/O output (1 reduction)MATH (11) - Arithmetic operation (1 reduction)Supported Syntax:
spawn(module_name) - Spawn process from modulesend(pid, message) - Send messageprint(value) - Output to consolegoto(label) - Jump to labellabel: - Label definition% comment - CommentsExample:
% Main module
print(starting_system)
spawn(worker)
halt
Compiles to:
[
{ op: 10, val: 'starting_system' },
{ op: 8, arg: 'worker' },
{ op: 9 }
]
nisse-vm.js)Executes bytecode with process scheduling, memory isolation, and concurrent execution.
Process Structure:
pid - Process identifiercode - Bytecode instructions arraypc - Program counter (instruction pointer)mailbox - Message queueregs - Register storage (local variables)stack - Call stack for function returnsstatus - Process state (runnable, waiting, terminated, crashed)reductions - Current time slice quantum remainingtotalReductions - Lifetime reduction counter for profilingScheduler:
Code Server:
Execution Model:
while (runnable) {
1. Dequeue next process from run queue
2. Grant 10 reduction quota
3. Execute instructions until quota exhausted
4. If still runnable, enqueue back to run queue
5. Repeat
}
nisse-cli.js)Entry point demonstrating concurrent process execution.
Example Program:
const pingPongCode = `
print(starting_ping_pong)
spawn(player)
spawn(player)
halt
`;
const playerCode = `
start:
print(ping)
print(pong)
goto(start)
`;
vm.load('main', compile(pingPongCode));
vm.load('player', compile(playerCode));
vm.spawn('main');
vm.step();
Output:
--- N.I.S.S.E. VM Booting ---
[<1>] starting_ping_pong
[<1>] Spawned <2>
[<1>] Spawned <3>
[<2>] ping
[<2>] pong
[<3>] ping
[<3>] pong
[<2>] ping
[<3>] pong
...
{ op: 2, fn: 'factorial' } // Cost: 2 reductions
When executing a CALL instruction:
pc (return address) onto call stackproc.codepc to 0 (start of function){ op: 3 } // Cost: 1 reduction
When executing a RET instruction:
pc to continue after the call{ op: 4, label: 'loop' } // Cost: 1 reduction (stack-free)
When executing a JUMP instruction:
pc directly (no stack push)Example: Tail-Recursive Loop
loop:
print(ping)
print(pong)
goto(loop) % Tail call - runs forever without stack overflow
Tail calls are "optimized" because they avoid stack growth, not because they're free. Each iteration still costs 1 reduction to ensure fair scheduling. After 10 reductions, the process yields to other processes in the run queue.
| Category | Operation | Cost | Rationale |
|---|---|---|---|
| Memory | MOVE | 1 | Simple register write |
| Control | JUMP | 1 | No stack growth, still scheduled fairly |
| Control | JUMP_IF | 1 | Conditional evaluation |
| Control | RET | 1 | Stack pop |
| Function | CALL | 2 | Stack push + context switch |
| Process | SPAWN | 3 | Process creation overhead |
| IPC | SEND | 2 | Message passing cost |
| IPC | RECV | N/A | Suspends if blocking |
| Math | MATH | 1 | Arithmetic operation |
| I/O | 1 | Output operation | |
| System | HALT | 0 | Process termination |
Key Insight: JUMP costs 1 reduction (same as other operations) to prevent CPU monopolization. The "optimization" is zero stack growth, allowing infinite tail recursion. Compare to CALL which costs 2 reductions and grows the stack.
This architecture mirrors the real BEAM VM:
.beam files)regs)cd /workspaces/hh/src/code
node nisse-cli.js
Watch two concurrent processes (<2> and <3>) run in interleaved time slices, demonstrating true concurrent execution managed by the VM scheduler.
SEND, RECV opcodes)function evalExpr(ast, env) {
if (ast.type === 'call') {
// Parse function name
// Evaluate arguments
// Execute function
}
}
function execute(proc, instr) {
if (instr.op === OP.CALL) {
// Direct dispatch to handler
// No parsing, just execution
}
}
The bytecode approach eliminates parse overhead on every execution, enables code serialization, and provides clear compilation boundaries for optimization passes.
Version: N.I.S.S.E. VM 0.1 Date: 2025-11-26 Author: Happi Hacking