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).
A field-like access on a builtin INTEGER type name folds to a compile-time
constant of the queried type, driven by (width, signedness) arithmetic:
sN: min=-(2^(N-1)), max=2^(N-1)-1; uN: min=0, max=2^N-1
for every width s1..s64 / u1..u64 (not just power-of-two), plus usize/isize.
- type_resolver.zig: extract the single width parser (parseWidthInt) reused by
resolveNamed AND the new accessors (no second parser — issue-0083 class);
add resolveBuiltinName / integerWidthSign / integerLimitBits / integerLimitFor.
- lower.zig: lowerNumericLimit intercept beside the error.X / Struct.CONST /
pack-arity identifier-receiver intercepts; folds ints via constInt, emits a
clean diagnostic for a non-numeric receiver (bool/string/void/Any/noreturn),
falls through for floats (NL.2).
- expr_typer.zig: mirror the result type so inferExprType reports the queried type.
- program_index.zig: recognize the accessors in the comptime-int / array-dim path
so [u8.max]T (255) / [s16.max]T (32767) work; [u64.max]T is rejected oversized.
- u64.max / usize.max stored as the all-ones bit pattern with TYPE u64 (i64 -1),
asserted via union { u: u64; s: s64 } reinterpret.
Docs: specs.md numeric-limits subsection (formulas + result-type + u64 note);
readme.md language overview. Examples 0148 (positive) / 0149 (negative-receiver).
Unit tests for the value computation in type_resolver.test.zig.
Gate: zig build, zig build test (359/359), tests/run_examples.sh (416 ok, 0 failed).
Per Agra ruling: user-facing sx changes must update all relevant docs
including readme.md. Replaces the prior 'Original syntax sketches. Do
not modify.' rule so the docs-track-changes review criterion is
enforceable from the next step onward.
The count description claimed every count must be "positive integral",
which is wrong: zero is context-dependent. Verified at HEAD — an array
dimension (`[0]s64`) and a generic value-param count (`Box(0)`, $N:u32)
both accept zero as a length-0 instantiation, while a `Vector` lane
count stays strictly positive (`Vector(0,f32)` rejected). Negatives are
rejected for array dims and unsigned value-params, but a signed
value-param accepts a negative; only the integral requirement (folds
4.0, rejects 4.5) is common to all three.
Split the count paragraph into per-consumer bullets stating the exact
range each accepts. Range-bound paragraph unchanged. Pin the zero
contrast with examples 0147 (array-dim + value-param zero accepted) and
1505 (Vector zero-lane rejected). No compiler-code change.
specs.md lumped `inline for` / `for` range bounds in with counts (array
dimension, Vector lane count, generic value-param count) under the
count negative-rejection rule. A range bound is a range ENDPOINT, not a
count: negative endpoints are valid and an empty/inverted range runs zero
iterations. The compiler already implements this correctly (Agra ruling:
spec-text bug, no code change).
- specs.md: counts and range bounds are now described separately. Counts
reject negatives; bounds accept any compile-time integer (negatives
valid, integral floats fold) but still reject a non-integral float
because the loop cursor must be an integer.
- examples/0612-comptime-inline-for-range-bounds.sx: `inline for -2..1`
and `for -2..1` both sum -3; `inline for 0..(-2.0)` runs zero
iterations (empty range). Runtime/comptime parity asserted.
- examples/1138-diagnostics-inline-for-non-integral-bound.sx: a
non-integral float bound `inline for 0..4.5` is a clean diagnostic,
exit 1 (must-be-integer still applies to bounds).
Count consumers (1132/1133/1134/1135) unchanged and green.
A failed value-param bind on a type-returning function (e.g.
`MakeC :: ($K: Count, $T: Type) -> Type { return [K]T; }` with
`a : MakeC(5_000_000_000, s64)`) emitted its correct range diagnostic
but then `instantiateTypeFunction` returned `null`, so
`resolveParameterizedWithBindings` fell through to an empty-struct
placeholder named after the function. The binding `a` got that
placeholder type, so a later `a.len` cascaded a bogus second error
`field 'len' not found on type 'MakeC'`.
The struct binder (`instantiateGenericStruct`) already returns
`.unresolved` here; the type-fn binder now matches it — a failed
value-param bind poisons to `.unresolved` instead of `null`, so the
caller propagates the diagnosed poison and the existing
`emitFieldError` suppression yields one clean diagnostic. Covers
every type-fn value-param failure mode: overflow via an aliased
constraint, a non-const arg, and an unknown type arg.
Regression: examples/1137-diagnostics-value-param-type-fn-no-cascade.sx
Three adjacent cells of the shared count surface still diverged from the
rest; all now route through the same leaf+fold+narrow+diagnose path.
1. Aliased integer constraint bypassed the value-param range gate — only
builtin constraint names matched intTypeRange, so Box(5_000_000_000)
with `$K: Count` (Count :: u32) compiled and bound a truncated value.
resolveValueParamArg (shared by both the struct AND type-fn binder) now
resolves the constraint to its underlying builtin via
canonicalIntConstraintName (Count -> u32, Small -> s8) before
range-checking, so an aliased integer constraint behaves exactly like
the builtin it names.
2. A named const with an expression RHS (M :: 2; N :: M + 1) did not fold
as a count — moduleConstInt read only a literal RHS node. It now folds
every const's RHS through the shared evalConstIntExpr, cycle-guarded
(mutual / self cycles fold to null, not a stack overflow), and pass-0
pre-registers expression-RHS consts. N :: M + 1 == 3 at every consumer:
dim (direct + alias), Vector lane, value-param (struct + type-fn),
inline for.
3. Stateful resolveArrayLen still fabricated length 0 after a failed fold;
it now returns null -> the .unresolved sentinel (no fabrication). The
binding's lowering never reaches sizeOf (alloca defers it; hasErrors
aborts first) and a field access on an already-diagnosed .unresolved
value is poison-suppressed (emitFieldError), so a failed-fold dim emits
ONE clean diagnostic with no panic.
Regressions: examples/0146 (full positive matrix — every consumer x leaf
form), 1135 (aliased u32 + s8 overflow), 1136 (direct non-const dim halts
cleanly). The cascade cleanup also tightened 1502/1503 to one diagnostic.
Unit test added for moduleConstInt expression-folding + cycle detection.
Item 2 (Agra ruling): a compile-time INTEGRAL float (`4.0`, `N : f64 :
4.0`, `N :: 4.0`) used as an array dimension / Vector lane / generic
value-param count / `inline for` bound now folds to its integer at the
shared leaf — `program_index.floatToIntExact`, used by both the
`.float_literal` arm of `evalConstIntExpr` and `moduleConstInt`. All four
consumers route through the one evaluator, so `[4.0]s64` lays out the same
`[4]s64` uniformly; a non-integral (`4.5`) or negative value stays
rejected by the downstream `foldDimU32` gate. Pass-0 now pre-registers
float-valued module consts for forward-alias parity with int consts.
Item 1: a generic value-param bind (`Box($K: u32)`) never range-checked
the folded arg, so `Box(5_000_000_000)` compiled and ran. The bind now
range-checks against the param's declared type — a `u32` count through the
shared `foldDimU32` gate (making program_index's "single u32 gate for
value-param counts" doc true), any other integer type through the new
`program_index.intTypeRange` — and emits a clean "value N does not fit in
u32 parameter K" otherwise. The declared type is threaded via a new
`TemplateParam.value_type`.
Regressions: examples 0145 (integral-float array dim), 1504 (Vector lane),
0611 (inline-for bound), 0209 (value-param integral-float), 1132
(non-integral float dim rejected), 1133 (negative float dim rejected),
1134 (oversized u32 value-param rejected) + program_index float-fold unit
tests. Gate: zig build, zig build test, 406/0 run_examples.
The stateless alias-registration array-dim path collapsed foldDimU32's
distinct .too_large / .below_min outcomes into null, so an oversized type
alias (Big :: [5000000000]s64) emitted the FALSE 'an array dimension is not
a compile-time integer constant' message while the direct form correctly
reported 'array dimension 5000000000 does not fit in u32'.
Add program_index.reportDimError as the single source of dim-error wording
(the stateful path now emits through it too) and type_bridge.foldArrayDim to
surface the DimU32 reason at the alias-registration site. An oversized/negative
alias dim now routes to reportDimError for the same precise message as the
direct form; a genuinely non-const alias dim keeps the alias-specific message.
Regression: examples/1131-diagnostics-array-dim-oversized-u32-alias.sx
Two remaining siblings in F0.4's comptime-int path.
1. Type-returning function with a value param used as a TYPE annotation
(`b : Make(N, s64)` where `Make :: ($K: u32, $T: Type) -> Type`):
- `isValueParamPosition` (semantic_diagnostics) now also skips a value
param of a `fn_ast_map` type-returning function, so `N` is not walked
as the type name "N" ("unknown type 'N'").
- `resolveParameterizedWithBindings` routes a type-returning-function
name to `instantiateTypeFunction` (the `.call` path already did).
- `instantiateTypeFunction` resolves a general return-type expression
(`return [K]T`) with bindings active — not just struct/union returns.
`Make(N, s64)`, `Make(M + 1, s64)`, `Make(3, s64)` all resolve to one
`[3]s64`.
2. Oversized dim/lane fold panicked the compiler (0087): an array dim /
Vector lane folded to a valid i64 (5e9) then narrowed to u32 with an
unchecked `@intCast`. New single gate `program_index.foldDimU32` folds
via `evalConstIntExpr` then range-checks `[min, maxInt(u32)]`; the three
narrowing sites (resolveArrayLen stateful + stateless, resolveVectorLane)
all route through it and emit a clean diagnostic + halt instead of
panicking. Value-param args stay i64 until used as a dim/lane, where the
same gate checks them.
Regressions: examples/0208 (value-param type function), examples/1130
(oversized array dim clean halt), examples/1503 (oversized Vector lane
clean halt). Marks issue 0087 RESOLVED.
Gate: zig build, zig build test, bash tests/run_examples.sh — 398 passed,
0 failed, 0 timed out.
While fixing 0083 (attempt 5) noticed a distinct, pre-existing bug:
writing to a Vector component (`v.x = 1.0`) aborts with "unresolved type
reached LLVM emission" in emitStore. Reading a lane works; a literal lane
count triggers it, so it is NOT the lane-count class. Confirmed
reproducible on the pristine pre-attempt-5 compiler (not introduced by
the lane-count fix). The standard vector idiom (`.[…]` construction +
component reads / arithmetic, examples/1500) is unaffected. Filed for a
separate session; not worked around here.
Attempts 1–4 fixed the array-dimension paths but the same length-0
fabrication class survived on every other site that resolves a
compile-time integer. Unify them all on the single shared
`program_index.evalConstIntExpr` so they cannot diverge:
- All three Vector lane resolvers (resolveTypeCallWithBindings,
resolveParameterizedWithBindings, resolveArrayLiteralType) and both
generic value-param binders (instantiateGenericStruct,
instantiateTypeFunction) hand-rolled an `else => 0` switch. A
module-const lane `Vector(N, f32)` fabricated a 0-lane `<0 x float>`
(LLVM "huge alignment" abort); a value-param `Vec(N, f32)` fabricated
a 0 binding / wrong mangled name. They now fold through the shared
evaluator and emit a clean diagnostic + `.unresolved` on a non-const
operand (resolveVectorLane / resolveValueParamArg) — never 0.
- evalComptimeInt (inline-for bounds) delegated to the shared evaluator,
so `inline for 0..M` / `0..(M+1)` fold like array dims. The `<pack>.len`
leaf moved into the shared folder via a new `ctx.lookupPackLen`.
- The unknown-type semantic checker no longer walks a value-param
position (`Vector(N, …)` / `Vec(N, …)`) as a type name (was reporting
"unknown type 'N'").
- The parameterized-type-arg parser and the function-body lookahead
(hasFnBodyAfterArrow) accept a const-EXPRESSION in a value position, so
`Vector(M + 1, f32)` and `[M + 1]T` parse as a return type too (the
latter a pre-existing array-dim sibling that the same heuristic broke).
Regressions: examples/1501 (named-const + const-expr lane, direct +
alias, 3/4-lane reads), 1502 (runtime lane clean-halts, exit 1, no LLVM
crash), 0207 (Vec(N)/Vec(M+1) == Vec(3) instantiation), 0610 (inline-for
const bounds). Shared-evaluator unit test extended with the pack-len arm.
zig build && zig build test && bash tests/run_examples.sh: 395 passed,
0 failed.
A constant-FOLDABLE expression array dimension (`[M + 1]`, `[M * N]`,
`[N - M]`, nested `[M + N - 1]`, parenthesised `[(M + 1) * 2]`, mixing
untyped and typed module consts) was wrongly rejected as "not a
compile-time integer constant" even though every operand is
compile-time-known. Attempts 1-3 resolved only a bare named-const dim or
a literal; an expression dim must be EVALUATED, not rejected.
Fix: the shared dim resolver now routes the dimension through a single
constant integer-expression evaluator (`program_index.evalConstIntExpr`)
that folds integer `+ - * / %` and unary negate over literals and
named/typed module consts, recursively (parentheses carry no AST node).
The leaf-name lookup is delegated via `ctx.lookupDimName`, so the
stateful body-lowering path (`Lowering`, which also sees comptime
constants and generic `$N` values) and the stateless registration path
(`type_bridge.StatelessInner`, module consts only) share the EXACT SAME
folding logic and cannot diverge — an expression dim via a type alias
resolves identically to the direct form.
No-fabrication discipline unchanged: a genuinely non-comptime dimension
(runtime local, non-comptime call, unbound name) or arithmetic that
overflows / divides by zero still yields null -> `.unresolved` -> the
same clean compile-halting diagnostic, never a fabricated length.
- examples/0144-types-const-expr-array-dim.sx: every expression form,
direct vs alias, scalar / string / struct element types (fails on the
pre-fix compiler, passes after).
- examples/1129 re-pointed at a genuinely non-const dimension
(`[get()]s64`, a runtime call) so it still proves the stateless
clean-halt (a foldable expression is no longer an error).
- program_index.test.zig: unit test for evalConstIntExpr folding and
clean-halt-on-non-const.