- types.Type: add dedicated `unresolved` variant (mirrors ir.TypeId.unresolved)
with eql/displayName arms; bridgeType maps it to TypeId.unresolved.
- sema.inferExprType + signature/field resolution: every Type.fromTypeExpr /
fromName / symbol lookup miss and call/field/index fallthrough now yields
Type.unresolved instead of a fabricated s(64). A variadic `..xs: []T` slice
element is taken from T, not a guessed "s32". Genuine literal defaults
(int=>s64, float=>f32, .len=>s64) kept.
- Builder.getRefType: an unlocatable ref (no active function / out-of-range)
returns .unresolved, not .s64 -- this is the accurate type source the pack
mono / binop / null-cmp fixes rely on, so it must not fabricate.
236 examples + unit tests (incl sema) green.
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.
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.
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.
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.
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.
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.
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.
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.