Skip to content

Commit 6a050bd

Browse files
feat(vm): VM runs the core language + enforces consent (coverage, opcodes, consent) (#92)
## What Advances the bytecode VM toward being a real compilation target. Before this, the VM **crashed at runtime** on most real programs (the machine rejected `MakeOkay`/`MakeOops`/`TryUnwrap`/`Index`/`MakeRecord`/`Len`/… with "Unimplemented opcode"), the compiler couldn't compile ~10 statement / 4 expression forms, and — most importantly — **consent blocks were silently bypassed** (the body ran unconditionally). This PR closes the execution + coverage gaps and makes the VM **enforce consent**. Part of the VM-compilation-target roadmap from the repo state audit. Two commits: ### 1. Full expression coverage + Result/record/index execution - **Machine** (`vm/machine.rs`): implemented the opcodes the compiler already emitted but the machine rejected — `MakeOkay`, `MakeOops`, `IsOkay`, `TryUnwrap`, `Index` (array/string/record), `Len`, `MakeRecord`, `Concat`, `ToString`, `Nop` — plus a new `Throw` (for `complain`) and a `VMError::Runtime` variant. - **Compiler** (`vm/compiler.rs`): `compile_expr` is now **exhaustive over all 16 `Expr` variants** (added `RecordLiteral`, `FieldAccess`, `UnitMeasurement`, `GratitudeLiteral`); `compile_statement` adds `While` (with a loop-context stack for `Break`/`Continue`), `Complain`, and `EmoteAnnotated`. The statement fallthrough now covers only the 5 worker/concurrency forms. ### 2. Consent enforcement under `#care` (the headline) - `CompiledProgram` gains a `care` flag; new `OpCode::CheckConsent(perm)`. - The compiler detects the `#care` pragma and compiles `only if okay "…" { … }` to `CheckConsent; JumpIfFalse(skip); body` — a **denied** permission now **skips** the body. - The machine resolves `CheckConsent`: `#care` off ⇒ always granted; `#care` on ⇒ granted only if pre-granted (**deny-by-default**, matching the interpreter's non-interactive consent path). A `granted` set is the seam for future grant sources. ## Verification - **Full lib test suite green: 181 passed, 0 failed** (was 173; +8 new VM tests). - New tests: `while`, `while`+`break`, array index, `complain`→error (source level); record/index/`Okay`/unwrap and array `len` (bytecode level); **consent skipped under `#care on` / runs without it**. - `cargo check` + `cargo test` clean. ## Deferred follow-ups (clearly scoped, noted in code) - **Closures**: `Lambda`/`CallExpr` emit `MakeClosure`/`Call` which need a value-call calling convention. - **Result from surface syntax**: `Okay(x)` parses as a *call*, so it needs `Okay`/`Oops` VM builtins to be reachable from source (the opcodes work; the lowering doesn't emit them yet). - **Consent grant sources**: seed `granted` from `superpower` declarations / stored consent; capability wildcard matching. - **Worker statements**, **bytecode serialization** (`.wbc`), and an **interpreter↔VM parity harness** over the conformance suite. 🤖 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 b141752 commit 6a050bd

4 files changed

Lines changed: 398 additions & 16 deletions

File tree

src/vm/bytecode.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ pub enum OpCode {
102102
Nop,
103103
/// Halt execution
104104
Halt,
105+
/// Pop a string and raise it as a runtime error (compiles `complain "msg"`)
106+
Throw,
107+
/// Push a Bool indicating whether consent for the given permission is
108+
/// granted. Under `#care`, ungranted permissions are denied (deny-by-default);
109+
/// with `#care` off the check always passes. Compiles `only if okay "..."`.
110+
CheckConsent(String),
105111
}
106112

107113
/// A compiled function
@@ -175,6 +181,8 @@ pub struct CompiledProgram {
175181
pub entry: Option<usize>,
176182
/// Global variables (name -> value)
177183
pub globals: HashMap<String, Value>,
184+
/// Whether the `#care` consent-checking pragma is enabled for this program.
185+
pub care: bool,
178186
}
179187

180188
impl CompiledProgram {
@@ -183,6 +191,7 @@ impl CompiledProgram {
183191
functions: Vec::new(),
184192
entry: None,
185193
globals: HashMap::new(),
194+
care: false,
186195
}
187196
}
188197

src/vm/compiler.rs

Lines changed: 132 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ struct Local {
3232
depth: usize,
3333
}
3434

35+
/// Jump fix-ups for `break`/`continue` inside the innermost `while` loop.
36+
struct LoopCtx {
37+
/// Where `continue` jumps to (re-test the loop condition).
38+
continue_target: usize,
39+
/// `break` jump-instruction indices, patched to just after the loop.
40+
break_jumps: Vec<usize>,
41+
}
42+
3543
/// Compiler state
3644
pub struct BytecodeCompiler {
3745
/// Current function being compiled
@@ -44,6 +52,8 @@ pub struct BytecodeCompiler {
4452
function_indices: HashMap<String, usize>,
4553
/// Compiled program being built
4654
program: CompiledProgram,
55+
/// Stack of enclosing `while` loops (for `break`/`continue`)
56+
loop_stack: Vec<LoopCtx>,
4757
}
4858

4959
impl BytecodeCompiler {
@@ -54,6 +64,7 @@ impl BytecodeCompiler {
5464
scope_depth: 0,
5565
function_indices: HashMap::new(),
5666
program: CompiledProgram::new(),
67+
loop_stack: Vec::new(),
5768
}
5869
}
5970

@@ -74,6 +85,16 @@ impl BytecodeCompiler {
7485
}
7586
}
7687

88+
// Record whether the `#care` consent-checking pragma is enabled, so the
89+
// VM enforces consent blocks (deny-by-default) rather than running them.
90+
for item in &program.items {
91+
if let TopLevelItem::Pragma(p) = item {
92+
if matches!(p.directive, PragmaDirective::Care) {
93+
self.program.care = p.enabled;
94+
}
95+
}
96+
}
97+
7798
// Second pass: compile function bodies
7899
for item in program.items.iter() {
79100
if let TopLevelItem::Function(func) = item {
@@ -267,22 +288,21 @@ impl BytecodeCompiler {
267288
}
268289

269290
Statement::ConsentBlock(consent) => {
270-
// Runtime consent checking
271-
// Push permission string as constant
272-
let perm_const = func.add_constant(Value::String(consent.permission.clone()));
273-
func.emit(OpCode::Const(perm_const));
274-
275-
// TODO: Add OpCode::CheckConsent when security system is integrated
276-
// For now, assume consent is granted and execute body
277-
278-
// Pop permission string
279-
func.emit(OpCode::Pop);
291+
// Gate the body on a runtime consent check (`only if okay "..."`).
292+
// `CheckConsent` pushes a Bool; if denied, skip the body. Under
293+
// `#care` an ungranted permission is denied (deny-by-default,
294+
// mirroring the non-interactive interpreter); with `#care` off the
295+
// check passes. Seeding grants from `superpower` declarations is a
296+
// follow-up.
297+
func.emit(OpCode::CheckConsent(consent.permission.clone()));
298+
let skip_body = func.emit(OpCode::JumpIfFalse(0));
280299

281-
// Execute body
282300
for stmt in &consent.body {
283301
self.compile_statement(stmt, func)?;
284302
}
285303

304+
let after_body = func.current_offset();
305+
func.patch_jump(skip_body, after_body);
286306
Ok(())
287307
}
288308

@@ -412,6 +432,72 @@ impl BytecodeCompiler {
412432
Ok(())
413433
}
414434

435+
Statement::While(while_loop) => {
436+
// Re-evaluate the condition at the top of every iteration.
437+
let loop_start = func.current_offset();
438+
self.compile_expr(&while_loop.condition.node, func)?;
439+
let exit_jump = func.emit(OpCode::JumpIfFalse(0));
440+
441+
// `continue` re-tests the condition; `break` exits the loop.
442+
self.loop_stack.push(LoopCtx {
443+
continue_target: loop_start,
444+
break_jumps: Vec::new(),
445+
});
446+
447+
for stmt in &while_loop.body {
448+
self.compile_statement(stmt, func)?;
449+
}
450+
451+
// Back-edge to the condition, then patch the false-exit.
452+
func.emit(OpCode::Jump(loop_start));
453+
let after_loop = func.current_offset();
454+
func.patch_jump(exit_jump, after_loop);
455+
456+
// Patch every `break` in this loop to jump past it.
457+
let ctx = self.loop_stack.pop().expect("loop_stack underflow");
458+
for bj in ctx.break_jumps {
459+
func.patch_jump(bj, after_loop);
460+
}
461+
Ok(())
462+
}
463+
464+
Statement::Break(_span) => {
465+
let jump_idx = func.emit(OpCode::Jump(0));
466+
match self.loop_stack.last_mut() {
467+
Some(ctx) => {
468+
ctx.break_jumps.push(jump_idx);
469+
Ok(())
470+
}
471+
None => Err(CompileError::Internal(
472+
"`break` used outside of a while loop".to_string(),
473+
)),
474+
}
475+
}
476+
477+
Statement::Continue(_span) => match self.loop_stack.last() {
478+
Some(ctx) => {
479+
let target = ctx.continue_target;
480+
func.emit(OpCode::Jump(target));
481+
Ok(())
482+
}
483+
None => Err(CompileError::Internal(
484+
"`continue` used outside of a while loop".to_string(),
485+
)),
486+
},
487+
488+
Statement::Complain(complain) => {
489+
// `complain "msg"` raises a runtime error carrying the message.
490+
let msg_idx = func.add_constant(Value::String(complain.message.clone()));
491+
func.emit(OpCode::Const(msg_idx));
492+
func.emit(OpCode::Throw);
493+
Ok(())
494+
}
495+
496+
Statement::EmoteAnnotated(emote) => {
497+
// The emote tag is metadata; compile the underlying statement.
498+
self.compile_statement(&emote.statement, func)
499+
}
500+
415501
_ => Err(CompileError::NotImplemented(format!(
416502
"Statement compilation: {:?}",
417503
stmt
@@ -605,10 +691,41 @@ impl BytecodeCompiler {
605691
Ok(())
606692
}
607693

608-
_ => Err(CompileError::NotImplemented(format!(
609-
"Expression compilation: {:?}",
610-
expr
611-
))),
694+
Expr::RecordLiteral(_type_name, fields) => {
695+
// Push each field as a (key, value) pair: key constant, then value.
696+
// `MakeRecord` pops them into a structural record (the type name is
697+
// compile-time only).
698+
for (key, value_expr) in fields {
699+
let key_idx = func.add_constant(Value::String(key.clone()));
700+
func.emit(OpCode::Const(key_idx));
701+
self.compile_expr(&value_expr.node, func)?;
702+
}
703+
func.emit(OpCode::MakeRecord(fields.len()));
704+
Ok(())
705+
}
706+
707+
Expr::FieldAccess(object, field) => {
708+
// `obj.field` lowers to indexing the record by the field name.
709+
self.compile_expr(&object.node, func)?;
710+
let key_idx = func.add_constant(Value::String(field.clone()));
711+
func.emit(OpCode::Const(key_idx));
712+
func.emit(OpCode::Index);
713+
Ok(())
714+
}
715+
716+
Expr::UnitMeasurement(inner, _unit) => {
717+
// Units of measure are a compile-time concern; at runtime the
718+
// value is just the underlying number.
719+
self.compile_expr(&inner.node, func)
720+
}
721+
722+
Expr::GratitudeLiteral(text) => {
723+
// A gratitude literal is a string-valued acknowledgement.
724+
let idx = func.add_constant(Value::String(text.clone()));
725+
func.emit(OpCode::Const(idx));
726+
Ok(())
727+
} // All `Expr` variants are now handled; `Lambda`/`CallExpr` closure
728+
// *calls* still need a calling-convention fix (tracked separately).
612729
}
613730
}
614731

0 commit comments

Comments
 (0)