Skip to content

Commit f659790

Browse files
feat(vm): Result constructors from surface syntax + closures (#93)
## What The next two VM-completion follow-ups from #92's roadmap: **Result constructors from surface syntax** and **closures**. Two commits. ### 1. Result constructors + built-ins from surface syntax (`da10ac5`) `Okay(x)`/`Oops(x)` parse as ordinary **calls**, so they were unreachable on the VM (`UndefinedFunction`). The compiler's `Expr::Call` now recognises them — and a few stdlib-style ops the VM already has opcodes for — before the user-function lookup, mirroring the interpreter's inline built-in dispatch: `Okay`→`MakeOkay`, `Oops`→`MakeOops`, `isOkay`→`IsOkay`, `isOops`→`IsOkay;Not`, `unwrap`→`TryUnwrap`, `len`→`Len`, `toString`→`ToString`, `print`/`printInline`→`Print`. Also fixed `OpCode::Print` to use `Display` (not `Debug`), matching the interpreter. ### 2. Non-capturing closures (`5d958df`) The compiler emitted `MakeClosure`/`Call` for lambdas, but the machine never implemented `MakeClosure` and `CallExpr` used a broken `Call(args.len())` (`Call` expects a *function index*, not an arg count). Now immediately-invoked lambdas run: - `Value` gains a `VmClosure(func_idx)` variant (non-capturing — uses params + globals, not enclosing locals); the 5 exhaustive `Value` matches updated (`is_truthy`, `Display`, `value_to_string`, repl, json). - New `OpCode::CallValue(argc)`; `MakeClosure` pushes a `VmClosure`; `CallValue` removes the closure from below its args and enters its frame (args become locals `0..argc`). The opcode match is now exhaustive — the `Unimplemented opcode` catch-all is gone. - `CallExpr` emits `CallValue` instead of the buggy `Call`. Closures are invoked via `CallExpr` (e.g. `(|x| x*2)(21)`); the parser lowers `identifier(args)` to a named `Call` (top-level functions only), so this is **at parity with the interpreter**, which also calls closures only via `CallExpr`. ## Verification - **Full lib suite green: 186 passed, 0 failed** (+5 new VM tests). - New tests: `Okay`/`Oops`/`isOkay`/`isOops`/`unwrap`/`len`/`toString` from source; `(|x| x*2)(21)` and `(|a,b| a+b)(15,27)`. - `rustfmt` + `clippy` (CI mode) clean. ## Deferred follow-ups - **Upvalue capture** for closures (enclosing locals) — needs a capture mechanism + upvalues on `VmClosure`. - Consent grant sources (`superpower` decls), worker statements, `.wbc` serialization, interpreter↔VM parity harness (from #92). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_013wg3Mtq2QFhYi4XVw1Z6z7 --- _Generated by [Claude Code](https://claude.ai/code/session_013wg3Mtq2QFhYi4XVw1Z6z7)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6a050bd commit f659790

8 files changed

Lines changed: 143 additions & 9 deletions

File tree

src/interpreter/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,7 @@ impl Interpreter {
10561056
Value::Okay(inner) => format!("Okay({})", self.value_to_string(inner)),
10571057
Value::Oops(msg) => format!("Oops({})", msg),
10581058
Value::Channel(_) => "<channel>".to_string(),
1059+
Value::VmClosure(_) => "<vm-closure>".to_string(),
10591060
}
10601061
}
10611062
}

src/interpreter/value.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ pub enum Value {
223223
Function(Closure),
224224
/// Go-style channel for concurrent communication
225225
Channel(ChannelHandle),
226+
/// A VM bytecode closure: an index into the compiled function table.
227+
/// Non-capturing for now (uses params + globals, not enclosing locals).
228+
VmClosure(usize),
226229
}
227230

228231
impl Value {
@@ -240,6 +243,7 @@ impl Value {
240243
Value::Oops(_) => false,
241244
Value::Function(_) => true,
242245
Value::Channel(ch) => !ch.is_closed(),
246+
Value::VmClosure(_) => true,
243247
}
244248
}
245249

@@ -304,6 +308,7 @@ impl fmt::Display for Value {
304308
None => write!(f, "<chan {}>", status),
305309
}
306310
}
311+
Value::VmClosure(idx) => write!(f, "<vm-closure {}>", idx),
307312
}
308313
}
309314
}

src/repl.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ impl Repl {
8585
crate::interpreter::Value::Float(f) => println!("{}", f),
8686
crate::interpreter::Value::String(s) => println!("\"{}\"", s),
8787
crate::interpreter::Value::Bool(b) => println!("{}", b),
88+
crate::interpreter::Value::VmClosure(idx) => println!("<vm-closure {}>", idx),
8889
crate::interpreter::Value::Array(arr) => {
8990
print!("[");
9091
for (i, val) in arr.iter().enumerate() {

src/stdlib/json.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ fn value_to_json_string(value: &Value) -> Result<String, StdlibError> {
147147
Value::Okay(_) | Value::Oops(_) => Err(StdlibError::RuntimeError(
148148
"Cannot stringify Result to JSON".to_string(),
149149
)),
150+
Value::VmClosure(_) => Err(StdlibError::RuntimeError(
151+
"Cannot stringify closure to JSON".to_string(),
152+
)),
150153
}
151154
}
152155

src/vm/bytecode.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ pub enum OpCode {
6969
Call(usize),
7070
/// Return from function
7171
Return,
72-
/// Create a closure
72+
/// Create a closure: push a `VmClosure` for the given function index
7373
MakeClosure(usize),
74+
/// Call a closure value sitting just below its `usize` arguments on the stack
75+
CallValue(usize),
7476

7577
// Array/Record operations
7678
/// Create an array from N elements on stack

src/vm/compiler.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,35 @@ impl BytecodeCompiler {
570570
}
571571

572572
Expr::Call(func_name, args) => {
573+
// Built-ins that lower directly to VM opcodes. `Okay(x)`/`Oops(x)`
574+
// parse as ordinary calls, so they (and a few stdlib-style ops the
575+
// VM already has opcodes for) are recognised here, before the
576+
// user-function lookup — mirroring the interpreter's inline
577+
// built-in dispatch.
578+
if args.len() == 1 {
579+
let unary = match func_name.as_str() {
580+
"Okay" => Some(OpCode::MakeOkay),
581+
"Oops" => Some(OpCode::MakeOops),
582+
"isOkay" => Some(OpCode::IsOkay),
583+
"unwrap" => Some(OpCode::TryUnwrap),
584+
"len" => Some(OpCode::Len),
585+
"toString" => Some(OpCode::ToString),
586+
"print" | "printInline" => Some(OpCode::Print),
587+
_ => None,
588+
};
589+
if let Some(op) = unary {
590+
self.compile_expr(&args[0].node, func)?;
591+
func.emit(op);
592+
return Ok(());
593+
}
594+
if func_name == "isOops" {
595+
self.compile_expr(&args[0].node, func)?;
596+
func.emit(OpCode::IsOkay);
597+
func.emit(OpCode::Not);
598+
return Ok(());
599+
}
600+
}
601+
573602
// Resolve the callee to its function-table index. The arity is
574603
// recovered at runtime from the called function's definition.
575604
let func_idx = *self
@@ -642,8 +671,8 @@ impl BytecodeCompiler {
642671
self.compile_expr(&arg.node, func)?;
643672
}
644673

645-
// Call the closure
646-
func.emit(OpCode::Call(args.len()));
674+
// Call the closure value (now below its args on the stack).
675+
func.emit(OpCode::CallValue(args.len()));
647676
Ok(())
648677
}
649678

src/vm/machine.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -333,8 +333,9 @@ impl VirtualMachine {
333333
}
334334

335335
OpCode::Print => {
336+
// Use Display (not Debug) to match the interpreter's `print`.
336337
let value = self.pop()?;
337-
println!("{:?}", value);
338+
println!("{}", value);
338339
self.push(Value::Unit);
339340
}
340341

@@ -468,11 +469,38 @@ impl VirtualMachine {
468469
self.push(Value::Bool(granted));
469470
}
470471

471-
_ => {
472-
return Err(VMError::TypeError(format!(
473-
"Unimplemented opcode: {:?}",
474-
opcode
475-
)));
472+
OpCode::MakeClosure(func_idx) => {
473+
// Non-capturing closure: just the bytecode function index.
474+
self.push(Value::VmClosure(func_idx));
475+
}
476+
477+
OpCode::CallValue(argc) => {
478+
// Stack: [callee_closure, arg0, .., arg{argc-1}]. Remove the
479+
// closure from below the args, then enter its frame with the
480+
// args as locals 0..argc.
481+
let cpos = self
482+
.stack
483+
.len()
484+
.checked_sub(argc + 1)
485+
.ok_or(VMError::StackUnderflow)?;
486+
let callee = self.stack.remove(cpos);
487+
let func_idx = match callee {
488+
Value::VmClosure(idx) => idx,
489+
other => {
490+
return Err(VMError::TypeError(format!(
491+
"cannot call non-closure value: {:?}",
492+
other
493+
)))
494+
}
495+
};
496+
if self.program.get_function(func_idx).is_none() {
497+
return Err(VMError::InvalidFunctionIndex(func_idx));
498+
}
499+
self.frames.push(CallFrame {
500+
function_idx: func_idx,
501+
ip: 0,
502+
bp: cpos,
503+
});
476504
}
477505
}
478506
}

src/vm/mod.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,4 +246,69 @@ mod tests {
246246
"#;
247247
assert_eq!(run_vm(source).unwrap(), Value::Int(99));
248248
}
249+
250+
// --- Result constructors + built-ins from surface syntax ---
251+
252+
#[test]
253+
fn test_vm_okay_oops_builtins() {
254+
assert_eq!(
255+
run_vm(r#"to main() { give back Okay(42); }"#).unwrap(),
256+
Value::Okay(Box::new(Value::Int(42)))
257+
);
258+
assert_eq!(
259+
run_vm(r#"to main() { give back Oops(99); }"#).unwrap(),
260+
Value::Oops("99".to_string())
261+
);
262+
}
263+
264+
#[test]
265+
fn test_vm_result_builtins() {
266+
assert_eq!(
267+
run_vm(r#"to main() { give back isOkay(Okay(1)); }"#).unwrap(),
268+
Value::Bool(true)
269+
);
270+
assert_eq!(
271+
run_vm(r#"to main() { give back isOops(Oops(1)); }"#).unwrap(),
272+
Value::Bool(true)
273+
);
274+
assert_eq!(
275+
run_vm(r#"to main() { give back unwrap(Okay(5)); }"#).unwrap(),
276+
Value::Int(5)
277+
);
278+
}
279+
280+
#[test]
281+
fn test_vm_len_and_tostring_builtins() {
282+
assert_eq!(
283+
run_vm(r#"to main() { give back len([1, 2, 3]); }"#).unwrap(),
284+
Value::Int(3)
285+
);
286+
assert_eq!(
287+
run_vm(r#"to main() { give back toString(42); }"#).unwrap(),
288+
Value::String("42".to_string())
289+
);
290+
}
291+
292+
// --- Closures (immediately-invoked lambda via CallExpr) ---
293+
294+
#[test]
295+
fn test_vm_immediately_invoked_lambda() {
296+
// (|x| x * 2)(21) -> 42 : MakeClosure + CallValue calling convention.
297+
let source = r#"
298+
to main() {
299+
give back (|x| x * 2)(21);
300+
}
301+
"#;
302+
assert_eq!(run_vm(source).unwrap(), Value::Int(42));
303+
}
304+
305+
#[test]
306+
fn test_vm_lambda_two_args() {
307+
let source = r#"
308+
to main() {
309+
give back (|a, b| a + b)(15, 27);
310+
}
311+
"#;
312+
assert_eq!(run_vm(source).unwrap(), Value::Int(42));
313+
}
249314
}

0 commit comments

Comments
 (0)