fixed for jump control flow

This commit is contained in:
bQUARKz 2026-02-08 12:10:43 +00:00
parent c68b300d5e
commit 937e0d70b6
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
4 changed files with 94 additions and 62 deletions

View File

@ -172,6 +172,15 @@ impl Linker {
// Internal call relocation (from module-local func_idx to global func_idx) // Internal call relocation (from module-local func_idx to global func_idx)
// And PUSH_CONST relocation. // 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<u8>, 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 mut pos = code_offset;
let end = code_offset + module.code.len(); let end = code_offset + module.code.len();
while pos < end { while pos < end {
@ -212,6 +221,14 @@ impl Linker {
pos += 4; 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::PushI32 | OpCode::PushBounded | OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue
| OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal | OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal
| OpCode::PopN | OpCode::Syscall | OpCode::GateLoad | OpCode::GateStore => { | 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[1], Value::String("hello".into()));
assert_eq!(result.constant_pool[2], Value::Int32(99)); 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);
}
} }

View File

@ -19,68 +19,7 @@
--- ---
## PR-00 — Compiler: Enforce PBS entry point and inject FRAME_SYNC in main.pbs::frame() # PR-01 — Linker: Relocate control-flow jump targets when concatenating modules
### 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
### Briefing ### Briefing