Skip to content

Commit 53d046f

Browse files
avrabeclaude
andcommitted
test(meld): add Meld ABI integration tests for import dispatch pipeline
Validates the full Synth→Kiln ABI chain: - Import call generates MOV R0, #idx + BL __meld_dispatch_import - Local calls still generate BL func_N - ELF builder produces .rel.text with R_ARM_CALL relocations - Multiple imports get distinct dispatch indices Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c10d9c4 commit 53d046f

1 file changed

Lines changed: 283 additions & 0 deletions

File tree

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
//! Meld ABI Integration Test
2+
//!
3+
//! Tests the full pipeline for WASM modules with imports:
4+
//! WASM (with imports) → instruction selection (with dispatch stubs)
5+
//! → ARM encoding → ELF with relocations for __meld_dispatch_import
6+
//!
7+
//! This validates the ABI contract between Synth-compiled code and
8+
//! the Kiln runtime's kiln-synth-bridge crate.
9+
10+
use synth_backend::{
11+
ArmEncoder, ArmRelocationType, ElfBuilder, ElfSectionType, Relocation, Section, SectionFlags,
12+
Symbol, SymbolBinding, SymbolType,
13+
};
14+
use synth_core::{ImportEntry, ImportKind};
15+
use synth_synthesis::{ArmOp, InstructionSelector, RuleDatabase, WasmOp};
16+
17+
/// Test that instruction selection generates __meld_dispatch_import stubs
18+
/// for imported function calls.
19+
#[test]
20+
fn test_import_dispatch_stub_generation() {
21+
// WASM ops that call an imported function (index 0)
22+
let wasm_ops = vec![
23+
WasmOp::I32Const(42), // Argument to the import
24+
WasmOp::Call(0), // Call import #0
25+
];
26+
27+
let db = RuleDatabase::with_standard_rules();
28+
let mut selector = InstructionSelector::new(db.rules().to_vec());
29+
selector.set_num_imports(1); // 1 import
30+
31+
let arm_instrs = selector
32+
.select(&wasm_ops)
33+
.expect("instruction selection should succeed");
34+
35+
// Should contain a BL to __meld_dispatch_import
36+
let has_dispatch_bl = arm_instrs.iter().any(|instr| {
37+
matches!(&instr.op, ArmOp::Bl { label } if label == "__meld_dispatch_import")
38+
});
39+
assert!(
40+
has_dispatch_bl,
41+
"expected BL __meld_dispatch_import for import call, got: {:?}",
42+
arm_instrs.iter().map(|i| &i.op).collect::<Vec<_>>()
43+
);
44+
45+
// Should contain MOV R0, #0 (import index)
46+
let has_index_mov = arm_instrs.iter().any(|instr| {
47+
matches!(&instr.op, ArmOp::Mov { rd, op2 }
48+
if matches!(rd, synth_synthesis::Reg::R0)
49+
&& matches!(op2, synth_synthesis::Operand2::Imm(0)))
50+
});
51+
assert!(
52+
has_index_mov,
53+
"expected MOV R0, #0 for import index, got: {:?}",
54+
arm_instrs.iter().map(|i| &i.op).collect::<Vec<_>>()
55+
);
56+
}
57+
58+
/// Test that local function calls (non-import) still generate func_N labels.
59+
#[test]
60+
fn test_local_function_call_not_dispatch() {
61+
// func_idx=1 with 1 import means func_idx >= num_imports → local call
62+
let wasm_ops = vec![WasmOp::Call(1)];
63+
64+
let db = RuleDatabase::with_standard_rules();
65+
let mut selector = InstructionSelector::new(db.rules().to_vec());
66+
selector.set_num_imports(1);
67+
68+
let arm_instrs = selector
69+
.select(&wasm_ops)
70+
.expect("instruction selection should succeed");
71+
72+
let has_local_bl = arm_instrs
73+
.iter()
74+
.any(|instr| matches!(&instr.op, ArmOp::Bl { label } if label == "func_1"));
75+
assert!(
76+
has_local_bl,
77+
"expected BL func_1 for local function call, got: {:?}",
78+
arm_instrs.iter().map(|i| &i.op).collect::<Vec<_>>()
79+
);
80+
81+
// Should NOT have __meld_dispatch_import
82+
let has_dispatch = arm_instrs.iter().any(|instr| {
83+
matches!(&instr.op, ArmOp::Bl { label } if label == "__meld_dispatch_import")
84+
});
85+
assert!(
86+
!has_dispatch,
87+
"local call should not generate __meld_dispatch_import"
88+
);
89+
}
90+
91+
/// Test that ELF builder can add relocations and undefined symbols
92+
/// for the Meld bridge.
93+
#[test]
94+
fn test_elf_relocation_for_meld_dispatch() {
95+
let mut elf_builder = ElfBuilder::new_arm32().with_entry(0x8000);
96+
97+
// Minimal .text section
98+
let code = vec![0u8; 16];
99+
let text = Section::new(".text", ElfSectionType::ProgBits)
100+
.with_flags(SectionFlags::ALLOC | SectionFlags::EXEC)
101+
.with_addr(0x8000)
102+
.with_align(4)
103+
.with_data(code);
104+
elf_builder.add_section(text);
105+
106+
// Add undefined symbol for __meld_dispatch_import
107+
let sym_idx = elf_builder.add_undefined_symbol("__meld_dispatch_import");
108+
assert!(sym_idx > 0, "symbol index should be > 0 (0 is STN_UNDEF)");
109+
110+
// Add relocation for a BL instruction at offset 8 in .text
111+
elf_builder.add_relocation(Relocation {
112+
offset: 8,
113+
symbol_index: sym_idx,
114+
reloc_type: ArmRelocationType::Call,
115+
});
116+
117+
// Build should succeed with relocation
118+
let elf_data = elf_builder.build().expect("ELF build with relocations");
119+
120+
// Validate ELF magic
121+
assert_eq!(&elf_data[0..4], &[0x7f, b'E', b'L', b'F']);
122+
assert_eq!(elf_data[4], 1); // 32-bit
123+
assert_eq!(elf_data[5], 1); // little-endian
124+
125+
// ELF should be larger than without relocations (has .rel.text and symbol)
126+
assert!(
127+
elf_data.len() > 100,
128+
"ELF with relocations should be substantial, got {} bytes",
129+
elf_data.len()
130+
);
131+
}
132+
133+
/// Test the complete Meld ABI pipeline: WASM with imports → ELF with relocations.
134+
#[test]
135+
fn test_full_meld_abi_pipeline() {
136+
// Simulate a WASM module that imports "env.log" and calls it
137+
let imports = vec![ImportEntry {
138+
module: "env".to_string(),
139+
name: "log".to_string(),
140+
kind: ImportKind::Function(0), // type index 0
141+
index: 0,
142+
}];
143+
144+
// WASM function body: call the imported log function with arg 42
145+
let wasm_ops = vec![
146+
WasmOp::I32Const(42),
147+
WasmOp::Call(0), // Call import #0 (env.log)
148+
];
149+
150+
// Step 1: Instruction selection with import awareness
151+
let db = RuleDatabase::with_standard_rules();
152+
let mut selector = InstructionSelector::new(db.rules().to_vec());
153+
selector.set_num_imports(imports.len() as u32);
154+
155+
let arm_instrs = selector
156+
.select(&wasm_ops)
157+
.expect("instruction selection failed");
158+
159+
// Step 2: Encode to ARM machine code
160+
let encoder = ArmEncoder::new_arm32();
161+
let mut code = Vec::new();
162+
let mut bl_offsets = Vec::new();
163+
164+
for instr in &arm_instrs {
165+
if matches!(&instr.op, ArmOp::Bl { label } if label == "__meld_dispatch_import") {
166+
bl_offsets.push(code.len() as u32);
167+
}
168+
let encoded = encoder
169+
.encode(&instr.op)
170+
.expect("ARM encoding failed");
171+
code.extend_from_slice(&encoded);
172+
}
173+
174+
assert!(!code.is_empty(), "should generate machine code");
175+
assert!(
176+
!bl_offsets.is_empty(),
177+
"should have at least one BL to __meld_dispatch_import"
178+
);
179+
180+
// Step 3: Build ELF with relocations
181+
let mut elf_builder = ElfBuilder::new_arm32().with_entry(0x8000);
182+
183+
let text = Section::new(".text", ElfSectionType::ProgBits)
184+
.with_flags(SectionFlags::ALLOC | SectionFlags::EXEC)
185+
.with_addr(0x8000)
186+
.with_align(4)
187+
.with_data(code.clone());
188+
elf_builder.add_section(text);
189+
190+
// Add export symbol
191+
let func_sym = Symbol::new("main")
192+
.with_value(0x8000)
193+
.with_size(code.len() as u32)
194+
.with_binding(SymbolBinding::Global)
195+
.with_type(SymbolType::Func)
196+
.with_section(4);
197+
elf_builder.add_symbol(func_sym);
198+
199+
// Add undefined symbol for the Meld dispatch bridge
200+
let dispatch_sym_idx = elf_builder.add_undefined_symbol("__meld_dispatch_import");
201+
202+
// Add relocations for each BL to __meld_dispatch_import
203+
for offset in &bl_offsets {
204+
elf_builder.add_relocation(Relocation {
205+
offset: *offset,
206+
symbol_index: dispatch_sym_idx,
207+
reloc_type: ArmRelocationType::Call,
208+
});
209+
}
210+
211+
// Build the relocatable ELF
212+
let elf_data = elf_builder.build().expect("ELF build failed");
213+
214+
// Validate
215+
assert_eq!(&elf_data[0..4], &[0x7f, b'E', b'L', b'F']);
216+
217+
// Verify the import metadata round-trips
218+
assert_eq!(imports[0].module, "env");
219+
assert_eq!(imports[0].name, "log");
220+
assert!(matches!(imports[0].kind, ImportKind::Function(_)));
221+
222+
println!(" Meld ABI pipeline test passed:");
223+
println!(" - {} import(s) declared", imports.len());
224+
println!(" - {} ARM instructions generated", arm_instrs.len());
225+
println!(" - {} bytes of machine code", code.len());
226+
println!(" - {} relocation(s) for __meld_dispatch_import", bl_offsets.len());
227+
println!(" - {} byte ELF with .rel.text section", elf_data.len());
228+
}
229+
230+
/// Test multiple imports generate distinct dispatch calls.
231+
#[test]
232+
fn test_multiple_import_dispatch() {
233+
// Two imports: env.log (index 0) and env.read (index 1)
234+
let wasm_ops = vec![
235+
WasmOp::I32Const(1),
236+
WasmOp::Call(0), // Call import #0
237+
WasmOp::I32Const(2),
238+
WasmOp::Call(1), // Call import #1
239+
];
240+
241+
let db = RuleDatabase::with_standard_rules();
242+
let mut selector = InstructionSelector::new(db.rules().to_vec());
243+
selector.set_num_imports(2);
244+
245+
let arm_instrs = selector
246+
.select(&wasm_ops)
247+
.expect("instruction selection failed");
248+
249+
// Count BL __meld_dispatch_import occurrences
250+
let dispatch_count = arm_instrs
251+
.iter()
252+
.filter(|i| matches!(&i.op, ArmOp::Bl { label } if label == "__meld_dispatch_import"))
253+
.count();
254+
assert_eq!(
255+
dispatch_count, 2,
256+
"expected 2 dispatch calls, got {}",
257+
dispatch_count
258+
);
259+
260+
// Should have MOV R0, #0 and MOV R0, #1 for the two import indices
261+
let mov_indices: Vec<i32> = arm_instrs
262+
.iter()
263+
.filter_map(|i| match &i.op {
264+
ArmOp::Mov { rd, op2 }
265+
if matches!(rd, synth_synthesis::Reg::R0)
266+
&& matches!(op2, synth_synthesis::Operand2::Imm(_)) =>
267+
{
268+
if let synth_synthesis::Operand2::Imm(val) = op2 {
269+
Some(*val)
270+
} else {
271+
None
272+
}
273+
}
274+
_ => None,
275+
})
276+
.collect();
277+
278+
assert!(
279+
mov_indices.contains(&0) && mov_indices.contains(&1),
280+
"expected MOV R0 with indices 0 and 1, got {:?}",
281+
mov_indices
282+
);
283+
}

0 commit comments

Comments
 (0)