This commit is contained in:
bQUARKz 2026-02-20 06:56:20 +00:00
parent 860f0db31c
commit 35f5b8fa86
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
4 changed files with 206 additions and 146 deletions

View File

@ -155,6 +155,12 @@ pub enum OpCode {
/// Returns from the current function.
/// Stack: [return_val] -> [return_val]
Ret = 0x51,
/// Creates a closure capturing values from the operand stack (Model B).
/// Operands: fn_id (u32), capture_count (u32)
/// Stack before: [..., captured_N, ..., captured_1]
/// Pops capture_count values (top-first), preserves order as [captured_1..captured_N]
/// and stores them inside the closure environment. Pushes a HeapRef to the closure.
MakeClosure = 0x52,
// --- 6.8 Peripherals and System ---
/// Invokes a system function (Firmware/OS).
@ -215,6 +221,7 @@ impl TryFrom<u16> for OpCode {
0x43 => Ok(OpCode::SetLocal),
0x50 => Ok(OpCode::Call),
0x51 => Ok(OpCode::Ret),
0x52 => Ok(OpCode::MakeClosure),
0x70 => Ok(OpCode::Syscall),
0x80 => Ok(OpCode::FrameSync),
_ => Err(format!("Invalid OpCode: 0x{:04X}", value)),
@ -271,6 +278,7 @@ impl OpCode {
OpCode::SetLocal => 2,
OpCode::Call => 5,
OpCode::Ret => 4,
OpCode::MakeClosure => 8,
OpCode::Syscall => 1,
OpCode::FrameSync => 1,
}

View File

@ -462,6 +462,18 @@ impl OpCodeSpecExt for OpCode {
may_trap: false,
is_safepoint: false,
},
OpCode::MakeClosure => OpcodeSpec {
name: "MAKE_CLOSURE",
// Two u32 immediates: fn_id and capture_count
imm_bytes: 8,
// Dynamic, depends on capture_count; keep 0 here for verifier-free spec
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Syscall => OpcodeSpec {
name: "SYSCALL",
imm_bytes: 4,

View File

@ -407,6 +407,26 @@ impl VirtualMachine {
self.cycles += OpCode::Trap.cycles();
return Err(LogicalFrameEndingReason::Breakpoint);
}
OpCode::MakeClosure => {
// Immediate carries (fn_id, capture_count)
let (fn_id, cap_count) = instr
.imm_u32x2()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
// Pop cap_count values from the operand stack, top-first.
let mut temp: Vec<Value> = Vec::with_capacity(cap_count as usize);
for _ in 0..cap_count {
let v = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
temp.push(v);
}
// Preserve order so that env[0] corresponds to captured_1 (the bottom-most
// among the popped values): reverse the temp vector.
temp.reverse();
// Allocate closure on heap and push its reference.
let href = self.heap.alloc_closure(fn_id, &temp);
self.push(Value::HeapRef(href));
}
OpCode::PushConst => {
let idx = instr
.imm_u32()
@ -2723,4 +2743,84 @@ mod tests {
assert_eq!(vm.heap.len(), 0, "All short-lived objects must be reclaimed deterministically");
}
#[test]
fn test_make_closure_zero_captures() {
use prometeu_bytecode::{FunctionMeta, Value};
// ROM: MAKE_CLOSURE fn_id=7, cap=0; HALT
let mut rom = Vec::new();
rom.extend_from_slice(&(OpCode::MakeClosure as u16).to_le_bytes());
rom.extend_from_slice(&7u32.to_le_bytes()); // fn_id
rom.extend_from_slice(&0u32.to_le_bytes()); // capture_count
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
let mut vm = new_test_vm(rom.clone(), vec![]);
vm.program.functions = std::sync::Arc::from(vec![FunctionMeta {
code_offset: 0,
code_len: rom.len() as u32,
..Default::default()
}]);
let mut native = MockNative;
let mut ctx = HostContext::new(None);
// step MAKE_CLOSURE
vm.step(&mut native, &mut ctx).unwrap();
// step HALT
vm.step(&mut native, &mut ctx).unwrap();
assert!(vm.halted);
assert_eq!(vm.operand_stack.len(), 1);
let top = vm.peek().unwrap().clone();
let href = match top { Value::HeapRef(h) => h, _ => panic!("Expected HeapRef on stack") };
assert!(vm.heap.is_valid(href));
assert_eq!(vm.heap.closure_fn_id(href), Some(7));
let env = vm.heap.closure_env_slice(href).expect("env slice");
assert_eq!(env.len(), 0);
}
#[test]
fn test_make_closure_multiple_captures_and_order() {
use prometeu_bytecode::{FunctionMeta, Value};
// Build ROM:
// PUSH_I32 1; PUSH_I32 2; PUSH_I32 3; // Stack: [1,2,3]
// MAKE_CLOSURE fn_id=9, cap=3; // Pops 3 (3,2,1), env = [1,2,3]
// HALT
let mut rom = Vec::new();
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&1i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&2i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&3i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::MakeClosure as u16).to_le_bytes());
rom.extend_from_slice(&9u32.to_le_bytes()); // fn_id
rom.extend_from_slice(&3u32.to_le_bytes()); // capture_count
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
let mut vm = new_test_vm(rom.clone(), vec![]);
vm.program.functions = std::sync::Arc::from(vec![FunctionMeta {
code_offset: 0,
code_len: rom.len() as u32,
..Default::default()
}]);
let mut native = MockNative;
let mut ctx = HostContext::new(None);
// Execute instructions until HALT
while !vm.halted {
vm.step(&mut native, &mut ctx).unwrap();
}
// After HALT, stack must contain only the closure ref
assert_eq!(vm.operand_stack.len(), 1);
let href = match vm.pop().unwrap() { Value::HeapRef(h) => h, _ => panic!("Expected HeapRef") };
assert_eq!(vm.heap.closure_fn_id(href), Some(9));
let env = vm.heap.closure_env_slice(href).expect("env slice");
assert_eq!(env.len(), 3);
assert_eq!(env[0], Value::Int32(1));
assert_eq!(env[1], Value::Int32(2));
assert_eq!(env[2], Value::Int32(3));
}
}

View File

@ -1,89 +1,14 @@
# PR-6.2 — Closure Capture Materialization
# PR-6.3 — CALL_CLOSURE (Model B Hidden Arg0)
## Briefing
Closures must capture values from the current stack frame into a heap-allocated environment.
Closures must be dynamically invokable.
This PR defines:
Under Model B, invocation semantics are:
* How captured values are materialized.
* How the environment layout is constructed.
No CALL_CLOSURE yet.
---
## Target
Define bytecode semantics for closure creation:
Introduce instruction (placeholder name):
`MAKE_CLOSURE fn_id, capture_count`
Semantics:
* Pop `capture_count` values from stack (top-first).
* Allocate closure object with those values stored in-order.
* Push resulting `HeapRef` to stack.
---
## Work Items
1. Define new opcode `MAKE_CLOSURE`.
2. Implement stack semantics.
3. Ensure captured values are copied (not borrowed).
4. Update interpreter to support opcode.
---
## Acceptance Checklist
* [ ] MAKE_CLOSURE opcode exists.
* [ ] Stack pops correct number of values.
* [ ] Closure allocated correctly.
* [ ] Closure ref pushed to stack.
---
## Tests
1. Create closure capturing 0 values.
2. Create closure capturing 2 values.
3. Validate env order correctness.
---
## Junie Instructions
You MAY:
* Add opcode.
* Modify interpreter dispatch.
* Add tests.
You MUST NOT:
* Implement CALL_CLOSURE yet.
* Modify GC behavior.
* Change verifier in this PR.
If capture order semantics unclear, STOP and ask.
---
## Definition of Done
Closures can be created with captured environment and exist as heap values.
---
# PR-6.3 — CALL_CLOSURE Instruction
## Briefing
Closures must be invokable at runtime. This PR introduces dynamic invocation semantics for closures.
* The closure object itself becomes hidden `arg0`.
* User-supplied arguments become `arg1..argN`.
* Captures remain inside the closure and are accessed explicitly.
---
@ -93,47 +18,53 @@ Introduce opcode:
`CALL_CLOSURE arg_count`
Semantics:
* Stack layout before call:
Stack before call:
```
[... args..., closure_ref]
[..., argN, ..., arg1, closure_ref]
```
* Pop closure_ref.
* Validate it is ObjectKind::Closure.
* Pop `arg_count` arguments.
* Create new call frame:
* Locals initialized with captured env first (design choice below).
* Arguments appended after captures.
* Jump to function entry (fn_id).
Execution steps:
1. Pop `closure_ref`.
2. Validate object is `ObjectKind::Closure`.
3. Pop `arg_count` arguments.
4. Read `fn_id` from closure object.
5. Create new call frame:
* Inject `closure_ref` as `arg0`.
* Append user arguments as `arg1..argN`.
6. Jump to function entry.
No environment copying into locals.
---
## Work Items
1. Add `CALL_CLOSURE` opcode.
2. Validate closure_ref type.
3. Integrate into call frame creation logic.
4. Respect function signature for ret_slots.
2. Implement dispatch logic.
3. Integrate with call frame creation.
4. Ensure stack discipline is preserved.
---
## Acceptance Checklist
* [ ] CALL_CLOSURE implemented.
* [ ] Correct stack consumption.
* [ ] Correct frame initialization.
* [ ] Error on non-closure value.
* [ ] closure_ref validated.
* [ ] arg_count respected.
* [ ] Hidden arg0 injected correctly.
* [ ] Errors thrown on non-closure call.
---
## Tests
1. Simple closure returning constant.
1. Closure returning constant.
2. Closure capturing value and using it.
3. Error when calling non-closure.
3. Calling non-closure results in trap.
4. Nested closure calls work.
---
@ -141,64 +72,65 @@ Semantics:
You MAY:
* Add opcode and dispatch.
* Modify call frame initialization.
* Modify interpreter call logic.
* Add tests.
You MUST NOT:
* Redesign stack model.
* Introduce coroutine behavior here.
* Change GC.
* Change stack model.
* Introduce coroutine semantics.
* Modify GC.
If frame layout decision is ambiguous, STOP and ask before choosing ordering.
If function signature metadata is insufficient to validate arg_count, STOP and ask.
---
## Definition of Done
Closures can be invoked dynamically and execute correctly.
Closures can be dynamically invoked with hidden arg0 semantics.
---
# PR-6.4 — GC Traversal for Closures
# PR-6.4 — GC Traversal for Closures (Model B)
## Briefing
Closures introduce heap-to-heap references through their captured environments.
The GC must traverse:
Under Model B, the closure object itself is passed at call time, but its environment remains stored in heap.
GC must traverse:
closure -> env -> inner HeapRefs
This PR updates the GC mark phase to correctly traverse closure environments.
---
## Target
Extend GC mark logic:
Extend GC mark phase to handle `ObjectKind::Closure`:
* When encountering ObjectKind::Closure:
When marking a closure:
* Iterate over env values.
* If a value contains HeapRef → mark referenced object.
No compaction. No relocation.
---
## Work Items
1. Update mark traversal switch for Closure.
2. Ensure no panics on malformed env.
3. Add tests for nested closure references.
1. Extend mark traversal switch.
2. Ensure safe iteration over env payload.
3. Add regression tests.
---
## Acceptance Checklist
* [ ] GC marks env HeapRefs.
* [ ] Closure env scanned.
* [ ] Nested closures retained.
* [ ] No regression in existing GC tests.
* [ ] Nested closures retained correctly.
---
@ -215,30 +147,34 @@ Extend GC mark logic:
You MAY:
* Modify mark traversal.
* Add GC tests.
* Add tests.
You MUST NOT:
* Implement compaction.
* Change sweep policy.
* Modify sweep policy.
* Introduce compaction.
If unsure whether env values may contain non-heap values, ask before assuming.
If unsure whether Value variants can embed HeapRef, STOP and ask.
---
## Definition of Done
GC correctly traverses closure environments.
GC correctly traverses closure environments under Model B semantics.
---
# PR-6.5 — Verifier Support for Closures
# PR-6.5 — Verifier Support for Closures (Model B)
## Briefing
The verifier must understand closure values as a distinct type and validate dynamic calls safely.
The verifier must understand closure values and enforce safe invocation rules.
Closures are heap objects but semantically represent callable values.
Under Model B:
* `CALL_CLOSURE` injects hidden `arg0`.
* User-visible arg_count excludes hidden arg.
* Captures are accessed via explicit instructions (future PR).
---
@ -246,36 +182,43 @@ Closures are heap objects but semantically represent callable values.
Extend verifier to:
* Introduce a stack type: `ClosureValue`.
* Validate MAKE_CLOSURE stack effects.
* Validate CALL_CLOSURE argument counts.
* Validate ret_slots against function signature.
1. Introduce stack type: `ClosureValue`.
2. Validate MAKE_CLOSURE effects.
3. Validate CALL_CLOSURE semantics:
* Ensure top of stack is ClosureValue.
* Ensure sufficient args present.
* Validate `arg_count` matches function signature expectations.
* Account for hidden arg0 when checking callee arg arity.
4. Validate ret_slots against function metadata.
---
## Work Items
1. Add closure type to verifier type lattice.
1. Extend type lattice with ClosureValue.
2. Define stack transitions for MAKE_CLOSURE.
3. Define stack transitions for CALL_CLOSURE.
4. Ensure deterministic failure on misuse.
4. Enforce strict failure on mismatch.
---
## Acceptance Checklist
* [ ] Verifier understands closure values.
* [ ] ClosureValue type exists.
* [ ] Invalid CALL_CLOSURE rejected.
* [ ] Hidden arg0 accounted for.
* [ ] ret_slots validated.
* [ ] All tests pass.
* [ ] All verifier tests pass.
---
## Tests
1. Valid closure call passes verification.
2. CALL_CLOSURE with wrong arg count fails.
3. CALL_CLOSURE on non-closure fails.
2. CALL_CLOSURE with wrong arg_count fails.
3. CALL_CLOSURE on non-closure fails verification.
4. Nested closure calls verify correctly.
---
@ -283,21 +226,18 @@ Extend verifier to:
You MAY:
* Extend verifier type model.
* Extend verifier model.
* Add tests.
You MUST NOT:
* Weaken verification rules.
* Introduce runtime-only checks instead of verifier checks.
* Replace verifier checks with runtime-only traps.
If closure typing conflicts with current stack model, STOP and ask.
If function metadata (arg_slots/ret_slots) is insufficient, STOP and request clarification.
---
## Definition of Done
Verifier fully supports closure creation and invocation.
---
Verifier fully supports closure creation and invocation under Model B semantics.