Commit Graph

790 Commits

Author SHA1 Message Date
agra
0369cb001f fix(lower): stamp caller source on variadic comptime pack args [stdlib B attempt-6]
lowerComptimeCall stamped the caller's source onto fixed comptime `$`-params
so their substituted bare names resolve in the caller's visibility context,
but the variadic comptime pack branch (`..$args`) recorded the pack-arg slice
without stamping. Those nodes are later re-lowered via packArgNodeAt under the
defining-module pin, so a caller-owned helper in a formatted-arg position
(`std.print("{}", caller_fn())`) was checked against the metaprogram's module
and rejected as "not visible". Stamp every pack-arg node with the caller source,
mirroring the fixed-param treatment — completing Problem 1 for pack args.

Regression: examples/0739-modules-comptime-pack-arg-caller-context.sx
(two caller-owned s64 helpers in std.print pack positions; fail-before both
"not visible", pass-after prints "42 7"). No exemption flag, no silent default.
2026-06-07 09:31:17 +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
35457cb614 Merge branch 'flow/stdlib/A' into wt-stdlib-base 2026-06-07 00:06:52 +03:00
agra
5f06b6504f fix(imports): diagnose namespace-alias dup + propagate buildImportFacts errors [stdlib A attempt-2]
Two defects from the Phase A attempt-1 review.

F1 — duplicate-name diagnostic missed NAMESPACE ALIASES (silent first-win).
`addNamespace` unconditionally put the alias into scope/own_decls, so a
same-module collision between an authored decl and a `dup :: #import "…"`
alias compiled clean in the fn-then-alias order (the scalar
ModuleRawDeclIndex silently first-won). Now `addNamespace` returns a bool
and refuses a same-module duplicate (mirroring addOwnDecl); the call site
surfaces it via the new `reportDuplicateName` (the import_decl node has no
declName, so the alias name is passed explicitly). The C-import namespace
site gets the same guard. Both orders now emit "duplicate top-level
declaration 'X'" and exit nonzero (alias-then-fn was already caught by
addOwnDecl seeing the alias in scope).

F2 — buildImportFacts errors were swallowed by `else |_| {}` in core.zig
(REJECTED-PATTERN catch-all leaving the borrowed store silently empty).
`resolveImports` returns !void, so the call is now a plain `try` and a
build failure propagates instead of producing a stale/empty store.

Tests: extend the dup-name regression with fn-vs-namespace-alias
collisions in both orders. No resolution behavior change (no lower.zig
edits; run_examples 471 byte-identical); m3te ios-sim builds via the
worktree binary.
2026-06-06 23:54:51 +03:00
agra
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.
2026-06-06 23:34:32 +03:00
agra
db7af02950 Merge branch 'wt-fix-0102-base'
Some checks failed
Build / build-linux (push) Has been cancelled
Build / build-windows (push) Has been cancelled
2026-06-06 16:51:44 +03:00
agra
1c6ea7d84e Merge branch 'flow/distribution/fix-0102d' into wt-fix-0102-base 2026-06-06 16:51:29 +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
b660ea6ed9 Merge branch 'flow/distribution/fix-0102c' into wt-fix-0102-base 2026-06-06 15:45:57 +03:00
agra
2131557669 fix(lower): bare-call resolver skips non-plain authors before ambiguity gate [0102c F3]
resolveBareCallee's flat-collect branch counted ALL same-name authors —
including #foreign / generic / builtin / #compiler — before the
isPlainFreeFn filter, so two flat-imported modules each #foreign-ing the
same libc symbol under one sx name returned `.ambiguous` and errored,
instead of falling to `.none` and the existing first-wins foreign path
(master behavior). Filter authors to plain free functions DURING
collection, before the count/ambiguity determination: a non-plain
collision now yields 0 reroutable authors -> `.none`; genuine plain-fn
collisions still yield >= 2 -> `.ambiguous` (0724 unchanged). The
now-redundant single-author isPlainFreeFn check is dropped.

Regression: examples/0729-modules-flat-same-name-foreign — two flat FILE
imports each #foreign the same libc "abs" under name `absval`; a bare
call resolves first-wins and runs (exit 0). Fails-before on this branch
(ambiguity error), passes-after.
2026-06-06 15:31:14 +03:00
agra
8c88504849 fix(lower): resolved author drives call param target typing [0102c F2]
Attempt-3 fix for the F2 review finding. After resolveBareCallee picks a
shadowed same-name author at a normal call site, the call's PARAMETER TARGET
TYPING still ran first-wins: resolveCallParamTypes' bare-identifier branch
resolved param types via resolveFuncByName(name) / fn_ast_map.get(name) — both
keyed by name, not by the resolved author. Because that runs in lowerCall
BEFORE the resolveBareCallee routing, a shadow author whose parameter TYPE
differs from the first-wins winner had its args lowered against the WINNER's
signature (no implicit address-of for a *T param typed as T), then the
correctly-resolved shadow FuncId was called with the mis-typed arg — a value
bit-cast to a pointer → segfault.

The bare-identifier branch now routes through the SAME resolveBareCallee
resolver one layer earlier and takes the param target types from the RESOLVED
author's lowered func.params (userParamTypes). Only the .func (single resolved
author) outcome reroutes; .ambiguous keeps the existing loud call-site
diagnostic and .none keeps the first-wins fallback, so single-author / local /
std / qualified resolution is byte-for-byte unchanged. Method-call / namespace /
foreign / generic branches of resolveCallParamTypes are untouched. The resolver
is idempotent (bareAuthorFuncId guards body lowering via lowered_fids) so the
extra call from param-type resolution is safe; lowerFunctionBodyInto already
saves/restores all lowering state for mid-call reentry.

Regression: examples/0728-modules-flat-same-name-paramtype — two flat file
imports each author `apply` with a divergent param type (a.sx value `s64`
winner, b.sx pointer `*s64` shadow). b.sx's from_b passes a value local to its
pointer-param author via implicit address-of (×2 → 42); a.sx's from_a (own ==
winner) is unchanged (value + 1 → 11). Fails on the pre-fix typing (segfault at
from_b); passes after.

Gate (worktree): zig build, zig build test (400/400), bash tests/run_examples.sh
(464 passed / 0 failed) all green. Matrix 0722-0727 unchanged. Guardrail: m3te
builds via the worktree binary (sx build --target ios-sim, exit 0) — single-
author / local resolution intact. Default-arg / closure / UFCS / comptime SITES
remain first-wins (fix-0102d).
2026-06-06 15:07:51 +03:00
agra
8f9c00dcdb fix(lower): resolved author drives variadic packing in bare call [0102c F1]
Attempt-2 fix for the F1 review finding. After `resolveBareCallee` picks a
shadowed same-name author's FuncId at a normal call site, the call path still
re-fetched the FIRST-WINS function AST by name to drive variadic argument
packing. When the resolved (shadow) author's variadic shape differs from the
first-wins author's, arguments were packed against the WRONG signature — a
fixed-arity shadow packed as if variadic, or a variadic shadow not packed at
all — producing IR with the wrong argument count (LLVM verification failure).

The `.func` arm now carries the resolved `*FnDecl` alongside its FuncId
(`BareCallee.func: ResolvedAuthor`), so `packVariadicCallArgs` reads THE
resolved author's signature. The rest of the arm already used the resolved
FuncId's IR function (ret/params/ctx/coercion), so the callee now has one
source of truth in the whole call lowering — no re-fetch by name after
resolution. Default-arg / closure / UFCS / comptime *sites* remain first-wins
(fix-0102d); `expandCallDefaults` runs before resolution and is a default site.

Regression: examples/0726-modules-flat-same-name-variadic — two flat file
imports each author `combine` and `pick` with OPPOSITE variadic shapes (a.sx
fixed `combine` / variadic `pick`; b.sx variadic `combine` / fixed `pick`).
Each module's bare call must pack against ITS OWN author. Fails on the pre-fix
re-lookup (LLVM "Incorrect number of arguments passed to called function" for
both `combine.1` and `pick.2`); passes after.

Gate: zig build, zig build test (400/400), bash tests/run_examples.sh
(463 passed) all green. Matrix 0722-0725/0727 unchanged; single-author / local
resolution byte-for-byte unchanged (the `.func` arm never runs for them).
2026-06-06 14:28:00 +03:00
agra
ea35a05b26 fix(lower): bare-call resolver binds same-name flat authors per source [0102c]
Third of four fix-0102 sub-steps — the behaviour fix for NORMAL call sites.
Adds THE bare-name resolver `resolveBareCallee(name, caller_file)` over
fix-0102a's `module_fns` + `flat_import_graph` and routes the primary call
path through it:

- own-author wins: a file's bare call to a name IT authors binds its OWN
  author, not the first-wins merge winner. (When the winner already is the
  caller's own — every single-author and first-importer case — the resolver
  returns `.none` so the existing path binds it byte-for-byte.)
- a bare call to a name two or more FLAT imports both provide is `.ambiguous`
  and rejected with a loud diagnostic ("declared by multiple imported
  modules — qualify the call"); a namespaced author never collides.
- a single flat-reachable author that differs from the winner binds that
  author; otherwise `.none`.

The resolved shadow author lowers into its OWN FuncId via fix-0102b's
identity-addressable `lowerFunctionBodyInto` (shared `bareAuthorFuncId`
helper, also used by `lowerRetainedSameNameAuthors`). Only plain free
functions route — generic / comptime / foreign / builtin authors and any
scope-mangled / UFCS-aliased / locally-shadowed name fall straight to the
existing dispatch, so single-author / local / std / qualified resolution is
unchanged (full example suite stays green, including bundle.sx and the
comptime format/pack examples).

Examples 0722 (flat file per-source bind), 0723 (flat vs namespaced, no false
ambiguity), 0724 (ambiguous → diagnostic), 0725 (flat directory per-source
bind), 0727 (user namespace literally named __m0). Each fails on
wt-fix-0102-base (first-wins mis-bind / no diagnostic) and passes here. The
fix-0102b unit test now calls a per-module wrapper (main can't bare-call the
2-author name) and asserts the resolver's three variants directly.

Gate: zig build, zig build test (400/400), bash tests/run_examples.sh
(462 passed) all green.
2026-06-06 14:04:03 +03:00
agra
b077e8e29c Merge branch 'flow/distribution/fix-0102b' into wt-fix-0102-base 2026-06-06 13:28:44 +03:00
agra
bb1ed7294b fix(lower): key fn_decl_fids by stable AST pointer in scanDecls [0102b F1]
scanDecls declared a bare `.fn_decl` via `declareFunction(&fd, ...)`,
where `fd` is the switch-capture COPY of `decl.data.fn_decl`. Its
address is a per-iteration stack temporary, so the winner author's
`fn_decl_fids` entry was keyed by an address no later decl-identity
lookup can reproduce — `fn_ast_map` and `module_fns` carry the stable
`&decl.data.fn_decl`, so a lookup by that pointer missed the winner's
FuncId. fix-0102c routes calls through exactly these stable pointers,
so the key has to match.

Record the entry under `&decl.data.fn_decl` (the persistent AST node
field) to match `fn_ast_map`/`module_fns`. The other declareFunction
sites already pass stable pointers (const_decl field, module_fns entry,
fn_ast_map entry, struct-method node field, heap-synthesized objc decl);
`lowered_fids` keys by FuncId value, so neither has the temporary-address
mistake.

Strengthen the fix-0102b regression test: assert the identity map
round-trips by the STABLE pointer for BOTH same-name authors — the
winner's `fn_ast_map` pointer resolves to the first-wins FuncId, and the
shadow's `module_fns` pointer resolves to a distinct FuncId. This
assertion fails on the pre-fix code (winner keyed by `&fd` → null) and
passes after. Call resolution unchanged (name path still default).

Gate (this worktree): zig build, zig build test (400/400),
bash tests/run_examples.sh (457 passed) all green.
2026-06-06 13:22:11 +03:00
agra
237f794585 feat(lower): identity-addressable function body lowering [0102b]
Second of four fix-0102 sub-steps. Makes function declaration + body
lowering addressable by decl/FuncId IDENTITY instead of name-first-wins,
so two same-name authors can each carry their OWN body in their OWN
FuncId. Purely additive: the existing name path stays the sole resolver,
so the suite is byte-for-byte unchanged (no call rerouting — that is
0102c).

- declareFunction records `*const FnDecl -> FuncId` in a new identity map
  (`fn_decl_fids`), alongside the existing name-keyed function table.
- Extract the body-lowering tail of lazyLowerFunction into a reusable
  `lowerFunctionBodyInto(fd, fid, name)` that promotes a SPECIFIC extern
  stub into a real body by EXPLICIT FuncId — not by name lookup (which
  returns the first author). The shared save/restore preamble becomes a
  `FnBodyReentry` guard struct, used by both lazyLowerFunction's found
  path and the null-FuncId `ns.fn` alias path; issue-0100 F1/F2 behaviour
  (own-import source context, block_terminated transparency) is preserved.
- Add `lowerRetainedSameNameAuthors`: walks fix-0102a's `module_fns`,
  and for each SHADOWED flat author (a same-name author that is not the
  fn_ast_map winner, in a direct flat import of the main file) declares a
  fresh same-name FuncId + lowers its body in its own module's visibility
  context. FuncId-keyed `lowered_fids` tracks which slots already have a
  body. Not invoked during a default compile (the name path stays the
  default); 0102c wires it into bare-flat-call routing.
- lower.test.zig: regression that compiles two flat-imported modules each
  authoring `greet` and asserts ONE real body before the pass (winner
  only; shadow dropped) and TWO distinct non-extern bodies after — the
  shadowed author is no longer dropped/extern.

Gate (this worktree): zig build, zig build test (400/400),
bash tests/run_examples.sh (457 passed) all green.
2026-06-06 13:02:49 +03:00
agra
ccffbbb441 Merge branch 'flow/distribution/fix-0102a' into wt-fix-0102-base 2026-06-06 12:02:57 +03:00
agra
3dbc6f8434 fix(imports): keep merged scope first-wins; index dups in module_fns only [0102a]
Attempt-1 retained a same-name cross-module FUNCTION author in the merged
decl list (mergeFlat + the directory merge), which is the list the existing
first-wins resolver consumes. That changed the data feeding resolution
(`mod.decls` carried two `greet`), violating this step's purpose: additive
indexes with ZERO resolution change.

Revert both merge sites to byte-for-byte first-wins, exactly as on
wt-fix-0102-base. The dropped same-name author is still retained — but only
in the SEPARATE `module_fns` index, which is built from each module's
`own_decls` (un-deduped, per-path) and which nothing reads yet. The
`flat_import_graph` side data is likewise untouched. Both are foundation
for fix-0102c's bare-name disambiguation; current resolution is unchanged.

Drop the now-unused `declAuthorsFn` helper (its only callers were the two
merge sites). `fnDeclOf` stays — it feeds the index.

Tests: the existing unit test now asserts the merged scope stays first-wins
(one `greet`, a.sx's author) while `module_fns` still retains BOTH authors
and `flat_import_graph` excludes the namespaced edge. Add a mixed non-fn/fn
collision test asserting the merged scope keeps a.sx's struct (first-wins),
unchanged by the function author.
2026-06-06 11:53:16 +03:00
agra
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.
2026-06-06 11:27:11 +03:00
agra
792ed55068 Merge branch 'flow/distribution/fix-0101'
Some checks failed
Build / build-linux (push) Has been cancelled
Build / build-windows (push) Has been cancelled
2026-06-06 07:47:23 +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
52310b6df1 Merge branch 'flow/distribution/fix-0100' 2026-06-06 03:24:56 +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
fb3fdaf454 Merge branch 'flow/distribution/lsp-corpus-sweep' 2026-06-05 23:57:55 +03:00
agra
503dfd8344 test(lsp): permanent corpus-sweep over the editor analyzer [dist B]
Adds `src/lsp/corpus_sweep.test.zig`: a permanent test that drives the
editor analyzer (`DocumentStore.analyzeDocument` — the exact path the
server's `textDocument/didOpen` uses) over EVERY `.sx` file in the
example + issue corpora, in process. The contract: analysis must
complete without panic/abort for any file. A panic aborts the test
binary — the loud CI signal that some new AST node shape crashes the
analyzer (the bug class issue 0099 fixed at sema.zig:397).

- Corpus dirs are injected as absolute paths at configure time
  (build.zig `corpus_paths` options module) so the sweep is
  CWD-independent; the FILE LIST is still read from disk at test time,
  so new examples are covered automatically with no test edit.
- Imports resolve against the shipped `library/` (root_path + stdlib
  path set), so the analyzer runs over real, fully-resolved code —
  maximum crash surface, mirroring an editor session opened on the repo.
- Wired into `zig build test` via the `src/root.zig` lsp barrel, same
  mechanism document.test.zig uses (refAllDecls reaches one struct deep,
  so the file is referenced directly).
- `SX_LSP_SWEEP_VERBOSE` prints each file before analysis; on a crash the
  last printed line names the offending file.

Coverage: 470 examples + 1 issue repro analyze with zero crashes.
Regression-guard proven: temporarily reverting A's sema.zig:397 fix
(`@intCast(ate.length.data.int_literal.value)`) makes the sweep abort
with `access of union field 'int_literal' while field 'identifier' is
active`; restoring it turns the sweep green.
2026-06-05 23:52:22 +03:00
agra
bef2c66be2 Merge branch 'flow/distribution/fix-0099-lsp' 2026-06-05 23:41:26 +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
8eb514a804 Merge branch 'flow/distribution/fix-0097' 2026-06-05 22:53:55 +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
e466bd5ddf Merge branch 'dist-foundation' 2026-06-05 21:32:34 +03:00
agra
40e5ea1bfb Merge branch 'flow/sx-foundation/FINAL-rem' into dist-foundation 2026-06-05 21:19:24 +03:00
agra
035ef6c613 fix(examples): 0712 sha256 streaming writes scratch to .sx-tmp not /tmp [FINAL-rem F2]
Scratch file path violated the project rule 'scratch files go in
.sx-tmp/, never /tmp'. Route the temp .bin through the repo-local,
gitignored .sx-tmp/ dir, creating it at runtime via create_dir_all
(mirroring 0713) so the test is self-contained on a clean checkout.
Digest assertions and output are unchanged.
2026-06-05 21:18:08 +03:00
agra
60e5939fdf Merge branch 'flow/sx-foundation/F0.12' into dist-foundation 2026-06-05 20:55:00 +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
794f2ef94a Merge branch 'flow/sx-foundation/F0.11' into dist-foundation 2026-06-05 20:07:52 +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
agra
4c12e1de38 fix(ir): unify float→int narrowing — integral folds, non-integral errors [F0.11]
Issue 0095: a typed local/param/field silently TRUNCATED a float initializer
to an integer annotation (`y : s64 = 1.5` → 1) with no diagnostic. Agra ruled
the UNIFIED rule (Option B): an implicit float→int in a typed binding behaves
like the array-dimension rule —

  - an INTEGRAL compile-time float FOLDS to its int (`4.0` → 4, `-2.0` → -2);
  - a NON-integral float is a COMPILE ERROR (`1.5`, `4.5`);
  - explicit `xx` / `cast(T)` ALWAYS truncates (the escape hatch).

Applied consistently to typed local / param-default / field-default, typed
module CONST, and array dim — all reusing the single
`program_index.floatToIntExact` / `evalConstIntExpr` facility (no second
integral check).

- `Builder.constFloatInfo` reads a compile-time `const_float` back from its
  Ref (value + span).
- `coerceToType` is now the IMPLICIT path: its `.float_to_int` arm folds an
  integral const-float to `constInt`, else emits the narrowing diagnostic.
  `coerceExplicit` is the raw truncating path; `xx` (lowerXX) and `cast(T)`
  route through it so the escape still truncates.
- Field-default lowering (struct-literal pad, named-field default,
  buildDefaultValue) now coerces the default to the field type at the IR level
  (was silently bit-coerced by emitStructInit).
- Const path: `typedConstInitFits` accepts an integral float (literal or a
  `M + 2.0`-style expression folding via `evalComptimeInt`); `emitModuleConst`
  / `constExprValue` / `globalInitValue` fold an integral float to its int and
  reject a non-integral one — relaxing F0.7's blanket float rejection.

Tests: examples/0168 (positive: local/field/param/const fold, xx/cast
truncate), examples/1146 (negative: local/param/field error), integral-float
const cases added to examples/0162; non-integral const cases in 1143 stay
errors. specs.md + readme.md document the unified rule, cross-referencing the
array-dim rule. issues/0095 marked RESOLVED.
2026-06-05 15:34:33 +03:00
agra
341b62c197 Merge branch 'flow/sx-foundation/F0.10' into dist-foundation 2026-06-05 14:49:54 +03:00