Reproducer
type Point
x i32
fn make_point Point
let p Point . x 1
ret p
pub fn main Void world World !
let p make_point()
if == p.x 1
check world.out.write "ok\n"bin/zero check passes silently. bin/zero build --target darwin-arm64 --out /tmp/p:
BLD004: direct backend call return type is unsupported
expected: direct AArch64 Mach-O object subset
actual: Point
help: choose a supported direct target or reduce the program to supported AArch64 direct-backend constructs
explain: zero explain BLD004Same failure across all 8 targets listed in bin/zero --version --json:
darwin-arm64,darwin-x64linux-musl-x64,linux-musl-arm64,linux-x64,linux-arm64win32-x64.exe,win32-arm64.exe
Why it matters
Source-backed stdlib (and user code) can't return any user-defined type from a function and have it lower to the direct backends. Built-in record types — Maybe<String>, Maybe<Span<u8>>, etc. — work fine via specialized backend lowering. User-defined records don't have an equivalent path.
This blocks:
- Source-backed stdlib helpers that want to return typed handles (e.g., a hypothetical
Iterator<T>,Decoder, or fallibility wrapper) - User code that wants to return small value types from functions
- The
assertDirectRuntimeOrUnsupportedconformance path silently swallows this — so existing fixtures touching user records don't actually exercise runtime behavior; they only typecheck
I hit this while prototyping std.decimal (PR #308, now closed). The workaround there used a per-module DecimalResult { has Bool, value i64 } shape which type-checked cleanly but couldn't run on any backend.
Related but distinct gap
mutref<UserRecord> parameters hit a sibling restriction:
BLD004: direct backend call parameter type is unsupported
expected: direct AArch64 Mach-O object subset
actual: mutref<Point>Listed here for completeness; may or may not share the fix.
Possible fix paths
- Generalize the existing record-lowering infrastructure used for
Maybe<X>andSpan<X>to handle user-definedtypedeclarations. - Restrict to plain-by-value records under a size budget (e.g., ≤ 16 bytes / 2 registers) — covers the common cases without ABI complexity.
Both are non-trivial; this is the kind of language work that's clearly maintainer-scope, not contributor-scope.
Compiler version
v0.1.4 (bin/zero --version --json).
Re-audit against current
main(51041a5, v0.2.0)The original repro used the removed
.rowsyntax; re-tested in canonical.0. The limitation still exists but has narrowed and shifted, and is no longer darwin-arm64-specific.What works now
A record local initialized from a shape literal builds cleanly on the direct backend:
zero build --target darwin-arm64 --emit obj→ OK.What still fails — consuming a call-returned record
Returning a record from a function is accepted; the rejection is at the call site that stores or uses the result.
zero check→ok.zero buildfails on all direct targets (darwin-arm64, darwin-x64, linux-arm64):Trying to avoid the local by using the field inline hits a different direct-backend gap:
Updated summary
checkpasses in every case; the gap is purely in direct-backend lowering.Retitling to match the current behavior.