Commit Graph

581 Commits

Author SHA1 Message Date
agra
932cdfa2ec fix(ir): resolve forward alias in top-level global annotations (issue 0070)
Issue 0069's resolveForwardIdentifierAliases fixpoint runs at the END of
scanDecls, but top-level var_decl globals and typed module constants had
their annotations resolved via resolveType(ta) inside the SAME scan loop,
before the fixpoint. So a forward identifier alias (`A :: B; B :: s32;`)
used as a global's type (`g : A = 7;`) was still absent from
type_alias_map: resolveType fabricated an empty-struct stub, and the global
got a type mismatching its initializer at LLVM verification (the typed-const
path `K : A : 42;` silently mistyped the constant instead).

Split scanDecls into two passes: pass 1 registers function/type/alias facts,
then resolveForwardIdentifierAliases converges the aliases, then pass 2
registers var_decl globals (registerTopLevelGlobal) and typed module
constants (registerTypedModuleConst) against the converged alias map.
Globals/typed-consts can't be named in a type position, so deferring them
past type/alias registration is order-safe; the untyped module-const branch
(no annotation to resolve) stays in pass 1.

One incidental IR snapshot reorder (examples/1309: user globals now emit
after foreign-class globals — semantically identical, program still exits 0).

Regression: examples/0133-types-forward-alias-global.sx (forward-alias global
+ typed const). Gate: zig build, zig build test, run_examples.sh -> 354/0.
2026-06-02 17:20:31 +03:00
agra
49a383df6d fix(ir): resolve forward identifier type aliases in scanDecls (issue 0069)
scanDecls' `.identifier` alias branch registered `A :: B` into
ProgramIndex.type_alias_map only when `B` was already known (in
type_alias_map or the TypeTable). A forward target declared later
(`MyChain :: MyInt; MyInt :: s32;`) was never present during the single
forward scan, so the alias name went unregistered and the A2.4
unknown-type pass — which treats type_alias_map keys as declared types —
flagged its uses as `unknown type 'MyChain'`.

Add a fixpoint post-pass `resolveForwardIdentifierAliases` at the end of
scanDecls that re-resolves identifier-RHS aliases until no progress, after
every top-level name has been seen. A value const is never an `.identifier`
node, and an alias whose target is a value const still misses both lookups,
so issue 0068's value-const rejection is preserved.

Regression: examples/0132-types-forward-type-alias.sx (forward alias +
forward chain). Gate: zig build, zig build test, run_examples.sh -> 353/0.
2026-06-02 16:59:20 +03:00
agra
877014578e fix(ir): value const used as a type must not satisfy unknown-type check (issue 0068)
The A2.4 unknown-type pass (semantic_diagnostics) added EVERY const_decl name to
its declared-type-name set. A value const (`NotAType :: 123`) thus satisfied
reportIfUnknownType, so `v: NotAType` was not flagged; lowering then hit
TypeResolver.resolveNamed's empty-struct-stub fallback and fabricated
`NotAType{}` (the program ran, printing it).

Fix: collectDeclaredTypeNames and harvestScopeDecls now gate the const-name-add
on a new constValueIntroducesType — true only when the value introduces a type
(declarations: struct/enum/union/error; type-expression aliases: type_expr,
pointer/many-pointer/slice/optional/array/function/closure/tuple, parameterized).
`.identifier` / `.call` aliases are intentionally excluded: the scan registers
the type-valued ones into ProgramIndex.type_alias_map / the TypeTable (both
queried separately by the pass), so a value-RHS alias is correctly left out and
flagged, while a type-RHS alias stays covered by the canonical facts.

Regression: examples/1117-diagnostics-value-const-as-type-rejected.sx (exit 1).
Issue-0064 regressions 1111-1116 and the 0115 aliases stay green. Gate: zig
build, zig build test, run_examples 352/0.
2026-06-02 16:33:38 +03:00
agra
8ff24472c9 refactor(ir): extract unknown-type diagnostic pass into semantic_diagnostics (A2.4)
Moves the issue-0064 unknown-type pass (checkUnknownTypeNames + 11 helpers:
collectDeclaredTypeNames, harvestScopeDecls, checkStructFieldTypes,
checkFnSignatureTypes, checkScope, walkBodyTypes, checkCastTarget,
checkTypeNodeForUnknown, reportIfUnknownType, isBuiltinTypeName, isIdentLike)
out of Lowering into a new src/ir/semantic_diagnostics.zig (UnknownTypeChecker).

The checker holds borrowed references (alloc, *DiagnosticList, *TypeTable,
*ProgramIndex, main_file) — not *Lowering — and queries the canonical facts:
declared top-level names from ProgramIndex, primitives from
TypeResolver.resolvePrimitive, registered concrete types from the TypeTable.
The AST decl/scope walk stays (it collects LOCAL type decls, which ProgramIndex
doesn't track — a per-pass scope need, not a parallel authoritative list).

Lowering.lowerRoot builds the checker only when diagnostics are active and runs
it; the 12 functions are deleted from lower.zig. Barrel-wired in ir.zig.
Example snapshots (issue-0064 regressions 1111-1115) are the guard, matching the
checkErrorFlow precedent (no .test.zig).

Phase A2 complete. Gate: zig build, zig build test, run_examples 351/0.
2026-06-02 16:12:28 +03:00
agra
7cc8057374 Merge Phase A2 (canonical type resolution) into master
Brings the architecture stream's Phase A2 onto master (A2.1–A2.3b + issue 0067):
- A2.1 (9eb85cf): ResolveEnv + TypeResolver shell (primitives + compounds).
- A2.2 (dd16bab): generic-binding + alias-aware name resolution into TypeResolver.
- A2.3 (3ed1b3a): pack projections → PackResolver; retire the TypeTable.aliases
  borrow (alias map threaded explicitly).
- A2.3b (9b50aac): converge structural type-shape resolution onto the single
  TypeResolver.resolveCompound; type_bridge reduced to a thin adapter.
- 0067 (744decc): reject non-type tuple-literal-as-type elements with a
  diagnostic instead of fabricating .s64.

Gate: zig build, zig build test, run_examples 351/0. Codex-reviewed (round 2).
2026-06-02 16:02:59 +03:00
agra
744decc6a1 fix(ir): reject non-type elements in tuple-literal-as-type (issue 0067)
`size_of((s32, 1))` treated the tuple literal as a tuple TYPE: for the non-type
element `1` it emitted a `std.debug.print` and substituted `.s64` for that field,
then compiled and printed a bogus size — a silent fabricated type (the forbidden
silent-fallback pattern).

Fix:
- type_bridge.resolveTupleLiteralAsType: a non-type element now yields
  `.unresolved` (no `.s64`, no debug print) — it refuses to fabricate a tuple.
  type_bridge is stateless, so this is the binding-free backstop.
- New stateful Lowering.resolveTupleLiteralTypeArg validates each element via
  isTypeShapedAstNode, emits a user-facing diagnostic at the offending element's
  span, and returns `.unresolved`. Wired into resolveTypeArg (size_of/align_of/…)
  and the resolveTypeWithBindings name-fallback; type_bridge builds the tuple
  only after validation passes.

Regression: examples/1116-diagnostics-tuple-type-nontype-element-rejected.sx
(exit 1 + diagnostic). Valid `(s32, s32)` still works (0115). Gate: zig build,
zig build test, run_examples 351/0.
2026-06-02 15:51:04 +03:00
agra
9b50aacbe4 refactor(ir): converge structural type-shape resolution onto resolveCompound (A2.3b)
Codex corrective step before the A2 merge gate: A2.3 left type_bridge with a
parallel structural type-resolution algorithm and an inline tuple-literal-spread
shape in lower.zig with a `.void` fallback.

Finding 1 — single owner for structural shapes:
- TypeResolver.resolveCompound is now the sole structural type-shape
  constructor. Namespaced on `table` (so the stateless type_bridge can call it)
  and extended to own function types, plain `Closure(P...) -> R`, and plain
  positional/named tuples (it already owned *T/[*]T/[]T/?T/[N]T). It returns
  null only for the pack-shaped forms that need caller state (`Closure(..p)`,
  spread tuples); OOM yields `.unresolved`.
- type_bridge: deleted its 8 independent structural resolvers
  (resolveArray/Slice/Pointer/ManyPointer/Optional/Function/Closure/TupleType).
  resolveAstType delegates those node kinds to resolveCompound via a binding-free
  StatelessInner adapter. The only residual stateless shape code is two tiny
  fallbacks for the pack-shaped forms resolveCompound defers
  (resolveClosurePackShape — used by Into(Block) at registration time —
  and resolveTupleSpreadShape) plus resolveParameterizedType (kept:
  generic-instantiation convergence is A4.1 per PLAN-ARCH).
- lower.zig: stateful resolveTypeWithBindings uses resolveCompound; the
  `.function_type_expr` switch arm is gone. PackResolver.resolveFunctionTypeWithBindings
  deleted (subsumed). Plain closures/tuples now resolve via resolveCompound in
  both paths; only pack closures / spread tuples reach PackResolver.

Finding 2 — no `.void` failure fallback in lower.zig pack handling:
- the inline tuple_literal-with-spread type assembly moved into
  PackResolver.resolveTupleLiteralType (returns ?TypeId; OOM `catch return .void`
  became `catch return .unresolved`).

Alias result preserved: TypeTable.aliases stays gone; no table.aliases reads;
ProgramIndex.type_alias_map threaded explicitly.

type_resolver.test.zig: resolveCompound test rewritten (namespaced + new
function/closure/tuple/pack-shape arms, arena-backed). Gate green: zig build,
zig build test, run_examples 350/0.
2026-06-02 15:20:31 +03:00
agra
3ed1b3a7a0 refactor(ir): pack projections → PackResolver + retire the alias borrow (A2.3)
A2-merge gate: both parts in one commit, behavior-preserving (350/0).

Part 1 — retire the TypeTable.aliases borrow (build-enforced):
- type_bridge.zig: add `AliasMap` and thread it as an explicit param through
  every name-resolving fn (resolveAstType, bridgeType, resolveTypeName, the
  compound resolvers, resolveTupleLiteralAsType, resolveParameterizedType, the
  inline enum/struct/union + error resolvers). resolveTypeName now forwards the
  threaded map to TypeResolver.resolveNamed instead of reading table.aliases.
- lower.zig: all 31 resolveAstType callers pass
  &self.program_index.type_alias_map; drop the lowerRoot loan.
- types.zig: remove the now-unused TypeTable.aliases field.
- type_bridge.test.zig: alias test passes alias_map explicitly; other calls
  pass null.

Part 2 — pack projections get one owner + no .void failure sentinel:
- New packs.zig (PackResolver, a *Lowering facade): moves
  resolveClosure/Tuple/FunctionTypeWithBindings, packTypeElems, packTypeArgs,
  elementProtocolTypeArg out of Lowering. Call sites route through
  Lowering.packResolver(); barrel-wired in ir.zig.
- The missing-projection `orelse .void` in packTypeArgs now emits a diagnostic
  and fills the slot with .unresolved (the tripwire sentinel), never a real
  .void; OOM `catch return .void` in the moved fns became .unresolved too.
  Legitimate no-return-type `else .void` defaults are preserved.
- packs.test.zig: packTypeArgs bound/unbound/no-constraint/no-state cases +
  the missing-projection backstop (diagnostic + .unresolved slot).
2026-06-02 14:43:47 +03:00
agra
dd16bab2c2 refactor(ir): move generic-binding + alias-aware name resolution into TypeResolver (A2.2)
Architecture phase A2.2 -- behavior-preserving. TypeResolver gains the
generic-binding and bare-name resolution it now owns:

- resolveBinding(node, env): $T / bare return-type T lookup via an explicit
  ResolveEnv (no hidden Lowering state).
- resolveNamed(name, table, alias_map): the full bare-name algorithm (primitive
  -> arbitrary-width int -> string-prefix [*]/*/?/[:0]u8 -> already-registered
  -> alias(alias_map) -> empty-struct stub), MOVED from
  type_bridge.resolveTypeName so it is single-sourced.
- resolveName(self, name): resolves through the canonical alias source
  ProgramIndex.type_alias_map -- the compiler path no longer reads the
  TypeTable.aliases borrow.

Lowering.resolveTypeWithBindings: the `if (self.type_bindings)` block (the $T
lookup plus parameterized/call/closure/function arms that were redundant with
the unconditional handling below) collapses to one resolveBinding delegation via
a new resolveEnv() snapshot; the bare-name fallback routes type_expr/identifier
to resolveName (index-based alias), other node kinds still to resolveAstType.

type_bridge.resolveTypeName becomes a 1-line delegate to resolveNamed, passing
its TypeTable.aliases borrow as the alias source. Single algorithm; the alias
map stays single-sourced in ProgramIndex.

Deferred to A2.3: removing the TypeTable.aliases borrow (its ~30 resolveAstType
callers must converge onto TypeResolver first) and type_bridge's stateless
compound resolvers. A2.2 #3 (templates/protocols/type-fns via ProgramIndex) was
already satisfied by A1.1b.

Tests: resolveBinding ($T bound/unbound/no-env), resolveName (alias->primitive,
alias->pointer via ProgramIndex), resolveNamed (width-int, string-prefix,
unknown->stub).

No new fallback path; no duplicate truth. Gate green: zig build, zig build test,
bash tests/run_examples.sh (350 passed, 0 failed).
lower.zig 19372->19367; type_bridge.zig 647->592; type_resolver.zig 90->159.
2026-06-02 13:56:32 +03:00
agra
9eb85cf9e3 refactor(ir): add ResolveEnv + TypeResolver shell; own primitives + compounds (A2.1)
Architecture phase A2.1 -- behavior-preserving. Introduce src/ir/type_resolver.zig
as the canonical AST-type-node -> TypeId resolver (Principle 1), starting with:

- ResolveEnv: the explicit resolution-context shape (Principle 2) -- type/pack/
  comptime bindings + target_type. Defined now; consumed as A2.2/A2.3 move the
  cases that need it.
- TypeResolver.resolvePrimitive(name): the builtin keyword table, MOVED here from
  type_bridge.resolveTypePrimitive (now a re-export -> single source; its 7
  callers are unaffected; no import cycle).
- TypeResolver.resolveCompound(node, inner): the structural compound types
  *T / [*]T / []T / ?T / [N]T. Element types recurse via inner.resolveInner (an
  anytype callback) so generic structs / bindings in element position keep their
  full stateful resolution.

Lowering.resolveTypeWithBindings duplicated the 5 simple compounds across its
bindings and no-bindings blocks (10 arms). Both are replaced with a single
self.typeResolver().resolveCompound(node, self) delegation; adds
Lowering.resolveInner (recursion hook) + typeResolver() (by-value view).

Deliberately deferred: tuples, closures, and function types stay on the existing
pack-aware helpers (resolveClosure/Tuple/FunctionTypeWithBindings); A2.3 owns
their pack-projection logic.

Tests: src/ir/type_resolver.test.zig (resolvePrimitive keyword/null cases;
resolveCompound for all 5 + null for non-compound; ResolveEnv defaults), wired
into the ir.zig barrel.

No new fallback path; no duplicate truth. Gate green: zig build, zig build test,
bash tests/run_examples.sh (350 passed, 0 failed). lower.zig 19393 -> 19372.
2026-06-02 13:25:27 +03:00
agra
8fbaf9ca6a docs(ir): mark sema/types as editor-only, not compiler truth (A1.2)
Architecture phase A1.2 — documentation/comment only, no behavior change.

Resolve the ambiguity over which type model compiler decisions trust:

- src/sema.zig: file-level module doc stating it is the editor symbol/type
  index for the language server (navigation/completion), NOT a compiler
  semantic pass. Its Type values are editor metadata; the compiler uses the
  canonical TypeId/TypeTable model in src/ir/. sx requires no as-you-type type
  checking -- authoritative diagnostics are produced on save by the canonical
  pipeline. Added notes on SemaResult, Analyzer, resolveTypeNode, inferExprType.
  No public API renamed (would churn LSP call sites).
- src/types.zig: note that Type is editor metadata only, not compiler truth;
  do not expand for new compiler semantics (A8 deletes/reduces it).
- src/ir/types.zig: fix stale TypeTable.aliases comment -- it borrows
  Lowering.program_index.type_alias_map (post-A1.1b).

Deleting the LSP's parallel sema diagnostic stream is A8.1, not this step.

Gate green: zig build, zig build test, bash tests/run_examples.sh (350 passed).
2026-06-02 12:54:30 +03:00
agra
fb262e9e59 refactor(ir): move declaration maps into ProgramIndex (A1.1b)
Architecture phase A1.1b — mechanical storage relocation. Move the 9
declaration-fact maps out of the Lowering state bag into ProgramIndex:

  high-fanout:   fn_ast_map, foreign_class_map, global_names, type_alias_map
  medium-fanout: struct_template_map, protocol_decl_map, protocol_ast_map,
                 module_const_map, ufcs_alias_map

168 self.<map> sites in lower.zig repointed to self.program_index.<map>;
external readers repointed too (core.zig foreign_class_map iteration;
lower.test.zig fn_ast_map / foreign_class_map). No duplicate storage, no
fallback path; zig build enforces no missed reference.

The four maps whose value types were Lowering-private pull those types into
program_index.zig as pub (GlobalInfo, StructTemplate + TemplateParam,
ProtocolDeclInfo + ProtocolMethodInfo, ModuleConstInfo); lower.zig aliases
them at file scope so call sites are unchanged.

Behavior is preserved exactly:
- per-map allocator unchanged — import_flags/fn_ast_map/global_names use the
  lowering allocator (ProgramIndex.init), the other 7 keep their page_allocator
  inline defaults;
- ProgramIndex.deinit frees only the 10 owned maps, never the borrowed
  module_scopes / import_graph;
- TypeTable.aliases still borrows &self.program_index.type_alias_map, loaned at
  lowerRoot with the same late-binding lifetime.

Extends program_index.test.zig with declaration-map round-trips (fn AST, type
alias, global, module const, foreign class, protocol decl/AST, struct template,
ufcs alias).

Registration logic (registerStructDecl / registerProtocolDecl /
registerForeignClassDecl, ...) stays in Lowering, writing through the index.

Gate green: zig build, zig build test, bash tests/run_examples.sh
(350 passed, 0 failed). lower.zig 19433 -> 19393 lines.
2026-06-02 12:30:11 +03:00
agra
90520eefeb refactor(ir): extract ProgramIndex, move low-fanout decl facts (A1.1a)
Architecture phase A1.1a. Introduce src/ir/program_index.zig as the single
storage owner for declaration-name / import / visibility facts, and move the
three low-fanout maps out of the Lowering state bag:

- import_flags     (owned by ProgramIndex)
- module_scopes    (borrowed pointer into a core.zig-owned map)
- import_graph     (borrowed pointer into a core.zig-owned map)

Lowering embeds one ProgramIndex by value and reaches every moved fact through
self.program_index.<field>; later phases hand collaborator modules a
*ProgramIndex instead of *Lowering. 8 call sites in lower.zig + 2 setters in
core.zig repointed. No duplicate storage, no fallback path; zig build enforces
no missed reference.

Mutation-heavy registration (registerStructDecl etc.) stays in Lowering and
now writes import_flags through the index. High-fanout maps are deferred to
A1.1b.

Adds src/ir/program_index.test.zig (init-empty, import_flags round-trip,
borrowed-view ownership) wired into the ir.zig barrel.

Behavior-preserving: zig build, zig build test, and bash tests/run_examples.sh
(350 passed, 0 failed) all green.
2026-06-02 12:04:31 +03:00
agra
795ce3dc7d test(runner): make example suite checkout-location independent
Diagnostics embed the absolute source path, but normalize() only scrubbed
hex addresses, so expected snapshots baked in the canonical checkout path
(/Users/agra/projects/sx/...). The suite only passed when run from that exact
directory; from a git worktree all 44 path-printing diagnostics mismatched.

Collapse any absolute `.../examples/` or `.../issues/` prefix to the repo-
relative form. The rule runs through normalize(), which is applied identically
to both expected and actual output, so it can only reconcile path noise — it
cannot desync an otherwise-matching pair. No snapshots regenerated.

Suite now reports 350 passed / 0 failed from a worktree as well as the
canonical tree.
2026-06-02 11:18:12 +03:00
agra
bd01d2224d fix(types): check nested closure/function bodies and cast targets (issue 0064)
Closes the two residual silent holes in the unknown-type diagnostic:

- Nested closure / function bodies. The body walk stopped at closure and
  nested-fn boundaries, so a typo'd type in a closure's local annotation
  silently became a 0-field struct. `walkBodyTypes` now descends control
  flow and expressions to re-enter each closure / nested fn via `checkScope`,
  which accumulates that scope's generic + value-`Type` params onto the
  parent's — so an inner closure still sees the outer function's `$T` (no
  false positive) while a genuine unknown is flagged at any nesting depth.
  `harvestScopeDecls` collects type-decl names across the whole body
  (including nested scopes) up front so locals are never false-flagged.

- Cast targets. `cast(T)` where `T` is a value-`Type` param (no `$`) cast to
  a fabricated empty struct silently; it now gets the tailored `$T` hint. An
  unknown *literal* cast target already errors via value resolution, so it's
  left to that path — no double diagnostic.

Suite: 350 passed, 0 failed. Regressions: examples/1114 (nested-closure
annotation), 1115 (cast value param).
2026-06-02 10:57:17 +03:00
agra
63b512a182 fix(types): extend unknown-type check into function bodies (issue 0064)
The signature/field check missed body-level type positions: a local
annotation naming a non-existent type flowed through the empty-struct stub
untouched, so `v: Coordnate = 5` silently compiled and ran (the value
dropped) — an invalid program accepted with no diagnostic.

`checkUnknownTypeNames` now also walks each main-file function body
(`checkBodyTypes`): local var/const type annotations — including inside
if / loop / match / push / defer / onfail blocks and decl-value blocks — are
validated with the enclosing function's generic params in scope, and
body-local `T :: struct/enum/union` declarations are collected first
(`collectBodyDeclNames`) so legitimate locals aren't false-flagged. Nested
function/closure bodies are their own scope and are not descended (safe
under-coverage); explicit `cast(T)` already surfaces its own `unresolved`
diagnostic and is left to it.

Regression: examples/1113 (local annotation of a non-existent type, exit 1).
2026-06-02 10:41:29 +03:00
agra
c490ffcfe9 fix(types): reject unknown type names instead of silent empty struct (issue 0064)
An identifier used in a type position that resolved to nothing fell through
to `type_bridge.resolveTypeName`'s empty-struct-stub fallback, silently
interning a 0-field struct named after the identifier. A value parameter
mistakenly used as a type (`(T: Type, ...) -> T`, missing the `$`) or a
typo'd type name therefore compiled and ran, rendering as `T{}`.

New post-scan diagnostic pass `checkUnknownTypeNames` (lower.zig Pass 1f)
walks every main-file function signature and non-generic struct field type
and rejects any leaf name that is not a primitive, an in-scope generic param
(`$T` / `type_params`), a declared type, or a real (non-stub) registered
type. The load-bearing empty-struct stub is left intact — forward references
and foreign-class opaque types still depend on it during the scan — and the
pass runs before body lowering, so `hasErrors()` halts the build before any
stub reaches codegen.

A value param used as a type gets a tailored hint to write `$T: Type`; a
genuine unknown gets "unknown type 'X'". Imported concrete types are
recognized via the type table, and inline compound spellings (`[:0]u8`),
arbitrary-width ints (`u1`/`u2`), and `$`-introduced generics (`-> $R`) are
exempted to avoid false positives.

Regressions: examples/1111 (tailored hint) + 1112 (typo'd field type).
2026-06-02 10:24:30 +03:00
agra
9214eefba1 chore(bench): build servers into .sx-tmp; fix stale example path
Some checks failed
Build / build-linux (push) Has been cancelled
Build / build-windows (push) Has been cancelled
The bench built sx-server/zig-server into bench/ (committed as stray
artifacts) and pointed at the pre-migration examples/32-http-server.sx.
Now builds into the gitignored .sx-tmp/ and uses the current
examples/1602-platform-http-server.sx, so a run leaves the tree clean.
2026-06-02 09:48:06 +03:00
agra
2c518f2dc4 chore: remove zig-server bench binary; don't gitignore artifacts
Drop the accidentally-tracked-in-working-tree zig-server bench binary
(built by bench/run.sh). Reverted the wasm_check* gitignore entry from
bce53b7 — these are removed, not ignored.
2026-06-02 09:43:05 +03:00
agra
bce53b720b chore: drop accidentally-committed wasm_check build artifacts
The wasm_check{,2}.{html,js,wasm} files are emscripten smoke-test output
committed by accident in f9ecf9d; nothing in the build/tests/docs
references them. Removed and gitignored the pattern so future WASM
output stays out of the tree.
2026-06-02 09:42:08 +03:00
agra
80abaf1e7d issues/0066: RESOLVED — match-value arms lowered against result type
A value-position match's arms are now lowered with `target_type` set to
the merge's `result_type`, so positive and negated integer literals pick
the same width. Fixes the `PHI node operands are not the same type as the
result` failure for `if n == { case 0: 100; else: -1; }`-style returns.

Regression: examples/0043-basic-match-value-mixed-width.sx.
Gates: zig build, zig build test, run_examples.sh -> 345 passed.
2026-06-02 09:35:41 +03:00
agra
92dba9078c issues/0065: RESOLVED — value-block destructure parses
The block-value rework routes value-position `{ … }` through the same
statement parser as every other block, so a destructure decl (and any
statement form) inside a value-bound block now parses, with the trailing
expression as the block's value. The `defer { … }` half was fixed
earlier (634cf9b). Regression: examples/0042-basic-block-value-destructure.sx.

Gates: zig build test, run_examples.sh -> 344 passed.
2026-06-02 09:31:18 +03:00
agra
bdd0e96d78 feat(lang): block value requires no trailing ; (Rust-style)
A block's value is now its last statement ONLY when that statement is a
trailing expression with no `;`. A trailing `;` discards the value,
leaving the block void. This makes value-vs-statement explicit and lets
the compiler reject "this block was supposed to produce a value".

Compiler:
- Parser records `Block.produces_value` (last stmt is a no-`;` trailing
  expression) + `Block.discarded_semi` (the `;` that discarded a value),
  via `expectSemicolonAfter`. A trailing expression before `}` may now
  omit its `;` (previously a parse error). Match-arm and else-arm bodies
  are built value-producing regardless of the arm `;` (arms are exempt —
  the `;` is an arm terminator).
- Lowering: `lowerBlockValue` / the block-expr path / `inferExprType`
  respect `produces_value`. A value-position block that discards its value
  is a hard error (`lowerValueBody` for function bodies; the value-context
  `.block` path for if/else branches, `catch` bodies, value bindings,
  match arms). Pure-failable `-> !` bodies (value rides the error channel)
  and a value-if whose branches are void are handled without false errors.
- `defer`/`onfail` cleanup bodies lower as statements (void), so a
  trailing `;` there is fine.

Migration (behavior-preserving — output unchanged):
- stdlib + ~210 examples: dropped the trailing `;` on value-position last
  expressions. `format` now ends with an explicit `#insert "return
  result;"` (it relied on `#insert`-as-block-value, which `;` discards).
- Two `main :: () -> s32` examples that relied on the old silent
  default-return got an explicit trailing `0`.
- Rejection snapshots 0412 / 1013 regenerated (their quoted source lines
  lost a `;`); the diagnostics themselves are unchanged.

Docs/tests: specs.md "Block values" section; examples 0040 (rules) + 0041
(rejection); 3 parser unit tests. Filed issue 0066 (pre-existing
match-arm negated-literal phi-width quirk, surfaced not caused here).

Gates: zig build, zig build test, run_examples.sh -> 343 passed,
cross_compile.sh -> 7 passed (also refreshed its stale example names).
2026-06-02 09:23:50 +03:00
agra
634cf9bc7f fix(parser): parse braced defer { … } body as a statement block (issue 0065)
A braced `defer` body routed through `parseExpr` + a mandatory trailing
`;`, so it parsed the `{ … }` as a block-EXPRESSION whose statement loop
doesn't handle a destructure decl or a `catch`-statement — `defer { v, e
:= f(); … }` and `defer { x() catch e … }` failed with "expected ';'",
and even `defer { stmt; }` needed a spurious trailing semicolon.

Now the `kw_defer` arm parses a braced body with `parseBlock` (the same
path `onfail` uses), so every statement form works; the bare-expression
form (`defer expr;`) is unchanged. `in_defer_body` is still set before
parsing, so the cleanup-body control-flow bans (return/break/continue/
try/raise) and the E1.7 failable-absorption check still fire.

Resolves the `defer` manifestation of issue 0065 (the general
value-block-in-binding-position destructure remains open). Regression:
examples/1050-errors-defer-block-body.sx.

Gates: zig build, zig build test, run_examples.sh -> 341 passed, 0 failed.
2026-06-01 23:29:07 +03:00
agra
c3bc6acd42 ERR/E1.7: reject bare failable calls in defer/onfail cleanup bodies
A `defer`/`onfail` body runs while the block is already exiting, so a
failable call there has nowhere to propagate its error. The parser
already bans `try`/`raise`/`return`/`break`/`continue` in cleanup bodies
(f9dd965); this adds the remaining sema rule — a bare (un-absorbed)
failable call must be absorbed locally with `catch` or `or <value>`.

Implemented in the shared error-flow pass (`checkCleanupBody` /
`checkCleanupNode` / `cleanupReject` in ir/lower.zig): when the walk hits
a `defer`/`onfail`, it scans the body transitively (through blocks, `if`,
loops, match arms, `catch` handlers; stopping at nested closures) and
flags any still-failable expression. `catch` / `or value` strip the
error channel, so `exprIsFailable` is false for them — only an unhandled
failable trips the check. This completes ERR PLAN E0–E5 plus the two
deferred E1 follow-ups (E1.7 + E1.8).

New regressions: 1048 (catch/or-value absorbed forms compile + run) and
1049 (bare failable in defer and onfail rejected, exit 1).

Filed issue 0065: a braced `defer { … }` / value-block body routes
through `parseExpr` (not `parseBlock` like `onfail`), so it can't parse a
destructure or `catch`-statement inside. Orthogonal to E1.7 — the spec'd
cleanup absorbers (`catch` / `or value`) parse fine in a `defer` body.

Gates: zig build, zig build test, run_examples.sh -> 340 passed, 0 failed.
2026-06-01 23:24:15 +03:00
agra
296c809d85 ERR/E1.8: path-sensitive value-slot liveness check
A `v, err := failable()` destructure now binds the value slot(s) "live
only where `err` is proven absent". Reading `v` where the compiler cannot
prove `err == null` is a compile error.

New diagnostic-only Pass 1e (`checkErrorFlow` in ir/lower.zig): a
structured, path-sensitive walk over each main-file function body. A
proven-null set is threaded across branches and joined by intersection
at each `if`'s merge. Proof shapes recognized:

  - `if !err { … v … }`           (proven inside the guard)
  - `if err { return/raise } … v` (proven on the fall-through)
  - `if err { … } else { … v … }` (proven in the else branch)
  - `!err and <reads v>`          (short-circuit refinement)

Error-set tag compares (`if err == error.X`) prove nothing about
absence — they narrow the tag only. Nested lambdas are analyzed as their
own boundaries. Library modules are trusted (skipped).

Migrated the canon value-failable examples (1011/1012/1018/1044) to read
their value slots under `if !err` guards — output unchanged. New
regressions: 1046 (every proof shape compiles + runs, exit 210) and 1047
(unproven reads rejected, exit 1).

Gates: zig build, zig build test, run_examples.sh -> 338 passed, 0 failed.
2026-06-01 23:14:24 +03:00
agra
5491acbcbb ERR/E5.4: add composition section to the errors smoke example
Extends 1036-errors-failable-smoke with an end-to-end Composition section
covering the E5.1 forms: a failable closure literal through a Closure(...)
param (try-propagated, caught), a non-failable closure literal widened
into a failable bare slot (∅-widening adapter), and generic ($T)
value-carrying failable composition. Completes E5.4 — the per-feature
examples (1039-1045) remain the focused units; this is the integrated
smoke.
2026-06-01 22:47:58 +03:00
agra
2e6e031233 ERR/E5.1: reject closure-value into bare function-pointer slot
A closure VALUE (a pre-bound variable) flowing into a bare (T)->U slot
was passed unsoundly: the bare ABI calls fn_ptr(ctx, args) with no env
channel, so the closure's underlying fn (which takes an env slot) had its
env dropped and args shifted — UB for a matching ABI, a wrong-tuple read
for the non-failable->failable widening (returned -1), and a segfault when
the closure captured.

coerceToType now rejects a .closure -> .function coercion with a
diagnostic pointing at the idiom (pass the literal directly, which gets
the static adapter, or type the parameter Closure(...) so the env is
carried). Closure LITERALS are unaffected — lowerLambda pre-adapts them to
a .function-typed value before coercion.

Regression: 1045-errors-closure-var-bare-slot-reject.sx.
2026-06-01 22:44:20 +03:00
agra
1c14383495 ERR/E5.1: verify generic failable composition (sub-feature 8); resolve 0062, file 0064
Generic value-carrying failable composition works with the documented
$T: Type generic form (catch / destructure / failure-propagation / a
second monomorphization at a different T). Issue 0062 was an invalid-repro
report — it used the non-generic T: type form, which is a plain Type-valued
param, not a generic type parameter. Marked 0062 resolved (not a bug).

The only real residual: a non-$ T: Type function param used as a type
silently resolves to an empty {} (renders T{}) instead of erroring. Filed
as 0064 (deferred, orthogonal to ERR — the $T idiom works).

Regression: 1044-errors-generic-failable-composition.sx.
2026-06-01 22:35:02 +03:00
agra
547148b8b6 fix(lower): free-fn UFCS auto-address-of + lazy lowering (issue 0063)
A free function called via UFCS (recv.fn(args)) whose first param is *T
was passed the receiver by value (LLVM "Call parameter type does not
match function signature"), and a function reached only via UFCS was
declared but never emitted (undefined symbol at link).

The bare-name UFCS fallback now mirrors the qualified-method path: it
lazily lowers the target body and calls fixupMethodReceiver +
coerceCallArgs, so the value receiver gets the same implicit address-of
as a struct-defined method and mutations through *T are visible.

Regression: 0039-basic-free-fn-ufcs-pointer-receiver.sx.
2026-06-01 22:28:15 +03:00
agra
a61685772d ERR/E5.1: lambda-specific raise-not-failable hint
A closure literal whose body raises but is annotated non-failable (or has
no ! in its return) now gets a lambda-specific diagnostic telling the user
to declare the failable return explicitly, instead of the generic "raise
is only valid inside a failable function". Failability is never inferred
for a lambda, so a raising lambda with no ! is a hard error that should
point at the fix.

New in_lambda_body flag (save/restore for nesting) set around the lambda
body lowering in lowerLambda; diagRaiseNotFailable branches on it.
Top-level functions keep the generic message.

Test: 1043-errors-lambda-raise-annotation-hint.sx.
2026-06-01 22:18:47 +03:00
agra
34bdf8b87c issues: file 0062 (generic failable return not monomorphized) + 0063 (free-fn UFCS pointer param by-value)
Both discovered while verifying ERR E5.1 "verify-only" sub-features against
the built compiler. 0062 is sub-feature 8 (generic + ! returns); 0063 is a
general UFCS/address-of miscompile orthogonal to ERR.
2026-06-01 22:13:12 +03:00
agra
39c21468ee ERR/E5.1: program-wide inferred-! union per closure/fn shape
All occurrences of Closure(<sig>) -> (T, !) with a structurally identical
value-signature now share one inferred error-set node; every bare-!
closure literal of that shape unions its escape tags in, and a
`try slot(x)` against any matching-shape slot widens the caller's named
set against that union. This closes the gap where a slot call (no static
function name) skipped the widening check entirely.

- shape_inferred_sets keyed by closureShapeKey (params + value-return via
  mangleTypeName, error slot excluded) so bare-!, non-failable, .function
  and .closure of one value-sig collapse to a single key.
- convergeClosureShapeSets pre-pass (lowerRoot Pass 1d', after the
  name-keyed convergeInferredErrorSets): collectClosureShapes walks fn
  bodies through lambda boundaries; recordClosureShape resolves each
  concrete bare-! literal's shape and unions its raises (+ try named_fn()
  edges via calleeEscapeTags) into the shape node.
- checkEscapeWidening falls back to shapeKeyOfCallee for bare-! slot calls
  (computed from the callee expr's .function/.closure type). Empty union
  is silently allowed (sub-feature 6).

Scope: concrete shapes only (generic lambdas skipped); closure-to-closure
try edges are not fix-pointed (under-approximation = a missed diagnostic,
never a miscompile).

Tests: 1041 (positive — union composes, runs), 1042 (reject — two
widening diagnostics, exit 1).
2026-06-01 22:01:38 +03:00
agra
0e1afa3eba fix(lower): drop dead statements after a return/raise terminator (issue 0061)
A bare `return X;` / `raise` in the middle of a block closed the current
LLVM basic block, but lowerBlock / lowerBlockValue only stopped the
statement loop on the `block_terminated` flag — which lowerReturn
deliberately never sets (it would leak past an `if cond { return }` merge
block). So trailing dead statements were emitted into the already-closed
block, tripping the LLVM verifier with "Terminator found in the middle of
a basic block".

Fix: also stop the statement loop when currentBlockHasTerminator() is
true. That is CFG-level termination of the *current* block, which is
naturally false at an if / inline-if merge block, so conditional returns
still fall through to their trailing statements.

This unblocks ERR E5.1: the canonical failable-closure form
`closure((x) -> (s32,!) { raise error.X; return x; })` has a dead
`return x;` after the unconditional raise and tripped the verifier.

Regression: examples/0038-basic-dead-code-after-terminator.sx.
2026-06-01 21:42:20 +03:00
agra
b113e03fa3 ERR/E5.1: bare failable fn-type param resolution + non-failable->failable widening
Two more E5.1 composition pieces:
- inferExprType .call: a callee that's a local variable of bare  type
  () now resolves to its declared return type (only
  was handled before), so  /  on the call see the failable result
  instead of .
- createClosureToBareFnAdapter now widens: when a NON-failable closure literal
  flows into a failable bare slot (∅ ⊆ slot set, success type matches), the
  adapter wraps the value into the slot's  tuple via
  lowerFailableSuccessReturn — previously rejected. The failable->non-failable
  and capturing->bare crossings stay rejected.

Adapter generation fires for closure LITERALS flowing into a bare-fn slot; a
pre-bound closure VARIABLE into a bare-fn slot is a separate coercion-site path,
still unhandled (noted in CHECKPOINT-ERR). Regression:
examples/1040-errors-failable-closure-composition. Suite: 329 passed.
2026-06-01 20:56:10 +03:00
agra
06e2685350 fix(lower): closure literals compose with bare function-type slots (issue 0060)
A closure's underlying function carries a hidden env arg that a bare (T)->U slot
doesn't pass, so a closure flowing into a bare function-type slot dropped the
env — the first user arg landed in the env slot and the rest read garbage
(apply(closure((x)->s64 { x*2 })) returned 192 instead of 10; non-failable too).

- createClosureToBareFnAdapter: a capture-free closure into a bare (T)->U slot is
  bridged by a generated adapter carrying the bare ABI (forwards a null env);
  lowerLambda returns its func_ref. Rejected (no silent miscompile): a capturing
  closure into a bare slot (env has nowhere to live) and a failable closure into
  a non-failable slot (the ERR E5.1 FFI-boundary rule).
- Arrow-body failable closures (-> (T,!) => expr) now wrap the bare success value
  into {value, 0} via lowerFailableSuccessReturn (the implicit return previously
  returned a malformed tuple → caught value read as 0).

The isLambda .bang parser fix (failable closure literals parse) already landed in
485b4fa. Regressions: examples/0309-closures-literal-as-bare-fn-param (non-
failable, block + arrow, called in callee) + 1039-errors-failable-closure-literal
(failable, block + arrow, direct + Closure(...) param). Resolves issue 0060
(remaining E5.1 follow-ups noted in the .md). Suite: 328 passed.
2026-06-01 20:35:25 +03:00
agra
485b4fa618 issues: file 0060 — closure-literal composition miscompiles (blocks ERR/E5.1)
Probing ERR/E5.1 (composition with closures) surfaced pre-existing closure-
literal lowering bugs: a closure literal passed as a function-type argument and
called inside the callee returns wrong values (block-body 192, arrow-body 20,
want 10 — non-failable too; the working contrast passes the value as a separate
arg, examples/0302). On top of that, failable closure returns don't parse
(isLambda omits .bang — one-line fix in the issue) and arrow-body failable
closures miscompile (return 0); block-body failable closures called directly
work. Runnable repro + parser patch + investigation prompt in the issue.

E5.1 paused per the impassable rule rather than built on miscompiling closures;
the parser fix + a regression example were reverted to avoid landing silently-
miscompiling failable closures on master.
2026-06-01 20:18:25 +03:00
agra
549f97c731 ERR/E5.2: comptime #run of an escaping failable → diagnostic + halt
A bare failable `#run` (no catch/or) whose error escapes used to segfault (const
form `x :: #run f()`) or silently succeed (statement form `#run f();`). Now the
compiler reports the raised tag name + the resolved return trace at the #run site
and halts with a non-zero exit.

- lower.zig: a failable #run's comptime function returns the full failable tuple
  (so the error slot is inspectable) while the global is typed as the success
  value; failable side-effects return the tuple instead of void.
- emit_llvm.zig: read the always-on comptime trace buffer (extern sx_trace_*);
  comptimeErrChannel + checkComptimeFailable split the result (non-zero tag →
  reportComptimeEscape + comptime_failed flag; success → value part). Wired into
  emitGlobals (const) and runComptimeSideEffects (statement, now filtered by the
  __run name; buffer cleared before each eval).
- core.zig: generateCode returns error.ComptimeError when comptime_failed, so the
  driver aborts before JIT/link.

catch / or / onfail compose at comptime exactly as at runtime; a successful bare
#run yields the value. Regressions: examples/1037-errors-comptime-run-escape
(diagnostic, exit 1) + 1038-errors-comptime-run-handled (exit 164). Suite: 326.
2026-06-01 20:04:17 +03:00
agra
9e660f30c2 docs: update CLAUDE.md testing workflow to the new test layout
Reflect the migrated layout: XXXX-category-test-name naming with per-category
100-blocks; expected output in an expected/ dir next to each test, split into
.exit/.stdout/.stderr (+ optional .ir); runner scans examples/ and issues/.
Replace the old 50-smoke / tests/expected / examples/issue-* workflow with:
add a feature as a focused example, file open bugs as issues/NNNN-slug.{md,sx}
co-located, and resolve an issue by moving its repro into examples/ as a
regression test + marking the .md RESOLVED. Update stale test count (29 -> 324).
2026-06-01 19:40:39 +03:00
agra
34819f05de issues: relocate legacy examples/issue-* repros into issues/
Clear the examples/issue-* namespace (new layout keeps open-issue repros under
issues/, co-located with their .md). Two legacy files:

- issue-0030 was a feature-request placeholder (trivial main, no real test).
  `extern G : T;` cross-file sx globals are still unimplemented (parse error),
  so it's an open feature request: issues/0030-extern-global-declarations.{md,sx}.
- issue-0019 was a broken/superseded multi-file fixture (relative imports, not
  runnable from root; the non-transitive-#import scenario is covered by the
  passing 0706-modules-import-non-transitive). Moved to
  issues/0019-import-non-transitive-c-scope/ with a status note; safe to delete.

Suite unchanged: 324 passed.
2026-06-01 19:38:09 +03:00
agra
e12f817e52 test: split 50-smoke.sx into per-section examples + add errors smoke
Break the monolithic examples/50-smoke.sx into 30 focused per-section examples,
filed into their category blocks (basic/types/comptime/memory/protocols/ffi),
each carrying only the top-level decls its section references (the protocols
section keeps the full preamble — its deps flow through UFCS method calls that
name-based extraction can't see). Outputs verified identical to the original
section blocks.

Add examples/1036-errors-failable-smoke.sx — an end-to-end error-handling example
(the E5.4 work): named + inferred error sets consumed via destructure, try (in
helpers), catch (bare-expr / match-body / diverging / no-binding), or
value-terminator, onfail+defer interleave, and error.X value + {} tag
interpolation.

Remove examples/50-smoke.sx. Suite: 324 passed, 0 failed.
2026-06-01 19:34:21 +03:00
agra
ba3c094283 fix(lower): infer no-annotation return type with params in scope (issue 0059)
A function with no explicit return type (arrow `=> expr`, or a block whose
`return <v>` drives the type) has its return type inferred from the body — but
the body references the function's own params. resolveReturnType ran that
inference before the params were pushed into self.scope (they're bound later, at
body lowering), so inferExprType couldn't resolve them and yielded .unresolved,
which reached LLVM emission and panicked. It only worked when a same-named
binding lingered in scope from earlier lowering (e.g. inside the big smoke file).

Bind the function's plain annotated value params into a temporary scope during
return-type inference. Resolve their types via resolveTypeWithBindings rather
than resolveParamType — the latter does variadic/pack bookkeeping that must run
exactly once, at body lowering; calling it here too corrupted the format/index
path. Variadic/pack/comptime/unannotated params are skipped (no by-name return
dependency; their types come from substitution).

Regression: examples/0308-closures-arrow-inferred-return.sx (arrow + block
inferred-return, top-level + local). Resolves issue 0059. Suite: 293 passed.
2026-06-01 19:28:35 +03:00
agra
70b4341682 issues: file 0059 — expr-bodied lambda inferred return type panics LLVM emission
An expression-bodied lambda `f :: (x: s32) => x * 2;` without an explicit
return type reaches emit_llvm declareFunction with func.ret = .unresolved and
trips the emission guard panic. Explicit `-> s32` works; the same lambda inside
a large module (the old 50-smoke.sx) resolves fine — so inferred-return
resolution for => lambdas runs only conditionally. Repro co-located.

Surfaced while splitting 50-smoke.sx (test-layout migration); the functions
section uses this exact construct, so the split is paused per the impassable
rule (no workaround).
2026-06-01 19:17:21 +03:00
agra
4e942b5373 test: migrate examples to XXXX-category-name layout + split expected streams
Rename all example tests/companions to the XXXX-category-test-name scheme
(per-category 100-blocks: basic 0010, types 0100, ... errors 1000,
diagnostics 1100, ffi 1200, ffi-objc 1300, ffi-jni 1400, vectors 1500,
platform 1600). Companions and dir/C fixtures move in lockstep with their
parent test; #import/#source/#include paths rewritten to match.

Expected output now lives in examples/expected/ (a sibling dir of the
tests) split into three streams per the new convention:
  <name>.exit / <name>.stdout / <name>.stderr  (+ optional <name>.ir)

run_examples.sh rewritten: scans examples/ and issues/ for an
expected/<name>.exit marker, captures stdout and stderr separately (no
more 2>&1), compares each stream + exit + optional IR snapshot.

Behavior validated unchanged: every renamed test reproduces its prior
merged output + exit (diffs limited to file paths/basenames embedded in
diagnostics + traces, which correctly reflect the new names). Suite:
292 passed, 0 failed. 50-smoke.sx split + issue relocation + docs follow
in subsequent commits.
2026-06-01 19:05:15 +03:00
agra
e86e41b719 ERR/E5.3: specs.md §12 Error Handling (fold locked design into spec)
Add a top-level §12 Error Handling distilling the locked error design +
surface syntax: failable signatures (-> (T,!) / -> ! / multi-value),
named `error { }` + inferred `!` sets, raise/try/catch/or/onfail, the
path-marker rule, set widening, error.X as a value, discard rejection +
flow-check, closures-with-!, return traces, and the u32-last-slot ABI.

Renumber Grammar §12→§13 and Open Questions §13→§14 (insert sits after
§10.5, so §3/§10.5 — the only section numbers referenced from CLAUDE.md
— stay valid). Cross-link the `!` channel from the Keywords list,
Operator Precedence, Function Definition, and §11 Program Structure;
extend the §13 grammar with error_decl, raise_stmt, onfail_stmt, a
catch_expr tier, `try` in unary, and failable type productions.

Pure docs; no compiler change. Gates: build, test, run_examples (293/0).
2026-06-01 18:19:26 +03:00
agra
49dc622234 fix(lower): auto-ref compound lvalues passed to *T params
The implicit address-of that gives `*T` params reference semantics only
fired for plain identifiers (`mut(v)`). For a field-access / index /
deref lvalue (`make_move(self.board, m)`, `mut(w.s)`), the branch was
skipped: the arg was loaded into a temporary and the callee mutated a
throwaway copy — silent data loss, with the type check satisfied through
the temp so no diagnostic fired.

Now compound lvalues auto-ref too: take the real lvalue address via
`lowerExprAsPtr`, normalizing the "place" ref to `*T` exactly as
`@field_access` does. Mutations through the pointer are now visible to
the caller, matching the identifier case.

Regression: examples/255-autoref-compound-lvalue.sx.
2026-06-01 17:56:51 +03:00
agra
497d450ba7 fix(lower): diagnose .* on a non-pointer instead of codegen panic
`lowerDerefExpr` left the deref's result type `.unresolved` when the
operand wasn't a pointer (e.g. a stale `value.*` after a parameter
changed from `*T` to `T`), and emitted the `.deref` anyway. That
unresolved type slipped through to emit_llvm's "unresolved type reached
LLVM emission" panic with no source location.

Now it emits a clean diagnostic at the deref site
("cannot dereference with `.*`: 'T' is not a pointer") and recovers.
Regression: examples/254-deref-non-pointer-reject.sx.
2026-06-01 17:37:27 +03:00
agra
5d275ee274 vscode: declare contributes.breakpoints for sx (enable breakpoint gutter)
VSCode disables the breakpoint gutter for a language unless an extension
declares breakpoints are valid for it. The sx extension registered the
language but never contributed breakpoints, so clicking the gutter in a
.sx file did nothing. Add the breakpoints contribution so users can set
breakpoints in .sx files without the per-workspace
debug.allowBreakpointsEverywhere hack.
2026-06-01 16:52:56 +03:00
agra
0d7f786db2 fix(dwarf): non-empty comp_dir so ld keeps the debug map (issue 0058)
A source path with no directory component (`sx build main.sx` from the
project dir — what the chess app does) made `diFileFor` emit a `DIFile`
with an empty `directory:`, so the compile unit's `DW_AT_comp_dir` was
"". Apple's ld then silently drops the *entire* object's debug map (0
N_OSO) and the binary is undebuggable — lldb resolves no sx source.
Builds whose path had any directory (`.sx-tmp/x.sx`, `examples/x.sx`)
were unaffected, which is why small repros + the stepping smoke passed
and only the bundled chess app hit it.

Fix: diFileFor falls back to "." (and "/" for a root-level file) when
the path has no directory component, so comp_dir is never empty.

Verified: chess (`sx build --target macos --emit-obj main.sx`) now
links with OSO=1 and lldb resolves `frame at main.sx:82:8`. Regression
guard added to the DWARF unit test (asserts `DIFile(... directory: ".")`
for a bare filename). Gates: zig build, zig build test, run_examples.sh
-> 291 passed, debug-stepping smoke ok.
2026-06-01 16:47:51 +03:00
agra
b2ebf774bc ERR/E3.0 (slice 3e rung 2): iOS-simulator stepping verified
Closes out E3's stepping-verification ladder to the extent possible
headlessly.

- Verified `sx build --target ios-sim --emit-obj` produces an
  arm64-ios-simulator Mach-O that runs under `simctl spawn` and steps
  in lldb (the backtrace shows a dyld_sim frame — the sim runtime).
- Verified the device-applicable .dSYM path: dsymutil collects the
  DWARF, and after removing the .o lldb still resolves source via the
  .dSYM.
- debug_stepping_smoke.sh gains an optional iOS-sim rung that reuses an
  already-booted simulator (never boots one — single-sim policy) and
  exercises the .dSYM path; skips cleanly when no sim is booted.
- docs/debugger.md: rungs 1-2 marked verified; the iOS-device rung is
  documented as a manual checklist (needs hardware + get-task-allow
  signing) — no compiler gap, --emit-obj + standard Apple tools suffice.

E3 is functionally complete and verified across macOS + iOS-simulator.
2026-06-01 16:10:33 +03:00