Commit Graph

67 Commits

Author SHA1 Message Date
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
agra
f0569a8a3e dot-shorthand and more 2026-02-25 15:51:22 +02:00
agra
4abc7abb54 auto type erasure for protocols & struct literal init bloc 2026-02-25 12:25:06 +02:00
agra
b98711a1d3 imports 2026-02-24 13:37:27 +02:00
agra
170e236764 vtables, protocol 2026-02-24 06:20:38 +02:00
agra
0cc7b69441 closures 2026-02-23 13:45:44 +02:00
agra
1cc67f9b5a optionals 2026-02-22 22:16:30 +02:00
agra
d3e574eae5 import c 2026-02-22 17:24:04 +02:00
agra
6f927361aa pipes 2026-02-20 13:28:38 +02:00
agra
1ecac79642 bit ops 2026-02-20 12:12:51 +02:00
agra
e0e655cd36 tuples 2026-02-19 01:26:04 +02:00
agra
188ffed5af arena 2026-02-18 15:59:49 +02:00
agra
ae5e6cd507 ... 2026-02-18 03:22:06 +02:00
agra
4fd87309d9 http server 2026-02-17 16:57:12 +02:00
agra
58e2a5bdb1 multiple assign 2026-02-16 01:13:34 +02:00