Commit Graph

508 Commits

Author SHA1 Message Date
agra
f96bcc4fe4 ERR: use the catch match-body form in examples/235
The preceding parser fix (parenthesized match-arm value vs payload capture)
fully enables `catch e == { case .X: (tuple) }` — both scalar and tuple arm
values. Tuple literals in statement/binding position already worked, so the
match-body form runs end-to-end.

Add a `classify` to examples/235 exercising multi-value catch match-body with
per-tag value-tuple arms; exit 164 -> 170. Regenerate the snapshot.

(Corrects an earlier note that wrongly claimed a separate "issue 0059" blocked
the tuple match-body form — no such issue exists; the capture-parse bug was the
whole problem.)

Gates: zig build, zig build test, 273/273 examples.
2026-06-01 00:16:39 +03:00
agra
d4b1248f65 parser: parenthesized match-arm value vs payload capture
A match arm `case PAT: (expr)` — e.g. `case 0: (5)` — failed to parse:
parseMatchBody unconditionally consumed an `(` after `case PAT:` as a
payload-capture `(ident)`, so a non-identifier first token produced
"expected capture name".

Disambiguate: treat `(` as a capture only when it encloses exactly a lone
identifier — `( ident )` — via a new isLoneIdentParen() helper (peekTag-based
two-token lookahead). Otherwise the parens belong to the arm-body expression.
Payload capture (`case .b: (v) { ... }`, examples/128) still binds.

This fixes the scalar paren arm value (`case 0: (5)` now parses and runs).
The tuple arm-value form (`case .X: (a, b)`) additionally needs a tuple
literal in statement/binding position, tracked separately as issue 0059.

Tests: two inline parser unit tests (paren arm value is not a capture; lone
`(ident)` still binds). Gates: zig build, zig build test, 273/273 examples.
2026-06-01 00:12:11 +03:00
agra
ae330365b4 ERR/E2: multi-value failables -> (T1, ..., !)
Generalize the single-value `-> (T, !)` error-channel ABI to any value
arity. Retire the five `fields.len == 2` bails (lowerFailableSuccessReturn,
lowerTry, lowerCatch, lowerFailableOr, and the inferExprType try/catch/or
arms); lowerRaise + emitErrorReturn already looped over N value slots.

New helpers centralize "value-part = every slot but the last (error) one":
failableSuccessType (lone value type, or a value-tuple), extractSuccessValue,
extractErrorSlot.

Fix one latent bug the feature surfaced: coerceToType had no tuple->tuple
arm, so a value-tuple flowing into a differently-typed success slot (e.g.
(s64,s64) catch body into (s32,s32)) fell through unchanged. Add element-wise
coercion. No lowerTupleLiteral change is needed: a `return (a, b)` literal
against a 3-field failable target already gets target_fields=null via the
arity mismatch, so it types as a plain value-tuple that
lowerFailableSuccessReturn consumes.

examples/235-multi-value-failable.sx exercises producer return/raise,
destructure (binding every slot incl. the error tag), multi-value try
(success + propagation), catch (bare-expr tuple body), and or-tuple
terminator. Match-body tuple arms are left out: `(` after `case PAT:` is
parsed as a payload capture (a pre-existing, multi-value-unrelated parser
bug). Gates: zig build, zig build test, 273/273 examples.
2026-05-31 23:32:16 +03:00
agra
57d8e327cd ERR/E1.7: onfail — cleanup on error-exit, interleaved with defer
`onfail [e] BODY` runs cleanup only when an error LEAVES the enclosing block
(a `raise` or a propagating `try`), and is skipped on success — unlike `defer`,
which runs on every exit. On an error exit, defers and onfails run interleaved
in reverse declaration order; `onfail e` binds the in-flight error tag.

- Cleanup stack: defer_stack now holds CleanupEntry { body, is_onfail, binding }
  (one declaration-ordered stack so defer/onfail interleave). lowerDefer pushes
  a defer entry; lowerOnFail (new `.onfail_stmt` arm) pushes an onfail entry,
  rejecting `onfail` outside a failable function.
- emitBlockDefers (success exits — return / normal block exit) now emits only
  `defer` entries and discards onfails.
- emitErrorCleanup (new; wired at the error exits — lowerRaise pure +
  value-carrying, lowerTry propagation) emits both kinds interleaved in reverse,
  binding the in-flight tag for `onfail e`.

Block-rooted: an error propagating to the function drains all enclosing blocks'
onfails; a block that exits normally discards its onfails. Per-attempt-`try`
gating is moot for now (no compilable `or` chain can absorb a mid-block try
failure yet — E2.4b). Body restrictions beyond the parser's raise-in-onfail
ban are deferred.

Tests: examples/233-onfail.sx (interleave order on error vs success + binding;
deterministic trace), examples/234-onfail-reject.sx (onfail outside a failable
fn rejected; exit 1). Gates: zig build, zig build test, 272/272 examples.
2026-05-31 22:29:40 +03:00
agra
50e5515080 ERR/E2.4a: failable or value-terminator
`lhs or value` where `lhs` is a value-carrying failable (`-> (T, !E)`): on
success the result is the LHS value, on failure the LHS error is discarded and
the result is the terminator value — the whole expression is non-failable (T).
Unblocked by the value ABI (E2.1); needs no fallback-routing (it's a 2-operand,
non-chained `or`).

- lowerBinaryOp `.or_op`: a failable LHS now routes to lowerFailableOr instead
  of the E1.4a loud bail; non-failable `or` (boolean / optional-unwrap)
  unchanged.
- lowerFailableOr: chain form (a `try`-marked LHS, whose own type is its
  success value, or a failable RHS) bails → E2.4b (fallback routing). Pure
  failable `or value` rejected ("no success value to fall back to — use
  catch"). Value-carrying: tuple_get the value/error, condBr, merge the LHS
  value (success) or the terminator (failure) through a block-param phi.
  Multi-value bails (E2).
- inferExprType `.or_op`: a failable `or value` types as the LHS success type
  (was always `.bool`); non-failable `or` still `.bool`.

Tests: examples/231-failable-or.sx (success + Bad + Empty terminators; exit
116), examples/232-failable-or-reject.sx (pure-failable `or value` rejected;
exit 1). Gates: zig build, zig build test, 270/270 examples.
2026-05-31 22:16:28 +03:00
agra
a049e2940c ERR/E2.1b: value-carrying failable consumers (try / catch)
The consumer side of the error-channel tuple ABI. A value-carrying `-> (T, !E)`
failable can now be consumed by `try` and `catch` (not just destructured).
Single-value; multi-value `-> (T1, T2, !)` consumers bail (E2).

- lowerTry: a value-carrying callee returns `{v, err}`. Extract `err`
  (tuple_get field 1), branch; on success the try value is `tuple_get(field 0)`,
  on error propagate via emitErrorReturn (pure caller → `ret(tag)`;
  value-carrying caller → `ret {undef..., tag}`). Widening now runs for
  value-carrying callees too. Retires the two value-carrying bails.
- lowerCatch: a value-carrying LHS merges through a block-param phi — the
  success edge feeds `tuple_get(field 0)`, the handler edge feeds the body's
  value (coerced to the success type). runCatchBody factors the bound-tag body
  lowering (force_block_value for the value case). Pure-failable catch
  unchanged.
- A non-diverging value-carrying catch body that yields no value is now a
  clean diagnostic ("`catch` body must produce a value … or diverge") instead
  of coercing `void` into a bad ref / failing LLVM verification — caught by an
  adversarial review of the lowering.

Tests: examples/229-value-failable-consume.sx (try in value-carrying + pure
callers, catch block/bare/match-body/diverging bodies; exit 32),
examples/230-value-failable-reject.sx (void catch body rejected; exit 1).
Gates: zig build, zig build test, 268/268 examples.
2026-05-31 22:05:44 +03:00
agra
17c19d5d30 ERR/E2.1a: value-carrying failable producer (return value + raise → tuple ABI)
The producer side of the error-channel tuple ABI for value-carrying `-> (T, !)`
functions. A failable that returns a value OR an error now lowers correctly;
the result is consumed via destructure (`v, err := f()`). Single-value
`-> (T, !)`; multi-value `-> (T1, T2, !)` and the value-carrying try/catch
consumers (E2.1b) follow.

- lowerReturn: a value-carrying failable's `return v;` assembles the success
  tuple `{v, 0}` (compiler appends the no-error slot) via lowerFailableSuccessReturn
  (tuple_init). Forwarding a full failable tuple (`return other_failable()` /
  explicit `return (v, e)`) returns as-is. Multi-value returns bail loudly (E2).
- lowerRaise: the value-carrying branch (previously a loud bail) now builds
  `{undef value slots..., tag}` (constUndef per value slot + the error tag) and
  returns it — any arity.
- helpers: buildFailableTuple (tuple_init from value refs + tag) + emitTupleRet
  (return honoring inline-comptime targets).

Value-carrying `try` / `catch` still bail (E2.1b). Tests:
examples/228-value-failable.sx (return value + both raises, consumed by
destructure; exit 60). Gates: zig build, zig build test, 266/266 examples.
2026-05-31 21:42:51 +03:00
agra
0bbff9d7fb ERR/E1.5: catch sema (pure-failable slice) + error-set match subjects
`expr catch [e] BODY` consumes a failable's error inline. Pure-failable slice
(value-carrying `-> (T, !)` catch deferred to E2's tuple ABI).

- lowerExpr `.catch_expr` -> lowerCatch; inferExprType `.catch_expr` ->
  operand's success type (void for pure-failable).
- lowerCatch: operand must be failable (else "catch requires a failable
  expression"); pure-failable LHS only (value-carrying bails to E2). Eval
  operand -> err tag; condBr to handle (error) / merge (success). In handle:
  child scope binds `e` to the tag (typed as the error set), lower body
  (block or expr); if the body didn't diverge, br merge. Result is void.
  `catch` needs no failable enclosing function — it handles the error locally.
- All four body forms work: block, no-binding `catch { }`, bare-expr, and
  the match-body `catch e == { case ... }`. Re-raise (`raise e`) and diverging
  bodies (`return`) rely on E1.3 / E1.4c.

Also: lowerMatch now supports error-set subjects — `case .X` resolves to the
global tag id (was the arm index, dispatching wrong), and the switch operand
is the error-set value (its u32 tag) directly rather than via enumTag. This
is what the catch match-body form (and a plain `if e == { case .X }`) needs.

Tests: examples/226-catch.sx (block / no-binding / match-body / re-raise /
diverging body / success-skip; exit 18), examples/227-catch-rejections.sx
(operand-not-failable; exit 1). Gates: zig build, zig build test,
265/265 examples.
2026-05-31 21:10:56 +03:00
agra
28b18f812a fix(issue-0057): all-diverging match arms no longer fail LLVM verification
A match (`if subject == { case ... }`) whose arms all diverge (each
`return`s / `raise`s) failed LLVM verification with a `void` phi plus
"Terminator found in the middle of a basic block". Two causes in lowerMatch:

- The value-arm path did `lowerBlockValue(arm.body) orelse constInt(0, …)`,
  emitting the fallback `const` into a block the body had ALREADY terminated
  (a diverging arm), so `currentBlockHasTerminator()` then saw the const (not
  the `ret`) and emitted a `br merge` after the terminator. Fix: materialize
  the fallback value + branch only when the block hasn't terminated.
- A fully-diverging match infers `result_type == .noreturn` yet still built a
  value-merge phi. Fix: `has_value_merge` excludes `.noreturn`, so such a
  match builds no phi; its arms terminate and the merge block is unreachable.

Also: inferMatchResultType now skips `.noreturn` arms (a diverging arm doesn't
decide the result type) and reports `.noreturn` only when EVERY arm diverges —
so a mixed match (some arms yield values, some diverge) infers the value type.

This unblocks ERR E1.5's `catch` match-body form (`x catch e == { case .A:
return …; else: raise e; }`), which desugars to an all-diverging match.

Regression: examples/225-match-diverging-arms.sx (all-diverging + mixed,
exit 134). Gates: zig build, zig build test, 263/263 examples.
2026-05-31 21:04:06 +03:00
agra
696a749bd5 ERR/E1.4c: noreturn plumbing for divergence shapes
Type the divergence shapes as `noreturn` so a `catch` body that diverges
(E1.5) unifies with the failable's success type. The plan's original
"E1.4b", renumbered E1.4c (the SCC slice took the "E1.4b" label).

- inferExprType: `return` / `raise` / `break` / `continue` -> .noreturn
  (removed `.return_stmt` from the statements-are-`.void` group)
- if-else unification: a `.noreturn` branch yields the other branch's type;
  both diverging -> `.noreturn`
- block-ending-in-divergence propagates `.noreturn` (existing block arm)
- calls to `-> noreturn` already type via Function.ret (verified)
- made inferExprType pub for the unit test

Scope: the essential divergence shapes. Deferred `unreachable` (not a
keyword in sx — a separate feature, no current consumer) and infinite-loop
`noreturn` detection (rare). No observable consumer until E1.5's catch body,
so validated by a unit test, not an example.

Tests: unit test `E1.4c noreturn typing` in lower.test.zig (each shape ->
noreturn; block propagation; if-else unification). Gates: zig build,
zig build test, 262/262 examples (no new examples).
2026-05-31 20:33:13 +03:00
agra
d2cba4e460 ERR/E1.4b: whole-program inferred error sets + empty-inferred warning
The type-convergence side of E1.4 (the SCC slice). A bare `-> !` function's
error set is now converged whole-program from its literal raises plus the
sets of the pure-failable functions it `try`s.

- convergeInferredErrorSets: a pre-lowering fix-point pass (lowerRoot Pass
  1d, after scanDecls / before body lowering) that walks each top-level
  bare-`!` function's body AST (collectErrorSites, stopping at nested-fn
  boundaries) for literal `raise error.X` tags + pure `try g()` edges, then
  unions each set with its edges' sets until stable. Stored in a side map
  `inferred_error_sets` (fn name -> sorted []u32) — sidesteps the name-only
  error-set interning collision (the shared `!` placeholder stays empty).
- lowerTry widening: a named caller `try`-ing a bare-`!` callee now checks
  the callee's converged set (previously a false-negative — the empty
  placeholder was trivially a subset). Factored diagTagsNotInSet out of
  checkErrorSetSubset.
- empty-inferred warning: a top-level non-main bare-`!` function with an
  empty converged set warns. Not user-visible yet (the compile driver
  renders diagnostics only on failure — a LANG follow-up), so unit-tested
  on the DiagnosticList.
- corrected two now-stale bail messages (failable-`or` -> E2.4;
  value-carrying `try` -> E2).

Deferred to E2.4: failable-`or` chains / value-terminators (and `try`
fallback routing) — gated on the value-carrying tuple ABI.

Tests: examples/223-inferred-error-sets.sx (transitive convergence +
widening passes, exit 7), examples/224-inferred-widening-reject.sx
(transitive widening rejection, exit 1), unit test in lower.test.zig.
Gates: zig build, zig build test, 262/262 examples.
2026-05-31 20:21:44 +03:00
agra
aa1aa63bb3 ERR/E1.4a: standalone try sema + pure-failable propagation + named widening
`try f()` (standalone form) now propagates a failable callee's error to the
enclosing failable function. E1.4 was split: E1.4a = standalone try (failure
target = function-propagation); E1.4b = fallback-target routing +
failable-`or` + whole-program SCC for inferred sets + empty-inferred warning.

- lowerExpr: `.try_expr` -> lowerTry
- lowerTry: (1) try legal only inside a failable fn; (2) the sole
  failable-operand check (errorChannelOf(inferExprType(operand))); (3)
  named-caller widening (checkErrorSetSubset at the propagation site); (4)
  pure-failable lowering — condBr on tag != 0: propagate (run defers + ret
  the widened tag) vs continue on success
- inferExprType: `.try_expr` arm (success type: void for pure-failable)
- lowerBinaryOp .or_op: bail loudly on a failable LHS (exprIsFailable);
  the optional-`or` path is unchanged for non-failable LHS
- value-carrying callee/caller `try` bail loudly (pending E2's tuple ABI)

Tests: examples/221-try.sx (positive propagation, exit 5),
examples/222-try-rejections.sx (3 stable rejections: outside-failable,
non-failable operand, named-widening miss; exit 1). Gates: zig build,
zig build test, 260/260 examples.
2026-05-31 19:47:19 +03:00
agra
9984fa6b96 ERR/E1.3: raise sema + pure-failable lowering
`raise EXPR` now terminates a failable function via the error channel.
Scope (Option 2): full raise sema checks + lowering for the pure-failable
shape (`-> !` / `-> !Named`); the value-carrying `-> (T..., !)` shape bails
loudly, deferred to E2's error-channel tuple ABI.

- lowerStmt + tryLowerAsExpr: `.raise_stmt` -> lowerRaise (also routes a
  raise that is a block's last statement, which previously hit unknown_expr)
- lowerRaise: failable-context check (effectiveReturnType + errorChannelOf);
  literal membership via lowerErrorTagLiteral; variable form subset-checked
  via checkErrorSetSubset; pure-failable emits ret(tag)
- lowerErrorTagLiteral skips membership for the bare-`!` inferred placeholder
- plain `return;` in a pure-failable fn emits ret(0) (success / no error)
- parser: in_defer_body flag rejects `raise` inside a `defer` body

Tests: examples/219-raise.sx (positive, exit 8),
examples/220-raise-rejections.sx (3 sema rejections, exit 1), inline parser
test for raise-in-defer. Gates: zig build, zig build test, 258/258 examples.
2026-05-31 19:09:32 +03:00
agra
5a24a1177d ERR/E1.2: failable signatures — resolve the ! / !Named error channel
Adds the `.error_type_expr` arm to type_bridge.resolveAstType (the gating site
that still returned `.unresolved`):
- `!Named` → resolveTypeName(name) → the declared error set (E1.1).
- bare `!` → a shared inferred placeholder error set (reserved name "!", empty
  tags), refined per failable function by the E1.4 SCC pass.

The error channel then falls out of the existing multi-return + tuple
machinery: `-> (s32, !Named)` is a tuple_type_expr whose last field is the
error_type_expr → resolves to a tuple {s32, error_set} — exactly the locked
ABI (error slot = last return slot, u32). `-> !Named` resolves to the set.

Verified end-to-end via scratch: `parse :: (n) -> (s32, !ParseErr) { ...;
return (n, e); }` compiles + runs, `v, err := parse(5)` destructures (err typed
as the error set), `err == error.X` works; `-> !Named` single return too.

3 unit tests in type_bridge.test.zig (!Named, bare ! placeholder, tuple ending
in the error set). No examples/ — the only current usage path (return
(value, error)) will be flow-check-rejected at E1.8; the blessed example waits
for E1.3 (raise) + try/catch consumption.

zig build, zig build test (275), and 256/256 examples green.
2026-05-31 18:30:22 +03:00
agra
f5974e5846 ERR/E1.1 (slice 2): error.X value lowering + enum-like == typing
Completes E1.1. All in ir/lower.zig (the IR layer, per slice 1's finding).

- lowerFieldAccess intercepts `error.X` (parsed as field_access(identifier
  "error", X)) → lowerErrorTagLiteral: interns the tag; when target_type is a
  named error set, types the value as that set and validates X ∈ set (out-of-set
  → diagnostic); otherwise emits the raw u32 global tag id (the spec's
  context-free default — not a silent guess).
- tryLowerErrorSetEquality (early branch in lowerBinaryOp) + errorSetTypeOf /
  isErrorTagLiteralNode: an error-set value or `error.X` literal forces the other
  operand to be one too, else a diagnostic ("compares only with an error.X tag or
  another error-set value; coerce with `xx`"). Both sides lower under the set type
  as context (error.X resolves + membership-checks); two bare tag literals with no
  context compare as global u32 ids. Handles both operand orders.

First ERR examples (end-to-end): 217-error-sets.sx (declared set + error.X +
== true/false + u32 coercion → "error-set result: 25", exit 25) and
218-error-set-typing.sx (out-of-set literal + tag-vs-raw-int → 2 diagnostics).

Failable `!`/`!Named` signatures and raise/try/catch/onfail semantics remain
(E1.2+). zig build, zig build test, and 256/256 examples green.
2026-05-31 17:59:47 +03:00
agra
73232ce170 ERR/E1.1 (slice 1): error-set type + global tag registry + decl registration
First sema/types step. Implemented in the IR layer (ir/types.zig +
type_bridge.zig + lower.zig), NOT src/sema.zig — lowering doesn't consume
sema; the frontend Type is LSP-only. Mirrors how enums are handled.

- ir/types.zig: new `.error_set` TypeInfo kind (ErrorSetInfo {name, tags:
  []u32}; identity = name, like enum) with a u32 runtime layout (size/align
  4, LLVM i32) per the locked error-slot ABI. New TagRegistry on TypeTable
  (global tag pool: name -> u32, monotonic, id 0 reserved for "no error").
  internTag/getTagName/errorSetType helpers; `.error_set` arms in all 7
  exhaustive switches + findByName.
- emit_llvm: toLLVMTypeInfo -> i32. print: writeType -> set name.
- type_bridge: resolveInlineErrorSet (mirrors resolveInlineUnion) +
  .error_set_decl arm.
- lower.zig: registerErrorSetDecl (rejects empty `error { }` with a
  diagnostic) wired into both top-level decl switches + the block-local one.
- tests: ir/types.test (TagRegistry 0-reserved + identity; errorSetType u32
  layout + named display + dedup; sorted storage) and ir/type_bridge.test
  (decl -> type + tag interning + re-resolve dedup).

End-to-end: `Foo :: error { A, B }` + main compiles + runs (exit 0) — first
ERR syntax to survive the full pipeline; empty set rejects with a diagnostic.
Inferred bare `!`, error.X value, and == typing deferred to slice 2 / E1.2.

zig build, zig build test, and 254/254 examples green.
2026-05-31 17:39:11 +03:00
agra
fdeab0efd4 ERR/E0.3: parser test consolidation — Phase E0 complete
Fills the E0.3 coverage gaps E0.1/E0.2's inline tests hadn't hit and adds
an end-to-end integration parse. Test-only; no production code change.

- `try` in statement position (`try must_init();`).
- `try` over a parenthesized or-chain (`try (foo() or boo())`) — distinct
  from `try foo() or try boo()`.
- `or` value-terminator (`parse(s) or 0`).
- Integration: a full `parse :: (s) -> (s32, !ParseErr) { onfail / try / or /
  catch / if { raise } / return }` — asserts the trailing `!ParseErr`, the
  five body statement kinds, and that `in_onfail_body` is correctly scoped
  (the later if-block `raise` is allowed).

Tests stay inline in parser.zig (consistent with the existing 24 + E0.1/E0.2
inline tests). 37 ERR parser tests total; every new AST node has a round-trip
test. zig build test (268) and 254/254 examples green.
2026-05-31 17:14:02 +03:00
agra
1b777dd6ab ERR/E0.2: raise / try / catch / onfail + precedence + consumer-aware pipe (parser)
Parser-only second step of the error-handling stream. No sema/codegen.

- token: 4 keywords — `raise`, `try`, `catch`, `onfail`.
- ast: RaiseStmt, TryExpr, CatchExpr {operand, binding?, body, is_match_body},
  OnFailStmt {binding?, body}.
- parser:
  - `try` is a unary prefix (binds tighter than `or`; right-recursive so it
    stacks under `xx`/`@`/etc).
  - `or` is already left-associative (precedence-climbing loop) — no change.
  - `catch` is a postfix with four body shapes (no-binding block / block /
    bare-expr / `== { case }` match-body, the latter reusing parseMatchBody
    with the binding as subject).
  - `raise EXPR;` and `onfail [e] { } | onfail EXPR;` statements; `error`
    parses in expression position so `raise error.X` works; raise rejected
    in expression position and inside an onfail body (in_onfail_body flag).
  - consumer-aware `|>`: pipes the LHS into the head call through a
    try/catch/or wrapper, preserving the wrapper.
- print: printExpr + match-arm printing for round-trips (anyerror!void to
  break the printExpr<->printMatchArms inferred-error-set loop).
- sema/lsp: exhaustive switch arms for the 4 nodes + 4 keyword tokens.
- tests: ~22 inline parser tests (precedence, all catch forms, both
  rejections, pipe cases, round-trip prints incl. match-body).

zig build, zig build test (264), and 254/254 examples green.
2026-05-31 17:07:49 +03:00
agra
e88ee66953 ERR/E0.1: error-set decls + ! / !Named type exprs (parser)
Parser-only first step of the error-handling stream. No sema/codegen.

- token: `kw_error` keyword (`!` reuses existing `.bang`).
- ast: `ErrorSetDecl { name, tag_names }` + `ErrorTypeExpr { name: ?[] }`
  (null = inferred `!`, non-null = `!Named`); wired into Node.Data and
  declName.
- parser: `parseErrorSetDecl` (comma-separated tags, optional trailing
  comma/`;`) dispatched from parseConstBinding; `!` / `!Named` parsed in
  parseTypeExpr; result-list loop enforces error type as trailing-only;
  hasFnBodyAfterArrow skips `.bang` so failable-return fns are recognised.
- print: new focused AST round-trip printer (decls + type exprs); loud
  `error.UnsupportedNode` otherwise. Registered in root.zig.
- sema/lsp: exhaustive switch arms for the two new nodes.
- tests: 11 inline parser unit tests (shapes + 3 round-trip prints + 2
  trailing-position rejections).

zig build, zig build test, and 254/254 examples green.
2026-05-31 16:40:22 +03:00
agra
f7f9def0e7 error handling 2026-05-31 16:10:36 +03:00
agra
8775ffa778 lsp: whole-program diagnostics from the real compiler on save
Reuse the compiler's lowering pass instead of re-implementing its checks
in sema. A module can't be lowered standalone — lowering only type-checks
functions reachable from a root — so the open file alone misses errors
like a *Move passed into a by-value method parameter. Drive the workspace
entry (main.sx) through parse → resolveImports → lowerToIR, then attribute
each diagnostic back to its file via source_file and publish per file
(clearing files whose errors are gone).

Runs on didOpen/didSave (disk-based); sema stays the live per-keystroke
layer. Advertise textDocumentSync.save so the editor sends didSave.

collectProjectDiagnostics is split out (transport-free) and covered by a
hermetic temp-project test.
2026-05-31 15:10:59 +03:00
agra
b497b74acb lsp/sema: diagnose passing *T where a T value is expected
Bring the lower.zig call-argument check to the LSP analyzer so the
`*T`-where-`T` mismatch (a `for xs: (*m)` capture or a `*T` parameter
forwarded into a by-value parameter) is reported inline as you type,
not only at build time.

The fn-signature registry resolved parameter types with the shallow
Type.fromTypeExpr, which yields 'unresolved' for user structs, so the
argument type never matched the parameter. Resolve params through the
registry-aware fieldType instead (as the param symbols already do).
Restricted to direct identifier calls so args align 1:1 with params.

Add a regression test.
2026-05-31 14:37:11 +03:00
agra
14d1d9d3a8 lower: generalise the *T-where-T-expected diagnostic to any pointer
The check only caught `for xs: (*m)` loop captures; passing a `*T`
parameter or any pointer local where `T` is expected still slipped through
to the LLVM verifier. Key the diagnostic on the lowered argument's type
instead of the capture, so a `*Move` parameter forwarded into a by-value
parameter is reported the same way. Ref-capture wording is preserved.

Add example 216 (pointer-parameter case) alongside 215 (loop capture).
2026-05-31 14:17:25 +03:00
agra
39d51fc26d lower: diagnose passing a by-ref loop capture where a value is expected
`for xs: (*m)` binds `m` to a `*T`. Passing it directly to a parameter
that wants `T` produced invalid IR that only LLVM's verifier caught, with
the opaque 'Call parameter type does not match function signature'. Detect
it at the call site and emit a clear error with a fix-it suggesting `m.*`.

Add example 215 + expected output as a regression test.
2026-05-31 13:56:45 +03:00
agra
00f6fad51c lsp: go-to-definition on a member declaration resolves to itself
Cmd+clicking a struct field / method / enum-variant at its own
declaration returned null, so nothing happened — while find-references on
the same token worked. Resolve a definition-site click to its own
location; the editor then surfaces references on a definition-click
instead of doing nothing. Member uses still resolve to their definition.

Add selfMemberDefAt + a regression test.
2026-05-31 13:42:49 +03:00
agra
292fd937c6 lsp: project-wide find-references + revive the LSP test suite
find-references only searched documents the editor had open, so asking
for references to a field from a file whose users were all closed
returned just the definition. Load every .sx under the workspace root
before matching so uses in unopened files are found too.

The LSP server's own tests were dormant: nested under the `lsp` struct in
root.zig, refAllDecls never reached them, and they had bit-rotted (stale
DocumentStore.init arity, an unaligned dummy io, fake /test/ paths that
no longer resolve). Reference the lsp files directly so their tests run,
give the doc-store tests a real Threaded io with bare paths, and fix the
stale extractIdentAtOffset expectation.

Extract referencesPayload from the transport so it is unit-testable, and
add tests covering cross-document field references, includeDeclaration,
the for-loop capture inlay hint, and workspace file loading.
2026-05-31 13:36:20 +03:00
agra
8b4006a68d lsp: resolve for-loop capture types (go-to-def + inlay hints)
The sema analyzer bound a for-loop capture with no type, so navigating
or hinting through it (m.flag, m: Move) failed. Instantiate generic
field types (legal_moves: List(Move)) and infer the capture's element
type from the iterable — List-like structs, slices, arrays, many-
pointers, and a pointer followed to its pointee. By-ref captures bind a
pointer to the element; range cursors bind s64.

Inlay hints now descend into struct method bodies and emit a type label
for the capture itself.
2026-05-31 13:00:03 +03:00
agra
ce7a4862a5 docs: rename planned errdefer keyword to onfail in CLAUDE.md 2026-05-31 12:34:55 +03:00
agra
9a9198511d lsp/sema: clearer message for 'context' inside a callconv(.c) function
Now that 'context' resolves as an implicit global, accessing it inside a callconv(.c) function (an FFI callback/trampoline) would silently resolve — but the C ABI carries no implicit context parameter, so it's actually unavailable there. Sema now tracks the current function's calling convention and, for 'context' under callconv(.c), emits a specific diagnostic ('unavailable in a callconv(.c) function — pass what you need explicitly') instead of resolving it or saying 'undefined variable'.
2026-05-31 12:19:02 +03:00
agra
847468938b lsp/sema: recognise the implicit 'context' global
context (the context system — context.allocator, context.data) was reported as an 'undefined variable'. It's now registered as a Context-typed global when Context is in scope, so the field chain (context.allocator) resolves too, with a builtins-list fallback when Context isn't present.
2026-05-31 12:15:21 +03:00
agra
3437e77938 lsp: find-references for fields, methods, and enum variants
Members aren't symbols, so their uses were never recorded. Adds a member-reference list (declaration + uses) tracked during analysis: struct fields/methods and enum variants as declarations; field access, method calls, bare enum literals, qualified Type.variant, and match-arm patterns as uses. Spans are derived from the source-relative name slices; uses carry the owner type (via inferExprType, dereferencing pointers). find-references matches by (owner, name) across loaded documents, treating an unknown owner as a wildcard.

Verified: references for a field (legal_moves), a method (clear_valid_targets), and a variant (promote_rook — decl + comparisons + case patterns + struct-literal values across 5 files).
2026-05-31 12:11:05 +03:00
agra
4f7d4b7725 lsp/sema: analyse struct method bodies (record their references)
analyzeTopLevelDecl skipped struct_decl entirely, so identifiers used inside method bodies were never recorded as references — find-references (and reference-based features) missed method callers. Each method body is now analysed in its own scope. Verified: references to generate_legal_moves now include game.sx's call inside update_valid_targets.
2026-05-31 11:55:58 +03:00
agra
28e02a8372 lsp: implement textDocument/references (find references)
cmd-clicking a definition (or any use) now lists all references. Same-file matches are precise (by symbol index); cross-file matches a top-level name across loaded documents. Advertises referencesProvider. Verified: references to a free function resolve across files (rules.sx def + internal calls + main.sx caller).
2026-05-31 11:51:06 +03:00
agra
0cb4a5a342 lsp: go-to-definition on a field through a *Struct variable
resolveStructTypeName returned null unless the variable's type was exactly a struct, so 'board.castling' (board: *Board) couldn't locate the Board declaration. It now also returns the pointee struct name for a pointer-to-struct, read from the resolved symbol type. Verified: board.castling navigates to board.sx's castling field.
2026-05-31 11:39:52 +03:00
agra
ef0d9a9477 lsp/sema: resolve pointer-typed params and field access through pointers
Two gaps made 'piece := board.squares[move.from.index]' (board: *Board) <unresolved>: analyzeParams typed params with fromTypeExpr (bare-name only), so *Board / []T / *List params became null; and field_access only handled a struct value, not a *Struct. Params now resolve via fieldType, and field_access auto-derefs a pointer object (p.field on *T resolves on T). Regression test added.
2026-05-31 11:22:24 +03:00
agra
5c9d8c23ca lang: for-loop over List(T); deref a *T method receiver
The collection for-loop now iterates a List(T)-like struct ({ items: [*]T, len, … }) — and a *List — by viewing it as items[0..len]. So 'for legal: (m)' / 'for pieces: (*p)' work like iterating a slice, with by-ref captures writing back into the backing.

fixupMethodReceiver also derefs a *T receiver when the method takes T by value, so a 'for xs: (*x)' capture can call value-self methods (x.method()). Regression: examples/for-list.sx.
2026-05-31 11:13:57 +03:00
agra
6b5edc77b4 lang: require ':' before a for-loop range cursor
The cursor clause now matches the collection form's ': (capture)' — 'for 0..N: (i)' instead of 'for 0..N (i)'. The colon is required when a cursor is present; the no-cursor form 'for 0..N { }' is unchanged. Updated examples/200, the pack-index doc comment, and the spec.
2026-05-31 10:57:21 +03:00
agra
c08433b345 ui: drop redundant .* on pointer match subjects
event_position and translate_sdl_event matched on e.* / sdl.*; lowerMatch now auto-derefs a pointer subject, so 'if e ==' / 'if sdl ==' are equivalent (same load + tag-switch in IR). Pure cleanup.
2026-05-31 10:46:51 +03:00
agra
cd2ab1c4b6 ui: platform-neutral Keycode enum; map SDL keycodes once
KeyData.key was a raw u32 carrying SDL_Keycode values, so app code had to reinterpret it as SDL_Keycode (xx e.key) — a leaky, unchecked cross-platform cast only valid because the backend happened to be SDL. Add a neutral Keycode enum; translate_sdl_event maps SDL_Keycode to it via keycode_from_sdl. App code compares e.key == .escape with no platform type and no cast; a new backend maps its own native codes in one place.
2026-05-31 10:42:55 +03:00
agra
d70a7084ff specs: document for-loop by-reference capture (for xs: (*elem))
Covers the *elem pointer binding: zero-copy pass to *T params, write-back via elem.*, value-position auto-deref, and pointer-subject match.
2026-05-31 10:32:05 +03:00
agra
185df9afb7 lang: for-loop by-ref element capture (for xs: (*x))
(*x) binds x to a pointer into the collection (index_gep) instead of a per-element value copy: passing it on (e.g. to a *T param) is zero-copy and mutations write back. In a value position x auto-derefs — a binary-op operand loads the element, a pointer-typed slot keeps the pointer, and an 'if x == {...}' match derefs the pointee for its tag/payload. Arrays GEP through their storage so writes hit the original. Regression test: examples/for-by-ref-capture.sx.
2026-05-31 10:29:16 +03:00
agra
4415274894 lsp/sema: resolve method-return types, slice .ptr/.len, tagged enums
ev := events.ptr[i] (events := g_plat.poll_events()) was <unresolved> through three gaps:

1. Return types went through Type.fromTypeExpr, which only handles a bare type_expr — so any []T / *T / List(T) return became void. An impl method 'poll_events -> []Event' registered as void and, merged after the protocol's correct signature, clobbered it. resolveReturnType now uses fieldType.

2. Struct/protocol methods were never put in fn_signatures, so recv.method() and Type.static() return types never resolved. registerMethodSig now adds them by bare name (first-wins), which is what resolveCalleeName already assumed.

3. .ptr/.len field access was string-only (and string.ptr wrongly returned string_type); now handles slices/arrays and returns the proper many-pointer element.

4. Tagged enums (payload variants) were only a symbol, never in a lookup registry; now also recorded in enum_types so the name resolves as a type.

Net: events -> []Event, events.ptr -> [*]Event, ev -> Event. Regression test added; confirmed end-to-end via the LSP inlay hint.
2026-05-31 10:04:08 +03:00
agra
8be1deea93 claude.md: register LANG and ERR workstreams
Add LANG (already had files in current/ but missing from the workstream
list) and ERR (new error-handling design, plan + checkpoint in current/
PLAN-ERR.md and CHECKPOINT-ERR.md — gitignored).

Updates the "On every session start" enumeration, the per-step
checkpoint-update guidance, and the File roles table to reference all
five streams.
2026-05-31 09:40:52 +03:00
agra
13bd3c85ea lsp/sema: regression tests for generic indexing through import merge
Covers List(Move).items[i] -> Move via the LSP's flat-import struct_types merge (pre-registered, not self-declared) and with realistic methods/cross-referencing fields. Confirmed end-to-end against the real binary: the inlay hint for 'm := legal.items[i]' now resolves to Move.
2026-05-31 09:40:05 +03:00
agra
7c7a5ad5c7 lsp/sema: resolve generic-struct field indexing in hover
inferExprType returned <unresolved> for 'legal.items[i]' (a List(Move) indexed) for two reasons: index_expr only handled string/array — not many-pointers/slices — and generic instantiation was dropped (List(Move) tracked as bare List, so T never bound to Move).

Fixes: (1) fieldType preserves pointer/slice element names (the old Type.fromTypeExpr only handled plain type_expr nodes, so [*]T became unresolved); (2) index_expr/slice_expr resolve many-pointer + slice elements via a registry-aware resolveTypeNameStr that knows user structs/enums (unlike Type.fromName); (3) instantiateGeneric monomorphizes List(Move) into a struct_types entry with T->Move substituted. So legal.items -> [*]Move and m -> Move. Regression test added.
2026-05-30 18:25:28 +03:00
agra
fb8a5399f1 objc: remove ns_string/c_string helpers
ns_string's only caller was impl Into(*NSString) for string, so +stringWithUTF8String: is inlined there. c_string's one use (NSBundle.resourcePath in uikit) becomes rsrc.UTF8String() with resourcePath retyped *NSString. ffi-objc-call-06 and ffi-objc-dsl-07 .ir snapshots regenerated — they only drop the now-absent extern declares.
2026-05-30 18:01:27 +03:00
agra
a29ede0383 objc: migrate remaining ns_string call sites to xx NSString
NSLog's fmt, addObserver's name, UIApplicationMain's principal-class, CADisplayLink's run-loop mode, and metal's newLibraryWithSource/newFunctionWithName string args are retyped *NSString, so their call sites read xx "..." instead of ns_string("...".ptr). ns_string is now used only by impl Into(*NSString) for string.
2026-05-30 17:54:23 +03:00
agra
8e3c3ae981 objc: NSString type + Into(*NSString) for string
Adds an NSString foreign class and impl Into(*NSString) for string so a string literal flows into any *NSString slot via xx. uikit's keyboard userInfo lookups now read objectForKey(xx "...") instead of ns_string("...".ptr), and objectForKey's key param is retyped *NSString.

ffi-objc-call-06 .ir snapshot regenerated: declaring the NSString type adds its reflection thunks (struct_to_string/pointer_to_string), same as the existing NSObject/NSDictionary. Runtime output unchanged.
2026-05-30 17:39:38 +03:00
agra
29a4891374 imports: dedup flat decl list by node identity (issue 0056 FIXED)
Impl blocks are anonymous (no declName), so a parameterised-protocol impl in a module reached via a diamond import was appended once per path and registered twice — 'duplicate impl Into for source s64'. mergeFlat and the directory-import merge loop now also dedup by node pointer; a physical AST node is lowered once regardless of how many import paths reach it.

Regression: examples/issue-0056-diamond-param-impl.sx.
2026-05-30 17:36:35 +03:00
agra
ac7f1d10e5 lang: extend operand-type check to ordering + bitwise/shift (issue 0055 follow-up)
The arithmetic-only check from the previous commit shared a hole with the
comparison and bitwise/shift ops: lowerBinaryOp derives the result type
from the LHS, so `s64 < string` fed mismatched types to `icmp` (LLVM
verifier failure) and `s64 & string` reinterpreted the string's bytes.

Add isOrderingOperand (numeric / enum / pointer / bool / vector) and
isBitwiseOperand (integer / enum / bool / vector), and route `< <= > >=`
and `& | ^ << >>` through them alongside the existing arithmetic check, all
sharing one diagnostic + placeholder-sentinel path. Flags-enum bitwise
(`.read | .write`, `perm & .read`), enum/pointer comparison, and int
literals stay legal (50-smoke unaffected).

Equality `== / !=` is deliberately left unchecked — its path is heavily
special-cased (str_eq, Any unbox, optional == null); folding a check in
without regressing those is a separate change, noted in the issue.

Regression test renamed arith→binop and broadened to cover `+ * < & <<`
against a string operand: examples/214-binop-operand-type-check.sx.
2026-05-30 10:30:57 +03:00