Commit Graph

81 Commits

Author SHA1 Message Date
agra
f9dd965b69 ERR/E1.7: ban return/break/continue/try in defer & onfail bodies
A defer or onfail body runs while the block/function is already exiting, so it
has no target to transfer control to. `raise` was already rejected (E1.3); this
adds the rest of the locked set — `return` / `break` / `continue` / `try`.

In parseStmt, the return/break/continue/try parse sites now call a new
rejectInCleanup() helper, gated on in_onfail_body || in_defer_body (the existing
flags, whose doc-comments already scoped this follow-up). The ban is transitive
through nested catch bodies and loops, but parseLambda clears both flags for the
closure body — a closure is its own function boundary, so a `return` from a
closure created inside a cleanup body stays legal. The diagnostic names the
cleanup kind ("an `onfail`" / "a `defer`").

examples/237-cleanup-body-restrictions.sx covers the rejected forms (exit 1);
six inline parser tests cover each banned exit, the transitive-through-loop
case, the closure-boundary exception, and flag-restore after the defer.

Note: examples/213-canonical-map.sx is the user's uncommitted heterogeneous-
variadic-pack WIP (prints 40 vs expected 42); it fails on the committed parser
too, independent of this change, and is left unstaged.

Gates: zig build, zig build test (288 pass), bash tests/run_examples.sh (all
green except the unrelated 213 WIP).
2026-06-01 01:14:24 +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
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
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
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
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
8e74e4acb2 lang F1 Phase 6: canonical heterogeneous map — $R inference through closure params
The full canonical `map` now compiles and runs (examples/213 → 42):

    map :: (mapper: Closure(..sources.T) -> $R, ..sources: VL) -> VL($R)

Final piece: infer a pack-fn's generic return `$R` from a closure-typed
prefix param's lowered return type.

- collectGenericNames descends into closure_type_expr (params + return),
  so `$R` in `Closure(..) -> $R` registers as a function type-param.
- matchTypeParam/extractTypeParam descend into closures: `$R` is extracted
  from the lowered mapper's closure `.ret`.
- lowerPackFnCall infers type-param bindings from the lowered prefix args,
  folds them into the mangle, and threads them into monomorphizePackFn,
  which installs self.type_bindings for return-type resolution + body
  lowering (`-> VL($R)` ⇒ VL(s64); `Combined($R, ..)` ⇒ Combined(s64, ..)).

s64-elimination follow-through:

- An unbound generic `$R` resolves to `.unresolved` in resolveTypeWithBindings
  rather than fabricating an empty-struct stub (`R{}`).
- Lambda return-type inference skips an `.unresolved` target-closure ret and
  infers from the body, so the concrete return drives `$R`.
- The `.unresolved` codegen tripwire then caught a latent bug: a generic-struct
  source impl (`impl VL($R) for Combined($R, ..$Ts)`) was declaring its template
  method `Combined.get` (`-> $R`) as a standalone IR function. Fixed: a
  generic-struct source registers methods as TEMPLATES only (findable in
  fn_ast_map for per-instance monomorphization via createProtocolThunk), never
  declareFunction'd.

Feature 1 (heterogeneous variadic packs) all six phases complete.
248 examples + all unit tests green.
2026-05-30 03:46:46 +03:00
agra
e395a08331 lang F1 6: pack-spread in parameterized-type args (Combined($R, ..sources.T))
Parser now accepts a `..` spread in a parameterized-type arg list; in
instantiateGenericStruct a spread arg bound to the variadic type-param expands
via packTypeElems (so `..sources.T` projects each source pack element protocol
type-arg into ..$Ts). `Combined(s64, ..sources.T)` for a VL(s64) source
instantiates Combined(s64, s64). examples/209 (with explicit per-element xx
erase). 244 examples + unit green.

Next: (..sources) whole-pack materialization with per-element erasure into the
protocol-typed field (c.sources = (..sources) currently segfaults).
2026-05-30 03:06:03 +03:00
agra
39d77ff886 lang: tuple element assignment + named-tuple field names
Two fixes:
- Element assignment `t.0 = v` (the known Phase-4.2 gap): the lvalue path
  looked the element up by NAME via getStructFields, never matched a tuple
  (positional), and left field_ty .unresolved -> ptr(.unresolved) -> codegen
  panic. Added a tuple branch to the field-assignment lowering that indexes by
  position (numeric) or name (tup.names), mirroring the read path. Fixes
  `c.sources.0 = v` on a generic-instance pack field too.
- Named tuples: the parser dropped captured field names for a tuple TYPE
  `(x: T, y: U)` (passed field_names=null), and resolveTupleTypeWithBindings
  also nulled them. Both now preserve names (synthesizing _<i> for any unnamed
  slot), so `t.x` reads/writes by name and `.0` by position.

examples/208. 243 examples + unit green.
2026-05-30 03:00:58 +03:00
agra
b48766d153 lang F1 4.2 (core): generic struct pack type-param + (..$Ts) tuple field
A generic struct can take a pack type-param ..$Ts: []Type that binds the
remaining type args as a sequence, and a pack-shaped tuple field (..$Ts)
resolves to a tuple of those per-position types.

- parser/ast: accept a leading .. on a struct generic param; StructTypeParam
  gains is_variadic.
- registration: TemplateParam carries is_variadic (and is a type param).
- instantiateGenericStruct: a variadic type-param consumes the remaining args
  into pack_bindings + pack_arg_types (mangled into the name); restored after.
- resolveTypeWithBindings: a tuple-literal-as-type containing a pack spread
  (e.g. (..$Ts)) expands via packTypeElems.

Instantiate + correct per-position field types + whole-tuple store + element
read all work (examples/205). Not yet: protocol-applied field (..F(Ts)) (the
canonical (..VL(Ts)) shape) and nested element assignment b.pair.0 = v.
240 examples + unit green.
2026-05-30 02:30:49 +03:00
agra
27c88d4d26 lang F1: range-based for + inline-for unroll over packs
Add range loop syntax:
- runtime  for start..end (i) { }   counting loop, cursor optional, end exclusive
- comptime inline for start..end (i) { }   comptime-unrolled body

The inline form binds the cursor as an int_val comptime constant per
iteration, so xs[i] over a heterogeneous pack substitutes the concrete
per-position element -- the canonical's pack-iteration vehicle
(inline for 0..sources.len (i) { sources[i].addListener(...) }).

- AST: ForExpr.range_end, ForExpr.is_inline
- parser: parseForExpr range vs collection form; suppress_call flag so
  N (i) is not read as a call N(i) while parsing a range bound
- lower: lowerRuntimeRangeFor / lowerInlineRangeFor; evalComptimeInt;
  comptimeIndexOf extends pack-index resolution beyond int literals

Revises spec's inline for i in 0..N to the no-in, range-first, paren-cursor
form. Regression: examples/200-for-range.sx.
2026-05-29 21:36:17 +03:00
agra
4defadf513 test: make zig build test actually run all tests + fix latent rot
root.zig had no `test` block, so the test binary discovered zero tests and
trivially "passed" — every src test had silently rotted. Add
`refAllDecls(@This())` to root.zig so all 185 tests run, then fix the rot it
surfaced:

- emit_llvm.test: operands were constants, so LLVM folded the very
  instructions being asserted (fadd/sub/icmp/insertvalue/extractvalue/sext).
  Rewrite to use function-parameter operands; `main` now returns i32 (entry
  convention); tagged-union enum_init lowers via memory, not insertvalue.
- interp.test: switch the per-test allocator to an arena (the interpreter is
  arena-style and intentionally frees little) — clears the transient-Value
  leaks without an ownership-ambiguous source change.
- lower.test: pass `is_imported` to lowerFunction; mark two helpers `pub`; the
  if/else block test now uses a runtime (param) condition since lowering folds
  `if true`.
- print.test: SSA numbering — params occupy %0/%1, so consts start at %2.
- jni_java_emit.test: nested-class refs render in Java source form
  (`SurfaceHolder.Callback`), not the JNI `$` form.

Leaks fixed at the source where ownership was clear: Module gains an arena for
the operand slices the Builder dupes (struct/call/branch/switch args, block
params, lowerFunction params); objcDefinedStateStructType builds its field
slice in that arena and frees its temp name string.
2026-05-29 15:25:00 +03:00
agra
98526ab9b4 lang 1.2: parse pack-expansion forms in all four positions
Pack/tuple spread now parses in tuple-value `(..xs)` / `(..xs.field)`,
tuple-type `(..F(Ts))` / `(..F(Ts.Arg))`, call-arg `f(..xs)` (already),
and closure-sig `Closure(..Ts)` / `Closure(..sources.T)` positions.

Design: the uniform spread node is the existing `spread_expr` (its
operand sub-expression carries the projection `xs.field` and
type-application `F(Ts)` shapes) rather than a new PackExpansion node —
call-arg slice-spread (`..arr`) and pack-spread (`..pack`) are
syntactically identical, so they must share one node, and spread_expr
already serves it with working slice lowering. Closure-sig packs gain
`ClosureTypeExpr.pack_projection` alongside the existing `pack_name`.

Parser-only; sema/lowering land in Phase 2. 6 new parser unit tests +
examples/probes/pack-expansion-parses.sx. Build + 225-suite green.
2026-05-29 12:33:27 +03:00
agra
87f739cef2 lang 1.1: parse pack-constrained variadic parameter
`..xs: Protocol` (a bare protocol, no `[]`, no `$`) on a variadic
parameter now parses to `ast.Param.is_pack = true` — a heterogeneous
protocol-constrained pack, distinct from a slice variadic
(`..xs: []T`, is_pack=false) and the comptime type-pack (`..$args`,
is_comptime=true). Parser-only: sema/lowering for the pack form land in
Phase 2; existing forms are unaffected (zero examples used a bare
non-slice variadic annotation). Adds three parser unit tests and
examples/probes/pack-param-parses.sx.
2026-05-29 12:15:50 +03:00
agra
abfd30c44d ffi: drop legacy-variadic-form migration diagnostic
The special-case `return self.fail("legacy variadic syntax ...")`
in `parseParams` is gone. `parseTypeExpr` already errors naturally
on a leading `..` (now reported as "expected type name"), which
is enough — the language-level cutover happened in the previous
commit; no need for the parser to keep a migration breadcrumb.
2026-05-27 22:03:24 +03:00
agra
952dc0e161 ffi: drop legacy name: ..T variadic syntax
Parser hard-rejects the legacy `name: ..T` form with a one-line
migration message pointing at the new `..name: []T` shape. The
leading-`..` form is the one the lowering paths
(`resolveParamType` / `packVariadicCallArgs`) treat as canonical
post-issue-0049; leaving both forms accepted invited the same
class of cross-module emit crashes any time a `..T`-form decl in
stdlib crossed an import boundary.

`specs.md` updated alongside: the Variadic Functions section now
documents `..name: []T` as the surface form, with notes on
homogeneous vs `[]Any` boxing and the `..` spread at call sites.
Inline references to `args: ..Any` in §7 and §8 refreshed.
2026-05-27 21:32:45 +03:00
agra
5a4a19b3ab ffi M5.A.next.4A.bare.1.B: bare $args lowers to []Type slice value
Step 4A final-slice fix. Bare `$<pack_name>` (no `[<int>]`)
in expression position now parses + lowers to a comptime
`[]Type` slice value carrying one `const_type(TypeId)` per
pack element.

Plumbing:

- src/ast.zig: new `ComptimePackRef { pack_name }` node +
  `comptime_pack_ref` variant in Data.
- src/parser.zig: `parsePrimary`'s `$` arm makes `[` optional
  after the pack name. With `[<int>]` → existing
  `pack_index_type_expr` (single Type value). Without → new
  `comptime_pack_ref` (whole pack as []Type).
- src/sema.zig: adds the no-op switch arms for the new node
  in `analyzeNode` and `findNodeAtOffset`.
- src/ir/lower.zig: `lowerExpr` arm reads `pack_arg_types[name]`
  and calls `buildPackSliceValue(arg_tys)`. The helper allocas
  a `[N x Any]` array, emits one `const_type(arg_tys[i])` per
  slot, then a slice `{data_ptr, len}` aggregate. No active
  binding → focused diagnostic + null slice placeholder. The
  IR slice element type is `Any` (matches the today's
  `Type → .any` mapping in type_bridge); the interp stores
  raw `.type_tag` Values directly (NOT Any-boxed) so
  `args[i]` at interp time reads a Type value.
- src/ir/emit_llvm.zig: relaxed `const_type` to silently emit
  undef-i64 instead of the previous stderr-noisy bail. Storage
  of Type values in runtime aggregates is harmless (undef in,
  undef out). Use-site misuse is caught by the bails on
  type_name/type_eq/has_impl and the bitcast guard.

`examples/170-pack-bare-value.sx` flips from the parse-error
lock-in to "0/1/3/4" — four call shapes of `len_of(..$args) ->
s64 { list := $args; return list.len; }`. The slice's `.len`
field carries the per-mono pack arity.

210/210 example tests + `zig build test` green.

The remaining 4A.bare slices (4 and 5) — resolveTypeArg
silent-arm fix for index_expr + smoke test of a real builder
walking $args — are separate commits per the cadence rule.
2026-05-27 19:10:37 +03:00
agra
fd03b5812f ffi M5.A.next.4.3: $args[$i] in expression position — source construction
Final slice of the .type_tag activation. Sx code can now
construct Type values through the `$<pack>[<int_literal>]`
syntax in expression position. Lowering emits the new
`const_type(TypeId)` opcode; the interp materialises
`Value.type_tag(TypeId)`; reflection intrinsics + cmp_eq
read it kind-honestly.

Plumbing:

- src/parser.zig: `parsePrimary` accepts `$<ident>[<int_literal>]`
  at the front of every expression. Emits a `pack_index_type_expr`
  AST node — same node already used in TYPE positions in step 3,
  now extended to expression positions.

- src/ir/lower.zig: two places teach the new node.
  - `lowerExpr` arm: looks up `pack_arg_types[name][index]`, emits
    `builder.constType(arg_tys[index])`. OOB / no-binding paths
    emit a focused diagnostic + a `constType(.void)` placeholder
    (loud failure preserves silent-error budget).
  - `resolveTypeArg` arm: the same lookup, but returns the
    TypeId directly. Used by the lower-time fast paths in
    `tryLowerReflectionCall` + `tryConstBoolCondition` so
    `type_name($args[0])`, `type_eq($args[0], s64)`, and
    `has_impl(...)` all see the bound TypeId rather than
    falling through to the `.s64` default that the silent-arm
    rule forbids.

The two arms ensure both runtime AND compile-time paths use
the same source-of-truth (`pack_arg_types`), so per-mono
dispatch via `inline if type_eq($args[0], s64) { ... }` folds
at compile time as expected.

`examples/169-pack-value-dispatch.sx` exercises both shapes:
- `type_name($args[0])` returns the per-mono concrete type
  name ("s64", "string", "f64").
- `inline if type_eq($args[0], s64) { ... }` ladder dispatches
  per-mono ("got s64", "got string", "got bool", "got other").

209/209 example tests + `zig build test` green.

What's now possible end-to-end:

  show :: (..$args) -> string => type_name($args[0]);
  show(42)    // "s64"
  show("hi")  // "string"

  describe :: (..$args) -> string {
      inline if type_eq($args[0], s64) { return "got s64"; }
      ...
  }

The "by the book" activation is complete:
- foundation (const_type opcode, interp variant, helpers) — 4.0
- interp reflection arms (type_name / type_eq / has_impl) — 4.1
- box_any/display audit + bitcast guard — 4.2
- source-language construction via $args[$i] — 4.3

Step 5 (generic Into(Block) impl in stdlib) is now fully
unblocked — its trampoline body can interpolate per-mono types
both in type positions AND in expression positions.
2026-05-27 18:52:41 +03:00
agra
3df58febb6 ffi M5.A.next.3a.B: $args[$i] in type positions — parser + resolver
Step 3 first slice. `$<pack>[<int_literal>]` now parses in
every type position and resolves against the active pack
binding (`pack_arg_types` map set up by `monomorphizePackFn`).

Plumbing:

- src/ast.zig: new `PackIndexTypeExpr { pack_name, index }`
  AST node + `pack_index_type_expr` variant in `Data`.
- src/parser.zig: in `parseTypeExpr`'s `$<ident>` arm, peek
  for `[`. If found, parse a non-negative `int_literal` index
  followed by `]` and emit a `pack_index_type_expr` node.
  Plain `$T` / `$T/Eq` paths unchanged.
- src/ir/lower.zig::resolveTypeWithBindings: handles
  `pack_index_type_expr` first — looks up the pack name in
  `pack_arg_types`, returns `arg_tys[index]` when in range.
  OOB and "no active pack binding" cases emit focused
  diagnostics at the node span.
- src/ir/type_bridge.zig::resolveAstType: handles the same
  node but falls back to `.s64` with a stderr note — the bare
  type_bridge has no access to lowering state. Pack-aware
  callers route through `resolveTypeWithBindings`.
- src/sema.zig: adds `pack_index_type_expr` to the no-op
  arms in `analyzeNode` and `findNodeAtOffset` so the sema
  pass doesn't reject the new variant.

Tests:

- examples/165-pack-type-position.sx (lock-in from 69dcee8)
  flips from parse error to "42 first". Exercises both a
  return-type position (-> $args[0]) AND a local-var
  annotation (second : $args[1] = args[1]); two
  heterogeneous call shapes confirm distinct monos pick
  distinct concrete types per pack index.
- examples/166-pack-type-position-three.sx — three-element
  pack with $args[2] (third element) as return type. Three
  call shapes: (s64,s64,string), (bool,f64,s64),
  (string,string,bool). Prints "third 99 false".

Out of scope (deferred):
- $args[$i] where $i is a comptime-bound expression (only
  literal int supported in this slice).
- $args[$i] in fn-pointer type LITERALS (works for named
  decls but nested fn type expressions need an audit).
- $args[$i] in struct field types.

206/206 example tests + `zig build test` green.
2026-05-27 17:23:47 +03:00
agra
65824494a7 ffi M5.A.next.1c.B: pack type rep — Closure(..$args) parses + interns
`parseTypeExpr`'s `Closure(...)` arm now accepts a trailing
`..$name` (sigil optional) as a variadic-pack marker. Pack must
be terminal — `)` is the only token accepted after the name.
`ClosureTypeExpr` AST gains `pack_name: ?[]const u8` carrying the
identifier so later slices can name the binding.

`FunctionInfo` / `ClosureInfo` in src/ir/types.zig grow a
`pack_start: ?u32 = null` field. `Closure(..$args) -> R` interns
as `params = []`, `pack_start = Some(0)` — distinct from any
concrete `Closure(...) -> R` shape thanks to updated hash/eql
arms. New constructor pair `closureTypePack` /
`functionTypePack` keeps the existing single-shape constructors
unchanged.

`type_bridge.resolveClosureType` calls `closureTypePack` when
`pack_name != null`. The pack starts after the fixed prefix,
so `Closure(Prefix, ..$args)` resolves with `params = [Prefix]`,
`pack_start = Some(1)`.

No semantic effect yet — the signature exists in the type table
but no matching code reads `pack_start`. Step 1d wires impl
matching: `Closure(..$args) -> $R` binds against any concrete
closure source type in `tryUserConversion` / `registerParamImpl`.

`examples/154-pack-type-rep.sx` flips from rejecting-with-error
to positive parse smoke (prints "pack type rep ok").

192/192 example tests + `zig build test` green.
2026-05-27 12:12:16 +03:00
agra
a51fe26cbf ffi M5.A.next.1b: parser accepts ..$args as a variadic-pack param
Extends parseParams in src/parser.zig:1558 to recognize a leading
`..` before the optional `$` sigil and the parameter name. The
old `args: ..T` form (variadic marker after the colon) still
works — both paths set the same `is_variadic` flag.

A pack declaration `..$args` parses as:
- `is_variadic = true` (from the leading `..`)
- `is_comptime = true` (from the `$` sigil)
- `type_expr = inferred_type` (no `:` annotation)

The no-colon branch now propagates `is_variadic` and `is_comptime`
onto the Param struct so later slices (type rep, impl matching,
monomorphisation) can read both flags from the parsed AST without
re-deriving from token sequence.

`examples/150-pack-parse.sx` flips from rejecting-with-error to
positive parse smoke. No semantic effect yet — `foo` is declared
but never instantiated.

191/191 example tests + `zig build test` green.
2026-05-27 09:49:41 +03:00
agra
95f13849af ffi M2.2 (first pass): #property directive on foreign-class fields
Adds:
  field: T #property[(modifier, modifier, ...)];

inside #objc_class declarations. For FOREIGN classes (this slice),
'obj.field' and 'obj.field = x' lower as objc_msgSend dispatches —
no struct GEP, no per-field storage on the sx side. The receiver
is opaque and the Obj-C runtime owns the data.

Selector mangling (Apple convention):
  getter: <fieldName>            (e.g. 'count')
  setter: set<FieldName>:        (e.g. 'setBackgroundColor:')

So:
  view.backgroundColor          → [view backgroundColor]
  view.backgroundColor = red    → [view setBackgroundColor:red]

Plumbing:
- New token hash_property + lexer entry + LSP keyword classification.
- ForeignFieldDecl gains 'is_property' + 'property_modifiers' slice;
  the parser captures both. Modifiers are recorded verbatim (strong,
  weak, copy, readonly, getter("name"), ...) — semantic interpretation
  lands with M4.2 ARC wiring.
- lowerFieldAccess: lookupObjcPropertyOnPointer() detects the case
  before the auto-deref / struct-GEP path and dispatches via
  lowerObjcPropertyGetter (objc_msg_send).
- lowerAssignment: same check on the field_access LHS routes to
  lowerObjcPropertySetter (objc_msg_send with set<Field>:).
- inferExprType: 'obj.field' returns the property's declared type
  so chained access / coerced assignment work.

151-objc-property-foreign.sx round-trips:
  inst.tag        → [inst tag]       → reads g_probe_tag → 0
  inst.tag = 42   → [inst setTag:42] → writes g_probe_tag
  inst.tag = -7   → ditto
  Final: 0 -> 42 -> -7  (real Obj-C runtime dispatch).

DEFERRED for M2.2 (later passes):
- Sx-defined property IMPs (synthesized getter/setter trampolines
  reading/writing the state struct).
- Modifier-driven setter behavior: readonly (compile error on
  write), copy (deep-copy), weak (objc_storeWeak), strong/assign
  (Month 4.2 ARC ops).
- getter("name") / setter("name:") selector overrides.

181 example tests pass (+1). zig build test green.
2026-05-26 01:45:21 +03:00
agra
d6ef691e42 ffi M2.1(a): class-level constants 'name :: Type = expr;'
Inside a '#objc_class { ... }' block, 'name :: Type = expr;' is
accepted alongside the existing method form. Parsed as sugar for
'name :: () -> Type => expr;' — a niladic class method with an
expression body. The synthesized class method flows through the
M2.1(b) class-method pipeline: a C-ABI IMP is emitted and
registered on the metaclass.

Apple's runtime sees zero distinction — '[Cls foo]' dispatches to
our IMP regardless of source spelling. The constant form is
purely syntactic sugar; it reads better for static metadata
returns:

  SxGLView :: #objc_class("SxGLView") {
      layerClass :: Class = CAEAGLLayer.class();
  }

vs. the equivalent method form:

  layerClass :: () -> Class => CAEAGLLayer.class();

Parser change: after 'name ::' if the next token isn't '(' we
take the constant branch — parse a type expr, expect '=', parse
the value expr, expect ';'. The result is a ForeignMethodDecl
with is_static=true, empty params, return_type=Type, body=block
wrapping the expr. Pure parser-level transformation; no new AST
nodes, no new lowering passes.

150-objc-class-level-constant.sx exercises both shapes on macOS:
a primitive (s32 answer) and a pointer ('*NSObject seedClass'
— the canonical '+layerClass'-style factory return).

180 example tests pass (+1). zig build test green.

M2.1 complete: both (a) the constant form and (b) the
expression-bodied class method shape land.

Next: M2.2 — 'field: T #property(modifiers...)' synthesizes
getter/setter pairs.
2026-05-25 23:43:46 +03:00
agra
86c1127c46 ffi M1.0 (3/3): accept '=>' body in '#objc_class' member methods
Extends parseForeignClassDecl ([src/parser.zig:1262]) with an
arrow arm that mirrors the existing parseFnDecl shape — single-
expression body wrapped in a one-statement block so downstream
lowering sees the same AST as a brace-body method.

Closes the M1.0 surface: '=> expr;' is now valid for top-level
functions, struct methods, AND '#objc_class' member methods.
The sx-defined class lowering (A.7 dispatch gate) is still gated,
so 140-expression-bodied-objc-method.sx exercises parse-only —
the body is reachable but the method is never invoked. When M1.2
lights up sx-defined class instantiation, the arrow-body form
will flow through unchanged.

169 examples pass (+1 from 140 now green); zig build test green.
2026-05-25 21:18:09 +03:00
agra
572ab12142 ffi 3.2 A2: implement #selector("explicit:string") override
Make-green half of the cadence step started in A1. Wires the
`#selector` directive end-to-end:

- Lexer token `hash_selector` at src/token.zig + lookup row in
  src/lexer.zig.
- AST field `selector_override: ?[]const u8 = null` on
  `ForeignMethodDecl` (src/ast.zig).
- Parser block in src/parser.zig that mirrors
  `#jni_method_descriptor` — both occupy the same slot after the
  optional `-> ReturnType` and before the body/terminator. Not
  mutually exclusive at parse time.
- LSP semantic-token list (src/lsp/server.zig) updated.
- Lowering: `deriveObjcSelector` returns
  `{ sel, keyword_count, is_override }`. When `is_override` is true,
  the selector string is the user's literal and `keyword_count` is
  the colon count in that literal. Both `lowerObjcMethodCall` and
  `lowerObjcStaticCall` use the result.

Diagnostic policy when override colon-count ≠ call arity:

- Default mangling path: stays an error (`.err`). The user can fix
  the sx-side name to produce the right keyword count.
- Override path: downgrades to a warning (`.warn`). Rationale:
  Obj-C's `objc_msgSend` doesn't validate colon-vs-arg the way JNI's
  `GetMethodID` validates the descriptor — the runtime dispatches
  regardless and the wrong-arity case becomes silent calling-
  convention corruption. The compiler is the last line of defense
  for this typo class, but the warning preserves the override's
  escape-hatch character (deliberate mismatches still proceed).

Snapshot for `examples/ffi-objc-dsl-06-selector-override.sx` flips
from the pre-3.2 parser-error to working output:

  static override non-null: true

The mismatch diagnostic text in
`examples/ffi-objc-dsl-04-mismatch.sx`'s snapshot is updated to
drop the "once that lands (3.2)" phrasing now that 3.2 is here.

165/165 example tests.
2026-05-25 17:00:23 +03:00
agra
56414407fc ffi: drop static keyword on foreign-class methods; param type discriminates
`static name :: ...` was redundant — instance methods always declare
`self: *Self` as their first param by convention. The parser now derives
`is_static` from the first param's TYPE: if it's `*Self` the method is
an instance method; anything else (including no params at all) is a
class method. Removes a token from the surface, keeps the dispatch
behavior identical.

The receiver param's NAME doesn't matter — only its type. Calling the
first param `this`, `me`, `receiver`, etc. is fine as long as the type
is `*Self`. This mirrors how the rest of sx handles receiver dispatch.

Migration of every site that used the keyword:

- `library/modules/platform/android.sx` — `SurfaceView.new(ctx)`.
- `examples/ffi-jni-class-03-static.sx` — `Math.abs(n)`.
- `examples/ffi-jni-main-03-ctor.sx` — `SurfaceView.new(ctx)` in the
  `#jni_main` body.
- `examples/ffi-objc-dsl-05-static.sx` — NSObject's `.class()` /
  `.description()`.

164/164 example tests; chess clean on macOS / iOS sim / Android via
`tools/verify-step.sh`.
2026-05-25 16:32:32 +03:00
agra
29784c22a8 mem: implicit-context foundation + many compiler fixes
The session-long set of changes that lay the groundwork for the
Jai-literal implicit-Context-parameter refactor. Lots of accumulated
work; the new arrival is the implicit-ctx foundation (steps 1+2 of
the plan in current/CHECKPOINT-MEM.md):

  Step 1 — `CAllocator :: struct {}` stateless allocator in
    library/modules/allocators.sx, delegating directly to
    libc_malloc/libc_free. `ConstantValue` in src/ir/inst.zig gains a
    `func_ref: FuncId` leaf so nested aggregates can carry function
    pointers (the inline Allocator value's fn-ptr fields). Switch
    sites updated in emit_llvm.zig, print.zig, interp.zig.

  Step 2 — `emitDefaultContextGlobal` in src/ir/lower.zig synthesises
    a static `__sx_default_context` global with a nested-aggregate
    init_val pointing at the CAllocator → Allocator thunks. The
    second-pass `initVtableGlobals` in emit_llvm.zig is generalised
    to handle `.aggregate` init_vals (re-emits after func_map is
    populated so func_ref leaves resolve to real symbols).

Also folded in from earlier work this session:

  - Phase 1.1: `xx value` heap-copy in `buildProtocolValue` routes
    through `context.allocator` via the new `allocViaContext` helper.
  - interp.zig: `marshalForeignArg` double-offset bug fixed —
    `heapSlice` already adds `hp.offset` to the slice ptr, so the
    extra `+ hp.offset` was scribbling memcpy/memset into adjacent
    heap state, corrupting `heap.items[0]`. Symptom: `build_format`
    at comptime produced zero bytes, all `print` calls failed.
  - Lazy lowering: `lazyLowerFunction` now declares foreign-body
    functions as extern stubs in the local (comptime) module so
    cross-module foreign calls resolve.
  - Allocator API: all stdlib allocators on one-line `init() -> *T`
    (CAllocator/GPA: libc-backed; Arena/TrackingAllocator: parent-
    backed; BufAlloc: embeds state at head of user buffer).
  - issues 0038 (transitive #import), 0039 (chess + stdlib migration
    fallout), 0040 (generic struct method dot-dispatch), 0041
    (pointer types as type-arg), 0042 (alias name resolution) — all
    fixed; regression tests in examples/.
  - Diagnostic: `emitError` now embeds the lowering's
    `current_source_file` and enclosing function in the literal
    message; SX_TRACE_UNRESOLVED=1 dumps a Zig stack trace at the
    emit site so misattributed spans can't hide where the failure
    is.
  - tools/verify-step.sh (all-platforms gate) and tools/scratch.sh
    (interp/codegen parity tester) added.

Test suite: 152 example tests pass; chess builds + screenshots on
macOS / iOS sim / Android.
2026-05-24 22:59:20 +03:00
agra
5cc62e63c3 bundling: fs/process stdlib + post-link callback + Apple .app in sx
Campaign Weeks 3-6 of /Users/agra/.claude/plans/lets-plan-to-move-splendid-pumpkin.md
land in one push: the bundling pipeline that used to live in
src/target.zig (createBundle, embedFramework, extractEntitlements,
buildInfoPlist, codesign) now lives in
library/modules/platform/bundle.sx and runs in the IR interpreter
after target.link() returns.

New language-side surface:
- library/modules/fs.sx — POSIX libc bindings (open/read/write/close,
  mkdir/unlink/rmdir, chmod, rename, access, basename/dirname). Variadic
  open() lowers to C's varargs via the new args: ..T form. Direct libc
  calls bypass *File method dispatch so they work from the post-link
  IR interpreter.
- library/modules/process.sx — popen-based run(cmd) returning
  ProcessResult{ exit_code, stdout }, plus env() and find_executable().
- library/modules/std.sx — xml_escape(s) and variadic path_join(parts).
- library/modules/compiler.sx — BuildOptions grows
  set_post_link_callback / set_post_link_module / binary_path
  accessors; bundle_path/bundle_id/codesign_identity/provisioning_profile
  setters + accessors; per-target predicates is_macos/is_ios/
  is_ios_device/is_ios_simulator + target_triple; framework_count /
  framework_at(i) / framework_path_count / framework_path_at(i);
  add_asset_dir(src, dest) + asset_dir_count / src_at / dest_at.

Compiler-side wiring:
- src/ir/compiler_hooks.zig — BuildConfig now carries post_link_callback_fn,
  post_link_module, binary_path, bundle_*, target_triple,
  target_frameworks, target_framework_paths, asset_dirs. Hook registry
  exposes every accessor; getters return "" / 0 for unset fields so
  bundle.sx can treat absent values uniformly.
- src/ir/host_ffi.zig (new) — dlsym(RTLD_DEFAULT) + arity-switched cdecl
  trampolines so #foreign("c") declarations resolve through the host
  libc during #run / post-link interpretation.
- src/ir/interp.zig — callForeign dispatch; build_config pointer
  injection so accessor hooks see live state during re-entry.
- src/core.zig — keeps the IR module alive past generateCode; exposes
  invokeByName / invokeByFuncId so main.zig can re-enter the
  interpreter after linking.
- src/main.zig — wires bundle/codesign/provisioning CLI flags +
  target_triple + framework lists into BuildConfig; invokes the
  post-link callback (by FuncId or by <module>.bundle_main lookup) once
  target.link() returns. When --bundle is set but no callback is
  registered, auto-falls-back to post_link_module = "platform.bundle"
  so the legacy --bundle CLI keeps working for any program that imports
  modules/platform/bundle.sx.

Apple .app bundler (library/modules/platform/bundle.sx):
- Single bundle_main entry covers macOS, iOS simulator, iOS device.
  Per-target Info.plist switch keys off is_ios()/is_ios_simulator() —
  iOS emits UIDeviceFamily / LSRequiresIPhoneOS /
  UIApplicationSceneManifest / DTPlatformName (iPhoneOS or
  iPhoneSimulator); macOS emits the minimal CFBundle* set.
- iOS-only steps:
  - Provisioning embed: fs.read_file + fs.write_file to
    <bundle>/embedded.mobileprovision.
  - Framework embed: recursive cp -R per -F search path into
    <bundle>/Frameworks/<Name>.framework/ (until fs.sx grows list_dir).
  - Entitlements extraction: four process.run calls (security cms -D,
    plutil -extract Entitlements xml1, plutil -extract
    ApplicationIdentifierPrefix.0, plutil -replace application-identifier)
    resolving the wildcard <TEAM>.* -> <TEAM>.<bundle_id>.
  - Real codesign with --entitlements when present.
- Asset dirs (add_asset_dir): recursive cp -R src/. into <bundle>/dest/.
  Missing src is treated as "nothing to do" so projects can register
  add_asset_dir("assets", "assets") unconditionally.

Parser:
- parseStmt() now accepts #import \"path\"; and #framework \"Name\"; as
  statement-position tokens. Needed for top-level
  inline if OS == .android { #import \"modules/platform/android.sx\"; }
  blocks (issue-0042 flatten pass surfaces them); chess's
  inline-if-with-#import was rejected at parse time before this fix.

Removals from src/target.zig:
- createBundle, embedFramework, extractEntitlements, buildInfoPlist,
  codesign (~210 lines). main.zig no longer calls createBundle after
  link(); the sx callback is the single entry point.

Tests / regression markers (all run under sx run host JIT):
- examples/115-post-link-callback.sx — callback registration round-trip.
- examples/116-fs-roundtrip.sx — fs.write_file -> fs.read_file -> exists.
- examples/117-process-roundtrip.sx — process.run + env + find_executable.
- examples/118-macos-bundle.sx — macOS .app via bundle_main callback.
- examples/119-interp-cast-ptr-cmp.sx — cast(T) val under interpreter.
- examples/120-interp-variadic-any.sx — variadic ..Any indexing in IR
  interpreter.
- examples/121-ios-sim-bundle.sx — iOS-sim cross-compile + .app with
  iOS-shaped Info.plist (added to tests/cross_compile.sh as the
  ios-sim tuple).
- examples/122-ios-device-bundle.sx — iOS device cross-compile +
  full codesign pipeline (provisioning embed + entitlements
  extraction + --entitlements codesign). Manually verified end-to-end:
  installed via xcrun devicectl device install app + launched
  successfully on iPhone 17 Pro.
- examples/123-inline-if-import-in-body.sx — locks in the parser fix.

zig build && zig build test && bash tests/run_examples.sh => 141 passed,
0 failed; bash tests/cross_compile.sh => 7 passed, 0 failed.
2026-05-22 19:03:31 +03:00
agra
8d1816018a ffi: define-by-default #jni_class + #foreign modifier + #jni_main token
Flip the surface semantics for type-introducer directives: bare
`Foo :: #jni_class("path") { ... }` now means "DEFINE a new Java class
at that path" (sx-side provides the implementations). The `#foreign`
prefix modifier flips it back to "REFERENCE an existing class on the
foreign runtime." Matches how `#foreign` already reads in sx for C
function declarations (`printf :: ... #foreign;`).

    Foo :: #foreign  #jni_class("path/to/Foo") { ... }  // reference
    Foo ::           #jni_class("path/to/Foo") { ... }  // define
    Foo :: #jni_main #jni_class("path/to/Foo") { ... }  // define + main Activity

Compiler-side changes:
- New `hash_jni_main` lexer token (the launchable-Activity marker).
  Existing `hash_foreign` is reused; no new modifier token there.
- `ForeignClassDecl` gains `is_foreign: bool` + `is_main: bool`.
  `ForeignMethodDecl` gains `body: ?*Node` so defined-class methods
  can carry sx-side implementations (foreign-class methods stay
  `;`-terminated).
- Parser learns `tryParseForeignClassPrefix` — peek-and-consume the
  modifier tokens, then dispatch to the unchanged
  `parseForeignClassDecl` with the flags threaded through.
- Sema rejects two illegal combinations: `#foreign + #jni_main`
  (can't be both an external reference and the app's main entry),
  and bodied methods on `#foreign` decls (foreign methods are
  runtime-provided).
- Lower's foreign-class dispatch errors on non-foreign decls with
  a pointer to the runtime-synthesis follow-up; defined-class
  codegen (Java class emission, RegisterNatives wiring, manifest
  entry generation) lands in a separate session.

Migration:
- `library/modules/platform/android_jni.sx`: all four foreign class
  decls (`Activity`, `Window`, `View`, `WindowInsets`) gain `#foreign`.
- `examples/ffi-jni-class-{01..08}*.sx`: every test's `#jni_class` /
  `#jni_interface` / `#objc_class` / `#objc_protocol` / `#swift_class`
  / `#swift_struct` / `#swift_protocol` usage gains `#foreign`. All
  9 files mechanical perl rename; snapshots unchanged.

Verified locally:
- `zig build test` clean.
- `bash tests/run_examples.sh` 129/129.
- `bash tests/cross_compile.sh` 3/3.
- Chess APK rebuilds, reinstalls, launches on Pixel; safe-area
  clearance preserved.
2026-05-20 12:46:40 +03:00
agra
5bd2c84bb6 ffi 2.16a green: parser + AST + sema for #jni_env(env) { body }
New `hash_jni_env` lexer token; `parsePrimary` dispatches to a small
`parseJniEnvBlock` that consumes `(env) { body }` and returns a new
`JniEnvBlock` AST node (env_expr + body block).

Sema's analyzeNode arm recurses into env + body inside a pushed
scope; findNodeAtOffset descends through both children for go-to-
definition.

Lowering treats it as a syntactic wrapper around the block: env is
evaluated for side effects, body lowers as a normal block. The TL
push/pop semantics (synthesizing the env stack so `#jni_call`'s env
arg can become optional) land in 2.16b.

`expectSemicolonAfter` recognises `jni_env_block` as block-form so
statement-position uses don't need a trailing `;` — matches `if` /
`while` / `for` / bare blocks.

Test runs through the block body and prints expected output; xfail
snapshot flips to green. 127/127 examples green.
2026-05-20 10:41:24 +03:00
agra
5fd8e0fbbe ffi 2.7 green: parser accepts all seven type-introducer directive forms
Six new lexer tokens (`hash_jni_interface`, `hash_objc_class`,
`hash_objc_protocol`, `hash_swift_class`, `hash_swift_struct`,
`hash_swift_protocol`) join the existing `hash_jni_class`. All seven
share the body grammar from Phases 2.1–2.6.

AST refactored: `JniClassDecl` → `ForeignClassDecl` with a
`runtime: ForeignRuntime` enum discriminator; `JniMethodDecl` →
`ForeignMethodDecl` (with `jni_descriptor_override` renamed for
clarity since it's JNI-only); `JniFieldDecl` → `ForeignFieldDecl`;
`JniClassMember` → `ForeignClassMember`. AST variant renamed
`jni_class_decl` → `foreign_class_decl`.

`parseForeignClassDecl` takes the runtime as a parameter; the
`parseConstBinding` dispatch table now maps each of the seven
directive tokens to its `ForeignRuntime` variant via
`foreignRuntimeForCurrent`. No codegen yet — Phase 3 picks up Obj-C
runtime, Phase 4 picks up Swift. Runtime-specific body items (fields,
descriptor override) are validated at sema time in later steps.

126/126 examples green.
2026-05-20 10:15:10 +03:00
agra
11021d800d ffi 2.6 green: #jni_method_descriptor("(Sig)Ret") override
New `hash_jni_method_descriptor` lexer token + LSP keyword
classification. `JniMethodDecl` gains `desc_override: ?[]const u8`.
parseJniClassDecl accepts an optional `#jni_method_descriptor("...")`
clause between the return type and the terminating `;`, stashing the
literal as the override. Auto-derivation in Phase 2.8 will treat
this as the precedence override when present.

The 2.6 xfail commit (0ed4799) used the working name `#desc` in its
test file; this commit renames to `#jni_method_descriptor` for
parallel naming with the rest of the FFI directive set (`#jni_call`,
`#jni_class`, `#jni_env`, ...). Test snapshot flips xfail → green.

125/125 examples green.
2026-05-20 10:10:23 +03:00
agra
a703eeec2a ffi 2.5 green: name: Type; field body items in #jni_class
New `JniFieldDecl` AST struct (name + field_type); `JniClassMember`
gains a `field` variant. After consuming a member-name identifier
in the body loop, the parser branches on the next token: `:` →
field path (parse type expr + `;`), `::` → method path (existing).

`static` fields aren't part of the grammar yet and error explicitly
("static fields not yet supported"); only instance fields land here.
Lowering to JNI `Get<Type>Field` / `Set<Type>Field` arrives in 2.13.

124/124 examples green.
2026-05-20 10:05:30 +03:00
agra
a5c6f754a8 ffi 2.4 green: #extends and #implements body items
Two new lexer tokens `hash_extends` / `hash_implements` (global tokens,
context-meaningful inside #jni_class bodies — same pattern as #using).
`JniClassDecl.methods` refactored into `members: []const JniClassMember`,
a tagged union with `method` / `extends` / `implements` variants.
Body loop dispatches on the leading token: `#extends Alias;` /
`#implements Alias;` consume the alias name and push a non-method
member; everything else falls through to the existing method path.

The alias on the right of `#extends` is the sx-side name (resolved
to the corresponding #jni_class at sema time in a later step), not
the foreign Java path — the path lives only in the alias's own
directive arg.

123/123 examples green.
2026-05-20 10:02:56 +03:00
agra
ecce8cdca3 ffi 2.3 green: static body items in #jni_class
`JniMethodDecl` gains `is_static: bool = false`. parseJniClassDecl's
body loop now recognises a `static` identifier prefix (context-sensitive
— `static` stays a plain identifier elsewhere) and consumes it before
the method name, setting `is_static` on the resulting decl. Dispatch
to `GetStaticMethodID` / `CallStatic*Method` arrives in Phase 2.12.

122/122 examples green.
2026-05-20 09:35:09 +03:00
agra
a2a2e83af0 ffi 2.2 green: parser collects #jni_class instance method body items
New `JniMethodDecl` AST struct (name, params, param_names,
return_type — no body, foreign declaration). `JniClassDecl.body`
becomes `methods: []const JniMethodDecl`. parseJniClassDecl loops
over body items, parsing each `name :: (self: *Self, args...) -> Ret;`
similarly to parseProtocolDecl but requiring `;` (no body brace).

`static`, fields, `#extends`, `#implements`, and the other six
directive forms land in 2.3–2.7. Sema/lower still treat the decl
as an opaque type alias — descriptor derivation arrives in 2.8+.

121/121 examples green.
2026-05-20 09:30:02 +03:00
agra
32b464e959 ffi 2.1: parser accepts Foo :: #jni_class("path") { } opaque form
New `hash_jni_class` token + lexer entry, `JniClassDecl` AST node
(alias + java path; body deferred to 2.2+), `parseJniClassDecl`
consuming `("...") { }` and rejecting non-empty bodies for now.
Sema registers the alias as a type_alias symbol; LSP classifies
the directive as a keyword. The 2.0 xfail snapshot flips to
`parse-only ok`, exit 0.

120/120 examples green; zig test clean.
2026-05-20 09:24:14 +03:00
agra
85bbb29e9e ffi 1.1: parser accepts #objc_call / #jni_call / #jni_static_call
98/98 regression tests pass; ffi-objc-call-01-parse flips from
parse-error xfail to passing.

Shape: `#<intrinsic>(ReturnT)(args...)`. The return-type generic
sits in the first parens, the actual call args in the second. All
three intrinsics share the same parse rule; only the kind tag and
the downstream lowering differ.

  token.zig    | three new hash_* tags
  lexer.zig    | matches the directive keywords with the same
                 isIdentContinue boundary check as the rest
  ast.zig      | FfiIntrinsicCall node with `kind`, `return_type`,
                 and `args` fields; FfiIntrinsicKind enum
  parser.zig   | parseFfiIntrinsicCall — same call-arg loop shape
                 as Call, with the leading return-type slot
  sema.zig     | analyzeNode + findNodeAtOffset arms walk the args
                 + return-type child nodes
  lsp/server.zig | classify the new tokens as ST.keyword

Codegen for the new intrinsic isn't wired yet — examples that
reach the body of a non-suppressed call would fail at lowering.
The current parse test uses `inline if false { ... }` to suppress
the dead branch, so sema/codegen don't see the node. Phase 1.3+
adds the lowering and the gate comes off.

Chess Android + iOS-sim builds clean — no regression on the
existing `objc_msgSend` cast pattern or the JNI helper.
2026-05-19 12:45:49 +03:00
agra
f9ecf9d00e iOS lock step keyboard + metal 2026-05-18 17:40:10 +03:00
agra
63565e41ff abi: pass >16B aggregates by ptr-in-next-reg (Apple ARM64 ABI) + Path B for fn-ptr casts
Three stacked compiler bugs were causing iOS-sim chess to crash inside
[MTLTexture replaceRegion:...]. Fixing them lets every replaceRegion call
site succeed (1×1 RGBA8, 1MB R8 atlas, 440×440 chess pieces).

Path B for callconv(.c) fn-pointer casts:
- FunctionInfo now carries call_conv: CallConv (TypeInfo.CallConv) so
  function-type interning distinguishes sx-CC from C-CC. Inst.zig's
  Function.CallingConvention aliases the same enum.
- Parser accepts an optional `callconv(.c)` suffix on fn-pointer type
  spellings (factored into parseOptionalCallConv() shared with parseFnDecl
  and parseLambda).
- resolveFunctionType passes the parsed CC through functionTypeCC().
- .call_indirect reads fp.call_conv == .c and applies the C-ABI
  alloca+materialize for >16B aggregate args (Path A's behaviour at .call).

Apple ARM64 ABI (drop LLVM byval):
- Side-by-side asm diff vs clang's emission for the equivalent C call site
  showed LLVM's `byval` attribute lowers Apple-arm64 byval on the stack,
  while clang passes the struct via a pointer in the next int register
  (x2 for replaceRegion:). The runtime objc_msgSend dispatch path expects
  clang's convention.
- Dropped the byval attribute from the function-signature emission and
  from both call sites (.call and .call_indirect). The materialize-into-
  alloca + pass-plain-ptr pattern stays — the call site now matches
  clang's `mov x2, sp` exactly.
- Path A's sx-to-sx case continues to work since both ends use plain ptr
  (caller does alloca+store+pass, callee loads from the ptr in prologue).

Protocol dispatch (emitProtocolDispatch):
- Untargeted `null` lowers as const_null with type .void (per
  target_type orelse .void). The "wrap-value-in-alloca-pass-pointer"
  branch alloca'd a void slot, which LLVM's IRBuilder asserts on —
  EXC_BREAKPOINT in getTypeSizeInBits, manifesting as exit 133 / SIGTRAP
  when building the chess game. Fixed by re-emitting as
  constNull(void_ptr) when arg_ty == .void && expected_ty == void_ptr.
- is_pointer_ty only recognized .pointer, so [*]T (many_pointer) was
  alloca-wrapped — the heap pixels pointer from stbi_load was stored
  into a stack slot and the slot's address was passed as the *void arg.
  Fixed by extending the check to `.pointer or .many_pointer`.

metal.sx call sites + lifecycle guards:
- msg_replace (replaceRegion:, MTLRegion = 48B) and the two setScissorRect:
  sites (MTLScissorRect = 32B) now spell their fn-pointer types with
  by-value params + callconv(.c) — the *MTLRegion/@local workaround is
  gone.
- metal_begin_frame_ios bails before nextDrawable when pixel_w/h are 0
  (drawableSize 0×0 makes nextDrawable abort via XPC).
- metal_init_ios only sets drawableSize when dims are positive.
- begin_frame's encoder/cmd_buffer failure paths now clear self.drawable
  so a partial failure doesn't leak a drawable back into the pool.

Examples + tests:
- examples/86-callconv-c-fnptr-large-aggregate.sx — new, covers Path B
  with C-CC fn-ptr cast.
- examples/87-fnptr-cast-large-aggregate.sx — renamed from issue-0025.sx,
  covers Path B with default sx-CC (the negative case).
- examples/85-cc-c-large-aggregate.sx — from Session 60, covers Path A.
- examples/issue-0014.sx, issue-0024.sx, issue-0025.sx — removed
  (resolved earlier this work).

71 regression tests pass, 0 failed. Chess game builds clean for iOS sim
and reaches its frame loop without aborting. Runtime: chess UI still
doesn't render — remaining issue is in the UIKit lifecycle / CAMetalLayer
setup (legacy-app vs scene-API hybrid), not a compiler bug. See
current/CHECKPOINT.md "Next step" for the diagnosis + options.
2026-05-18 00:11:23 +03:00
agra
1c32d54e01 ios + ir cleanup
- ios: --target ios/ios-sim shorthands, iOS SDK auto-discovery,
  #framework directive + BuildOptions.add_framework hook,
  .app bundle + Info.plist + codesign (ad-hoc and real),
  --codesign-identity/--provisioning-profile/--entitlements flags,
  modules/std/{objc,uikit}.sx, dynamic class registration,
  typed objc_msgSend cast pattern, UIApplicationMain handoff,
  UIWindow scene attach. Runs on iPhone hardware.
- ir: silent .s64 defaults → loud diagnostics,
  resolveReturnType infers from body, sub-byte int sizes match LLVM,
  tuple type interning includes names, compile errors exit 1
- issue-NNNN convention: resolved bugs rename to focused features
- 50 regression tests passing
2026-05-17 13:19:08 +03:00
agra
f9dda972d2 fixes 2026-03-05 16:20:36 +02:00
agra
0336f361c7 issue 06 2026-03-03 16:18:58 +02:00
agra
004aff5f67 wasm shell + destructuring 2026-03-03 13:21:54 +02:00
agra
6c5672c7df trailing commas 2026-03-03 09:42:01 +02:00
agra
03074472e5 build options #compiler 2026-03-03 09:35:50 +02:00
agra
bbb5426777 sm 2026-03-02 21:00:55 +02:00
agra
2f4f898d54 asm... 2026-03-02 17:19:41 +02:00