#317

Direct backend rejects record locals/fields from a call result (BLD004)

OpenPeterXMRopened 5mo ago

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 BLD004

Same failure across all 8 targets listed in bin/zero --version --json:

  • darwin-arm64, darwin-x64
  • linux-musl-x64, linux-musl-arm64, linux-x64, linux-arm64
  • win32-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 assertDirectRuntimeOrUnsupported conformance 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

  1. Generalize the existing record-lowering infrastructure used for Maybe<X> and Span<X> to handle user-defined type declarations.
  2. 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).

Activity

1
PeterXMR5mo ago

Re-audit against current main (51041a5, v0.2.0)

The original repro used the removed .row syntax; 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:

type Point { x: i32, }
pub fn main(world: World) -> Void raises {
    let p: Point = Point { x: 1 }
    if p.x == 1 { check world.out.write("ok\n") }
}

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.

type Point { x: i32, }
fn make_point() -> Point { return Point { x: 1 } }
pub fn main(world: World) -> Void raises {
    let p: Point = make_point()
    if p.x == 1 { check world.out.write("ok\n") }
}

zero check → ok. zero build fails on all direct targets (darwin-arm64, darwin-x64, linux-arm64):

:4:5 BLD004: direct backend record locals require shape literal initialization
  expected: direct AArch64 Mach-O object subset
  actual: p

Trying to avoid the local by using the field inline hits a different direct-backend gap:

if make_point().x == 1 { ... }
:4:21 BLD004: direct backend member access supports Maybe<MutSpan<u8>> .has and .value

Updated summary

  • Resolved/narrowed: record locals from shape literals now build; the original "rejects record return types" framing is no longer accurate (the return itself is fine).
  • Still open: the direct backend cannot bind a record local to a call result (requires a shape-literal initializer), nor access a field on a call-returned record. Consistent across all three direct targets, not darwin-arm64-only.
  • check passes in every case; the gap is purely in direct-backend lowering.

Retitling to match the current behavior.