fixed for jump control flow
This commit is contained in:
parent
c68b300d5e
commit
937e0d70b6
@ -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<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 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user