e5db8244772c780734dfb51c817a00453f3aae73
25 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5df4ac61a7 |
fix(stdlib/E5): source-aware same-name VALUE consts (own-wins / ambiguous / cross-module expr-chains)
Re-land the value-const analog of the E1-E4 type work, reconciled onto the
current source-keyed resolver and hardened. A same-name VALUE const declared in
multiple flat-imported modules is now resolved per declaring source, not the
global last-wins `module_const_map`.
- imports.zig: `isPerSourceDecl` retains every non-function `const_decl`
per-source (value consts + type aliases), so each same-name author reaches
registration as a distinct author of its own module. Functions and var_decls
keep first-wins.
- lower.zig:
* `selectModuleConst` over `module_consts_by_source` — own-wins; exactly one
flat-visible resolves; >=2 flat-visible bare -> loud ambiguous (consistent
with the 0755 type / 0724 fn / 0782 generic ambiguities). Rewires every
consumer: `comptimeIntNamed`, the runtime-id read, the global-init read,
and the float-name path (`lookupFloatName` / `nameIsFloatTyped`).
* `SourceConstCtx` + `foldSourceConstInt`/`Float` + `sourceConstIsFloatTyped`
fold a selected const's RHS with nested same-name leaves re-selected in
their own author source, so VALUE and array-DIMENSION results are coherent.
* `pinConstAuthorSource` pins each fold level to the SELECTED const's author
(F1), including multi-level cross-module chains.
* cycle guard keyed on (name, author-source), not name alone (F3), so
same-name nested consts across modules do not trip a false cycle.
* `emitModuleConst` takes the author source and pins while folding/lowering.
Registration-time struct/inline-type field dimensions route through the now
source-aware stateful reader; the type-alias dimension path resolves each
alias against its own author's consts.
- program_index.zig: expose `isFloatConstType` / `isCountableConstType` for the
source-aware folds.
examples: 0786 own-wins, 0787 ambiguous (exit 1), 0788 expr-chain value+dim
coherent, 0789 leaf-author-pin, 0790 cross-module cycle-guard (F3), 0791
multi-level cross-module chain, 0792 struct-field registration-time dim.
Single-author corpus byte-identical (524 prior markers green); 531 total.
|
||
|
|
6406d0fb1f |
fix(stdlib/E4): collapse generic-struct author matrix into four choke-points
The generic-struct author-selection matrix {bare,qualified} × {site} × {layout,
body} drifted per-site across 12 attempts because method bodies were resolved by
bare template name in `fn_ast_map["Box.method"]`, independent of which author
produced the instance's layout. Collapse it into four choke-points so
layout-author ≡ body-author by construction:
CP-1 `selectGenericStructHead` — the single layout-head selector every generic
struct head site funnels through (alias-RHS .call/.parameterized, array-
literal, static head, resolveTypeCall/ParameterizedWithBindings). Emits the
visibility / missing-member diagnostics inline; returns a control-flow-only
union. No head site reads `struct_template_map` for selection directly.
CP-2 author stamp — non-optional `decl: *StructDecl` on `StructTemplate` (set at
the sole producer `buildGenericStructTemplate`) + `struct_instance_author`
written at `instantiateGenericStruct` from the SAME `tmpl` that builds the
layout; re-stamped on the dedup fast-path so an instance is never returned
without an author.
CP-3 alias metadata copy — mirror template/bindings/author from the mangled
instance onto the alias display name, so an `ABox`-typed receiver is a
first-class dispatch instance (Counter-2).
CP-4 `genericInstanceMethod` / `ensureGenericInstanceMethodLowered` — the single
body reader: inline methods select via the stamped author (`structMethodFn`,
source-pin follows for free); impl-block methods fall back to the template-
keyed `fn_ast_map` entry. Routes the four bespoke body sites (static head,
instance dispatch, param typing, protocol thunk) + the new qualified static
head (`a.Box(s64).make(7)`, finding #2).
A debug assert locks `struct_instance_author` / `struct_instance_template` keyset
coincidence so a future third writer that forgets the author trips a test.
Goldens 0777/0778/0780 (bare instance method — ptr/by-value/param-typed, finding
#1), 0779/0785 (qualified static head + missing member, finding #2), 0783 (alias
instance dispatch, Counter-2), 0782 (ambiguity containment). 0414/0415/0543 and
the FFI suites stay green.
|
||
|
|
81621703ca |
fix(diag): imported generic struct field with bad type → diagnostic, not .unresolved/silent stub [stdlib E3 attempt-4]
attempt-3 closed the MAIN-file value-param-as-type quadrant (0172) in the
UnknownTypeChecker, but the checker only walks main-file decls — an IMPORTED
generic struct's field with a bad type name was never checked. Worse, the
generic-struct INSTANTIATION resolved its field type nodes in the (possibly
cross-module) instantiation site's source context, not the template's module.
So for `Bad :: struct($N: u32) { x: N; }` declared in an imported module and
used as `Bad(3)` from main, the field `x: N` resolved against the main file:
the value-param-as-type leaf poisoned it with `.unresolved` and PANICKED at
LLVM emission, and the genuinely-undeclared sibling (`y: Missing` in a generic
import, distinct from the non-generic 0759 case) silently fabricated a 0-field
stub.
Root cause + uniform fix: capture the declaring module on each StructTemplate
and resolve its field type nodes in THAT source context during
instantiateGenericStruct. The source-aware nominal leaf then classifies main vs
imported by the TEMPLATE's file, so both failure modes are diagnosed at the
right authority for every quadrant — main + imported, undeclared name + value
param used as a type:
- imported `.undeclared` field → the existing leaf emits "unknown type 'X'"
(now reached because `from` is the template's module, not main).
- imported value-param-as-type → the `is_generic` leaf, when the name is bound
as a comptime VALUE (`comptime_value_bindings`), emits the tailored
"'N' is a value parameter, not a type" hint (gated to non-main; the
UnknownTypeChecker owns the main-file case). Caught in every type position
(`x: N`, `*N`, `[3]N`, `?N`). A genuinely-unbound type param (`$R`) stays a
silent `.unresolved`.
No `.unresolved` reaches LLVM for these cases (hasErrors halts after lowering);
the emit_llvm `.unresolved` @panic tripwire stays as the last-resort sentinel.
Valid value-param VALUE positions (`[N]u8` dim, `Vector(N,T)` lane) and
`$T:Type`/`$T:Protocol` type-param fields still resolve.
Regressions:
- 0760-modules-imported-generic-value-param-as-field-type (panic-before / clean
diagnostic-after).
- 0761-modules-imported-generic-undeclared-field (silent-compile-before / clean
diagnostic-after).
0171/0172/0759 stay green; main-file quadrants emit exactly one error.
Gate: zig build; zig build test (423/423 + LSP corpus sweep); run_examples 501
passed / 0 failed (prior 499 byte-identical); m3te ios-sim build exit 0.
|
||
|
|
662142c388 |
feat(resolver): source-keyed alias/const/global caches, write-side only [stdlib E0]
Phase E0 of the unified resolver (R5 §#4): add the source-partitioned
analogues of the global `type_alias_map` / `module_const_map` /
`global_names`, keyed `source path -> name -> X`, and POPULATE them from
the existing scan. Purely additive and behavior-preserving — the global
maps remain the ONLY readers; the read-side cutover to
`selectedAuthor.source` is E1.
ProgramIndex:
- type_aliases_by_source / module_consts_by_source / globals_by_source
(StringHashMap of inner StringHashMap), owned + freed on deinit.
- put{TypeAlias,ModuleConst,Global}BySource + removeModuleConstBySource
helpers; retain `module.alloc` to lazily create inner per-source maps.
lower.zig scan: every global `type_alias_map`/`module_const_map`/
`global_names` write (and each module_const_map.remove) now mirrors into
its by-source analogue, keyed by the registering decl's source
(decl.source_file / current_source_file), the analogue of module_fns.
Tests:
- program_index.test.zig: same alias/const/global name under two sources
lands two distinct entries (not last-wins); compat globals stay
single-keyed; removeModuleConstBySource scoped to its source.
- lower.test.zig: end-to-end two-source namespace fixture — the scan
populates the by-source caches per declaring source while the global
maps stay single-keyed by name.
Gate: zig build + zig build test (423, incl. 2 new) + run_examples
(477, byte-identical) + m3te ios-sim build, all exit 0.
|
||
|
|
b5ec121645 |
feat(imports): buildImportFacts raw-fact store (ModuleRawDeclIndex + NamespaceEdges) [stdlib A]
Phase A of the unified resolver (R5 locked design). Additive infrastructure
with NO behavior change — builds the import-side raw-fact store; nothing
consumes it yet.
- imports.zig: add RawDeclRef / RawAuthor / ModuleRawDeclIndex / ModuleDecls /
NamespaceTarget / NamespaceEdges, plus buildImportFacts (mirrors
buildModuleFns) producing a scalar per-module name→RawDeclRef index + the
namespace edges. Callable without IR lowering (LSP reuses it later).
- ast.zig: NamespaceDecl gains target_module_path, captured at resolution time
(the resolved_path otherwise lost on the node) so the namespace edge records
the alias target.
- imports.zig: same-module duplicate top-level name is now DIAGNOSED
("duplicate top-level declaration 'X'") where addOwnDecl would silently drop
the second author — replaces the discarded `_ =` at the three call sites.
- program_index.zig: borrowed views module_decls / namespace_edges (like
module_fns); deinit does not free them.
- core.zig: build the facts alongside buildModuleFns and point the borrowed
views at them.
- imports.test.zig: index unit tests (flat / directory / namespaced file /
namespaced directory / C-import namespace / same-name fn / same-name struct /
value-vs-type same spelling / raw const_decl) + the duplicate-name diagnostic
regression (fails pre-fix, passes after).
Gate (worktree): zig build, zig build test (incl. LSP corpus sweep), and
run_examples (471, byte-identical) all green; m3te ios-sim build exits 0.
|
||
|
|
ff9cb50079 |
refactor(imports): retain dup same-name fn authors + build identity indexes [0102a]
First of four fix-0102 sub-steps. Purely additive: retains data that the flat/directory merge currently first-wins-drops and builds two identity indexes for later bare-name disambiguation (fix-0102c). No resolution change — the existing first-wins bare path still wins; suite unchanged. - mergeFlat + directory merge: stop dropping a same-name FUNCTION authored by a different module/file. Non-function decls keep first-wins dedup; node identity dedup is untouched. - flat_import_graph: a flat-only subset of import_graph, recording an edge only for a bare `#import` (imp.name == null), never a namespaced `ns :: #import`. Threaded through resolveImports/resolveDirectoryImport and into ProgramIndex. - module_fns (path -> name -> *const FnDecl): per-module authored-function index mirroring module_scopes, built in core.zig from the main module + cache. Same-name cross-module authors stay distinct under their own paths. - imports.test.zig: asserts both a.sx/b.sx greet authors are retained in module_fns and in the global flat list, and that flat_import_graph excludes the namespaced edge while import_graph includes it. Gate (this worktree): zig build, zig build test (398/398), bash tests/run_examples.sh (457 passed) all green. |
||
|
|
9274d47adf |
fix(lower): qualified alias lowers in its own source context [0100 F1]
The 0100 identity fix registers a namespaced import's own functions under a module-qualified name (ns.fn) in fn_ast_map WITHOUT an eager declareFunction, so the alias is lowered through lazyLowerFunction's null-FuncId lowerFunction path. That path had no Function.source_file to restore (the non-null path does setCurrentSourceFile(func.source_file)), so the alias lowered in the CALLER's visibility context. A qualified function that called a helper from its OWN module's flat import was then rejected "not visible". Fix: - ProgramIndex.qualified_fn_source maps each ns.fn alias to its declaring source file, populated in registerQualifiedFn (current_source_file is pinned to the decl's source by registerNamespaceQualifiedFns). - lazyLowerFunction's null-FuncId branch restores that source before lowerFunction, so ns.fn's body lowers in its own module's context and its intra-module / own-import callees resolve. - lowerFunction records Function.source_file = current_source_file on the freshly-begun function (matching declareFunction), so the lowered alias carries its own module for diagnostics/emit. Regression: examples/0720-modules-qualified-own-import.sx — calc.compute (a qualified alias) calls triple/base from calc.sx's own flat import; reports "'triple' is not visible" on the attempt-1 code, passes after. 0719's cross-module dual-parse assertion stays green. issues/0100 RESOLVED banner extended with the F1 follow-up. |
||
|
|
95adc52609 |
fix(ir): const evaluators' field-access arm is raw value-shadow aware [F0.11]
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. |
||
|
|
e442cdf5e7 |
fix(ir): float / folds as FLOAT division under the unified narrowing rule — int folder refuses a float-operand / [F0.11]
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. |
||
|
|
74f675ac0b |
fix(ir): evalConstFloatExpr reaches parity with evalConstIntExpr — numeric-limit float leaves + float % fold under the unified rule [F0.11]
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. |
||
|
|
b73363ca4c |
fix(ir): array-dim/count path joins the unified float→int rule — all 5 sites consistent [F0.11]
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.
|
||
|
|
b6d66d9c56 |
fix(ir): complete const-float evaluator — resolve float-const leaves too [F0.11]
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. |
||
|
|
43d44fff75 |
fix(ir): narrow non-integral const-float EXPRESSIONS at typed local/field/param; align const message [F0.11]
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). |
||
|
|
156edf8e28 |
fix(ir): reject typed module const whose initializer mismatches annotation [F0.7]
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. |
||
|
|
04f46ef384 |
feat(lang): integer numeric-limit accessors (s64.max, u8.min, s3.max) [NL.1]
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).
|
||
|
|
a821323c3c |
fix(ir): converge the comptime-int count surface (0083)
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. |
||
|
|
e03c087e5a |
fix(ir): integral-float counts + range-checked value-param binds (0083)
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. |
||
|
|
e8cc9d03de |
fix(ir): precise oversized-dim diagnostic on the alias path (0083)
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 |
||
|
|
efc09699e8 |
fix(ir): value-param type functions + range-checked dim/lane fold (0083, 0087)
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.
|
||
|
|
a491a1bf73 |
fix(ir): route every comptime-int through the shared evaluator (0083)
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. |
||
|
|
cd39316f5e |
fix(ir): evaluate constant-expression array dimensions (0083)
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. |
||
|
|
d2bf8f3f2d |
fix(ir): unify named-const array-dim resolution + kill length-0 fabrication (0083)
A type alias whose dimension is a named const (`Arr :: [N]T`) resolves its dimension eagerly during scanDecls pass 1, on the stateless registration path, which can only read `module_const_map`. Typed consts (`N : s64 : 16`) register only in pass 2 and a forward-declared untyped const had not registered yet, so the stateless resolver saw an empty table, printed a non-fatal warning, fabricated length 0, and continued — yielding a 0-byte alloca, garbage reads, and a segfault for slice/struct elements. - scanDecls pass 0 pre-registers every integer-valued module const before any type alias resolves, so typed, untyped, and forward-referenced consts all resolve identically. - Both dim resolvers now share `program_index.moduleConstInt`, so the stateful body-lowering path and the stateless registration path cannot diverge. - `resolveArrayLen` returns `?u32`; `resolveCompound` yields `.unresolved` on null instead of a 0-length array. The stateful path emits a diagnostic; the alias-registration path surfaces an unresolved alias as a clean compile error that aborts the build. The Vector lane-count `else => 0` is fixed the same way. Regressions: examples/0143 (typed-const dim direct + via alias for s64/string/ struct, forward-ref alias, nested) and examples/1129 (an unresolvable computed dim halts with a clean diagnostic + non-zero exit). Both fail on the pre-fix compiler (garbage/segfault; warning+exit0) and pass after. |
||
|
|
3ed1b3a7a0 |
refactor(ir): pack projections → PackResolver + retire the alias borrow (A2.3)
A2-merge gate: both parts in one commit, behavior-preserving (350/0). Part 1 — retire the TypeTable.aliases borrow (build-enforced): - type_bridge.zig: add `AliasMap` and thread it as an explicit param through every name-resolving fn (resolveAstType, bridgeType, resolveTypeName, the compound resolvers, resolveTupleLiteralAsType, resolveParameterizedType, the inline enum/struct/union + error resolvers). resolveTypeName now forwards the threaded map to TypeResolver.resolveNamed instead of reading table.aliases. - lower.zig: all 31 resolveAstType callers pass &self.program_index.type_alias_map; drop the lowerRoot loan. - types.zig: remove the now-unused TypeTable.aliases field. - type_bridge.test.zig: alias test passes alias_map explicitly; other calls pass null. Part 2 — pack projections get one owner + no .void failure sentinel: - New packs.zig (PackResolver, a *Lowering facade): moves resolveClosure/Tuple/FunctionTypeWithBindings, packTypeElems, packTypeArgs, elementProtocolTypeArg out of Lowering. Call sites route through Lowering.packResolver(); barrel-wired in ir.zig. - The missing-projection `orelse .void` in packTypeArgs now emits a diagnostic and fills the slot with .unresolved (the tripwire sentinel), never a real .void; OOM `catch return .void` in the moved fns became .unresolved too. Legitimate no-return-type `else .void` defaults are preserved. - packs.test.zig: packTypeArgs bound/unbound/no-constraint/no-state cases + the missing-projection backstop (diagnostic + .unresolved slot). |
||
|
|
fb262e9e59 |
refactor(ir): move declaration maps into ProgramIndex (A1.1b)
Architecture phase A1.1b — mechanical storage relocation. Move the 9
declaration-fact maps out of the Lowering state bag into ProgramIndex:
high-fanout: fn_ast_map, foreign_class_map, global_names, type_alias_map
medium-fanout: struct_template_map, protocol_decl_map, protocol_ast_map,
module_const_map, ufcs_alias_map
168 self.<map> sites in lower.zig repointed to self.program_index.<map>;
external readers repointed too (core.zig foreign_class_map iteration;
lower.test.zig fn_ast_map / foreign_class_map). No duplicate storage, no
fallback path; zig build enforces no missed reference.
The four maps whose value types were Lowering-private pull those types into
program_index.zig as pub (GlobalInfo, StructTemplate + TemplateParam,
ProtocolDeclInfo + ProtocolMethodInfo, ModuleConstInfo); lower.zig aliases
them at file scope so call sites are unchanged.
Behavior is preserved exactly:
- per-map allocator unchanged — import_flags/fn_ast_map/global_names use the
lowering allocator (ProgramIndex.init), the other 7 keep their page_allocator
inline defaults;
- ProgramIndex.deinit frees only the 10 owned maps, never the borrowed
module_scopes / import_graph;
- TypeTable.aliases still borrows &self.program_index.type_alias_map, loaned at
lowerRoot with the same late-binding lifetime.
Extends program_index.test.zig with declaration-map round-trips (fn AST, type
alias, global, module const, foreign class, protocol decl/AST, struct template,
ufcs alias).
Registration logic (registerStructDecl / registerProtocolDecl /
registerForeignClassDecl, ...) stays in Lowering, writing through the index.
Gate green: zig build, zig build test, bash tests/run_examples.sh
(350 passed, 0 failed). lower.zig 19433 -> 19393 lines.
|
||
|
|
90520eefeb |
refactor(ir): extract ProgramIndex, move low-fanout decl facts (A1.1a)
Architecture phase A1.1a. Introduce src/ir/program_index.zig as the single storage owner for declaration-name / import / visibility facts, and move the three low-fanout maps out of the Lowering state bag: - import_flags (owned by ProgramIndex) - module_scopes (borrowed pointer into a core.zig-owned map) - import_graph (borrowed pointer into a core.zig-owned map) Lowering embeds one ProgramIndex by value and reaches every moved fact through self.program_index.<field>; later phases hand collaborator modules a *ProgramIndex instead of *Lowering. 8 call sites in lower.zig + 2 setters in core.zig repointed. No duplicate storage, no fallback path; zig build enforces no missed reference. Mutation-heavy registration (registerStructDecl etc.) stays in Lowering and now writes import_flags through the index. High-fanout maps are deferred to A1.1b. Adds src/ir/program_index.test.zig (init-empty, import_flags round-trip, borrowed-view ownership) wired into the ir.zig barrel. Behavior-preserving: zig build, zig build test, and bash tests/run_examples.sh (350 passed, 0 failed) all green. |