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 TYP003Same 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.readBytesetc. →IR_VALUE_FS_READ_BYTES_PATHwithIR_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)
| Approach | Result |
|---|---|
ret 42_i64 from Maybe<i64> fn | TYP003 (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 fn | BLD004: direct backend call return type unsupported (separate gap, filing alongside) |
Generic fn some<T: Type>(value T) Maybe<T> / ret value | TYP003 (same root cause as the first row) |
Possible fix paths
- Source-level constructor. Add a
Maybe.some(value)helper or(value as Maybe<T>)coercion accepted by the checker. - Implicit wrap. Extend the current implicit-wrap behavior (which works for
Span<u8> → Maybe<String>) to primitives. - IR intrinsic helper in std. A
fn std.some<T: Type>(value T) Maybe<T>implemented likenone<T>is today (ret nullworks becausenulltyping 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).
Re-audit against current
main(51041a5, v0.2.0)Still reproduces. The original repro used the removed
.rowsyntax — here it is in canonical.0:Same
TYP003for everyMaybe<primitive>payload — confirmed onmainforMaybe<u8>(←7) andMaybe<Bool>(←true).The asymmetry
Byte-view payloads already implicitly wrap, but primitives don't:
So source-backed functions can return
Maybe<String>by wrapping aSpan<u8>, but cannot construct a non-nullMaybe<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 theseTYP003s.