Skip to content

Commit 21b3694

Browse files
committed
fix: LLVM backend string format and void function returns
- Emit strings in Haxe format (length prefix + bytes) instead of C-style null-terminated strings to match Cranelift backend and runtime expectations - Handle HirValueKind::Undef and Global in value compilation - For void-returning functions, emit proper return instead of unreachable instruction (which generates SIGTRAP) matching Cranelift behavior - Add globals_map to persist global references across function compilation - ForLoopTest now works correctly with both LLVM and Cranelift backends
1 parent 6e203cf commit 21b3694

2 files changed

Lines changed: 146 additions & 8 deletions

File tree

crates/compiler/src/llvm_backend.rs

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ pub struct LLVMBackend<'ctx> {
7171

7272
/// Current function being compiled (for accessing locals, blocks, etc.)
7373
current_function: Option<FunctionValue<'ctx>>,
74+
75+
/// Maps HIR global IDs to compiled LLVM global values (persists across functions)
76+
globals_map: IndexMap<HirId, BasicValueEnum<'ctx>>,
7477
}
7578

7679
impl<'ctx> LLVMBackend<'ctx> {
@@ -93,6 +96,7 @@ impl<'ctx> LLVMBackend<'ctx> {
9396
block_map: IndexMap::new(),
9497
phi_map: IndexMap::new(),
9598
current_function: None,
99+
globals_map: IndexMap::new(),
96100
}
97101
}
98102

@@ -259,6 +263,20 @@ impl<'ctx> LLVMBackend<'ctx> {
259263
let undef_value = llvm_type.const_zero(); // or use undef if available
260264
self.value_map.insert(*value_id, undef_value);
261265
}
266+
HirValueKind::Undef => {
267+
// Map undef values to zero constants (for IDF-based SSA)
268+
// This handles void-returning function calls where SSA creates undef placeholders
269+
let llvm_type = self.translate_type(&value.ty)?;
270+
let undef_value = llvm_type.const_zero();
271+
self.value_map.insert(*value_id, undef_value);
272+
}
273+
HirValueKind::Global(global_id) => {
274+
// Map global references to their LLVM global values
275+
// The global should have been compiled in phase 1
276+
if let Some(&global_value) = self.globals_map.get(global_id) {
277+
self.value_map.insert(*value_id, global_value);
278+
}
279+
}
262280
_ => {}
263281
}
264282
}
@@ -325,12 +343,48 @@ impl<'ctx> LLVMBackend<'ctx> {
325343
/// This creates LLVM global variables with appropriate linkage and initializers.
326344
/// For vtables, the initializer contains an array of function pointers.
327345
fn compile_global(&mut self, id: HirId, global: &HirGlobal) -> CompilerResult<()> {
328-
// Translate the global's type to LLVM type
329-
let llvm_ty = self.translate_type(&global.ty)?;
330-
331346
// Create unique name for the global
332347
let global_name = format!("global__{:?}", id);
333348

349+
// Handle string constants specially - emit in Haxe String format: [length: i32][utf8_bytes...]
350+
// This matches the Cranelift backend format so runtime functions work correctly
351+
if let Some(HirConstant::String(s)) = &global.initializer {
352+
let actual_string = s.resolve_global().unwrap_or_else(|| {
353+
log::warn!("Could not resolve InternedString for global, using empty string");
354+
std::string::String::new()
355+
});
356+
357+
// Get UTF-8 bytes
358+
let bytes = actual_string.as_bytes();
359+
let length = bytes.len() as i32;
360+
361+
// Create Haxe String structure: [length: i32][utf8_bytes...]
362+
// The struct is { i32, [N x i8] }
363+
let i32_type = self.context.i32_type();
364+
let byte_array_type = self.context.i8_type().array_type(bytes.len() as u32);
365+
let haxe_string_type = self.context.struct_type(&[i32_type.into(), byte_array_type.into()], false);
366+
367+
// Create the length constant
368+
let length_const = i32_type.const_int(length as u64, false);
369+
370+
// Create the byte array constant (no null terminator needed)
371+
let byte_const = self.context.const_string(bytes, false);
372+
373+
// Create the struct constant
374+
let haxe_string_const = haxe_string_type.const_named_struct(&[length_const.into(), byte_const.into()]);
375+
376+
let global_value = self.module.add_global(haxe_string_type, Some(AddressSpace::default()), &global_name);
377+
global_value.set_linkage(inkwell::module::Linkage::External);
378+
global_value.set_initializer(&haxe_string_const);
379+
380+
// Store the pointer to the global (address of the Haxe string struct)
381+
self.globals_map.insert(id, global_value.as_pointer_value().into());
382+
return Ok(());
383+
}
384+
385+
// Translate the global's type to LLVM type
386+
let llvm_ty = self.translate_type(&global.ty)?;
387+
334388
// Add global variable to module
335389
let global_value = self.module.add_global(llvm_ty, Some(AddressSpace::default()), &global_name);
336390

@@ -376,8 +430,8 @@ impl<'ctx> LLVMBackend<'ctx> {
376430
global_value.set_initializer(&llvm_ty.const_zero());
377431
}
378432

379-
// Store the global in value_map so it can be referenced
380-
self.value_map.insert(id, global_value.as_pointer_value().into());
433+
// Store the global in globals_map so it can be referenced across functions
434+
self.globals_map.insert(id, global_value.as_pointer_value().into());
381435

382436
Ok(())
383437
}
@@ -547,7 +601,32 @@ impl<'ctx> LLVMBackend<'ctx> {
547601
}
548602

549603
HirTerminator::Unreachable => {
550-
self.builder.build_unreachable()?;
604+
// For void-returning functions, emit a return instead of unreachable/trap
605+
// This handles Haxe/other languages where main() returns Void and has no explicit return
606+
if let Some(func) = self.current_function {
607+
let return_type = func.get_type().get_return_type();
608+
if return_type.is_none() {
609+
// Void function - emit return
610+
self.builder.build_return(None)?;
611+
} else if let Some(ty) = return_type {
612+
// Check if it's an empty struct (our representation of Unit/Void type)
613+
if let inkwell::types::BasicTypeEnum::StructType(st) = ty {
614+
if st.count_fields() == 0 {
615+
// Empty struct (Unit) - emit return with undef value
616+
let ret_val = st.get_undef();
617+
self.builder.build_return(Some(&ret_val))?;
618+
} else {
619+
self.builder.build_unreachable()?;
620+
}
621+
} else {
622+
self.builder.build_unreachable()?;
623+
}
624+
} else {
625+
self.builder.build_unreachable()?;
626+
}
627+
} else {
628+
self.builder.build_unreachable()?;
629+
}
551630
}
552631

553632
HirTerminator::PatternMatch { value, patterns, default } => {
@@ -2725,7 +2804,12 @@ impl<'ctx> LLVMBackend<'ctx> {
27252804

27262805
// String constant
27272806
String(s) => {
2728-
let string_value = self.context.const_string(s.to_string().as_bytes(), true);
2807+
// Resolve the InternedString to get the actual string value
2808+
let actual_string = s.resolve_global().unwrap_or_else(|| {
2809+
log::warn!("Could not resolve InternedString, using empty string");
2810+
std::string::String::new()
2811+
});
2812+
let string_value = self.context.const_string(actual_string.as_bytes(), true);
27292813
string_value.into()
27302814
}
27312815

reflaxe.zyntax/src/zyntax/Generator.hx

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ package zyntax;
44

55
import haxe.Json;
66
import haxe.macro.Expr.Position;
7+
using Lambda;
8+
using StringTools;
9+
10+
// Track runtime functions encountered during generation
11+
// Maps function name -> {params: [...], return_type: ...}
12+
private var runtimeFunctions: Map<String, {params: Array<AST.ZType>, returnType: AST.ZType}> = new Map();
713

814
/**
915
* Convert Haxe Position to span {start, end}
@@ -25,6 +31,9 @@ function posToSpan(pos: Position): Dynamic {
2531
* Generate Zyntax TypedAST JSON from Module
2632
*/
2733
function generateModule(m: AST.Module): Null<String> {
34+
// Reset runtime functions tracking for each module
35+
runtimeFunctions = new Map();
36+
2837
var declarations = [];
2938

3039
for (func in m.functions) {
@@ -46,7 +55,7 @@ function generateModule(m: AST.Module): Null<String> {
4655
visibility: "Public",
4756
is_async: false,
4857
is_external: func.is_external,
49-
calling_convention: func.is_external ? "C" : "Rust",
58+
calling_convention: func.is_external ? "Cdecl" : "Rust",
5059
link_name: func.link_name
5160
}
5261
},
@@ -56,6 +65,38 @@ function generateModule(m: AST.Module): Null<String> {
5665
declarations.push(funcDecl);
5766
}
5867

68+
// Emit external declarations for all runtime functions encountered
69+
for (funcName => funcInfo in runtimeFunctions) {
70+
var params = funcInfo.params.mapi((i, p) -> {
71+
name: "arg" + i,
72+
ty: generateType(p),
73+
mutability: "Immutable",
74+
kind: "Regular",
75+
default_value: null,
76+
attributes: [],
77+
span: {start: 0, end: 0}
78+
});
79+
80+
var externDecl = {
81+
node: {
82+
Function: {
83+
name: funcName,
84+
params: params,
85+
return_type: generateType(funcInfo.returnType),
86+
body: null, // External functions have no body
87+
visibility: "Public",
88+
is_async: false,
89+
is_external: true,
90+
calling_convention: "Cdecl",
91+
link_name: funcName // Use the $ prefixed name as link name
92+
}
93+
},
94+
ty: {Primitive: "Unit"},
95+
span: {start: 0, end: 0}
96+
};
97+
declarations.push(externDecl);
98+
}
99+
59100
var program = {
60101
declarations: declarations,
61102
span: {start: 0, end: 0}
@@ -280,6 +321,19 @@ function generateExpression(exprWithPos: AST.ExprWithPos): Dynamic {
280321
};
281322

282323
case Call(callee, args):
324+
// Track runtime functions (those starting with $)
325+
switch(callee.expr) {
326+
case Variable(name) if (StringTools.startsWith(name, "$")):
327+
// Extract function type from callee
328+
switch(callee.ty) {
329+
case Function(paramTypes, returnType):
330+
if (!runtimeFunctions.exists(name)) {
331+
runtimeFunctions.set(name, {params: paramTypes, returnType: returnType});
332+
}
333+
default:
334+
}
335+
default:
336+
}
283337
{
284338
node: {
285339
Call: {

0 commit comments

Comments
 (0)