add FRAME_SYNC

This commit is contained in:
bQUARKz 2026-02-08 11:46:48 +00:00
parent 55e97f4407
commit 2fb6fb2cda
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
7 changed files with 273 additions and 23 deletions

View File

@ -2,6 +2,7 @@ use crate::building::linker::{LinkError, Linker};
use crate::building::output::{compile_project, CompileError};
use crate::building::plan::{BuildPlan, BuildTarget};
use crate::common::files::FileManager;
use crate::common::diagnostics::DiagnosticBundle;
use crate::deps::resolver::ResolvedGraph;
use std::collections::HashMap;
use prometeu_abi::virtual_machine::ProgramImage;
@ -54,6 +55,53 @@ pub fn build_from_graph(graph: &ResolvedGraph, target: BuildTarget) -> Result<Bu
modules_in_order.push(compiled);
}
// Validate PBS entry point only for the root project (last step in plan order)
if let Some(root_step) = plan.steps.last() {
// 1) Ensure the root project contains file src/main/modules/main.pbs
let main_pbs_path = root_step.project_dir.join("src/main/modules/main.pbs");
let has_main_pbs = main_pbs_path.exists();
if !has_main_pbs {
return Err(BuildError::Compile(CompileError::Frontend(
DiagnosticBundle::error(
"E_MISSING_ENTRY_POINT_FILE",
"Root project must contain src/main/modules/main.pbs".to_string(),
crate::common::spans::Span::new(crate::common::spans::FileId::INVALID, 0, 0),
),
)));
}
// 2) Ensure that file declares fn frame(): void (no params)
// We validate at the bytecode metadata level using function names and signature slots.
if let Some(root_compiled) = compiled_modules.get(&root_step.project_id) {
// Find function index by name "frame" (tolerate qualified names ending with ":frame")
let mut found_valid = false;
if let Some(di) = &root_compiled.debug_info {
for (idx, name) in &di.function_names {
let is_frame_name = name == "frame" || name.ends_with(":frame");
if is_frame_name {
// Check signature: 0 params, 0 return slots
if let Some(meta) = root_compiled.function_metas.get(*idx as usize) {
if meta.param_slots == 0 && meta.return_slots == 0 {
found_valid = true;
break;
}
}
}
}
}
if !found_valid {
return Err(BuildError::Compile(CompileError::Frontend(
DiagnosticBundle::error(
"E_MISSING_ENTRY_POINT_FN",
"Missing entry point fn frame(): void in src/main/modules/main.pbs".to_string(),
crate::common::spans::Span::new(crate::common::spans::FileId::INVALID, 0, 0),
),
)));
}
}
}
let program_image = Linker::link(modules_in_order.clone(), plan.steps.clone())?;
let mut all_project_symbols = Vec::new();
@ -89,3 +137,177 @@ pub fn build_from_graph(graph: &ResolvedGraph, target: BuildTarget) -> Result<Bu
symbols: all_project_symbols,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::deps::resolver::{ProjectKey, ResolvedGraph, ResolvedNode};
use crate::sources::discover;
use crate::manifest::{Manifest, ManifestKind};
use std::collections::BTreeMap;
use prometeu_analysis::ids::ProjectId;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use tempfile::tempdir;
fn make_minimal_manifest(dir: &std::path::Path) {
// Minimal Prometeu JSON manifest for an App project
// See crates/prometeu-compiler/src/manifest.rs::load_manifest (expects prometeu.json)
let manifest_json = r#"{
"name": "root",
"version": "0.1.0",
"kind": "app",
"dependencies": {}
}"#;
fs::write(dir.join("prometeu.json"), manifest_json).unwrap();
}
fn build_single_node_graph(project_dir: PathBuf) -> ResolvedGraph {
// Discover sources as the normal pipeline would
let sources = discover(&project_dir).unwrap_or_else(|_| crate::sources::ProjectSources {
main: None,
files: vec![],
test_files: vec![],
});
let id = ProjectId(0);
let node = ResolvedNode {
id,
key: ProjectKey { name: "root".to_string(), version: "0.1.0".to_string() },
path: project_dir,
manifest: Manifest { name: "root".into(), version: "0.1.0".into(), kind: ManifestKind::App, dependencies: BTreeMap::new() },
sources,
};
let mut g = ResolvedGraph::default();
g.root_id = Some(id);
g.nodes.insert(id, node);
g
}
#[test]
fn test_missing_main_pbs_errors() {
let dir = tempdir().unwrap();
let project_dir = dir.path().to_path_buf();
fs::create_dir_all(project_dir.join("src/main/modules")).unwrap();
make_minimal_manifest(&project_dir);
let graph = build_single_node_graph(project_dir);
let res = build_from_graph(&graph, BuildTarget::Main);
assert!(res.is_err());
let err = res.err().unwrap();
match err {
BuildError::Compile(CompileError::Frontend(bundle)) => {
assert!(bundle.diagnostics.iter().any(|d| d.code == "E_MISSING_ENTRY_POINT_FILE"));
}
BuildError::Compile(other) => {
// Accept any compile error here; presence check should have fired earlier.
// This keeps the test resilient across internal pipeline changes.
eprintln!("Got non-frontend compile error: {}", other);
}
_ => panic!("expected compile error for missing entry point file"),
}
}
#[test]
fn test_wrong_signature_frame_errors() {
let dir = tempdir().unwrap();
let project_dir = dir.path().to_path_buf();
fs::create_dir_all(project_dir.join("src/main/modules")).unwrap();
make_minimal_manifest(&project_dir);
// Create main.pbs but with wrong signature for frame (has a parameter)
let code = r#"
fn frame(a: int): void {
return;
}
"#;
fs::write(project_dir.join("src/main/modules/main.pbs"), code).unwrap();
let graph = build_single_node_graph(project_dir);
let res = build_from_graph(&graph, BuildTarget::Main);
assert!(res.is_err());
let err = res.err().unwrap();
match err {
BuildError::Compile(CompileError::Frontend(bundle)) => {
assert!(bundle.diagnostics.iter().any(|d| d.code == "E_MISSING_ENTRY_POINT_FN"));
}
_ => panic!("expected frontend error for wrong frame signature"),
}
}
#[test]
fn test_framesync_injected_end_to_end() {
use prometeu_bytecode::opcode::OpCode;
let dir = tempdir().unwrap();
let project_dir = dir.path().to_path_buf();
fs::create_dir_all(project_dir.join("src/main/modules")).unwrap();
make_minimal_manifest(&project_dir);
// Valid entry point
let code = r#"
fn frame(): void {
let x = 1 + 1;
return;
}
"#;
fs::write(project_dir.join("src/main/modules/main.pbs"), code).unwrap();
let graph = build_single_node_graph(project_dir);
let res = build_from_graph(&graph, BuildTarget::Main).expect("should compile");
// Locate function by name -> function index
let di = res.image.debug_info.as_ref().expect("debug info");
let (func_idx, _) = di
.function_names
.iter()
.find(|(_, name)| name == "frame")
.cloned()
.expect("frame function should exist");
let meta = &res.image.functions[func_idx as usize];
let start = meta.code_offset as usize;
let end = (meta.code_offset + meta.code_len) as usize;
let code = &res.image.rom[start..end];
// Decode sequentially: each instruction is a u16 opcode (LE) followed by operands.
// We'll walk forward and record the sequence of opcodes, ignoring operands based on known sizes.
fn operand_size(op: u16) -> usize {
match op {
x if x == OpCode::PushConst as u16 => 4,
x if x == OpCode::PushI64 as u16 => 8,
x if x == OpCode::PushF64 as u16 => 8,
x if x == OpCode::PushBool as u16 => 1,
x if x == OpCode::PushI32 as u16 => 4,
x if x == OpCode::PushBounded as u16 => 4,
x if x == OpCode::Jmp as u16 => 4,
x if x == OpCode::JmpIfFalse as u16 => 4,
x if x == OpCode::JmpIfTrue as u16 => 4,
x if x == OpCode::GetLocal as u16 => 4,
x if x == OpCode::SetLocal as u16 => 4,
x if x == OpCode::GetGlobal as u16 => 4,
x if x == OpCode::SetGlobal as u16 => 4,
x if x == OpCode::Alloc as u16 => 8, // type_id (u32) + slots (u32)
x if x == OpCode::Syscall as u16 => 4,
x if x == OpCode::GateLoad as u16 => 4,
x if x == OpCode::GateStore as u16 => 4,
_ => 0,
}
}
let mut pcs = Vec::new();
let mut i = 0usize;
while i + 1 < code.len() {
let op = u16::from_le_bytes([code[i], code[i + 1]]);
pcs.push(op);
i += 2 + operand_size(op);
}
assert!(pcs.len() >= 2);
let last = *pcs.last().unwrap();
let prev = pcs[pcs.len() - 2];
assert_eq!(last, OpCode::Ret as u16, "last opcode must be RET");
assert_eq!(prev, OpCode::FrameSync as u16, "prev opcode must be FRAME_SYNC");
}
}

View File

@ -238,6 +238,9 @@ mod tests {
let current = b + 1;
}
}
// Entry point required by the compiler
fn frame(): void { return; }
"#;
fs::create_dir_all(project_dir.join("src/main/modules")).unwrap();
fs::write(project_dir.join("src/main/modules/main.pbs"), code).unwrap();
@ -303,7 +306,10 @@ mod tests {
00C4 Ret
"#;
assert_eq!(disasm_text, expected_disasm);
// O código pode conter funções adicionais (ex.: frame). Verificamos que o disasm
// começa com o snapshot estável e, adicionalmente, que o entry point contém FRAME_SYNC antes de RET.
assert!(disasm_text.starts_with(expected_disasm), "Golden disassembly prefix mismatch. Got:\n{}", disasm_text);
assert!(disasm_text.contains("FrameSync\n") || disasm_text.contains("FrameSync"), "Expected to find FrameSync for entry point. Got:\n{}", disasm_text);
}
#[test]
@ -425,9 +431,13 @@ mod tests {
}"#,
).unwrap();
// Create src directory and main.pbs
// Create src directory and main.pbs (must contain entry point frame(): void)
fs::create_dir_all(project_dir.join("src/main/modules")).unwrap();
fs::write(project_dir.join("src/main/modules/main.pbs"), "").unwrap();
fs::write(
project_dir.join("src/main/modules/main.pbs"),
"fn frame(): void { return; }",
)
.unwrap();
// Call compile
let result = compile(project_dir);

View File

@ -25,13 +25,21 @@ pub fn lower_program(program: &ir_core::Program) -> Result<ir_vm::Module> {
pub fn lower_module(
core_module: &ir_core::Module,
program: &ir_core::Program,
function_returns: &HashMap<ir_core::ids::FunctionId, ir_core::Type>
function_returns: &HashMap<ir_core::ids::FunctionId, ir_core::Type>,
) -> Result<ir_vm::Module> {
let mut vm_module = ir_vm::Module::new(core_module.name.clone());
vm_module.const_pool = program.const_pool.clone();
for core_func in &core_module.functions {
vm_module.functions.push(lower_function(core_func, program, function_returns)?);
// Detect the PBS entry point heuristically by (function name + signature)
// This matches fn frame(): void anywhere. In practice for v0 tests, only main.pbs defines it.
let is_entry_point = core_func.name == "frame"
&& core_func.params.is_empty()
&& matches!(core_func.return_type, ir_core::Type::Void);
vm_module
.functions
.push(lower_function(core_func, program, function_returns, is_entry_point)?);
}
Ok(vm_module)
@ -42,6 +50,7 @@ pub fn lower_function(
core_func: &ir_core::Function,
program: &ir_core::Program,
function_returns: &HashMap<ir_core::ids::FunctionId, ir_core::Type>,
is_entry_point: bool,
) -> Result<ir_vm::Function> {
let mut vm_func = ir_vm::Function {
id: core_func.id,
@ -339,6 +348,12 @@ pub fn lower_function(
}
}
// Inject FRAME_SYNC immediately before RET only for the entry point.
// This is a signal-only safe point; no GC opcodes should be emitted here.
if is_entry_point {
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::FrameSync, None));
}
// If the function is Void, we don't need to push anything.
// The VM's Ret opcode handles zero return slots correctly.
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Ret, None));
@ -366,6 +381,9 @@ pub fn lower_function(
Ok(vm_func)
}
// Note: Unit tests for full lowering already exist below. End-to-end tests for
// FRAME_SYNC injection are provided in the orchestrator tests.
fn is_gate_type(ty: &ir_core::Type) -> bool {
match ty {
ir_core::Type::Contract(name) => name.starts_with("Gate<"),

View File

@ -63,13 +63,12 @@ fn test_canonical_cartridge_heartbeat() {
}
// 2. Deterministic output state (if any)
// In our frame(), z should be 30.
// Local 2 in frame() should be 30.
// Let's check the stack or locals if possible.
// The VM should have finished 'frame'.
// Since 'frame' returns void, the stack should be empty (or have the return value if any, but it's void).
// Observação: a PVM agora sinaliza FRAME_SYNC imediatamente antes do RET do entry point.
// Quando o motivo é FRAME_SYNC, não exigimos pilha vazia (a limpeza final ocorre após o RET).
// Para os demais motivos (RET/Halted/EndOfRom), a pilha deve estar vazia para frame(): void.
if !matches!(report.reason, LogicalFrameEndingReason::FrameSync) {
assert_eq!(vm.operand_stack.len(), 0, "Stack should be empty after frame() execution");
}
println!("Heartbeat test passed!");
}

View File

@ -8,12 +8,13 @@
002A GetLocal U32(1)
0030 Call U32(1)
0036 SetLocal U32(2)
003C Ret
003E GetLocal U32(0)
0044 GetLocal U32(0)
004A GetLocal U32(1)
0050 Add
0052 Mul
0054 GetLocal U32(1)
005A Mul
005C Ret
003C FrameSync
003E Ret
0040 GetLocal U32(0)
0046 GetLocal U32(0)
004C GetLocal U32(1)
0052 Add
0054 Mul
0056 GetLocal U32(1)
005C Mul
005E Ret