#316

Source-backed Zero cannot construct non-null Maybe<primitive>

OpenPeterXMRopened 5mo ago

Reproducer

fn maybe_42 Maybe<i64>
  ret 42_i64

pub fn main Void world World !
  let x maybe_42()
  if x.has
    check world.out.write "ok\n"
$ bin/zero check repro.0
repro.0:2:3 TYP003: return type does not match function return type
  expected: Maybe<i64>
  actual: i64
  help: return a value compatible with the function signature
  explain: zero explain TYP003

Same error for Maybe<i32>, Maybe<u32>, Maybe<u8>, Maybe<usize>, Maybe<Bool>, and every other Maybe<primitive>.

Why it matters

C-implemented stdlib helpers construct Maybe<primitive> directly via custom IR nodes:

  • std.parse.parseU8/U16/U32 → ir_new_maybe_scalar_literal (native/zero-c/src/ir.c:1582)
  • std.fs.readBytes etc. → IR_VALUE_FS_READ_BYTES_PATH with IR_TYPE_MAYBE_SCALAR (native/zero-c/src/ir.c:2128)

Source-backed stdlib modules (std/math.0, std/path.0, std/str.0) have no equivalent path. The result: any contributor writing a source-backed fallible-primitive helper has to invent a per-module workaround shape with .has/.value fields, polluting the stdlib namespace and rejecting the Maybe<T> caller convention.

It's also the structural reason why no existing source-backed module returns Maybe<primitive> — std.math is all u32/Bool, std.path and std.str return Maybe<Span/String> (which auto-wraps for those types).

Workarounds I ruled out while prototyping std.decimal (PR #308, now closed)

ApproachResult
ret 42_i64 from Maybe<i64> fnTYP003 (this issue)
ret Maybe<i64> . has true value 42_i64 (shape literal)TYP005: unknown shape literal
Per-module pub type Result { has Bool, value i64 } returned from fnBLD004: direct backend call return type unsupported (separate gap, filing alongside)
Generic fn some<T: Type>(value T) Maybe<T> / ret valueTYP003 (same root cause as the first row)

Possible fix paths

  1. Source-level constructor. Add a Maybe.some(value) helper or (value as Maybe<T>) coercion accepted by the checker.
  2. Implicit wrap. Extend the current implicit-wrap behavior (which works for Span<u8> → Maybe<String>) to primitives.
  3. IR intrinsic helper in std. A fn std.some<T: Type>(value T) Maybe<T> implemented like none<T> is today (ret null works because null typing flows from the return type).

Option 1 or 3 is minimal; option 2 changes existing semantics.

Compiler version

v0.1.4 (bin/zero --version --json).

Activity

1
PeterXMR5mo ago

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

Still reproduces. The original repro used the removed .row syntax — here it is in canonical .0:

fn maybe_42() -> Maybe<i64> {
    return 42
}
pub fn main(world: World) -> Void raises {
    let x: Maybe<i64> = maybe_42()
    if x.has {
        check world.out.write("ok\n")
    }
}
$ zero check repro.0
repro.0:2:5 TYP003: return type does not match function return type
  expected: Maybe<i64>
  actual: i32

Same TYP003 for every Maybe<primitive> payload — confirmed on main for Maybe<u8> (← 7) and Maybe<Bool> (← true).

The asymmetry

Byte-view payloads already implicitly wrap, but primitives don't:

fn g() -> Maybe<String> { return std.mem.span("hi") }   // ✅ check ok — Span<u8> auto-wraps
fn f() -> Maybe<i64>    { return 42 }                    // 🔴 TYP003 — primitive does not

So source-backed functions can return Maybe<String> by wrapping a Span<u8>, but cannot construct a non-null Maybe<primitive> the same way — the gap this issue describes.

Proposed fix

Open PR: #325 — extends the existing byte-view implicit-wrap to primitive payloads so source-backed functions can return Maybe<i64>/Maybe<u8>/Maybe<Bool> etc., eliminating these TYP003s.