This commit is contained in:
bQUARKz 2026-02-20 07:03:55 +00:00
parent 35f5b8fa86
commit 29736e9577
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
5 changed files with 286 additions and 93 deletions

View File

@ -161,6 +161,15 @@ pub enum OpCode {
/// 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,
/// Calls a closure value with hidden arg0 semantics (Model B).
/// Operand: arg_count (u32) — number of user-supplied args (excludes hidden arg0)
/// Stack before: [..., argN, ..., arg1, closure_ref]
/// Behavior:
/// - Pops `closure_ref` and validates it is a Closure.
/// - Pops `arg_count` user args.
/// - Fetches `fn_id` from the closure and creates a new call frame.
/// - Injects hidden arg0 = closure_ref, followed by user args as arg1..argN.
CallClosure = 0x53,
// --- 6.8 Peripherals and System ---
/// Invokes a system function (Firmware/OS).
@ -222,6 +231,7 @@ impl TryFrom<u16> for OpCode {
0x50 => Ok(OpCode::Call),
0x51 => Ok(OpCode::Ret),
0x52 => Ok(OpCode::MakeClosure),
0x53 => Ok(OpCode::CallClosure),
0x70 => Ok(OpCode::Syscall),
0x80 => Ok(OpCode::FrameSync),
_ => Err(format!("Invalid OpCode: 0x{:04X}", value)),
@ -279,6 +289,7 @@ impl OpCode {
OpCode::Call => 5,
OpCode::Ret => 4,
OpCode::MakeClosure => 8,
OpCode::CallClosure => 6,
OpCode::Syscall => 1,
OpCode::FrameSync => 1,
}

View File

@ -474,6 +474,18 @@ impl OpCodeSpecExt for OpCode {
may_trap: false,
is_safepoint: false,
},
OpCode::CallClosure => OpcodeSpec {
name: "CALL_CLOSURE",
// One u32 immediate: arg_count (user args, excludes hidden arg0)
imm_bytes: 4,
// Dynamic: pops closure_ref + arg_count; keep 0 in spec layer
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: true,
is_safepoint: false,
},
OpCode::Syscall => OpcodeSpec {
name: "SYSCALL",
imm_bytes: 4,

View File

@ -7,6 +7,7 @@ use prometeu_bytecode::ProgramImage;
use prometeu_bytecode::Value;
use crate::roots::{RootVisitor, visit_value_for_roots};
use crate::heap::Heap;
use crate::object::ObjectKind;
use prometeu_bytecode::{
TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_SYSCALL, TRAP_OOB,
TRAP_STACK_UNDERFLOW, TRAP_TYPE, TrapInfo,
@ -427,6 +428,109 @@ impl VirtualMachine {
let href = self.heap.alloc_closure(fn_id, &temp);
self.push(Value::HeapRef(href));
}
OpCode::CallClosure => {
// Operand carries the number of user-supplied arguments (arg1..argN).
let user_arg_count = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
// Pop the closure reference from the stack (top of stack).
let clos_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
let href = match clos_val {
Value::HeapRef(h) => h,
other => {
return Err(self.trap(
TRAP_TYPE,
opcode as u16,
format!(
"CALL_CLOSURE expects a closure handle at TOS, got {:?}",
other
),
start_pc as u32,
))
}
};
// Validate that the heap object is indeed a Closure.
let header = self.heap.header(href).ok_or_else(|| {
self.trap(
TRAP_OOB,
opcode as u16,
format!("Invalid heap handle in CALL_CLOSURE: {:?}", href),
start_pc as u32,
)
})?;
if header.kind != ObjectKind::Closure {
return Err(self.trap(
TRAP_TYPE,
opcode as u16,
format!(
"CALL_CLOSURE on non-closure object kind {:?}",
header.kind
),
start_pc as u32,
));
}
// Pop user arguments from the operand stack (top-first), then fix order.
let mut user_args: Vec<Value> = Vec::with_capacity(user_arg_count);
for _ in 0..user_arg_count {
user_args.push(self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?);
}
user_args.reverse(); // Now in logical order: arg1..argN
// Resolve target function id from the closure payload.
let fn_id = self.heap.closure_fn_id(href).ok_or_else(|| {
LogicalFrameEndingReason::Panic(
"Internal error: malformed closure object (missing fn_id)".into(),
)
})? as usize;
let callee = self.program.functions.get(fn_id).ok_or_else(|| {
self.trap(
TRAP_INVALID_FUNC,
opcode as u16,
format!("Invalid func_id {} from closure", fn_id),
start_pc as u32,
)
})?;
// Copy required fields to drop the immutable borrow before mutating self
let callee_param_slots = callee.param_slots as usize;
let callee_local_slots = callee.local_slots as usize;
let callee_code_offset = callee.code_offset as usize;
// Validate arity: param_slots must equal hidden arg0 + user_arg_count.
let expected_params = 1usize + user_arg_count;
if callee_param_slots != expected_params {
return Err(self.trap(
TRAP_TYPE,
opcode as u16,
format!(
"CALL_CLOSURE arg_count mismatch: function expects {} total params (including hidden arg0), got hidden+{}",
callee_param_slots, expected_params
),
start_pc as u32,
));
}
// Prepare the operand stack to match the direct CALL convention:
// push hidden arg0 (closure_ref) followed by arg1..argN.
self.push(Value::HeapRef(href));
for v in user_args.into_iter() { self.push(v); }
let stack_base = self
.operand_stack
.len()
.checked_sub(callee_param_slots)
.ok_or_else(|| LogicalFrameEndingReason::Panic("Stack underflow".into()))?;
// Allocate and zero-init local slots
for _ in 0..callee_local_slots { self.operand_stack.push(Value::Null); }
self.call_stack.push(CallFrame { return_pc: self.pc as u32, stack_base, func_idx: fn_id });
self.pc = callee_code_offset;
}
OpCode::PushConst => {
let idx = instr
.imm_u32()
@ -2823,4 +2927,161 @@ mod tests {
assert_eq!(env[1], Value::Int32(2));
assert_eq!(env[2], Value::Int32(3));
}
#[test]
fn test_call_closure_returns_constant() {
use prometeu_bytecode::{FunctionMeta, Value};
// F0 (entry): MAKE_CLOSURE fn=1, cap=0; CALL_CLOSURE argc=0; HALT
// F1 (callee): PUSH_I32 7; RET
let mut rom = Vec::new();
let f0_start = 0usize;
rom.extend_from_slice(&(OpCode::MakeClosure as u16).to_le_bytes());
rom.extend_from_slice(&1u32.to_le_bytes()); // fn_id
rom.extend_from_slice(&0u32.to_le_bytes()); // capture_count
rom.extend_from_slice(&(OpCode::CallClosure as u16).to_le_bytes());
rom.extend_from_slice(&0u32.to_le_bytes()); // argc = 0 user args
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
let f0_len = rom.len() - f0_start;
// F1 code
let f1_start = rom.len() as u32;
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&7i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes());
let f1_len = rom.len() as u32 - f1_start;
let mut vm = new_test_vm(rom.clone(), vec![]);
vm.program.functions = std::sync::Arc::from(vec![
FunctionMeta { code_offset: f0_start as u32, code_len: f0_len as u32, ..Default::default() },
FunctionMeta { code_offset: f1_start, code_len: f1_len, param_slots: 1, return_slots: 1, ..Default::default() },
]);
let mut native = MockNative;
let mut ctx = HostContext::new(None);
vm.prepare_call("0");
let report = vm.run_budget(100, &mut native, &mut ctx).unwrap();
assert_eq!(report.reason, LogicalFrameEndingReason::Halted);
assert_eq!(vm.operand_stack.len(), 1);
assert_eq!(vm.operand_stack[0], Value::Int32(7));
}
#[test]
fn test_call_closure_with_captures_ignored() {
use prometeu_bytecode::{FunctionMeta, Value};
// F0: PUSH_I32 123; MAKE_CLOSURE fn=1 cap=1; CALL_CLOSURE 0; HALT
// F1: PUSH_I32 42; RET
let mut rom = Vec::new();
let f0_start = 0usize;
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&123i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::MakeClosure as u16).to_le_bytes());
rom.extend_from_slice(&1u32.to_le_bytes()); // fn_id
rom.extend_from_slice(&1u32.to_le_bytes()); // capture_count
rom.extend_from_slice(&(OpCode::CallClosure as u16).to_le_bytes());
rom.extend_from_slice(&0u32.to_le_bytes()); // argc = 0
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
let f0_len = rom.len() - f0_start;
let f1_start = rom.len() as u32;
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&42i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes());
let f1_len = rom.len() as u32 - f1_start;
let mut vm = new_test_vm(rom.clone(), vec![]);
vm.program.functions = std::sync::Arc::from(vec![
FunctionMeta { code_offset: f0_start as u32, code_len: f0_len as u32, ..Default::default() },
FunctionMeta { code_offset: f1_start, code_len: f1_len, param_slots: 1, return_slots: 1, ..Default::default() },
]);
let mut native = MockNative;
let mut ctx = HostContext::new(None);
vm.prepare_call("0");
let report = vm.run_budget(100, &mut native, &mut ctx).unwrap();
assert_eq!(report.reason, LogicalFrameEndingReason::Halted);
assert_eq!(vm.operand_stack, vec![Value::Int32(42)]);
}
#[test]
fn test_call_closure_on_non_closure_traps() {
use prometeu_bytecode::FunctionMeta;
// F0: PUSH_I32 1; CALL_CLOSURE 0; HALT -> should TRAP_TYPE on CALL_CLOSURE
let mut rom = Vec::new();
let f0_start = 0usize;
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&1i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::CallClosure as u16).to_le_bytes());
rom.extend_from_slice(&0u32.to_le_bytes());
// Leave HALT for after run to ensure we trap before
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
let f0_len = rom.len() - f0_start;
let mut vm = new_test_vm(rom.clone(), vec![]);
vm.program.functions = std::sync::Arc::from(vec![FunctionMeta { code_offset: f0_start as u32, code_len: f0_len as u32, ..Default::default() }]);
let mut native = MockNative;
let mut ctx = HostContext::new(None);
vm.prepare_call("0");
let report = vm.run_budget(10, &mut native, &mut ctx).unwrap();
match report.reason {
LogicalFrameEndingReason::Trap(info) => {
assert_eq!(info.code, TRAP_TYPE);
assert_eq!(info.opcode, OpCode::CallClosure as u16);
}
other => panic!("Expected Trap(TYPE) from CALL_CLOSURE on non-closure, got {:?}", other),
}
}
#[test]
fn test_nested_call_closure() {
use prometeu_bytecode::{FunctionMeta, Value};
// F0: MAKE_CLOSURE fn=1 cap=0; CALL_CLOSURE 0; CALL_CLOSURE 0; HALT
// F1: MAKE_CLOSURE fn=2 cap=0; RET // returns a closure
// F2: PUSH_I32 55; RET // returns constant
let mut rom = Vec::new();
// F0
let f0_start = 0usize;
rom.extend_from_slice(&(OpCode::MakeClosure as u16).to_le_bytes());
rom.extend_from_slice(&1u32.to_le_bytes()); // fn_id = 1
rom.extend_from_slice(&0u32.to_le_bytes()); // cap=0
rom.extend_from_slice(&(OpCode::CallClosure as u16).to_le_bytes());
rom.extend_from_slice(&0u32.to_le_bytes()); // argc=0 -> pushes a closure from F1
rom.extend_from_slice(&(OpCode::CallClosure as u16).to_le_bytes());
rom.extend_from_slice(&0u32.to_le_bytes()); // argc=0 -> call returned closure F2
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
let f0_len = rom.len() - f0_start;
// F1
let f1_start = rom.len() as u32;
rom.extend_from_slice(&(OpCode::MakeClosure as u16).to_le_bytes());
rom.extend_from_slice(&2u32.to_le_bytes()); // fn_id = 2
rom.extend_from_slice(&0u32.to_le_bytes()); // cap=0
rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); // return the HeapRef on stack
let f1_len = rom.len() as u32 - f1_start;
// F2
let f2_start = rom.len() as u32;
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&55i32.to_le_bytes());
rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes());
let f2_len = rom.len() as u32 - f2_start;
let mut vm = new_test_vm(rom.clone(), vec![]);
vm.program.functions = std::sync::Arc::from(vec![
FunctionMeta { code_offset: f0_start as u32, code_len: f0_len as u32, ..Default::default() },
FunctionMeta { code_offset: f1_start, code_len: f1_len, param_slots: 1, return_slots: 1, ..Default::default() },
FunctionMeta { code_offset: f2_start, code_len: f2_len, param_slots: 1, return_slots: 1, ..Default::default() },
]);
let mut native = MockNative;
let mut ctx = HostContext::new(None);
vm.prepare_call("0");
let report = vm.run_budget(200, &mut native, &mut ctx).unwrap();
assert_eq!(report.reason, LogicalFrameEndingReason::Halted);
assert_eq!(vm.operand_stack, vec![Value::Int32(55)]);
}
}

View File

@ -1,96 +1,3 @@
# PR-6.3 — CALL_CLOSURE (Model B Hidden Arg0)
## Briefing
Closures must be dynamically invokable.
Under Model B, invocation semantics are:
* The closure object itself becomes hidden `arg0`.
* User-supplied arguments become `arg1..argN`.
* Captures remain inside the closure and are accessed explicitly.
---
## Target
Introduce opcode:
`CALL_CLOSURE arg_count`
Stack before call:
```
[..., argN, ..., arg1, closure_ref]
```
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. Implement dispatch logic.
3. Integrate with call frame creation.
4. Ensure stack discipline is preserved.
---
## Acceptance Checklist
* [ ] CALL_CLOSURE implemented.
* [ ] closure_ref validated.
* [ ] arg_count respected.
* [ ] Hidden arg0 injected correctly.
* [ ] Errors thrown on non-closure call.
---
## Tests
1. Closure returning constant.
2. Closure capturing value and using it.
3. Calling non-closure results in trap.
4. Nested closure calls work.
---
## Junie Instructions
You MAY:
* Modify interpreter call logic.
* Add tests.
You MUST NOT:
* Change stack model.
* Introduce coroutine semantics.
* Modify GC.
If function signature metadata is insufficient to validate arg_count, STOP and ask.
---
## Definition of Done
Closures can be dynamically invoked with hidden arg0 semantics.
---
# PR-6.4 — GC Traversal for Closures (Model B)
## Briefing

View File

@ -1,3 +1,5 @@
vamos as PR7s todas em um unico canvas markdown ingles, devem ser auto contidas com briefing, alvo, checklist, test quando necessario e comandos do que a Junie pode ou nao fazer (Junie eh task operator nao arquiteta ou assume nada, questiona quando necessario).
7 — Coroutines (único modelo de concorrência, cooperativo)
7.1. Definir objeto Coroutine no heap: stack/frames próprios, status, wake time, mailbox/queue se existir.