diff --git a/crates/prometeu-compiler/src/building/linker.rs b/crates/prometeu-compiler/src/building/linker.rs index e70cf80e..a353d326 100644 --- a/crates/prometeu-compiler/src/building/linker.rs +++ b/crates/prometeu-compiler/src/building/linker.rs @@ -172,6 +172,15 @@ impl Linker { // Internal call relocation (from module-local func_idx to global func_idx) // And PUSH_CONST relocation. + // Also relocate intra-module jump target addresses when modules are concatenated. + + // Small helper to patch a 32-bit immediate at `pos` using a transformer function. + // Safety: caller must ensure `pos + 4 <= end`. + let mut patch_u32_at = |buf: &mut Vec, pos: usize, f: &dyn Fn(u32) -> u32| { + let current = u32::from_le_bytes(buf[pos..pos+4].try_into().unwrap()); + let next = f(current); + buf[pos..pos+4].copy_from_slice(&next.to_le_bytes()); + }; let mut pos = code_offset; let end = code_offset + module.code.len(); while pos < end { @@ -212,6 +221,14 @@ impl Linker { pos += 4; } } + // Relocate control-flow jump targets by adding module code offset + OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue => { + if pos + 4 <= end { + let code_off = module_code_offsets[i]; + patch_u32_at(&mut combined_code, pos, &|t| t.saturating_add(code_off)); + pos += 4; + } + } OpCode::PushI32 | OpCode::PushBounded | OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue | OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal | OpCode::PopN | OpCode::Syscall | OpCode::GateLoad | OpCode::GateStore => { @@ -436,4 +453,80 @@ mod tests { assert_eq!(result.constant_pool[1], Value::String("hello".into())); assert_eq!(result.constant_pool[2], Value::Int32(99)); } + + #[test] + fn test_jump_relocation_across_modules() { + // Module 1: small stub to create a non-zero code offset for module 2 + let key1 = ProjectKey { name: "m1".into(), version: "1.0.0".into() }; + let id1 = ProjectId(0); + let step1 = BuildStep { project_id: id1, project_key: key1.clone(), project_dir: "".into(), target: BuildTarget::Main, sources: vec![], deps: BTreeMap::new() }; + + let mut code1 = Vec::new(); + code1.extend_from_slice(&(OpCode::Add as u16).to_le_bytes()); + code1.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); + let m1 = CompiledModule { + project_id: id1, + project_key: key1.clone(), + target: BuildTarget::Main, + exports: BTreeMap::new(), + imports: vec![], + const_pool: vec![], + code: code1.clone(), + function_metas: vec![FunctionMeta { code_offset: 0, code_len: code1.len() as u32, ..Default::default() }], + debug_info: None, + symbols: vec![], + }; + + // Module 2: contains an unconditional JMP and a conditional JMP_IF_TRUE with local targets + let key2 = ProjectKey { name: "m2".into(), version: "1.0.0".into() }; + let id2 = ProjectId(1); + let step2 = BuildStep { project_id: id2, project_key: key2.clone(), project_dir: "".into(), target: BuildTarget::Main, sources: vec![], deps: BTreeMap::new() }; + + let mut code2 = Vec::new(); + // Unconditional JMP to local target 0 (module-local start) + let jmp_pc = code2.len() as u32; // where opcode will be placed + code2.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes()); + code2.extend_from_slice(&0u32.to_le_bytes()); + + // PushBool true; then conditional jump to local target 0 + code2.extend_from_slice(&(OpCode::PushBool as u16).to_le_bytes()); + code2.push(1u8); + let cjmp_pc = code2.len() as u32; + code2.extend_from_slice(&(OpCode::JmpIfTrue as u16).to_le_bytes()); + code2.extend_from_slice(&0u32.to_le_bytes()); + + // End with HALT so VM would stop if executed + code2.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes()); + + let m2 = CompiledModule { + project_id: id2, + project_key: key2.clone(), + target: BuildTarget::Main, + exports: BTreeMap::new(), + imports: vec![], + const_pool: vec![], + code: code2.clone(), + function_metas: vec![FunctionMeta { code_offset: 0, code_len: code2.len() as u32, ..Default::default() }], + debug_info: None, + symbols: vec![], + }; + + // Link with order [m1, m2] + let result = Linker::link(vec![m1, m2], vec![step1, step2]).unwrap(); + + // Module 2's code starts after module 1's code + let module2_offset = code1.len() as u32; + + // Verify that the JMP immediate equals original_target (0) + module2_offset + let jmp_abs_pc = module2_offset as usize + jmp_pc as usize; + let jmp_imm_off = jmp_abs_pc + 2; // skip opcode + let jmp_patched = u32::from_le_bytes(result.rom[jmp_imm_off..jmp_imm_off+4].try_into().unwrap()); + assert_eq!(jmp_patched, module2_offset); + + // Verify that the conditional JMP immediate was relocated similarly + let cjmp_abs_pc = module2_offset as usize + cjmp_pc as usize; + let cjmp_imm_off = cjmp_abs_pc + 2; + let cjmp_patched = u32::from_le_bytes(result.rom[cjmp_imm_off..cjmp_imm_off+4].try_into().unwrap()); + assert_eq!(cjmp_patched, module2_offset); + } } diff --git a/files/TODO.md b/files/TODO.md index df284fb4..8323572b 100644 --- a/files/TODO.md +++ b/files/TODO.md @@ -19,68 +19,7 @@ --- -## PR-00 — Compiler: Enforce PBS entry point and inject FRAME_SYNC in main.pbs::frame() - -### Briefing - -PBS requires a single logical entry point: `src/main/modules/main.pbs::frame(): void`. The VM relies on `FRAME_SYNC` as a **signal-only safe point** to perform GC work between logical frames. Today the compiler does not guarantee either the existence of this entry point nor the injection of `FRAME_SYNC`. - -### Target - -1. **Entry point validation** (fatal error at compile time): - - * Ensure the root project contains file `src/main/modules/main.pbs`. - * Ensure that file declares `fn frame(): void`. - * If missing, emit a **fatal diagnostic** and abort compilation. -2. **FRAME_SYNC injection** (only for the entry point): - - * In lowering/codegen for `main.pbs::frame(): void`, ensure the epilogue emits: - - * `FRAME_SYNC` **immediately before** `RET`. - -### Non-goals - -* Do not inject `FRAME_SYNC` into any other function named `frame`. -* Do not add any GC opcode or GC scheduling metadata into bytecode. -* Do not change runtime behavior besides the presence of `FRAME_SYNC` at the safe point. - -### Implementation notes - -* Identify the entry point by **(file path + function name + signature)**: - - * file: `src/main/modules/main.pbs` - * function: `frame` - * return: `void` - * (parameters: must be none) -* The safest place to inject is at the end of lowering for `Return` in that function: - - * Emit `FRAME_SYNC` just before emitting `RET`. -* Prefer to implement entry point existence checks in an early phase (project scan / module discovery) so errors are clear. - -### Tests - -1. **Positive**: project with `main.pbs` and `fn frame(): void` compiles. -2. **Injection**: compiled bytecode for entry point contains `FRAME_SYNC` right before `RET`. - - * Acceptable test forms: - - * bytecode disasm snapshot test, or - * inspect emitted instruction stream before encoding. -3. **Negative**: - - * Missing `main.pbs` => fatal compile error. - * `main.pbs` exists but missing `frame` => fatal compile error. - * `frame` exists with wrong signature (params or non-void) => fatal compile error. - -### Acceptance criteria - -* Compiler rejects projects without the entry point with a clear fatal diagnostic. -* Compiler injects `FRAME_SYNC` **only** in `main.pbs::frame(): void`. -* `FRAME_SYNC` is placed **immediately before** `RET` in the entry point epilogue. - ---- - -## PR-01 — Linker: Relocate control-flow jump targets when concatenating modules +# PR-01 — Linker: Relocate control-flow jump targets when concatenating modules ### Briefing diff --git a/test-cartridges/canonical/golden/program.pbc b/test-cartridges/canonical/golden/program.pbc index a079f217..9e80eb11 100644 Binary files a/test-cartridges/canonical/golden/program.pbc and b/test-cartridges/canonical/golden/program.pbc differ diff --git a/test-cartridges/test01/cartridge/program.pbc b/test-cartridges/test01/cartridge/program.pbc index 82c2196b..e9d74882 100644 Binary files a/test-cartridges/test01/cartridge/program.pbc and b/test-cartridges/test01/cartridge/program.pbc differ