KeyData.key was a raw u32 carrying SDL_Keycode values, so app code had to reinterpret it as SDL_Keycode (xx e.key) — a leaky, unchecked cross-platform cast only valid because the backend happened to be SDL. Add a neutral Keycode enum; translate_sdl_event maps SDL_Keycode to it via keycode_from_sdl. App code compares e.key == .escape with no platform type and no cast; a new backend maps its own native codes in one place.
(*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.
ev := events.ptr[i] (events := g_plat.poll_events()) was <unresolved> through three gaps:
1. Return types went through Type.fromTypeExpr, which only handles a bare type_expr — so any []T / *T / List(T) return became void. An impl method 'poll_events -> []Event' registered as void and, merged after the protocol's correct signature, clobbered it. resolveReturnType now uses fieldType.
2. Struct/protocol methods were never put in fn_signatures, so recv.method() and Type.static() return types never resolved. registerMethodSig now adds them by bare name (first-wins), which is what resolveCalleeName already assumed.
3. .ptr/.len field access was string-only (and string.ptr wrongly returned string_type); now handles slices/arrays and returns the proper many-pointer element.
4. Tagged enums (payload variants) were only a symbol, never in a lookup registry; now also recorded in enum_types so the name resolves as a type.
Net: events -> []Event, events.ptr -> [*]Event, ev -> Event. Regression test added; confirmed end-to-end via the LSP inlay hint.
Add LANG (already had files in current/ but missing from the workstream
list) and ERR (new error-handling design, plan + checkpoint in current/
PLAN-ERR.md and CHECKPOINT-ERR.md — gitignored).
Updates the "On every session start" enumeration, the per-step
checkpoint-update guidance, and the File roles table to reference all
five streams.
Covers List(Move).items[i] -> Move via the LSP's flat-import struct_types merge (pre-registered, not self-declared) and with realistic methods/cross-referencing fields. Confirmed end-to-end against the real binary: the inlay hint for 'm := legal.items[i]' now resolves to Move.
inferExprType returned <unresolved> for 'legal.items[i]' (a List(Move) indexed) for two reasons: index_expr only handled string/array — not many-pointers/slices — and generic instantiation was dropped (List(Move) tracked as bare List, so T never bound to Move).
Fixes: (1) fieldType preserves pointer/slice element names (the old Type.fromTypeExpr only handled plain type_expr nodes, so [*]T became unresolved); (2) index_expr/slice_expr resolve many-pointer + slice elements via a registry-aware resolveTypeNameStr that knows user structs/enums (unlike Type.fromName); (3) instantiateGeneric monomorphizes List(Move) into a struct_types entry with T->Move substituted. So legal.items -> [*]Move and m -> Move. Regression test added.
ns_string's only caller was impl Into(*NSString) for string, so +stringWithUTF8String: is inlined there. c_string's one use (NSBundle.resourcePath in uikit) becomes rsrc.UTF8String() with resourcePath retyped *NSString. ffi-objc-call-06 and ffi-objc-dsl-07 .ir snapshots regenerated — they only drop the now-absent extern declares.
NSLog's fmt, addObserver's name, UIApplicationMain's principal-class, CADisplayLink's run-loop mode, and metal's newLibraryWithSource/newFunctionWithName string args are retyped *NSString, so their call sites read xx "..." instead of ns_string("...".ptr). ns_string is now used only by impl Into(*NSString) for string.
Adds an NSString foreign class and impl Into(*NSString) for string so a string literal flows into any *NSString slot via xx. uikit's keyboard userInfo lookups now read objectForKey(xx "...") instead of ns_string("...".ptr), and objectForKey's key param is retyped *NSString.
ffi-objc-call-06 .ir snapshot regenerated: declaring the NSString type adds its reflection thunks (struct_to_string/pointer_to_string), same as the existing NSObject/NSDictionary. Runtime output unchanged.
Impl blocks are anonymous (no declName), so a parameterised-protocol impl in a module reached via a diamond import was appended once per path and registered twice — 'duplicate impl Into for source s64'. mergeFlat and the directory-import merge loop now also dedup by node pointer; a physical AST node is lowered once regardless of how many import paths reach it.
Regression: examples/issue-0056-diamond-param-impl.sx.
The arithmetic-only check from the previous commit shared a hole with the
comparison and bitwise/shift ops: lowerBinaryOp derives the result type
from the LHS, so `s64 < string` fed mismatched types to `icmp` (LLVM
verifier failure) and `s64 & string` reinterpreted the string's bytes.
Add isOrderingOperand (numeric / enum / pointer / bool / vector) and
isBitwiseOperand (integer / enum / bool / vector), and route `< <= > >=`
and `& | ^ << >>` through them alongside the existing arithmetic check, all
sharing one diagnostic + placeholder-sentinel path. Flags-enum bitwise
(`.read | .write`, `perm & .read`), enum/pointer comparison, and int
literals stay legal (50-smoke unaffected).
Equality `== / !=` is deliberately left unchecked — its path is heavily
special-cased (str_eq, Any unbox, optional == null); folding a check in
without regressing those is a separate change, noted in the issue.
Regression test renamed arith→binop and broadened to cover `+ * < & <<`
against a string operand: examples/214-binop-operand-type-check.sx.
lowerBinaryOp derived the result type from the LHS alone and emitted
add/sub/mul/div/mod without checking the RHS, so `s64 + string` lowered
as `add : s64` and reinterpreted the string's bytes — printing garbage
instead of erroring.
Add isArithOperand (int / float / vector / pointer, plus custom int
widths) and, for `+ - * / %`, diagnose `cannot apply '<op>' to operands
of type '<lhs>' and '<rhs>'` and return a placeholder sentinel instead of
the corrupting op. `.unresolved` operands pass through so a type we
couldn't infer is never falsely rejected; the existing optional-unwrap
and int×float promotion are accounted for before the check.
Ordering (`< <= > >=`) and bitwise/shift (`& | ^ << >>`) ops share the
same LHS-derived-type hole and are left as a noted follow-up in the issue.
Regression: examples/214-arith-operand-type-check.sx (s64 + string, and
non-numeric LHS string * s64).
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.
Two fixes, root-caused from xx Combined -> VL(s64) trapping:
- instantiateGenericStruct binds the template name to the concrete instance
(tb.put(tmpl.name, id)), so an impl method self: *Combined resolves self.field
to the instance (Combined__s64_s64), not the 0-field generic stub. This was a
general pre-existing bug: self.x on ANY generic-struct impl method failed.
- createProtocolThunk monomorphizes the template method for a generic-struct
instance (Combined.get -> Combined__s64_s64.get with the instance bindings),
so the erasure vtable dispatches instead of hitting an unreachable thunk.
xx c on a generic Combined now dispatches correctly (examples/212 -> 99).
247 examples + unit green.
lowerPackFnCall lowered the runtime prefix args with no target_type, so a
lambda arg (mapper: Closure(...) -> ...) could not infer its param types.
Now set target_type to the param type while lowering each prefix arg. With
the existing value-projection call-arg spread, mapper(..sources.get) works:
the lambda is contextually typed and the projected values spread into the
call. examples/211 ((a,b)=>a+b over two sources -> 42). 246 + unit green.
lowerTupleLiteral now coerces/erases each spliced spread element to the
contextual target tuple field type (computed even when a spread is present,
indexed by output position). New coerceOrErase: protocol target -> xx-erase
via buildProtocolErasure, else coerceToType. So c.sources = (..sources) on a
(..VL(Ts)) field erases each concrete pack element to its VL(Ti) slot.
examples/210 (build(IntCell, StrCell) -> 10 hi). 245 examples + unit green.
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).
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.
packTypeElems now handles a parameterized spread operand F(Ts): for each pack
element T_i it temporarily binds the pack name to T_i and resolves F(T_i),
yielding (VL(T0), VL(T1), ...). Combined with parameterized-protocol value
types, the canonical Combined struct field sources: (..VL(Ts)) now resolves to
a tuple of real protocol values.
End-to-end (examples/207): instantiate Combined(s64, s64, string), whole-store
c.sources = (xx IntCell, xx StrCell), and per-element dispatch c.sources.0.get()
/ c.sources.1.get() all work. 242 examples + unit green.
VL(s64) used as a value/field type resolved to a 0-field stub (size 0); a
plain protocol was already a 16-byte {ctx,vtable} value. New
instantiateParamProtocol materializes a parameterized protocol per
instantiation: a 16-byte protocol value (is_protocol), protocol_decl_map
methods resolved under the type-arg binding (get -> T becomes get -> s64 for
VL(s64)), a vtable struct, and the type-arg binding recorded for projection.
Hooked into resolveParameterizedWithBindings before the empty-struct fallback.
xx-erasing a conforming struct into VL(s64)/VL(string) + method dispatch now
works (examples/206). This is the keystone for the canonical Combined field
(..VL(Ts)). 241 examples + unit green.
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.
xx args with a slice target now bridges a comptime pack to a runtime slice:
[]Any boxes each element to Any; []P xx-erases each to the protocol (reusing
the slice-of-protocol erasure from 0052). New lowerPackToSlice; the unary-op
arm intercepts xx <pack> before the pack-as-value diagnostic. This is the
working forward to a runtime []Any/[]P helper -- log_count(xx args) -> 3 --
so the 2.7 pack-as-value diagnostics now suggest xx <name> for the call case.
examples/204-pack-xx-to-slice.sx (both []Any and []P paths); 203 help text
updated. issue 0053 FIXED. 239 examples + unit green.
Using a bare pack name where a runtime value is required was silent garbage
(f(xs)/return xs produced a stray pointer). Now a clear, context-tailored
compile error: isPackName + diagPackAsValue, caught at lowerVarDecl (storage),
lowerReturn (return), lowerFor (iterate), and an identifier-arm catch-all for
call/other. Storage binds a placeholder so there is no cascade error.
Suggestions point at WORKING fixes -- materialize (..xs), or declare the slice
form ..xs: []P for runtime use. The plan category-B "spread ..xs" is broken
(spreading a comptime pack into a []Any param crashes the LLVM verifier; filed
issue 0053), so the diagnostics steer to the slice-of-protocol variadic instead.
Repurposed examples/162-pack-bare-args.sx (was an aspirational bare-$args->[]Any
auto-materialise, contradicting Decision 1) into the slice-form forward
(..args: []Any). examples/203 is the four-category negative test. specs.md "Pack
as value" updated. 238 examples + unit green.
packVariadicCallArgs stored the raw concrete arg into a [N x P] array when the
element type was a protocol, so an 8-byte struct landed in a 16-byte {ctx,
vtable} slot -> garbage vtable -> Bus error on dispatch. Now, when the slice
element type is a protocol, each arg is xx-erased to the protocol value via
buildProtocolErasure (same impl-driven machinery as the xx cast). This makes
..xs: []P the runtime, protocol-erased counterpart to the comptime
heterogeneous pack ..xs: P (which stays comptime-only): xs[runtime_i].method()
now works in an ordinary loop.
specs.md: full variadic/pack form-comparison table (concrete-vs-erased,
comptime-vs-runtime). Regression: examples/202. Issue 0052 (FIXED). 237 green.
Per locked Decision 1 a pack is comptime-only with no runtime value, so xs[i]
is valid only for a comptime index. lowerIndexExpr now emits a clear error
("pack <p> must be indexed by a compile-time constant ...") for a runtime
index, instead of the confusing "unresolved <p>" the slice-index fall-through
produced. diagPackIndexOOB switched from int-literal-only to comptimeIndexOf so
an inline-for cursor that goes out of bounds is also caught.
Repurposed examples/163-pack-runtime-index.sx (was aspirational: expected
runtime indexing to materialise a []Any slice and print 4, contradicting
Decision 1) into the runtime-index error test. Comptime + OOB cases already
covered by examples/199/200/161. 236 examples + unit green.
The three post-diagnostic failure returns in resolveTypeArg (pack-index OOB,
no active pack binding, unresolved type name) returned .void as a sentinel.
Per the CLAUDE.md rule (.void is unacceptable for a failed type lookup -- it
conflates with the real void type), use the dedicated .unresolved sentinel.
They follow addFmt(.err) so compilation aborts before codegen; behavior is
unchanged, the sentinel is now correct. 236 + unit green.
A macOS .app launched with CWD=/ (Finder/open) could not find CWD-relative
assets (read_file_bytes("assets/...")) and crashed in stbtt with a null font.
SdlPlatform.init now chdirs to SDL_GetBasePath() when running from inside a
.app bundle (detected by ".app" in the base path), mirroring uikit.sx s iOS
chdir_to_bundle. Gated so the sx run dev flow (binary not bundled) keeps the
project CWD. Verified: direct-exec with CWD=/ now stays alive (was: instant
stbtt segfault). Filed issue 0051 with the analysis.
Note: launching via Finder/open additionally triggers Gatekeeper App
Translocation for the dev-signed bundle (separate code-signing concern, not
the asset path).
Var-init placeholders that could leak when a lookup failed now init to
.unresolved: struct field-not-found (lowerFieldAccess/store), match payload
variant-not-found, deref-of-non-pointer pointee, array-literal element type.
Also fixes checks that used .s64 as the "resolution failed" sentinel and broke
when the producing functions started returning .unresolved instead:
- array-literal: `resolved != .s64` -> `!= .unresolved`.
- parameterized type-alias registration and pack-fn return-type resolution:
`!= .s64` -> `!= .unresolved` (also fixes a latent bug where a genuine
`s64` alias / `-> s64` return was treated as a failure).
- the variadic Any-boxing refinement (infer, then upgrade via getRefType) now
triggers on .unresolved, not .s64, matching the honest inferExprType.
Every silent s64 fallback in the codebase is now gone; only genuine s64<->name
mappings and the defined int-literal/tag-width defaults remain. 236 + unit green.
- 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.
A signed/unsigned width other than 8/16/32/64 quantised to s64/u64, silently
changing the size. Intern the exact .signed/.unsigned width instead (the IR
supports arbitrary-width ints). The default tagged-union tag width
(tag_type orelse .s64) is kept -- it is a defined language default, not a
failed lookup. 236 + unit green.
Converts the leftover silent s64 guesses in lowering/type-resolution paths:
- target_type orelse .s64 in struct/tagged-union/enum-literal lowering and the
xx-cast destination (the isBuiltin-guarded ones skip cleanly; the rest now
surface instead of fabricating an int).
- resolveTypeArg / parameterized-type callee-name else arms.
- generic-mangle type-param binding miss (bindings.get orelse .s64).
- optional-child helper fallthrough.
Kept the genuine int/float-literal defaults (info.ty orelse .s64/.f64) which
are the language rule, not a lookup failure. 236 examples + unit green.
inferExprType now returns .unresolved when it genuinely cannot infer a type,
instead of silently guessing .s64. To keep codegen correct, every consumer
that turns inference into a concrete type was fixed to resolve it properly
rather than lean on the fake s64:
- pack-fn mono: value-pack params type from the lowered Ref (getRefType);
comptime ..$args prefers inference (int-literal default is s64) and falls
back to the lowered type only when inference cannot tell.
- if-expr / match merge result type: fall back to the contextual target_type
when the branch/arm type is not statically inferable; a statement match with
non-value arms stays void (do not let a leaked target_type make it a value).
- inferExprType call arm: resolve a not-yet-lowered function return type from
fn_ast_map (void for a return-less fn) instead of falling through.
- lowerBinaryOp: type the result from the lowered LHS when inference is
unresolved (e.g. #objc_call(...) * 2).
- null comparison (x == null): lower the non-null side first and take the
null type from it, never a guess.
A consequence: `xx enum` with no target type now boxes as Any (prints the
variant name) instead of the silent-s64 int -- examples/52 snapshot updated to
the honest output. 236 examples + unit tests green.
lowerPackFnCall computed pack arg types via inferExprType *before* lowering
the args, then lowered them anyway. For a value-pack (..xs: P) the lowered
value has an authoritative concrete type, so take the pack type from
getRefType of the lowered Ref instead of a speculative inferExprType guess --
this removes the dependency that made a monomorphised pack param able to end
up wrong/.unresolved from incomplete static inference. Comptime ..$args packs
keep inferExprType (their args may be type-position). Also drops the dead
runtime_arg_types list (collected, never read). 236/236 green.
resolveFieldType (field-not-found, tuple OOB/parse-fail), getElementType
(element-of-a-non-collection), resolveArrayLiteralType, and the named-type
lookup in the type-call resolver all guessed .s64 when resolution failed --
the issue-0042 silent-default class. Return .unresolved so a genuine
resolution failure surfaces (and trips the sizeOf/toLLVMType panic) instead
of fabricating an 8-byte int. Genuine results (.len => .s64) unchanged.
The OOB-index and missing-binding cases already emit a real user-facing
diagnostic, but returned a plausible .s64 -- which would silently fabricate
an 8-byte int if compilation continued past the error. Return the
.unresolved sentinel instead (trips the sizeOf/toLLVMType panic at codegen).
Diagnostic text unchanged, so snapshots are unaffected.
The pack-aware caller (resolveTypeWithBindings) resolves pack-index type
exprs against the active binding before delegating, so reaching this bare
type_bridge path means the binding was missing. .s64 silently fabricated
an 8-byte int; return the .unresolved sentinel so it surfaces (trips the
sizeOf/toLLVMType panic at codegen). Closes the last .s64 escape in
resolveAstType.
A null type node means a caller reached type resolution without a type
node. Every current caller passes a non-optional node or handles the
"no type" case itself (returning .void), so a null here is a caller bug;
.s64 silently fabricated an 8-byte int. Return the .unresolved sentinel
so it surfaces (trips the sizeOf/toLLVMType panic at codegen).
The only thing relying on the old behavior was a unit test asserting
null => .s64 -- i.e. a test pinning the silent default. Updated it to
pin .unresolved.
A non-type AST node reaching type resolution is a caller bug; returning a
plausible .s64 silently fabricated an 8-byte int. Return the .unresolved
sentinel so it surfaces (and trips the sizeOf/toLLVMType panic if it ever
reaches codegen). The stderr breadcrumb stays. No test exercised this arm
(suite unchanged), so nothing was relying on the fabricated s64.
An unannotated param resolving to a plausible .s64 was the classic
silent-default trap (root of the 2.5 multi-param-closure bug). Replace it
with a dedicated TypeId.unresolved at slot 0, so a zero-initialised or
forgotten TypeId trips the sentinel instead of masquerading as a real type.
- types.zig: TypeId.unresolved = 0 (void moves to 17); TypeInfo.unresolved;
sizeOf/toLLVMType @panic on it (codegen tripwire); hash/eql/printer cover it.
- type_bridge: inferred_type => .unresolved (was .s64).
- resolveParamType: emit "parameter 'x' has no type annotation" for a
genuinely-unannotated value param (comptime/variadic/pack params exempt --
they resolve via per-call substitution).
- lowerLambda: resolve unannotated params from the target closure signature;
otherwise emit "cannot infer type of lambda parameter".
- CLAUDE.md: .void documented as an UNACCEPTABLE failed-type sentinel (it
conflates with a real, heavily-checked type); prescribe a distinct
.unresolved-style value + codegen tripwire.
Snapshot churn: one .ir (ffi-objc-call-06) -- the runtime type-name table and
typeof match arms renumber by the new builtin slot; program output unchanged.
An untyped lambda (a, b, c) => ... now takes each param's type
positionally from the expected Closure(T0, T1, T2) -> R signature, for
heterogeneous param types, in both assignment and argument position.
Previously only the first param (or all-same-typed params) resolved:
lowerLambda's signature loop applied contextual typing into params, but
the return-type-inference temp scope and the body param binding both
re-resolved each param via resolveParamType -- which defaults an untyped
(inferred_type) param to s64. So b in Closure(s64, string) bound as s64
and b.len errored. Both sites now read the already-resolved signature
types params.items[user_param_base + i].ty (user_param_base skips the
pre-populated ctx/env slots).
Regression: examples/201-closure-contextual-params.sx.
Note: a generic return $R inferred through a closure-typed parameter is
still unresolved (folds into Phase 4 function monomorphization); concrete
returns work.
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.
`xs.T` projects each pack element's protocol type-arg into a type list, usable
in TYPE/signature positions:
- tuple type `(..xs.T)` → e.g. `(s64, string)` (new resolveTupleTypeWithBindings)
- closure sig `Closure(..xs.T) -> R` → e.g. `Closure(s64, s64) -> s64`, which
contextually types a closure literal (resolveClosureTypeWithBindings now
expands a protocol pack via packTypeArgs).
Wired `tuple_type_expr` into `resolveTypeWithBindings` (type_bridge's tuple
resolver is stateless — can't see packs). `packTypeArgs(pack_name, projection)`
is shared: bare `..xs` → element types (`pack_arg_types`); `..xs.T` → each
element's `impl Box(args) for elem` target_arg (`elementProtocolTypeArg` scans
`param_impl_map`). In type position `xs.T` parses as a dotted `type_expr`, so
packTypeElems splits on '.'. examples/199-pack-type-projection.sx.
This completes 2.3's core: all spread/projection forms — call-arg, tuple value,
tuple type, closure sig — now lower. The canonical's `Closure(..sources.T)` /
`mapper(..sources.value)` / `(..sources)` shapes are functional.
A `spread_expr` element inside a tuple literal now expands the pack into the
tuple's fields: `(..xs.get)` ≈ `(xs[0].get(), …, xs[N-1].get())` (Decision 2 —
a pack is stored by materializing a tuple). lowerTupleLiteral detects a
pack-spread element via packSpreadRefs and splices the per-element Refs as
fields (typed via getRefType); for Box(T) the materialized tuple is
heterogeneous. A spread whose operand isn't a pack falls through to the
existing spread_expr diagnostic (tuple-value spread not yet handled).
When any element is a spread, field-count ≠ element-count, so the contextual
target-tuple alignment is skipped (field types inferred from the expanded refs).
examples/198-pack-tuple-materialize.sx.
A pack spread in call-arg position now expands to N positional args:
`add2(..xs.get)` ≈ `add2(xs[0].get(), xs[1].get())` — the canonical's
`mapper(..sources.value)` shape. The call-arg loop detects a spread whose
operand is a pack (`..xs`) or a pack projection (`..xs.method`) and splices the
per-element Refs in; a runtime-slice spread (`..arr`) is still left to the
slice-variadic path.
Factored the per-element synthesis out of lowerPackValueProjection into
`lowerPackElems` (used by both projection-to-tuple and spread-to-args), plus a
`packSpreadRefs` helper. examples/197-pack-spread-call.sx (2- and 3-arg, mixed
element types).
`xs.<method>` over a constrained pack projects a (zero-arg) protocol method
across every element into a tuple: `xs.get` ≈ `(xs[0].get(), …, xs[N-1].get())`.
lowerFieldAccess intercepts `xs.<m>` on a pack base (where <m> is a protocol
method) and synthesizes/lowers `xs[i].<m>()` per element into a tuple_init.
For a parameterised `Box(T)` the projected tuple is heterogeneous (each element
returns its own T). examples/196-pack-value-projection.sx.
Surfaced and fixed a pre-existing bug: inferExprType didn't handle tuple field
access (`t.0` / `t.x`), so a mixed-size tuple like `(42, "hi")` inferred the
string field as s64 — the wrong type then drove a bad `print` pack mangle and
coerced the string to i64 (garbage). Added the tuple arm (numeric + named).
Regression: a `(s64, string)` case in examples/190-tuple-values.sx.