Part of #163
Description
When println is called with an array argument, the QBE backend passes the raw array pointer to _printf, which interprets it as a char*. This produces no visible output (or garbage), since the array's memory layout starts with a length header followed by raw element bytes.
The JS backend handles this correctly because JavaScript natively converts arrays to strings.
Reproduction
cargo run -- -t qbe run examples/bubblesort.sb
Expected output (from JS backend):
Actual output (QBE backend):
Problem
The QBE code generator emits println(arr) as a direct call to the string-based println implementation. There is no compile-time or runtime dispatch to handle non-string types like arrays. The C builtins (_printf, _str_concat) only operate on char*.
Research: how other compiled languages handle this
Before choosing an approach, it would be useful to survey how other languages with similar constraints solve this:
- Go:
fmt.Println uses runtime reflection (reflect package) to inspect the type and recursively format values. Arrays/slices are formatted as [1 2 3].
- Rust:
println! is a compile-time macro that expands based on the Display or Debug trait implementation. Vec<T> implements Debug which formats as [1, 2, 3]. The formatting code is generated at compile time per concrete type.
- Zig:
std.debug.print uses comptime type introspection (@typeInfo) to generate specialized formatting code at compile time for any type including arrays.
- C: No built-in array printing. Users must write explicit loops.
- Odin:
fmt.println uses runtime type info (RTTI) attached to every value to dispatch formatting.
Possible implementation approaches
1. Compile-time specialization (Rust/Zig-style)
Generate different code paths for println depending on the argument's AST type at compile time. When the argument is an array, emit a loop that converts each element to a string and joins them.
Pros: No runtime overhead, no C builtin changes needed
Cons: More complex code generation, println becomes a pseudo-generic rather than a simple function call
2. Runtime type-tagged values (Go/Odin-style)
Add a type tag to the array header (or pass a type tag as a hidden argument) and implement array formatting in the C builtins.
Pros: Simpler code generation, extensible to other types (structs, nested arrays)
Cons: Runtime overhead, requires changing the value representation or calling convention
3. Separate print_array builtin
Add a dedicated print_array(arr, elem_type) C builtin and have the compiler emit calls to it when it detects an array argument to println.
Pros: Minimal changes to existing code, straightforward
Cons: Doesn't scale well to nested types, special-cases one type
4. to_string method / trait
Implement a to_string conversion that the compiler inserts before println. Each type gets a _array_to_str, _int_to_str, etc. function, and the compiler chains them.
Pros: Composable, reusable beyond println
Cons: Requires broader type-aware code generation
Discussion
Which approach best fits Antimony's philosophy? Approach 1 (compile-time specialization) seems most aligned with the current architecture — the compiler already knows types at compile time and could emit the right code without runtime overhead. But approach 3 would be the quickest to implement as a first step.
Thoughts welcome.
Part of #163
Description
When
printlnis called with an array argument, the QBE backend passes the raw array pointer to_printf, which interprets it as achar*. This produces no visible output (or garbage), since the array's memory layout starts with a length header followed by raw element bytes.The JS backend handles this correctly because JavaScript natively converts arrays to strings.
Reproduction
Expected output (from JS backend):
Actual output (QBE backend):
Problem
The QBE code generator emits
println(arr)as a direct call to the string-basedprintlnimplementation. There is no compile-time or runtime dispatch to handle non-string types like arrays. The C builtins (_printf,_str_concat) only operate onchar*.Research: how other compiled languages handle this
Before choosing an approach, it would be useful to survey how other languages with similar constraints solve this:
fmt.Printlnuses runtime reflection (reflectpackage) to inspect the type and recursively format values. Arrays/slices are formatted as[1 2 3].println!is a compile-time macro that expands based on theDisplayorDebugtrait implementation.Vec<T>implementsDebugwhich formats as[1, 2, 3]. The formatting code is generated at compile time per concrete type.std.debug.printuses comptime type introspection (@typeInfo) to generate specialized formatting code at compile time for any type including arrays.fmt.printlnuses runtime type info (RTTI) attached to every value to dispatch formatting.Possible implementation approaches
1. Compile-time specialization (Rust/Zig-style)
Generate different code paths for
printlndepending on the argument's AST type at compile time. When the argument is an array, emit a loop that converts each element to a string and joins them.Pros: No runtime overhead, no C builtin changes needed
Cons: More complex code generation,
printlnbecomes a pseudo-generic rather than a simple function call2. Runtime type-tagged values (Go/Odin-style)
Add a type tag to the array header (or pass a type tag as a hidden argument) and implement array formatting in the C builtins.
Pros: Simpler code generation, extensible to other types (structs, nested arrays)
Cons: Runtime overhead, requires changing the value representation or calling convention
3. Separate
print_arraybuiltinAdd a dedicated
print_array(arr, elem_type)C builtin and have the compiler emit calls to it when it detects an array argument toprintln.Pros: Minimal changes to existing code, straightforward
Cons: Doesn't scale well to nested types, special-cases one type
4.
to_stringmethod / traitImplement a
to_stringconversion that the compiler inserts beforeprintln. Each type gets a_array_to_str,_int_to_str, etc. function, and the compiler chains them.Pros: Composable, reusable beyond println
Cons: Requires broader type-aware code generation
Discussion
Which approach best fits Antimony's philosophy? Approach 1 (compile-time specialization) seems most aligned with the current architecture — the compiler already knows types at compile time and could emit the right code without runtime overhead. But approach 3 would be the quickest to implement as a first step.
Thoughts welcome.