Commit Graph

145 Commits

Author SHA1 Message Date
agra
837b5d375f fix(0124): large stack arrays lower to in-place access, not first-class values
Two lowering sites materialized a local array as a whole LLVM value;
the legalizer scalarizes each such op into one SelectionDAG node per
element, and at ~64K elements the DAG combiner segfaults
(DAGCombiner::visitMERGE_VALUES → ReplaceAllUsesWith).

- lowerVarDecl: an array-typed `---` initializer emits NO store — the
  slot stays uninitialized instead of receiving a whole-array undef
  store. The tuple zero-init carve-out stays; non-array `---` keeps
  the undef store. The interp is unchanged either way (slots start
  .undef).
- lowerIndexExpr: element reads on an array with addressable storage
  GEP the storage and load one element — the general-expression
  sibling of 0110's lowerFor fix — without value-lowering the object
  (a dead whole-array load would still reach the DAG). Storage-less
  arrays keep the index_get fallback.

Sibling shape filed as 0125: any_to_string's per-array-type arms still
pass the array by value, so a 64K+ array type + any {} print crashes.

Regression: examples/0055-basic-large-stack-array.sx (sx build
segfaulted pre-fix). 22 .ir snapshots re-pinned: removed undef stores
and ig.tmp spills, in-place gep+load (instruction-shape-only churn,
reviewed).
2026-06-12 08:19:20 +03:00
agra
7c7bb2076a issues: file 0125 — any_to_string's array arms materialize every interned array type by value 2026-06-12 08:18:32 +03:00
agra
47110b37cf issues: file 0124 — 64K+ stack arrays emit whole-aggregate ops that segfault LLVM 2026-06-12 07:55:46 +03:00
agra
7f2b8b5cde fix(0123): wrong arg counts to fixed-arity fns error at the call site
checkCallArity compares the supplied count against the declared params
(min = params without trailing defaults, max = params.len, unbounded
past a variadic) at the five plain dispatch sites in lowerCall — bare
selected-author + lazy, namespace alias-gate + qualified, struct
method, ufcs. Pack / comptime / generic / #compiler / #builtin callees
keep their own dispatch. The method/ufcs sites also gain the
appendDefaultArgs fill the generic-instance leg already had, so
trailing defaults work on dot-calls instead of emitting under-arity
calls. lowerStmt's local fn_decl arm now registers a pointer into the
AST node in fn_ast_map, not a stack temporary that aliased every later
local fn.
2026-06-12 01:42:59 +03:00
agra
7d1e23ecc6 issues: file 0123 — wrong arg counts to fixed-arity fns reach LLVM emission 2026-06-12 00:40:00 +03:00
agra
340be402a5 ir: whole-program passes pin the source context per decl (fix 0122)
convergeClosureShapeSets, checkErrorFlow, and the unknown-type loop ran
under whatever current_source_file the previous phase left behind —
closure-literal annotations resolved (and reject/unknown-type
diagnostics rendered) against an arbitrary module. Latent while std.sx
was a single file (the ambient happened to be the main file); the
re-export facade restructure exposed it. Each walk now pins
setCurrentSourceFile per decl / per fn (body.source_file is already
stamped by resolveImports). Coverage: examples 0129/1047/1049/1052/
1053/1056 against the facade std.sx. Gates: zbt 426/426, suite 588/588.
2026-06-11 19:24:46 +03:00
agra
721369a711 lang: fn aliases dispatch like their target (fix 0121) — scan-time registration through the shared alias-chain walk
Renamed fn aliases failed for EVERY kind (the filed pack-only scope was
a same-name confound: same-name re-exports already resolved through the
name-keyed fn_ast_map). scanDecls now follows ident-/ns.X-RHS const
alias chains (aliasedFnDecl; 0120's hop walk extracted as
followAliasChain) and registers the alias name in fn_ast_map
(absent-only), so every dispatch path — early pack/comptime/generic,
plain lazy-lower, plan-side typing — sees the target decl unchanged.
my_print :: s.print; / my_format :: s.format; now work (the std.sx
re-export shape). Regression: examples/0546 (+rich). Gates: zig build
test 0, suite 588/588.
2026-06-11 18:47:16 +03:00
agra
f2db8ecc53 lang: generic struct head aliases bind the template (fix 0120) — alias-follow from each author's source in head selection; loud unknown-type on the .call type tail
BoxAlias :: Box; / Box :: r.Box; now resolve instantiation, methods,
annotations, and chains through the aliased template, and re-export one
flat-import level as ordinary own decls (the facade shape the std.sx
restructure needs). selectGenericStructHead consults aliasedStructTemplate
(nominal.zig) before the global template map — own-wins/single-flat alias
author, each hop pinned to the alias author's source, ns.X RHS through
namespaceAliasVerdictFrom, depth-capped. resolveTypeCallWithBindings'
silent .unresolved tail (panicked in LLVM emission) now diagnoses
"unknown type". Also aligns the stale pre-existing calls.test.zig UFCS
plan test with the opt-in model (a47ea14). Regression: examples/0211
(+rich/+facade). Gates: zig build test 426/426, suite 587/587.
2026-06-11 18:09:01 +03:00
agra
a47ea1416e lang: opt-in UFCS — ufcs-marked fns + alias dot-dispatch, generic binding via receiver; one binding builder for plan-side generic returns 2026-06-11 17:04:51 +03:00
agra
84e0fb0752 mem: typed allocation helpers + drop bare malloc/free (Phase 2.2); resolve 0119 as |>-contract clarification 2026-06-11 16:17:39 +03:00
agra
3e10809d7e issues: file 0119 — UFCS generic free functions unresolved 2026-06-11 15:46:49 +03:00
agra
03dc10bba3 fix(0118): cast accepts compound type args; compound type literals are first-class Type values 2026-06-11 14:09:22 +03:00
agra
c229f697bd docs: const-aggregate semantics + unchecked-pointer contract (PLAN-CONST-AGG step 6) 2026-06-11 13:54:35 +03:00
agra
40a94c4734 issues: file 0118 — cast(<compound type>) unresolved 2026-06-11 13:47:38 +03:00
agra
7f3bd69bd9 lang: reject writes through constants (PLAN-CONST-AGG step 2, fixes 0116)
Any assignment / compound-assignment whose target chain is ROOTED at a
constant — a const-flagged global (array consts, #run consts) or a
module value const (struct consts incl.) — diagnoses 'cannot assign
through constant X' at compile time. A struct const's field write used
to compile and bus-error at runtime (issue 0116); scalars misfired
silently. A deref along the chain (p.*) breaks the root — pointer
writes stay the documented escape until the const-ness steps; a local
shadowing the const name stays writable.

Also: typed struct constants ('W : Color : Color.{...}') register —
the shape list skipped struct_literal, leaving the typed form
unresolved while the untyped one worked.

Examples: 1162 (all rejection shapes incl. the 0116 crash repro),
0178 (typed struct const reads + copy independence).
2026-06-11 12:33:34 +03:00
agra
82d6b8da0e fix(0117): pointer-to-array indexing auto-derefs
A '*[N]T' receiver in an index expression reached LLVM emission with an
unresolved element type and tripped the panic sentinel — no read or
write spelling worked. ptrToArrayElem on Lowering recognises the shape;
the index READ path GEPs the pointee array through the pointer value
and loads the element; the write / compound-assign / lvalue /
addr-of-element paths and the expression typer resolve the element type
through the same helper (their GEP machinery already handled a pointer
base). Kept out of getElementType so slice paths don't half-accept a
raw pointer base.

Regression: examples/0176 (read, write, compound, element ptr + deref).
2026-06-11 12:15:45 +03:00
agra
57979ed8e6 issues: file 0117 — indexing through *[N]T panics at LLVM emission
Pre-existing (plain locals repro it); found pinning @K reads for
PLAN-CONST-AGG step 1, which is now blocked on it. No deref spelling
works: p[2] hits the unresolved-type tripwire, (*p)[2] doesn't parse.
2026-06-11 12:02:03 +03:00
agra
4f1a9738c7 plan: aggregate consts (PLAN-CONST-AGG) + file 0116 const-write hole
Array-typed '::' consts as immutable globals (approved design: storage
global + untyped inference + comptime-fold layer + const-write rejection
covering the pre-existing struct-const write crash, issue 0116).
2026-06-11 11:18:43 +03:00
agra
0b13498e25 fix(0115): source-aware global selection — own-wins for module globals
The globals registry (global_names) was last-wins across modules with no
per-importer gate: any module's bare K could read/write/type against an
unrelated module's same-named global (hash.sx's K table hijacked every
user K once std's namespace tail pulled hash into the program), and an
own const of an unsupported shape borrowed another module's const and
panicked at the unresolved-type tripwire.

- var_decl joins RawDeclRef: module globals are selectable raw authors.
- selectGlobalAuthor (the globals analogue of F2's selectModuleConst):
  own author wins, one flat-visible author resolves, >=2 distinct flat
  authors diagnose loudly, authored-but-not-visible diagnoses, and a
  compiler-synthesized global (no raw author) emits untracked. A var_decl
  author whose per-source registration was deduped at flat-merge (two
  modules declaring the same extern symbol) serves the symbol's
  registration.
- All bare-identifier global sites route through it: value read, addr-of,
  assignment (store + compound), lvalue address, fn-ptr call, call param
  typing, and expression type inference.
- selectModuleConst gains .own_opaque: an own const author with no
  materialized per-source value (e.g. an array '::' const) blocks
  borrowing another module's same-named const — the read diagnoses
  cleanly instead of panicking.
- The fn-as-VALUE arm admits raw-facts-only authors: an own fn whose name
  a flat-merge collision dropped from the global decl list (first-wins)
  now resolves via author selection for func_ref/closure/Any shapes too.

Regressions: examples 0835 (own const vs flat array global), 0836 (main
const vs namespaced array global, incl. inference), 0837 (own array
const never borrows cross-module — clean unresolved).
2026-06-11 10:47:30 +03:00
agra
37bea63302 issues: file 0115 — same-name consts of different shapes collide across modules
Scalar K vs array K in two modules: minimal repro panics (unresolved-type
LLVM tripwire), the std-tail topology silently clobbers (0786 family reads
hash.sx's SHA table as its own K). Blocks the PLAN-STDLIB full-tail
follow-up; co-blockers (eager global emission, 0601 comptime-meta,
error-int shifts) noted in the issue.
2026-06-11 09:24:11 +03:00
agra
fbbfcb268c fix(0114): gate alias-qualified calls to one-level carry, pin to target
The lowerCall namespace branch routed alias.fn() through the global
qualified registration (first-wins) at any import depth, and through the
global last-wins bare map for comptime/generic members. Plain-identifier
alias roots now resolve via the carry-aware namespaceAliasVerdict:

- visible alias (own edge or ONE flat hop): the member dispatches the
  TARGET module's own fn (namespaceFnMember + fd-keyed bareAuthorFuncId),
  so two modules' same-named aliases each call their own target.
- two direct flat imports carrying the alias to distinct targets:
  loud ambiguity diagnostic.
- alias only reachable beyond one hop: "namespace 'X' is not visible".
- foreign / builtin / #compiler members keep the literal-symbol path.

Regressions: examples 0832 (two-hop), 0833 (carried collision),
0834 (own-target pin / first-wins repair).
2026-06-11 09:16:03 +03:00
agra
2025bb361b issues: file 0114 — namespace aliases leak transitively, collide first-wins
Found while probing the alias-carry design for the stdlib restructure
(plan in current/PLAN-STDLIB.md): qualified members register globally
with no per-importer gate, so an alias is usable any number of flat
hops away, and same-name registrations silently first-win. The carry
rule's one-level + ambiguity semantics fix both; repro and fix shape
in the issue.
2026-06-11 05:36:22 +03:00
agra
12149eb548 fix(0113): negated-literal global initializers fold as constants
globalInitValue had no unary_op arm, so g : s64 = -1; fell into the
catch-all 'must be initialized by a compile-time constant' even though
constExprValue already folds negate(literal) for the module-const
identifier route. The new arm routes through constExprValue and applies
the direct-literal rules to the folded value: checkIntLiteralFits on
ints (g : s8 = -300 gets the range diagnostic), and a negated float at
an integer global narrows only when integral (-4.0 folds to -4, -4.5
errors). Binary-op initializers keep the specific non-constant
diagnostic.

Regression: examples/0175-types-negative-literal-global.sx.
2026-06-10 22:39:52 +03:00
agra
67313e1dad fix(0112): out-of-range int literals error instead of silently wrapping
checkIntLiteralFits range-checks a literal against its integer target
(builtins + custom widths via intLiteralRange; width-64 types skip —
every representable literal is a legal bit pattern there) and diagnoses
with the type's range and an xx/cast hint. Wired into the .int_literal
arm (covers decls, assignments, call args, struct-literal fields),
lowerStructConstant, and globalInitValue.

A negated literal now folds to a single constant so -128 range-checks
as -128 rather than as an out-of-range +128 intermediate. An explicit
xx operand skips the check — truncation stays available on request
(cast(T) was already exempt: its value arg lowers without the target).

examples/0300-closures-lambda.sx pinned 133 wrapping to -3 through an
s3 param — the exact class this outlaws; updated to a fitting value.

Found during the fix and filed separately: issue 0113 (negated-literal
global initializers rejected as non-constant; pre-existing).

Regressions: examples/1156-diagnostics-int-literal-out-of-range.sx,
examples/0174-types-int-literal-boundaries.sx.
2026-06-10 22:28:24 +03:00
agra
3cc34d54c1 fix(0108): break/continue run the loop body's pending defers
lowerBreak/lowerContinue emitted a bare br, and the enclosing block's
emitBlockDefers — seeing the terminator — discarded the pending entries
on the assumption a return had already drained them. The breaking
iteration's defers were silently skipped, leaking whatever the cleanup
released.

Lowering.loop_defer_base records the defer-stack height at each loop's
body start (while / for / range-for, saved and restored alongside
break_target); break/continue drain non-onfail entries down to it in
LIFO order via the non-truncating emitLoopExitDefers before branching.
Truncation stays with the lexical block exits — the same entries still
belong to the fall-through path after the branch containing the break.
break/continue outside a loop now diagnose instead of no-op'ing.

Regression: examples/0049-basic-defer-break-continue.sx (for and while,
break and continue, nested-block LIFO drain).
2026-06-10 17:43:58 +03:00
agra
bf47146085 fix(0110): for-over-array by-value fetch reads one element, not a full copy
lowerFor's by-value element fetch emitted index_get on the array VALUE;
the emitter realizes that as a whole-array spill to a stack temp + GEP,
per iteration — O(N^2) bytes copied per loop (and pre-0109 it also grew
the stack per iteration, segfaulting a [4096]s64 loop).

When the iterable is an array with addressable storage (and not deref'd
from a pointer, whose identifier alloca holds the pointer rather than
the array), the fetch is now index_gep on the storage + one element
load. Storage-less arrays keep the index_get fallback. The loaded
element remains a copy — mutating the capture does not write back.

Regression: examples/0048-basic-for-array-large.sx (sum over 4096
elements + by-value copy-guard).
2026-06-10 17:34:35 +03:00
agra
878c4226a6 fix(0109): hoist all per-instruction allocas to the function entry block
An alloca built at its use site re-executes on every pass through that
block, and LLVM reclaims allocas only at ret — so loop-body locals,
nested-loop index slots, and emitter spill temps (ig.tmp, sret slots, ABI
coercion temps, byval materialization) grew the stack per iteration and
long loops segfaulted on stack exhaustion.

New LLVMEmitter.buildEntryAlloca inserts after existing entry-block
allocas and restores the builder position; every LLVMBuildAlloca site
reachable during instruction emission now routes through it.
Initialization stores stay at the use site (per-iteration re-init is
unchanged), and entry slots become mem2reg-promotable. The 35 .ir
snapshot diffs are pure alloca position moves (type multisets verified
identical per file).

Regression: examples/0047-basic-loop-local-stack-reuse.sx (segfaulted
pre-fix on both the 1M-iteration body-local loop and the 3M-iteration
nested loop).
2026-06-10 17:27:11 +03:00
agra
e81780e32e fix(0111): unannotated decl literals no longer adopt the fn return type
lowerVarDecl (unannotated) and lowerDestructureDecl now clear target_type
around the initializer lowering: a declaration without annotation provides
no target, so int/float literals take their spec defaults (s64/f64) instead
of the enclosing function's implicit-return type (x := 0 in a -> s8 fn was
s8; big := 3000000000 in -> s32 silently wrapped to -1294967296).

Regression: examples/0173-types-int-literal-default-s64.sx. The remaining
explicit-annotation wrap (x : s8 = 300) is filed as issue 0112.
2026-06-10 17:21:44 +03:00
agra
2b8041a828 cleanup: drop resolved-issue citations from src comments
Sweep all src/**.zig comments that cite resolved issues (issue NNNN /
fix-NNNN / KB-N): the invariant or mechanism each comment states is
kept; the historical citation is dropped, per the no-conclusion-comments
rule. Pure-history parentheticals are removed outright. References to
the 16 still-open issues (0030, 0041-0056) are untouched, as are test
NAMES carrying regression provenance (matching the sanctioned
"Regression (issue NNNN)" example-header convention).

Also removes the issues/0019-import-non-transitive-c-scope/ fixture dir
— the issue is superseded and its behavior is covered by
examples/0706-modules-import-non-transitive.sx (the .md writeup stays).
issues/0030's repro .sx stays: that issue is an open feature request.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 16:34:17 +03:00
agra
010e644897 fix(R1): own const_decl pending alias wins over flat imports (issue 0107)
When a module declares `A :: B; B :: u64;` and both a flat import and a
namespaced import export `B :: u8`, the flat import's B was discovered by
flatTypeAuthorCount before the own B :: u64 was processed — binding A to
u8 and silently truncating values.

Fix: ownConstDeclIsPendingAlias guard added to selectNominalLeaf between
the own-alias check and the flat-import walk. If the querying module has
an own const_decl for the name that is not yet in type_aliases_by_source,
return .pending so the forward-alias fixpoint resolves it correctly.

Regression: examples/0830-modules-flat-ns-same-name-forward-alias.sx
(x : A = 300 prints 300, not 44). 541/541 tests pass.
2026-06-09 22:16:47 +03:00
agra
8875a28641 fix(lower): resolve substituted caller comptime $-args in caller context [stdlib B attempt-5]
attempt-3 pinned current_source_file to the metaprogram's defining module
across the whole body lowering (lowerComptimeCall / monomorphizePackFn). That
pin also covered caller-provided comptime $-arg nodes spliced into the body by
substituteComptimeNodes — but those are CALLER-authored and must resolve in the
caller's visibility context, not the callee's. Result: a caller-owned helper
passed to an imported metaprogram errored "'<name>' is not visible".

Fix: stamp each comptime $-arg node with the caller's source_file at the cpn
build site (stampCallerSource, in lowerComptimeCall + monomorphizePackFn);
lowerExpr switches current_source_file to a node's source_file when present, so
the substituted subtree resolves against the caller while the surrounding callee
code keeps the defining-module pin. No exemption / fall-open.

Regression: examples/0738-modules-comptime-arg-caller-context.sx — a caller-owned
helper passed as a comptime-ONLY $-arg through a namespaced import. Fail-before
(attempt-3 binary): "'caller_name' is not visible". Pass-after: prints
"hello world", exit 0. Comptime-only, so it does not exercise issue 0107.

0106 RESOLVED banner extended (point 3: body=defining context, substituted
$-args=caller context). run_examples 473 -> 474; zig build test 412/412.
2026-06-07 09:07:27 +03:00
agra
b62223edaf fix(lower): pin defining-module context for pack/comptime metaprograms; drop #insert exemption [stdlib B attempt-3]
ROOT FIX for issue 0106's library-metaprogram half — no exemption.

attempt-2 masked the 0106 fallout with an `in_insert_expansion` flag that
made the visibility adapters fall open during ANY `#insert` expansion,
including a USER's `#insert <expr>` — so a bare reach into a namespaced-only
import from user `#insert` code wrongly compiled (Adi's blocker). The flag
was the wrong shape. This removes it and fixes the real cause.

Root cause: a metaprogram's body (`std.print` / `std.format` / `log.*`,
whose `#insert build_format(fmt)` + `#insert "out(result);"` reference
std-internal bare names) was lowered under the CALL SITE's
`current_source_file`, so those names were policed against the consumer's
imports. Normal functions get this right via `lowerFunctionBodyInto`, which
pins `func.source_file`; the two monomorphizers don't:
  - `monomorphizePackFn`   — bare `print(...)` / `format(...)` (pack path).
  - `lowerComptimeCall`    — namespaced `std.print` / `log.warn` (reached via
                             the field-access `hasComptimeParams` branch).

Fix: both paths now save/set/restore `current_source_file` to the body's
DEFINING module around the BODY lowering only (call-site args stay in the
caller's context). The defining path is stamped onto each function body node
by `resolveImports` (`stampFnBodySource`), mirroring `Function.source_file`.
So library internals resolve in std.sx/log.sx naturally, while a USER's
`#insert <expr>` is still checked in the user's context.

- Exemption GONE: `in_insert_expansion` flag + both adapter fall-open checks
  deleted; `isNameVisible`/`isCImportVisible` are byte-identical adapters.
- New pinned regression: examples/0737-modules-insert-bare-not-visible.sx
  (+ a.sx) — a USER `#insert secret()` into a namespaced-only import errors
  ('secret' is not visible). fail-before exit 0 on the attempt-2 binary /
  pass-after exit 1.
- face #1 (0736) still errors; face #2 (0015/0700/0718/1030) pass again WITH
  NO exemption — the metaprogram body resolves in its own module.
- run_examples 472 -> 473; zig build test 412/412; m3te ios-sim build exit 0.
- issues/0106 RESOLVED banner updated (root cause + no-exemption fix).
2026-06-07 06:09:28 +03:00
agra
6f2bf84293 fix(lower): #insert-expansion visibility exemption + close 0106 [stdlib B attempt-2]
Folds the coupled 0106 fix into Phase B. attempt-1 tightened the bare-name
visibility adapters (isNameVisible/isCImportVisible) to the flat_import_graph
edge set via the unified isVisible(.user_bare_flat/.c_import_bare) predicate;
that surfaced issue 0106 — std.print / log.* expand `#insert build_format(fmt)`
(comptime call) and `#insert "out(result);"` (inserted stmt) in the CONSUMER's
current_source_file, so their library-internal bare names were policed against
the consumer's imports and errored (run_examples 471 -> 467).

Fix: a precise, named exemption. Lowering.in_insert_expansion is set across
lowerInsertExprValue (the comptime eval + the parsed-back statements); the two
visibility adapters fall open while it is set — mirroring the existing
UFCS-alias / mangled-local "compiler indirection" exemptions. NOT a blanket
skip: scoped to #insert-expanded code, ordinary bare references stay policed.
Library-internal call bodies (build_format's concat/substr) already resolve in
the defining module — lowerFunctionBodyInto pins their current_source_file.

The flat tightening stays: a bare reference to a namespaced-only import's
internal name now correctly errors ('<name>' is not visible). This is the
Agra-ratified user-visible semantics change.

- face #1 pinned: examples/0736-modules-namespaced-only-bare-not-visible.sx
  (+ a.sx) — exit 1 + stderr; fail-before (import_graph compiled it, exit 0) /
  pass-after (flat set errors, exit 1).
- face #2 restored: examples 0015 / 0700 / 0718 / 1030 pass again.
- run_examples 471 -> 472 (the new regression).
- issues/0106 marked RESOLVED; readme.md documents namespaced-only visibility.

Collectors + unified predicate from attempt-1 (resolver.zig) unchanged; nothing
routes resolution AUTHOR-SELECTION through them yet (that is Phase C).
2026-06-07 05:17:23 +03:00
agra
7158337c73 wip(resolver): collectors + unified predicate + tightened adapters [stdlib B, BLOCKED on 0106]
Collectors (resolver.zig: collectVisibleAuthors/collectNamespaceAuthors + AuthorSet
+ VisibilityMode, 4 unit tests) + unified visibility predicate + isNameVisible/
isCImportVisible adapters routed to flat modes. Tightening surfaces issue 0106
(stdlib comptime expansion relies on the over-permissive import_graph join), so
run_examples is 467/471 here. attempt-2 folds in the coupled comptime-context fix.
2026-06-07 04:52:56 +03:00
agra
96e3d2d5ae fix(lower): fn-value site lazily lowers winner only on resolver .none [0102d F1]
The bare-fn-as-value site (func_ref / fn-ptr / closure coercion) eagerly
lazily-lowered the name-keyed first-wins WINNER before the resolveBareCallee
block could reroute a genuine flat same-name collision to its per-source
author. Taking a SHADOW author's fn value therefore lowered (and could
mis-diagnose) the unused winner's body. Move lazyLowerFunction INSIDE blk_fv
onto the `.none` fallback only, mirroring the closure(fn) and free-function
UFCS sites: on `.func` use the resolved author's FuncId and never touch the
winner; on `.none` fall through to lazy-lower + resolveFuncByName the winner.

Regression: examples/0735-modules-flat-same-name-fn-value-winner — the
first-wins winner's body is independently broken and never used; a shadow
taken as a function value binds the shadow and runs (exit 0) while the winner
is not lowered. Fails-before (unresolved symbol in the winner), passes-after.
2026-06-06 16:41:01 +03:00
agra
bd24996d8b fix(lower): route remaining bare-name sites through resolver + close 0102 [0102d]
Final 0102 sub-step. fix-0102c landed resolveBareCallee and routed the
primary call path + parameter target typing through it, leaving four other
bare-name consumer sites on the old first-wins path. Route the SAME resolver
through all four, gated exactly as the call path (plain top-level identifier,
no scope-mangle / UFCS alias / local shadow; act on .func / .ambiguous, fall
through on .none so single-author / local / std / qualified / foreign-single
resolution is byte-for-byte unchanged):

1. Default-argument expansion (expandCallDefaults): omitted trailing args
   fill from the RESOLVED author's defaults, not the winner's.
2. Function-value conversion (closure(fn) and the bare-fn-as-value func_ref /
   fn-ptr / closure-coercion path): captures the resolved author's FuncId.
3. Free-function UFCS (recv.fn() -> fn(recv, ...)): dispatches the resolved
   author for the receiver's source.
4. Comptime #run of a bare call: lowerMainAndComptime now sets
   current_source_file per decl, so a `NAME :: #run f()` in an imported
   module resolves f from THAT module's flat imports (own-author wins) instead
   of the main file's perspective (which made it spuriously ambiguous).

Regression tests: examples/0730-0734 (default-arg, closure+fn-value, UFCS,
comptime #run, UFCS-ambiguity), each fails on pre-fix code and passes after.
issues/0102-flat-import-same-signature-collision.md written RESOLVED with the
4-sub-step root cause and regression-test paths.
2026-06-06 16:16:57 +03:00
agra
6f2a1dc3dc fix(types): type force-unwrap so opt!.field chains resolve [0101]
ExprTyper.inferType had no `.force_unwrap` arm, so `mk()!` typed as
`.unresolved`. The bind-first form (`v := mk()!; v.field`) worked because
lowerForceUnwrap produces a correctly typed value stored in a slot, but the
chained `mk()!.field` re-derives the receiver type via inferExprType and got
`.unresolved` — the struct-field lookup failed, the field read emitted as
`undef` (garbage), and `mk()!.method()` failed to resolve the method.

Add a `.force_unwrap` arm resolving the operand's optional child type. One
arm fixes every chained form — field, nested `opt!.a.b`, `opt!.method()`
(pointer + value receiver), and `opt![i]` all route receiver typing through
inferExprType.

Regression: examples/0905-optionals-unwrap-field-chain.sx — garbage / compile
error pre-fix, all correct after.
2026-06-06 07:42:17 +03:00
agra
d11f4c84b6 fix(lower): null-FuncId path restores full caller state [0100 F2]
lazyLowerFunction's three exit paths (non-null branch, already-promoted
early return, null-FuncId `ns.fn` qualified-alias branch) each duplicated
the caller-state restore, and the null branch's copy had drifted: it
restored every saved field EXCEPT `block_terminated`. A qualified alias
whose body terminates (e.g. a constant-folded `if true { return ... }`)
leaves `block_terminated = true` after lowerFunction; the null path
returned without resetting it, so the flag leaked into the CALLER's body
lowering and the caller's own trailing statements / `return` were rejected
as dead-after-terminator ("function ... body produces no value").

Fix: collapse the three restores into a single `defer` registered right
after the state is saved, so every exit path restores the identical full
set and the class cannot diverge again. Fields restored on all paths:
current_source_file (F1), scope, func_defer_base, block_terminated (F2),
force_block_value, builder.func/current_block/inst_counter. The
foreign-class / jni-env / pack-mono / inline-return fields already had
their own defers and are unchanged.

Regression: examples/0721-modules-qualified-terminating-callee.sx — a
qualified alias `m.foo` folds `if true { return helper(); }` (helper from
m.sx's own import) and is followed by caller statements + the caller's own
`return 0`. Reports "body produces no value" pre-fix; prints
"terminating-callee: ok" / "after" and exits 0 after. 0719 (collision) and
0720 (F1 own-import visibility) stay green. issues/0100 RESOLVED banner
extended with the F2 follow-up.
2026-06-06 03:15:29 +03:00
agra
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.
2026-06-06 02:51:09 +03:00
agra
3edc67521b fix(lower): resolve cross-module same-name functions by identity [0100]
Two modules each exporting a top-level function with the same short name
(std.cli.parse 3-param, std.json.parse 2-param) collided in IR lowering's
bare-name function table. fn_ast_map (name -> AST) was last-wins while
module.functions / resolveFuncByName are first-wins, so importing both and
calling one bound one function's AST against the other's FuncId and tripped
lazyLowerFunction's param-count assert (lower.zig:1606) — reached
unreachable code.

Fix:
- Register a namespaced import's OWN plain functions under their qualified
  name (ns.fn) in fn_ast_map, giving cli.parse / json.parse independent
  identities. The qualified resolution paths in CallResolver.plan /
  lowerCall already prefer ns.fn. NamespaceDecl now carries own_decls
  (populated in imports.addNamespace). Generic/comptime/pack/foreign
  functions are excluded (they dispatch by monomorphization off the bare
  template name); no eager declareFunction (it would resolve types before
  the forward-alias fixpoint).
- Make scanDecls' bare fn_ast_map registration first-wins so a later
  namespace recursion cannot clobber an earlier (flat) entry, aligning it
  with mergeFlat / resolveFuncByName.

Regression: examples/0719-modules-cli-and-json.sx imports both std.cli and
std.json under distinct namespaces and calls both parses; panics pre-fix,
passes after. issues/0100 marked RESOLVED.
2026-06-06 02:30:19 +03:00
agra
d515696e61 fix(lsp): identifier array dimension no longer panics the analyzer [0099]
`Analyzer.resolveTypeNode` read the array `.length` node's `.int_literal`
union field unconditionally. For a named-const dimension (`MAX :: 4;
[MAX]u8`) that node is an `identifier`, so the access tripped Zig's
checked-union panic and `sx lsp` aborted on didOpen. The main compiler
was unaffected (it folds the dim through the IR).

- New `arrayDimLength` helper switches on the dimension node tag:
  int_literal → value; identifier → a recorded module-const int value;
  anything else / out-of-u32-range → unknown. Never assumes a node shape.
- `Type.ArrayTypeInfo.length` is now `?u32`; null is an explicit "editor
  couldn't fold this dimension" marker (rendered `[_]T`), never a
  fabricated concrete length.
- New `const_int_values` registry records integer-literal consts at
  registration time for the identifier path.

Regression: first `src/lsp/*.test.zig` (the minimal LSP harness), wired
into the test graph via `src/root.zig`. Drives `analyzeDocument` over
`[MAX]u8` (folds to 4, no panic), `[64]u8` (happy-path guard), and
`[N]u8` (explicit unknown). Fail-before/pass-after verified.

Sibling audit of the resolveTypeNode/fieldType family: the array dim was
the only unchecked union-field access; all other arms recurse or
tag-check first. Noted a non-crashing display gap in server.zig hover
rendering for step B.
2026-06-05 23:33:22 +03:00
agra
1151d77e96 fix(ir): value-failable return — narrow target per-form + inline path [0097 F1/F2]
Attempt-1 narrowed lowerReturn's target to failableSuccessType(ret_ty) for
every value-carrying failable. That fixed the bare-enum success slot but
introduced two defects (attempt-2 review):

F1 — explicit full failable tuple `return (.v, error.X)` panicked. With the
target narrowed to the value type, the trailing error element no longer
resolved against the error set, leaving an `.unresolved` tuple field that
tripped "unresolved type reached LLVM emission" in backend/llvm/types.zig.

F2 — a `-> (Enum, !E)` body with a comptime parameter is inlined
(lowerComptimeCall), so its success `return .red` took the inline-return path,
which the first cut skipped: it stored `{value, undef}` (error slot undef) into
the inline slot, so the success error slot read garbage at runtime.

Fix: choose the return-expr target via failableReturnTarget(ret_ty, value_node)
— a BARE value resolves against failableSuccessType (real enum ordinal), while
an EXPLICIT full failable tuple literal (arity == full-tuple field count) keeps
the full-tuple target and is forwarded as-is. This applies on the inline path
too, and the inline value-failable return now routes through
lowerFailableSuccessReturn (whose emitTupleRet stores `{value, 0}` into the
inline slot + branches), so the success error slot is 0 there as well.

Regression: examples/1056-errors-enum-value-failable-tuple-and-comptime.sx —
F1 explicit-tuple error return + bare-value success in one fn (no panic, slot 0
on success, tag 1 on error); F2 comptime-param enum value-failable read at
runtime on the success path (cast, bare if, == error.X) + error path. Reads the
slot at runtime so an undef is caught, not masked by the `if !e` proof.
examples/1055 + the original 0097 repro still pass. Gate: zig build 0,
zig build test 0, run_examples.sh 453 ok / 0 failed / 0 timed out.
2026-06-05 22:42:12 +03:00
agra
82366a93df fix(ir): value-failable returning an enum zeroes the success error slot [0097]
A `-> (Enum, !E)` `return .variant` lowered the enum literal with
`target_type` set to the full failable tuple `(Enum, !E)` instead of the
success value type. The bare literal resolves its tag against `target_type`;
against a tuple it matched no variant (silent tag 0) and was stamped with the
tuple type, so `lowerFailableSuccessReturn` saw `val_ty == ret_ty` and took the
forwarding branch — returning the half-built `{value, undef}` aggregate and
never appending the `0` error slot. Every runtime read of the slot on the
success path (`cast(s64) e`, bare `if e`, `e == error.X`) saw garbage nonzero;
only the compile-time `if !e` proof masked it. The s32 case was already correct
because integer literals don't resolve variants against `target_type`.

Fix: in lowerReturn, narrow `target_type` to `failableSuccessType(ret_ty)` for
a value-carrying failable before lowering the returned expression. The enum
literal then resolves to its real ordinal and is typed as the value type, so
the success path correctly appends `0`. Forwarding (`return call()` / explicit
`(v, e)`) is unaffected — those still yield a value typed equal to the tuple.

Regression: examples/1055-errors-enum-value-failable-error-slot.sx reads the
error slot at runtime on the success path (cast, bare if, == error.X), checks a
non-zero ordinal (.blue=2, also corrupted to 0 pre-fix), and asserts the error
path still carries the right tag + error_tag_name. Fails pre-fix, passes after.
2026-06-05 22:10:14 +03:00
agra
b0d85a858c fix(ir): comptime print of an Any-held Type no longer silently stops [F0.12]
`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.
2026-06-05 20:48:49 +03:00
agra
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.
2026-06-05 20:02:11 +03:00
agra
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.
2026-06-05 19:26:22 +03:00
agra
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.
2026-06-05 18:15:17 +03:00
agra
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.
2026-06-05 17:43:45 +03:00
agra
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.
2026-06-05 17:00:12 +03:00
agra
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).
2026-06-05 16:28:12 +03:00