Scratch file path violated the project rule 'scratch files go in
.sx-tmp/, never /tmp'. Route the temp .bin through the repo-local,
gitignored .sx-tmp/ dir, creating it at runtime via create_dir_all
(mirroring 0713) so the test is self-contained on a clean checkout.
Digest assertions and output are unchanged.
`any_to_string` runs `type := type_of(val)`; for an `.any` operand
`type_of` lowers to `struct_get(val, 0)` to read the Any's tag. At
runtime a first-class Type value is the aggregate `{ tag=.any, value=tid }`
so the read succeeds, but the comptime interpreter stores a Type as a bare
`.type_tag(tid)` and the comptime `struct_get` arm had no case for it — it
raised `CannotEvalComptime`, which `runComptimeSideEffects` swallowed into
`void_val`, truncating the `#run` while still building with exit 0.
- interp.zig: comptime `struct_get` handles a `.type_tag(tid)` base by
mirroring the runtime Any-Type layout (field 0 -> `.any` tag, field 1 ->
the type id), so `type_of` of an Any-held Type evaluates as it does at
runtime and execution continues.
- emit_llvm.zig: `runComptimeSideEffects` no longer swallows a side-effect
bail; it prints a loud diagnostic and sets `comptime_failed`
(-> error.ComptimeError, non-zero exit), matching the const-init path.
A truncated `#run` can no longer ship a successful build.
Regression: examples/0613-comptime-print-any-type.sx (all five lines print,
exit 0). Resolves issue 0096.
A backtick raw value-shadow receiver (`` `f64 := … `` then `` `f64.epsilon ``,
`` `s8.max ``) was misclassified as the builtin numeric-limit accessor by the
shared compile-time evaluators. The sibling `isFloatValuedExpr` already guards
this with an `is_raw` check, but `evalConstFloatExpr` / `evalConstIntExpr` did
not — so once a raw value-shadow's field read flowed into the unified float→int
narrowing rule or an array-dim count, the float folder returned the BUILTIN
`f64.epsilon` (2.22e-16) and wrongly errored, and the integer folder turned
`` `s8.max `` into the builtin `127` (a fabricated 127-element array).
Both evaluators' field-access arms now mirror `isFloatValuedExpr`'s `is_raw`
guard: a raw receiver yields `obj_name = null`, so it is never a
numeric-limit/pack leaf and falls through to the ordinary runtime field read. A
raw value-shadow is a mutable-local field (an observable later reassignment),
so it is genuinely runtime and must not be const-folded — it now behaves exactly
like a plainly-named field read: `` `f64.epsilon `` narrowing into `s64`
truncates its field value (11.5 → 11, identical to `b.epsilon`), and `` `s8.max ``
as an array dimension is rejected as a non-constant count (identical to `b.max`).
The bare builtin path is unchanged.
Regression (issue 0095 / F0.11-7):
- examples/0169-types-value-shadow-field-narrowing.sx (positive — raw float-field
read narrows/truncates, mutation proves runtime, bare limit still folds)
- examples/1148-diagnostics-value-shadow-field-dim-not-const.sx (negative — raw
int-field dim rejected as non-const)
- program_index.test.zig "a backtick raw-shadow receiver is a field read, not a
numeric-limit fold (F0.11-7)"
specs.md + readme.md note the value-shadow rule extends into the narrowing/count
contexts.
The shared compile-time integer folder (`evalConstIntExpr`) accepts an
integral float literal/const as an integer leaf (`[4.0]` → 4) and then
applied INTEGER arithmetic to the whole expression — so `5.0 / 2.0` folded
as `divTrunc(5,2)` = 2 instead of float division (`2.5`). The bug fired at
all FIVE unified-rule sites (typed local, field default, param default,
typed const, array dimension), because the typed sites evaluate through
`evalConstFloatExpr` (which delegates the node to the int folder) and the
count sites through `foldCountI64` (int folder first).
Fix at the single root: `evalConstIntExpr`'s `.div` arm refuses to fold a
division whose lhs/rhs is float-valued (`isFloatValuedExpr`), so the value
surfaces through `evalConstFloatExpr` + the unified rule — an integral
quotient (`6.0 / 2.0` → 3) folds, a non-integral one (`5.0 / 2.0` = 2.5,
mixed `5 / 2.0`, float-const `F / G`) errors. Genuine integer `/` (`5 / 2`
→ 2) is unchanged; `*`/`+`/`-` need no guard (they agree between int and
float for the integral operands the int folder ever sees).
`isFloatValuedExpr` judges a const-leaf by VALUE (`moduleConstIsFloatTyped`
recurses into the const's value with the existing cycle-guard frame), so an
untyped float-EXPRESSION const (`ME :: 4.0 + 1.0`, placeholder type s64) is
caught at both the count path and — via `foldComptimeFloatInit`'s guard —
the typed-binding path. A backtick RAW receiver (`` `f64.epsilon ``) is a
field read, not a float limit (is_raw check, issues 0092/0093).
Regression: examples/1147 (negative — `5.0 / 2.0` errors at all five sites
plus untyped float-EXPR const div); 0168 extended (positive — `6.0 / 2.0`,
`12.0 / 4.0`, `[6.0/2.0]`, `xx (5.0/2.0)` → 2); unit tests "the int folder
refuses a FLOAT division" and "moduleConstIsFloatTyped judges a const by
VALUE". specs.md + readme.md state the float-`/` rule.
The compile-time float evaluator lagged the integer one: it had no
numeric-limit field-access arm, so `y : s64 = f64.true_min + 0.5` (=0.5)
silently truncated to 0 even though the direct `f64.true_min` already
errored; the arm-by-arm audit also found a missing `%` arm, so
`y : s64 = 5.5 % 2.0` (=1.5) silently truncated to 1.
Bring evalConstFloatExpr to PARITY with evalConstIntExpr:
- Add a `.field_access` arm resolving a builtin FLOAT numeric-limit
accessor (`f64.max`, `f32.epsilon`, `f64.true_min`, …) via the SAME
`type_resolver.floatLimitFor` that `lowerNumericLimit` uses — the float
twin of the int evaluator's `integerLimitFor` arm.
- Add a `.mod` arm via `@rem` (matching evalConstIntExpr and codegen's
`frem`): `6.0 % 4.0` folds to 2 (via int delegation), `5.5 % 2.0` = 1.5
is rejected.
The two evaluators now share every leaf/operator shape, so no
compile-time-const float form escapes the unified float→int rule at one
site while folding at another. All five sites (local/field/param/const/
array-dim) stay consistent.
Regression: 0168 (positive) adds `f64.max - f64.max` → 0, `6.0 % 4.0` → 2,
integer-limit `s8.max`/`[u8.max]` unregressed, `xx` escapes for both new
forms; 1146 (negative) adds `f64.true_min + 0.5` and `5.5 % 2.0` erroring
at a binding site; program_index.test.zig covers the floatLimitFor arm and
the `%` arm. specs.md + readme.md state the parity. issues/0095 RESOLVED
banner gains the attempt-5 entry.
The compile-time count fold (array dimension / Vector lane / value-param) was
integer-only: it folded a DIRECT integral float literal (`[4.0]`, `[N]` with
`N : f64 : 4.0`) but rejected an INTEGRAL expression built from a non-integral
float-const leaf (`[F + 1.5]` = 4.0, `F : f64 : 2.5`) — and a const folded from
one (`[K]` with `K : s64 : F + 1.5`) — as "must be a compile-time integer
constant". This was the last of issue 0095's five narrowing sites (local /
field / param / const / array-dim) still diverging.
Route the count fold through the SAME compile-time float evaluation the other
four sites use:
- New `program_index.foldCountI64` — the single int-or-integral-float count
fold: `evalConstIntExpr` first, then (only on failure) `evalConstFloatExpr` +
`floatToIntExact`. `foldDimU32` (dim/lane/u32 value-param), the non-u32
value-param gate, and `emitModuleConst`'s integer-const materialization all
delegate to it, so a const's emitted value and its use as a count come from
one fold (no parallel integral check, no two-resolver divergence — issue 0083).
- New `DimU32.non_integral_float` variant carries a non-integral float dim to a
distinct, accurate diagnostic ("array dimension must be an integer, but '2.75'
is a non-integral float") — the cast-escape advice the binding sites give does
not apply in a count position, so the dim wording omits it. `reportDimError`,
the Vector-lane resolver, and the top-level array-alias diagnostic all handle
the new variant, so the DIRECT and type-ALIAS forms emit the identical message.
- `type_bridge.StatelessInner.lookupFloatName` (via `moduleConstFloat`) is the
float twin of its `lookupDimName`, so the registration-time alias path folds a
float-const-leaf dimension to the SAME count as the stateful direct path.
`inline for` range bounds are spec endpoints, not counts (specs.md §2), so they
keep the int-only fold deliberately (no silent-truncation bug there).
Relaxes the F0.4 `examples/1132` wording: a non-integral float const dim now
reports the precise "non-integral float" message (it still errors).
Regression: 0168 (positive — `[F + 1.5]s64`, `[KF]s64`, alias `ArrFE` all fold
to len 4), 1146 (negative — `[F + 0.25]s64` errors), 1132 (precise wording), and
a `foldCountI64`/`foldDimU32` unit test. issues/0095 marked RESOLVED (attempt 4).
specs.md + readme.md state the unified rule across all five sites.
Completes issue 0095: a non-integral float→int narrowing via a FLOAT-const
leaf (`F : f64 : 2.5; y : s64 = F + 0.25` = 2.75) silently truncated to 2.
`evalConstFloatExpr` delegated only INTEGER leaves to `evalConstIntExpr` and
had no float-const leaf arm, so the unified rule never saw the value.
- program_index.zig: add `moduleConstFloat`/`moduleConstFloatFramed` — the f64
twin of `moduleConstInt` (same `isCountableConstType` gate, same cyclic-
definition frame), recovering a numeric module const's value via
`evalConstFloatExpr`. Add `lookupFloatName` to `ModuleConstCtx` and the
`.identifier`/`.type_expr` leaf arms to `evalConstFloatExpr` that call it.
Integer / integral-float leaves keep resolving through the existing
`evalConstIntExpr` delegation, so the unified rule now applies to ANY
compile-time-constant float expression — literal, int-const leaf, float-const
leaf, and combinations — at every binding site.
- lower.zig: add `Lowering.lookupFloatName` delegating to `moduleConstFloat`.
Route `typedConstInitFits`' integral-fold check through `evalConstFloatExpr` +
`floatToIntExact` (the SAME facility `foldComptimeFloatInit` uses) instead of
the int-only `evalComptimeInt`, which folded leaf-by-leaf in i64 and so
rejected an integral SUM built from a non-integral float leaf
(`K : s64 : F + 1.5` = 4.0 now folds; `K : s64 : F + 0.25` errors).
A LOCAL `::` const leaf is a scope ref (not in the const tables) so neither
the int nor float evaluator folds it — float now matches int exactly there.
Regression: examples/1146 (negative) + 0168 (positive) extended with
float-const-leaf cases at local/field/param/const; unit test in
program_index.test.zig covers the leaf resolution (F→2.5, F+0.25→2.75,
F+1.5→4.0). specs.md + readme.md state the rule covers any compile-time-const
float expression incl. float-typed const leaves. issues/0095 banner updated.
Gate: zig build + zig build test green; 447 examples pass, 0 failed.
Completes issue 0095 (attempt 2). The attempt-1 coerce arm only caught a direct
`const_float` literal, so a non-integral const-folded float EXPRESSION still
truncated silently at a typed local / field default / param default:
M :: 2;
local : s64 = M + 0.5; // → 2 (silent truncation — BUG; now ERRORS)
fld : s64 = M + 0.5; // field default — same
take(x : s64 = M + 0.5) // param default — same
while the typed-CONST site already errored. The integral expression
(`M + 2.0` → 4) folded but the runtime/explicit-cast paths must stay untouched.
Fix:
- New `program_index.evalConstFloatExpr` — the f64 counterpart to
`evalConstIntExpr`, delegating every integer subtree back to it (no parallel
integer logic) and adding only the float literal / unary-negate / `+ - * /`
arms. Pure (no diagnostics, no resolution side effects).
- `Lowering.foldComptimeFloatInit` applies the unified rule to a typed-binding
initializer EXPRESSION: an integral comptime float folds to its `constInt`, a
non-integral one errors, a genuine runtime float / `xx`-cast falls through to
the normal path. It runs `evalConstFloatExpr` FIRST (pure) so a `$pack[i]`
argument is never spuriously type-resolved outside an active binding, then
gates on `isFloat(inferExprType)` so a plain comptime int is left alone.
Wired into the typed-local path, the three struct field-default sites (via a
shared `lowerCoercedDefault`), and the call-argument loop (covers expanded
param defaults).
- One `Lowering.diagNonIntegralNarrow` now emits the narrowing wording at all
five sites (coerce arm, global init, const-expr value, the typed-binding
sites, and the typed-const path). The typed-CONST non-integral diagnostic
therefore reads "cannot implicitly narrow non-integral float …" instead of
the stale "initializer is a float literal / floating-point expression".
Tests: examples/1146 (negative) extended with non-integral const-EXPRESSION
cases at local/field/param; examples/0168 (positive) extended with integral
const-EXPRESSION folds and `xx (M + 0.5)` truncation; examples/1143 reconciled
to the aligned const message (G/BAD/BAD2 stay errors); unit test
`evalConstFloatExpr folds comptime float expressions`. Full gate green (447).
Issue 0095: a typed local/param/field silently TRUNCATED a float initializer
to an integer annotation (`y : s64 = 1.5` → 1) with no diagnostic. Agra ruled
the UNIFIED rule (Option B): an implicit float→int in a typed binding behaves
like the array-dimension rule —
- an INTEGRAL compile-time float FOLDS to its int (`4.0` → 4, `-2.0` → -2);
- a NON-integral float is a COMPILE ERROR (`1.5`, `4.5`);
- explicit `xx` / `cast(T)` ALWAYS truncates (the escape hatch).
Applied consistently to typed local / param-default / field-default, typed
module CONST, and array dim — all reusing the single
`program_index.floatToIntExact` / `evalConstIntExpr` facility (no second
integral check).
- `Builder.constFloatInfo` reads a compile-time `const_float` back from its
Ref (value + span).
- `coerceToType` is now the IMPLICIT path: its `.float_to_int` arm folds an
integral const-float to `constInt`, else emits the narrowing diagnostic.
`coerceExplicit` is the raw truncating path; `xx` (lowerXX) and `cast(T)`
route through it so the escape still truncates.
- Field-default lowering (struct-literal pad, named-field default,
buildDefaultValue) now coerces the default to the field type at the IR level
(was silently bit-coerced by emitStructInit).
- Const path: `typedConstInitFits` accepts an integral float (literal or a
`M + 2.0`-style expression folding via `evalComptimeInt`); `emitModuleConst`
/ `constExprValue` / `globalInitValue` fold an integral float to its int and
reject a non-integral one — relaxing F0.7's blanket float rejection.
Tests: examples/0168 (positive: local/field/param/const fold, xx/cast
truncate), examples/1146 (negative: local/param/field error), integral-float
const cases added to examples/0162; non-integral const cases in 1143 stay
errors. specs.md + readme.md document the unified rule, cross-referencing the
array-dim rule. issues/0095 marked RESOLVED.
Migrate lowerAssignment's `.field_access` target onto the shared
`fieldLvaluePtr` resolver, deleting its duplicated union / promoted /
tuple / vector / struct walk. All three lvalue field-store sites —
single-assign, address-of (lowerExprAsPtr), and multi-assign
(lowerMultiAssign) — now resolve through the one resolver, removing the
issue-0083 two-resolver divergence.
Fold vector-lane resolution into `fieldLvaluePtr` (reusing
vectorLaneIndex) so the single resolver covers struct fields, union
direct fields, promoted anonymous-struct union members, tuple elements,
and vector lanes — null only on a genuine miss, which every caller turns
into the read path's `emitFieldError` diagnostic.
`fieldLvaluePtr` now types every field GEP `*field_ty` (the convention
the single-assign path always used), not the bare field value type:
emitStore unwraps one pointer level to find the stored value's type.
The earlier lowerExprAsPtr / lowerMultiAssign walks typed the GEP with
the bare field type, so a field whose own type is a pointer-to-aggregate
(`*Pair`, a two-pointer struct) made emitStore unwrap to the aggregate
and coerceArg's closure auto-promotion store a 16-byte `{ptr,null}`
struct over the 8-byte slot, clobbering the neighbouring field.
Consolidating onto the one `*field_ty` resolver preserves single-assign
and fixes that pre-existing multi-assign / address-of clobber.
The types.zig `.unresolved` tripwire is untouched; no `.s64` / `.void` /
`.unresolved` default remains.
Regression: examples/0167-types-ptr-to-aggregate-field-store.sx (a
`*Pair` field stored via all three lvalue sites leaves the neighbour
intact) + a lowering unit test asserting the `*field_ty` GEP convention.
Completes the issue-0094 fix. attempt-1 made single-assign and address-of
diagnose a missing struct field; the stress-review found two remaining defects
in that change:
1. lowerMultiAssign's `.field_access` target kept the pre-fix shape — a
struct-only loop that defaulted `field_idx 0` / `field_ty .unresolved` on a
miss, then built the GEP and stored unconditionally. A missing field
(`p.q, y = 2, 3`) silently wrote field 0 (printed `x=2 y=3`, no diagnostic),
and a valid promoted-union / tuple member at a non-zero offset corrupted
field 0 instead of its own slot.
2. attempt-1's new union branch in lowerExprAsPtr resolved only DIRECT union
field names, so `@v.x` on a promoted anonymous-struct member reported
"field 'x' not found on type 'Vec2'" even though `v.x = 41` worked.
Both lvalue-pointer sites and the multi-assign store now route through one
shared resolver, `fieldLvaluePtr`, that handles struct fields, union direct
fields, promoted anonymous-struct union members, and tuple elements, and
returns null (no field-0 / `.unresolved` default) on a genuine miss. Each
caller emits the read path's `emitFieldError` on null. This collapses the
three previously-divergent field-lvalue walks into one, fixing the
multi-assign missing-field corruption, the promoted-member over-rejection,
and (as a side effect of correct resolution) non-zero-offset promoted-union
and tuple multi-assign stores. The types.zig tripwire is untouched.
Regression tests:
- examples/1145 extended: multi-assign missing field (`p.r, y`) errors, exit 1.
- examples/0166 (new): promoted union member written and address-of'd,
including a non-zero-offset member (`@v.y`), compiles and runs.
- src/ir/lower.test.zig: multi-assign missing-field field-not-found unit test.
Assigning to a nonexistent struct field (`p.q = 2` where Point has no `q`)
aborted the compiler with the `.unresolved` LLVM tripwire instead of a source
diagnostic (issue 0094). The lvalue field lookup never diagnosed a miss:
- `lowerAssignment`'s `.field_access` target left `field_ty = .unresolved` when
no struct field matched, then built `ptrTo(field_ty)` and stored — so a
pointer-to-`.unresolved` reached LLVM emission and tripped the panic.
- `lowerExprAsPtr`'s `.field_access` fallback returned
`structGepTyped(obj_ptr, 0, .s64, obj_ty)` on a miss — a silent field-0/`.s64`
default that mislowered the lvalue.
Both sites now reuse the read path's `emitFieldError` (the exact facility
`lowerFieldAccessOnType` uses), so read and write reject identically with
`field 'q' not found on type 'Point'`. `lowerExprAsPtr` also resolves
union/tagged-union fields via `union_gep` (the old `.s64` fallback was silently
standing in for union field access — e.g. `u.a[0] = v`), so that path is fixed,
not just made loud. The `types.zig` tripwire is untouched: the fix is to never
produce `.unresolved` for a missing-field store.
Regression tests:
- examples/1145-diagnostics-missing-struct-field-assign.sx — negative, both
sites error, exit 1.
- examples/0165-types-nested-struct-field-assign.sx — positive, nested struct
field write + address-of a matched field still work.
- src/ir/lower.test.zig — lowering unit test asserting the field-not-found
diagnostic for a missing-field assignment.
The 7 type-only builtins doc claimed all of them accept a runtime Type
value, but only type_name and type_is_unsigned do. The other five
(size_of, align_of, field_count, type_eq, is_flags) are compile-time-only
— a runtime Type value (type_of(x)) yields 'unresolved type' since
runtime reflection is deferred. Reword both docs to the accurate scope.
Verified: type_name(type_of(x))=u32, type_is_unsigned(type_of(x))=true;
size_of(type_of(x)) / align_of(type_of(x)) -> error: unresolved type.
The Type Category Matching example showed the old signed-only arm
(case int: result = int_to_string(xx val);), which would reproduce the
pre-fix unsigned mis-rendering (u64 -> -1) if followed. Update it to mirror
library/modules/std.sx:370 — branch on type_is_unsigned(type) so unsigned
types route to uint_to_string, with a one-line clause explaining the split.
`type_name` / `type_is_unsigned` on an `Any` argument unconditionally read
the Any's payload as a TypeId index. That is correct only when the Any holds
a Type value (`{ .any, tid }`); for an Any holding a runtime *value*
(`av : Any = 6`, tag s64, payload 6) it returned `types[6]` — `type_name(av)`
gave "u8" and `type_is_unsigned(av)` gave true.
Both backends now branch on the Any's runtime type-tag: tag == `.any` → the
box is a Type value, use the payload as the TypeId; otherwise the tag IS the
held value's type. So `type_name(av)` → "s64", `type_is_unsigned(av)` → false,
while `type_name(type_of(x))` still names the held type. The `{}` formatter is
unchanged (it already passed `type_of(val)`, a proper Type value).
- src/ir/interp.zig: shared `Value.reflectTypeId` tag-branching resolver; the
`type_name` / `type_is_unsigned` interp arms route through it.
- src/backend/llvm/ops.zig: shared `Ops.reflectArgTypeId` emits
extractvalue-tag / icmp-eq-.any / select for the runtime path; both
reflection arms route through it. The two backends agree.
- examples/0164-types-reflection-any-tag.sx: regression pinning type_name /
type_is_unsigned / print on an Any holding a value vs a Type.
- src/ir/interp.test.zig: unit test for `reflectTypeId`.
- 22 .ir snapshots: the new select appears in every std-importing program's
IR (any_to_string embeds these builtins) — benign, verified structurally
identical apart from the three new instructions.
- issues/0090, specs.md: documented the Any-tag rule.
size_of, align_of, field_count, type_name, type_eq, type_is_unsigned,
and is_flags silently reinterpreted a value argument as a type:
type_is_unsigned(6) read 6 as a TypeId index (types[6] = u8 -> true),
size_of(6)/size_of(true) sized its typeof (8), type_name(6) returned
types[6]'s name. Per Agra's ruling, all 7 now strictly require a type
(compile-time): a value argument is a compile error.
One shared guard (Lowering.reflectionTypeArgGuard, run at the top of
tryLowerReflectionCall) classifies each arg via reflectionArgIsType: a
spelled / compile-time type or generic type parameter (isStaticTypeArg),
or a runtime Type value (static type .any -- type_of(x), a []Type
element list[i], a Type-typed local/field/param) is accepted; anything
else is rejected with "<builtin> expects a type, got '<type>'". The
runtime path for type_name / type_is_unsigned is preserved (the {}
formatter calls type_is_unsigned(type_of(val)) at runtime). The 5
comptime-only builtins stay comptime-only (runtime reflection deferred).
Regression: examples/1144-diagnostics-reflection-builtin-needs-type.sx
(reject cases across all 7, exit 1). Unit test: reflectionArgIsType in
lower.test.zig. specs.md / readme.md document the strict type
requirement (and add the previously-undocumented align_of, type_eq,
type_is_unsigned). issues/0090 RESOLVED banner updated.
Resolves issue 0090. The `{}` integer formatter mis-rendered both ends of
the 64-bit range:
- `int_to_string` computed the magnitude as `0 - n`, which overflows for
`s64::MIN` (its magnitude is unrepresentable as a positive s64) — the
value stayed negative, the digit loop ran zero times, so only `-`
printed. It now extracts digits straight from `n` (per-digit
`|n % 10|`, `n` truncating toward zero), never negating MIN.
- `any_to_string`'s `case int:` formatted every integer as s64, so a u64
all-ones value printed as `-1`. There was no `uint` type-category to
distinguish signedness. Added an additive `type_is_unsigned(T)`
reflection builtin (static fold + dynamic interp/LLVM paths, mirroring
`type_name`), backed by the new `TypeTable.isUnsignedInt` predicate, and
a `uint_to_string` formatter (unsigned decimal via long-division over
four 16-bit limbs). `case int:` routes through `type_is_unsigned(type)`.
The 16-bit-limb split is factored into a shared `decompose_u16x4`, now
reused by `int_to_hex_string` (no second unsigned-math routine).
Regression: examples/0046-basic-int-formatter-extremes pins both extremes
plus a width spread; unit tests cover `isUnsignedInt`. Docs (specs.md
representation note, readme std API) updated for unsigned/extreme `{}`
behavior. IR snapshots refreshed for the two new std functions.
`ExprTyper.inferType`'s binary-op arm inferred every non-comparison op
from the LHS alone, so `M + 0.5` (s64 + f64) statically typed as s64
while `0.5 + M` typed as f64 — operand-order-dependent. The value path
(`lowerBinaryOp`) already promoted int×float → float, so static
inference disagreed with the value: `M + 0.5` formatted as a truncated
int and a typed const `BAD : s64 : M + 0.5` was accepted+truncated
(issue 0088 mixed-numeric escape).
Extract the value path's inline promotion into a shared
`Lowering.arithResultType(lhs, rhs)` and reuse it at both sites, so
arithmetic / bitwise / shift inference reports exactly the type the
lowered value carries — int LHS × float RHS → the float, order-
independent. The value-path behavior is unchanged (the block is moved
verbatim into the helper), so no IR shifts; the suite stays green. The
typed-const validation reuses `inferExprType`, so this auto-closes the
escape with no change to the validation logic.
- examples/1143: BAD/BAD2 (`s64 : M + 0.5`, `s64 : 0.5 + M`) rejected
in both operand orders.
- examples/0162: MF/MFR (`f64 : M + 0.5`, `f64 : 0.5 + M`) fold to 2.5.
- examples/0163 (new): pins the inference fix in a value context
(`print("{}", n + 0.5)` formats the float, both orders, +-*/, f32).
- expr_typer.test.zig: arithResultType + mixed-arithmetic inference.
- specs.md / readme.md: document the numeric-promotion rule.
- issues/0088: RESOLVED banner notes the inferExprType root fix.
Attempt 1 rejected only LITERAL initializers that mismatch a typed module
const's annotation; a const-EXPRESSION initializer escaped, so the same
issue-0088 root remained for `M :: 2; N : string : M + 2` — accepted at exit 0,
folding `[N]s64` to 4 and printing N as an integer.
Root cause: `registerTypedModuleConst` validated only the enumerated literal
node kinds; any other kind fell through to `else => {}`, and pass 0
pre-registers binary_op/unary_op consts as a `.s64` placeholder that was never
reconciled with the annotation.
Fix — validate by TYPE, not by node kind:
- lower.zig: `registerTypedModuleConst` now covers literals AND const-expressions
(binary_op/unary_op) through one path. `typedConstInitFits` keeps the literal
arms and routes any non-literal through the new `constExprInitFits`, which
compares the initializer's INFERRED type (`inferExprType`, the existing
type-inference facility — no second const evaluator) to the annotation with the
same integer/float compatibility. A mismatch emits the `type mismatch` diagnostic
(a const-expression is described by its inferred type, e.g. "an integer
expression") and evicts the pass-0 placeholder; a match registers the const at
its resolved annotation type (the same `put` the literal path always did), so a
const-expression folds and emits at its declared type.
- `literalKindName` → `initializerDescription` (+ `constExprDescription`) so the
message is accurate for both a literal and a const-expression initializer.
Regression:
- examples/1143: extended with `E : string : M + 2` and `V : string : -M`
(const-expr mismatches → exit 1, pinned diagnostics).
- examples/0162: extended with `KE : s64 : M + 2` (used as a count + printed) and
`WE : f32 : M + 2` (over-rejection guard — valid const-exprs still work).
- program_index.test.zig: count-gate test extended with a binary_op value node
declared `string` (must not fold as a count).
Docs: specs.md §3 + readme.md generalized from "initializer literal" to cover
constant expressions; issues/0088 RESOLVED banner updated.
A typed module-level constant whose initializer did not match its
annotation was silently accepted: `N : string : 4` compiled, then
`print(N)` segfaulted (an integer emitted as a `string` const → a bogus
pointer) and `[N]s64` folded `N` to 4 as an integer count. Issue 0088.
Root cause: `registerTypedModuleConst` stored the annotation type but never
validated the initializer literal against it, and
`program_index.moduleConstInt` folded a const into a count by inspecting
the initializer node alone, ignoring `ModuleConstInfo.ty`.
Fix at the declaration (kills both symptoms):
- lower.zig: `registerTypedModuleConst` now validates the initializer via
`typedConstInitFits` (arms mirror `emitModuleConst`'s faithful-emit
precondition: int→int/float, float→float, bool→bool, string→string,
null→pointer/optional, `---`→any). A mismatch emits a `type mismatch`
diagnostic at the initializer span and does not register the const (also
evicting the pass-0 placeholder). Not routed through
`coercionResolver().classify`: that runtime-coercion planner is unsound
here (null's natural type is void → false-rejects `*T`; bool is 1 bit →
false-accepts s64).
- program_index.zig: `moduleConstInt` now takes the `TypeTable` and gates
the fold on `isCountableConstType(ci.ty)` (integer of any width, or a
float), so a non-numeric typed const can never fold into a count off its
initializer node. Callers in lower.zig and type_bridge.zig updated.
Regression:
- examples/1143-diagnostics-typed-module-const-mismatch.sx (negative, exit 1)
- examples/0162-types-typed-module-const-roundtrip.sx (positive)
- program_index.test.zig: gate-on-declared-type unit test
Docs: specs.md §3 Constant Binding + readme.md note the compatibility rule.
Writing a Vector lane (`v.x = …`, `.y/.z/.w` + colour aliases) panicked
with "unresolved type reached LLVM emission". The store path had no
vector branch: a `.field_access` target on a Vector fell through to
struct-field lookup, matched nothing, left `field_ty = .unresolved`, and
built a `ptrTo(.unresolved)` that tripped the LLVM emission guard. The
read path resolved the lane fine — the two had diverged (issue-0083
two-resolver class).
Extract a shared `Lowering.vectorLaneIndex` resolver and route BOTH paths
through it. The read path (`lowerFieldAccessOnType`) delegates to it,
dropping its silent `else 0` fallback. A new vector branch in
`lowerAssignment` GEPs a typed pointer to the lane (`structGepTyped`) and
stores via `storeOrCompound` (plain + compound). `emitStructGep` now
addresses a vector base type with a `[0, lane]` GEP. A non-lane field now
reports field-not-found on both paths instead of silent-lane-0 / panic.
Regression: examples/1506-vectors-lane-store.sx (panicked pre-fix, now
reads back written values) + a vectorLaneIndex unit test. Resolves issue
0086; spec documents element assignment.
Foundation milestone close — the minimal exit-code / --json contract
`dist` relies on, in pure sx (no compiler change).
- EX_OK (0) / EX_USAGE (64, sysexits.h) / EX_UNAVAILABLE (70) named
constants in std.cli.
- exit_ok() / exit_usage() terminators routing through the canonical
process.exit(code: u8) — removes the hand-rolled cli_bail_exit `_exit`
binding; the unsupported-platform path now uses proc.exit(EX_UNAVAILABLE).
- --json read is parsed.json (already parsed by F3.2); documented as the
detection point with a stdout-pure / stderr-human convention.
- examples/0718-modules-cli-exit-json.sx exercises the contract: json true
with --json / false without, EX_USAGE == 64, and a usage path that exits
64 via exit_usage() (expected .exit = 64).
- readme.md gains a std.cli command-line-interface subsection.
The issue-0092 fix guarded the numeric-limit accessor intercept against
raw value shadowing using only lexical Scope.lookup. The ordinary
identifier field-access path resolves a value through THREE sources
(scope / program_index.global_names / program_index.module_const_map),
so a backtick raw identifier bound at module scope — a global
`` `f64 := Box.{…} `` or a module constant `` `f64 :: Box.{…} `` — still
folded `` `f64.epsilon `` to the numeric limit instead of reading the
value's field (issue 0093, plus the module-const variant: same root
cause, same fix).
Fix: a single shared helper Lowering.identifierBindsValue(name) that
returns true when the name resolves through scope OR global_names OR
module_const_map. Used in BOTH lowerNumericLimit (lower.zig) and the
numeric-limit inference arm (expr_typer.zig) so the two resolvers can't
desync (issue-0083 class). A bare `f64.epsilon` / `s32.max` (a
.type_expr receiver) still folds even when a raw value of the same
spelling is bound — the bare receiver is never value-shadowed.
- examples/0161: extended to exercise all three binding kinds — a
GLOBAL `` `f32 ``, a MODULE-CONST `` `s16 ``, and LOCAL
`` `f64 ``/`` `s32 ``/`` `u8 `` — each reading its field while the
bare spelling still folds.
- src/ir/expr_typer.test.zig: unit test pinning the global +
module-const sources of the shared guard.
- issues/0093: RESOLVED banner (3-source root cause + fix, module-const
variant folded in).
- specs.md / readme.md: numeric-limit shadow note now source-agnostic
(local / global / module-const).
The numeric-limit accessor intercept (NL.1 integer `.min`/`.max`, NL.2 float
`.epsilon`/`.min_positive`/`.true_min`/`.inf`/`.nan`) treated ANY receiver
whose text matched a builtin numeric type name as a TYPE receiver, without
first checking for an in-scope VALUE binding. An F0.6 backtick raw identifier
(`` `f64 := … ``) binds a local under the stripped name `f64`; field access on
it (`` `f64.epsilon ``) parses as an `.identifier` receiver, which the intercept
silently folded to the type's numeric limit — a silent-wrong-value bug
(issue 0092).
Fix: for `.identifier` receivers, prefer an in-scope value binding
(`Scope.lookup`) over the fold — defer to ordinary field lowering when the
identifier resolves to a value. `.type_expr` receivers are unambiguous types
and are never shadowed, so a bare `f64.epsilon`/`s32.max` still folds even in a
scope where `` `f64 `` is bound (the parser classifies a bare builtin name as a
`.type_expr`). Mirrored in expr_typer.zig so inference matches lowering
(avoids the issue-0083 two-resolver desync). Float-only-on-int and
non-numeric-receiver errors are unchanged.
- src/ir/lower.zig: value-binding guard in lowerNumericLimit.
- src/ir/expr_typer.zig: same guard in the numeric-limit inference arm.
- src/ir/expr_typer.test.zig: unit test pinning the two-resolver agreement.
- examples/0161-types-numeric-limit-value-shadow.sx: regression — raw
`` `f64 ``/`` `s32 ``/`` `u8 `` value reads coexisting with bare folds.
- issues/0092: RESOLVED banner.
- specs.md / readme.md: receiver-vs-shadowing-value-binding note.
Finish NL.2 on top of the WIP compiler impl (2e9e4fe): f32/f64 expose
.min/.max plus the float-only .epsilon/.min_positive/.true_min/.inf/.nan,
folded via the shared lowerNumericLimit intercept + builder.constFloat.
- examples/0159: pins every f32/f64 accessor by untagged-union bit
reinterpret against exact IEEE-754 hex (true_min read before any
arithmetic — FTZ/DAZ), plus the defining-property checks
((1+eps)!=1 / (1+eps/2)==1, inf>max, min==-max, true_min<min_positive,
true_min>0, nan!=nan).
- examples/0160: float-only accessor on an int (s32.epsilon/u8.inf/
s64.true_min) and any accessor on a non-numeric type compile-error
cleanly (exit 1, pinned stderr).
- type_resolver.test.zig: floatLimitFor bit-pattern + property tests for
f32/f64, isLimitField coverage, null for non-float/non-limit fields.
- specs.md Numeric Limits: float accessors + the min=-max / min_positive=
smallest-normal / epsilon=ULP-of-1.0 / true_min=smallest-subnormal
clarifications, with the mandatory FTZ/DAZ flush-to-zero caveat.
readme.md overview updated.
A protocol method signature omits the receiver; a bare `self` has no type, so
`protocol { … :: (self) … }` fails at parse with 'expected :'. Correct the three
member-exemption doc snippets (readme.md, specs.md, issues/0089) to the valid
signature form, matching examples/0158's `Speaker :: protocol { s2 :: () -> s64; }`.
The member-name exemption applies only to identifier-classified reserved
spellings (s1..s64, u1..u64, bool, string, void, usize, isize, Any). f32/f64
are lexer keywords (token.zig kw_f32/kw_f64) and member-name slots require an
identifier token, so a bare f32/f64 field/tag/method name is rejected at parse;
the backtick is required there too. specs.md + readme.md corrected.
AGRA RULING (issue 0089, attempt 7): bare reserved-name MEMBER positions are
intentionally exempt from the reserved-type-name rule, and the implementation
already does the right thing — this is a docs + one-example change, no code.
The exempt member positions are struct FIELD names, union TAG names, and protocol
method-SIGNATURE names: they sit in a member slot, are reached via obj.name (or
dispatched by string), and are never type-classified, so they never mis-lower.
The backtick is optional there. The exemption stops at member DEFINITIONS: an
impl method is a real function (reached through the impl_block -> fn_decl arm), so
a reserved-spelled impl method still needs the backtick, exactly like a free
function (cf. examples/1122) — and every bare reserved-name value binding /
declaration name still errors (0076 preserved).
- specs.md / readme.md: replace the "every binding site" / "any binding site"
overclaim with the precise rule — required positions (value bindings +
declaration names + impl method definitions) vs the exempt member-name
positions (field / tag / protocol signature; backtick optional).
- examples/0158-types-reserved-name-member-exempt.sx: pins the exempt behavior —
bare reserved-name struct fields + union tag read & written bare AND via
backtick, and a protocol with a bare reserved-name method dispatched through
the protocol (impl definition takes the backtick).
- issues/0089: document the member-name exemption in the RESOLVED banner + add
0158 to the regression list.
Gate: zig build, zig build test, bash tests/run_examples.sh — all green
(430 passed, 0 failed, 0 timed out).
The codegen-side resolver was already raw-aware for the universal model;
the sema/LSP editor index (the second classifier) only honored the DIRECT
raw type. A COMPOUND raw type (`*`s2`, `?`s2`, `[N]`s2`, `[]`s2`, `[*]`s2`)
stores its inner type-name as a bare string on the Type info struct, and
every resolution site re-read it with skip_builtin=false — so the index
reclassified a user type named `s2` as the builtin int, diverging from
codegen (issue-0083 class, LSP surface only; codegen unchanged).
Structural cure: every compound info struct (Pointer/Optional/Slice/
ManyPointer/Array) carries a REQUIRED is_raw bit (no default — a future
construction site cannot drop it). is_raw is set at every construction
site (resolveTypeNode arms, fieldType arms, variadic slice, .ptr/slice_expr
derivation, for-loop by-ref, substType) and passed as skip_builtin at every
resolution site (elementTypeOf, field-access pointer unwrap, index, deref,
optional unwrap/null-coalesce, if/while optional binding, match subject).
Optional-unwrap + deref sites converted from Type.fromName/pointerPointeeType
(builtin-only, divergent) to resolveTypeNameStr(name, is_raw); the now-dead
pointerPointeeType removed.
Tests: src/sema.test.zig gains pointer/optional/array raw-vs-bare
regressions (raw → user type, bare → builtin control) — each FAILS on
pre-fix sema, PASSES after — plus a parameterized-raw coverage test.
Closes the remaining three F0.6 findings so the universal backtick raw
identifier holds in BOTH classifiers and at EVERY parser construction site.
1. Struct-body constants thread is_raw + name_span. The struct-body const
forms (untyped `` `s2 :: 5 `` and typed `` `s2 : T : v ``) built the
const_decl node without name_span/is_raw, so a backtick const was falsely
rejected and a bare reserved-name const caretted at 1:1. They now capture
both. Structural cure: `ast.ConstDecl`'s name_span + is_raw carry NO
default, so the compiler rejects any construction site that omits them
(mirrors checkBindingName's required `is_raw` arg). FnDecl keeps its
defaults — every parser fn_decl routes through parseFnDecl whose
`name_is_raw` is a required parameter (equivalent guarantee).
2. Raw identifier in TYPE position flows through the normal continuations.
parseTypeExpr no longer returns a terminal type_expr for a raw atom; the
raw flag rides the atom through the qualified-path / Closure / parameterized
continuations, so `` `s2(s64) ``, `` *`s2 ``, `` ?`s2 `` all parse.
ParameterizedTypeExpr carries is_raw; resolveParameterizedWithBindings
skips the `Vector` intrinsic when raw.
3. sema/LSP (the second classifier) honors is_raw. Type.fromTypeExpr returns
null for a raw type_expr; resolveTypeNode skips the builtin classifier when
raw; resolveTypeNameStr takes a skip_builtin arg threaded from te/id.is_raw
(compound inner names pass false). A backtick reserved-name annotation now
resolves to the user type in the editor index, not the builtin.
Tests: examples/0156 (struct-body const), 0157 (parameterized raw type +
wrappers), 1142 (bare struct-body const errors, caret on name); src/sema.test.zig
pins the LSP raw-type resolution (fail-before verified). Gate: 365 unit tests,
429 examples, 0 failed.
AGRA ruling (attempt 4): `` `name `` is THE LITERAL identifier `name`, usable in
EVERY position — the backtick only means "treat this token as a plain identifier,
never the reserved keyword/type", and is never part of the name's text.
- Raw in TYPE position is now VALID (reverses attempt-2 "raw is not a type"):
`parseTypeExpr` emits a raw `type_expr`; `TypeResolver.resolveNamed` gains a
`skip_builtin` flag (threaded from `te.is_raw` via lower.zig + type_bridge) so a
`` `s2 `` reference resolves to a `` `s2 ``-declared type (struct/enum/union/alias),
else a normal "unknown type 's2'" error (reportIfUnknownType skips the builtin
exemption when raw). Bare `s2` in type position stays the builtin int.
- Every declaration-name site is is_raw-exemptible: `is_raw` added to TypeExpr +
StructDecl/EnumDecl/UnionDecl/ErrorSetDecl/ProtocolDecl/ForeignClassDecl/UfcsAlias/
NamespaceDecl/ImportDecl/CImportDecl/LibraryDecl; parser threads name_is_raw to
every decl parse fn; namespace imports carry it through imports.addNamespace.
Typed-const path (`` `s2 : s64 : 5 ``) now threads name_span+is_raw (fixes the
1:1-caret bug).
- Check<->exemption made structurally symmetric: checkBindingName/checkDeclName take
is_raw as a REQUIRED argument and skip inside the check, so no call site can
validate a name without honoring the exemption (the desync cause of prior rounds).
- Bare reserved-name declarations of every kind still error (0076 preserved);
`#import c` foreign names stay auto-raw + bare-callable.
specs.md + readme.md updated to the universal model. issue 0089 RESOLVED banner
rewritten. Examples: replace 1139 (raw-not-a-type) with 0154 (raw type reference);
add 0155 (typed const + union tag) and 1141 (bare type-decl negatives).
Gate: zig build + zig build test + run_examples (426 passed, 0 failed).
A bare reserved-type-name `::` declaration was silently accepted, and the
attempt-2 lowerCall rewrite then made a bare `s2 :: (…) {…}` function callable —
bypassing the backtick rule for handwritten sx. The reserved-name binding check
covered `:=` / typed-local / param / captures but NOT the `::` declaration form.
- ast: `ConstDecl`/`FnDecl` carry `is_raw` + `name_span` threaded from the parser
(parseConstBinding / parseFnDecl, all call sites incl. struct/impl methods).
- semantic_diagnostics: reject a bare reserved spelling at EVERY declaration-name
site — const, function (incl. struct/impl methods), struct/enum/union/error-set,
protocol, foreign-class, ufcs alias, namespaced/library/c-import name. Backtick
(`is_raw`) and the compiler's `#builtin` definition (`string :: []u8 #builtin`)
are the only exemptions; a value whose node is itself a named decl defers to
that node's own check.
- c_import: synthesized foreign fn_decls are `is_raw = true`, so a C function
whose own name collides with a reserved spelling (`int s2(int);`) imports and
bare-calls unedited.
- lower: scope the `.type_expr`→`.identifier` call rewrite to a callee FnDecl of
RAW provenance (`is_raw`) — only a backtick / `#import c` foreign fn can carry a
reserved-name spelling, so a non-raw match never gets rewritten.
- examples: 0153 (positive — backtick `::` const + fn, bare + tick call), 1140
(negative — bare `::` const + fn rejected).
- docs: specs.md + readme.md state the backtick is required at every binding site
including `::` const / function / type declarations; issue 0089 banner updated.
Completes the issue-0089 backtick raw-identifier / `#import c` exemption
across all remaining identifier positions and closes three boundary gaps
the F0.6 review found.
1. Exhaustive raw-binding coverage. The `is_raw` bit now threads through
`ast.Identifier` and EVERY binding/capture form — `IfExpr`/`WhileExpr`
optional bindings, `ForExpr` capture + index, `MatchArm` capture,
`CatchExpr`/`OnFailStmt` tag bindings, `DestructureDecl` per-name, and
the protocol-default-body / foreign-class method param lists — not just
`var_decl`/`param`. `UnknownTypeChecker` skips the reserved-name check at
each arm when raw, so a backtick works in every identifier position while
a bare reserved spelling still errors (issue 0076 preserved).
2. Raw identifier is never a type. `parseTypeExpr`'s atom rejects a raw
identifier in type position (`x : `s2 = 1`, `List(`s2)`) with an accurate
diagnostic instead of silently type-classifying it.
3. Reserved-name function bare-callable. A bare `s2(4)` parses its callee as
a `.type_expr` (reserved spelling); `lowerCall` now rewrites a type_expr
callee to an identifier when a function of that name is in scope, so a
backtick-declared sx fn and a `#import c` foreign fn whose C name collides
with a reserved type spelling both resolve by their bare name.
(`TypeName(val)` is not a cast, so there is no ambiguity.)
Tests: examples/0152 (every control-flow/capture form + bare ref/call/member
access), examples/1054 (catch/onfail tag bindings), examples/1139 (raw in
type position rejected), examples/1220 extended (foreign reserved-name
function bare-call). 0076 negatives 1119/1121/1122/1123/1124/1125 stay green.
Gate: zig build + zig build test + 422 examples pass. specs.md + readme.md
updated; issues/0089 RESOLVED banner refreshed.
Reserved type-name spellings (s1, s2, u8, …) can now be used as value
identifiers two ways, resolving issue 0089:
1. Backtick raw identifier: a leading backtick (`s2) lexes to an
.identifier token carrying a new Token.is_raw flag, with the backtick
excluded from the text. A raw identifier is never type-classified — the
parser skips Type.fromName for it — so it is always a value identifier.
The flag threads to VarDecl.is_raw / Param.is_raw at binding sites, and
the reserved-type-name check (UnknownTypeChecker) skips raw bindings.
Because the token tag stays .identifier, the escape works in every
position (local, global, param, field, fn name, struct member, later
reference) with no per-site parser change.
2. #import c exemption: c_import.zig synthesizes foreign decls with
Param.is_raw = true, so generated C param names that collide with
reserved type names (s1, s2) import unedited.
A bare reserved-name binding in sx still errors (issue 0076 preserved):
the is_raw-gated skip only fires for backtick / foreign names, and a raw
binding's address-of / autoref lowering stays correct because every
occurrence is an .identifier, never a .type_expr.
Tests: examples/0151 (backtick, every position),
examples/1220 (foreign exemption, compiled+run), lexer unit tests.
1119 (bare-binding rejection) stays green. specs.md + readme.md updated.
emitCmpNe lowered float `!=` to `LLVMRealONE` (ordered not-equal), which
is false when either operand is NaN. That made `nan != nan` false in
native code — breaking the canonical `x != x` NaN test, making `!=`
non-complementary with `==` for NaN, and disagreeing with the interpreter.
Change the float predicate to `LLVMRealUNE` (unordered not-equal): true
if either operand is NaN OR they are unequal. For all non-NaN operands
`UNE` ≡ `ONE`, so only NaN-involving comparisons change (toward correct).
The integer predicate (`LLVMIntNE`) and `emitCmpEq` (`OEQ`) are unchanged,
so `nan == nan` stays false and `!=` is now the exact complement of `==`.
- Regression: examples/0150-types-float-ne-unordered-nan.sx (fails before,
passes after; also pins #run/comptime == runtime agreement).
- specs.md: documents float comparison / NaN semantics (Operators).
- Resolves issue 0091 (issues/0091-float-ne-ordered-nan.md).