Skip to content

Virtual Machine

The Devlish VM (devlish_vm) is a register-based bytecode interpreter that executes compiled .dvlc.json packages. It delegates all I/O to a host through the HostEffects trait.

The VM maintains:

  • Constant pool: Immutable values referenced by index
  • Registers: Named temporary storage for intermediate values
  • Context: Input variables and program state (a JSON object)
  • Program counter: Current instruction index
  • Try stack: Nested error recovery frames
  • Events: Execution trace for debugging and observability
Terminal window
devlish run program.dvlc.json --input '{"customer_tier": "priority"}'

All side effects are delegated to the host through a trait interface:

HostEffects:
emit_event(event) - Execution tracing
write_file(request) - File output
read_file(request) - File input (JSON, CSV, text)
http_request(method, url, body, headers) - HTTP calls
call_service(request) - External service integration
respond(value) - Structured output
file_copy(src, dst) - Filesystem copy
file_move(src, dst) - Filesystem move
file_mkdir(path) - Create directory
file_delete(path) - Remove file/directory
file_exists(path) - Check existence
file_stat(path) - Get file metadata
file_list(path) - List directory
file_glob(pattern, dir) - Glob search
resolve_credential(key) - Secure credential access

The native CLI implements all effects. The WASM host implements only safe effects (emit_event, respond). Unimplemented effects return an error string.

The VM enforces a maximum instruction count to prevent infinite loops:

Default: 10,000,000 instructions

When the limit is reached, execution stops with an error. The limit is configurable:

// In devlish-runtime
const tool = await loadTool({
bytecode: compiled,
instructionLimit: 5000000
});

The VM emits structured events during execution for tracing and debugging:

Event Fired When
run_started Execution begins
variable_assigned A variable receives a value
effect_requested A host effect is about to execute
effect_completed A host effect finished
output_emitted Print or Show produces output
run_finished Execution completes successfully
run_failed Execution terminates with an error

Events can be suppressed for production use or consumed by debugging tools.

The VM uses a frame stack for error recovery. When a TRY_PUSH instruction executes, it registers a handler address:

TRY_PUSH handler=15 ; push frame pointing to instruction 15
... risky operations ...
TRY_POP ; pop frame on success, skip to after handler
; instruction 15: ; handler runs if error occurred

When an error occurs inside a try block, the VM:

  1. Pops the try frame
  2. Stores the error message in last_error
  3. Jumps to the handler address
  4. Continues execution in the Otherwise block

Try frames nest. An error propagates to the nearest enclosing frame.

When the VM encounters a CHECKPOINT instruction, it pauses execution and returns the checkpoint data to the caller:

{
"success": true,
"checkpoint": {
"message": "Review before continuing",
"context": {"amount": 50000}
}
}

The program can be resumed later with new input merged into context.

If the bytecode package includes a manifest with permissions, the VM checks every effect against the declared list before dispatching to the host. A mismatch produces a “Permission denied” error that can be caught by Try blocks.

On completion, the VM returns:

{
"success": true,
"context": {"all": "variables"},
"results": {"validations": "..."},
"events": [{"type": "run_started"}, "..."]
}

Failed runs include the error message and any events emitted before failure.