62 Commits

Author SHA1 Message Date
agra
b6a7378af4 feat(dist): bundled-zig link backend for hermetic macOS/Linux/Windows builds
Drive a bundled `zig` as `zig cc` for the AOT link step, supplying lld + CRT
+ libc (musl/glibc/mingw) so `sx build` produces native binaries with no host
toolchain. Default Linux output is static musl (portable-anywhere).

- src/zig_backend.zig: discover zig ($SX_ZIG / bundled-next-to-exe / PATH);
  bundled-vs-PATH provenance gates auto-activation.
- src/target.zig: selectZigLinker + emitZigLinkArgv + zigTargetTriple, dispatched
  before the per-OS branches; macOS/Linux/Windows in scope.
- src/ir/emit_llvm.zig: LLVMNormalizeTargetTriple so vendor-less zig triples
  (e.g. x86_64-windows-gnu) parse to the correct OS/object format (COFF not ELF).
- src/main.zig: --self-contained / --no-self-contained; linux-musl, linux-musl-arm,
  windows-gnu shorthands; de-vendor linux/linux-arm to match the corpus runner.
- examples/1660: Windows Win32 print-42 + exit(0) via kernel32 (ir-only off-Windows).

Auto-activates only for a bundled zig; a PATH-only zig engages under
--self-contained, so native dev/CI builds are never silently rerouted.

Docs: readme Cross-Compilation, design/bundled-zig-link-backend-design.md, current/PLAN-DIST.md.
2026-06-16 15:56:06 +03:00
agra
b4d1ce78c3 docs(asm): add user-facing inline-assembly guide
Adds docs/inline-assembly.md — a how-to guide for inline assembly in the
docs/error-handling.md style: mental model, operands (inputs / value
outputs / naming + auto-naming rule), the result-type table, volatile,
clobbers, all three `-> @place` forms (write-through / read-write /
indirect-memory), multi-instruction `#string` templates, global asm +
lib-less extern, the JIT/AOT-yes vs `#run`-no execution model, a
cookbook (read-register, x86_64 syscall, divmod), and rules of thumb.
All aarch64 snippets are verified to run; x86_64 ones are labeled. The
design doc (docs/inline-asm-design.md) stays as the internal rationale;
this guide is the user-facing companion, linked from readme.md.
2026-06-16 07:41:14 +03:00
agra
b8800a234c docs(asm): add Inline Assembly section to readme
Documents the `asm { … }` expression (template + `-> Type` / `= expr` operands +
clobbers), the §II.5 auto-naming rule (register pin → implicit name; echo form
rejected), the result-shape rule (0→void+volatile / 1→T / N→tuple), `#string`
multi-instruction templates, and top-level global asm + lib-less `extern`
call-into. Per the docs-track-changes rule (inline asm is a landed user-facing
feature). Examples are ones verified running in the corpus.
2026-06-15 22:28:10 +03:00
agra
e99383fcb4 docs(ffi-linkage): Phase 9.3-docs — purge 'foreign' from specs/readme/CLAUDE
Reword to the extern/runtime-class vocabulary: 'Foreign Function Interface' heading →
'C Interop'; 'foreign class'→'runtime class'; '#import c foreign decls'→'extern decls';
'foreign function calls'→'extern function calls'; the host_ffi #foreign("c") ref →
extern; the bundling 'foreign calls'→'extern calls'. Docs-only; zero 'foreign' left in
specs.md/readme.md/CLAUDE.md.
2026-06-15 09:24:55 +03:00
agra
3811311e12 feat(ffi-linkage)!: Phase 8.1 — parser hard-rejects #foreign (cutover)
The prefix #foreign linkage directive is removed. All four parse sites
(const-with-type, data global, fn body, runtime-class prefix) now reject it with
a migration message ('#foreign has been removed; use the postfix extern (import) /
export (define) linkage keyword instead'); added a span-aware failAt for the
runtime-class case (the lookahead consumes the token before the reject decision).
Greens the Phase 8.0 xfail 1176.

- Deleted obsolete tests: 1174 (#foreign+postfix conflict — unreachable now that
  #foreign alone is rejected) and 1620 (#foreign nosuchunit lib-ref — superseded by
  the extern twin 1231). Their assertions tested #foreign-specific behavior.
- Removed the GATE A→B unit test + lowerSrcToIr helper (lower.test.zig): it locked
  #foreign ≡ extern through the migration; with #foreign gone there is nothing to
  compare. Converted the in-source 'parse void function with foreign body' parser
  test to the surviving postfix 'extern' spelling (identical resulting AST).
- specs.md + readme.md drop #foreign; document extern/export as the sole C-linkage
  surface.

extern_export in parseFnDecl is now const (the fn-body arm that mutated it is gone).
Suite green (646 corpus / 444 unit, 0 failed). NOTE: comment-only #foreign in
examples + issues/*.md prose + internal foreign_* identifiers remain for Phase 9
(now unblocked: Decision 6 = purge everything).
2026-06-15 08:06:05 +03:00
agra
a8e0a8961b docs(ffi-linkage): document extern/export linkage keywords (Phase 4) 2026-06-14 15:40:37 +03:00
agra
e386a0d0b4 fix: reject direct assignment to a tagged-union variant member
A tagged union (enum-with-payload) is laid out { tag, payload }, but a
direct member write `s.rect = payload` lowered to a payload-only store
(union_gep into field 1) with no tag store — the discriminant went stale,
so a later match/== took the wrong arm with no diagnostic (issue 0136).
The read path already distinguishes tagged unions (enum_payload/enum_tag);
the write path treated them like plain unions.

A variant is set via construction (`s = .variant(payload)`, which writes
both tag and payload). A direct member write can't safely set the tag (the
active variant isn't known at the write site), so it is now rejected with a
diagnostic pointing to construction. A new diagTaggedUnionVariantWrite guard
— reusing the shared fieldLvalueResolve matcher, applied at both store sites
(lowerAssignment, lowerMultiAssign) — fires only for a whole-variant write
on a tagged union. Plain `union` writes and nested sub-field writes
(`s.rect.w = ...`) are unaffected.

Resolves issue 0136. Tests: examples/0185 (rejected), 0186 (nested write +
construction still work). specs.md / readme.md updated.
2026-06-13 21:18:40 +03:00
agra
d8076b9333 lang: rename signed integer types sN -> iN
Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.

Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).

Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.

zig build test: 426/426; examples suite: 595/595.
2026-06-12 09:31:53 +03:00
agra
49a36bb492 std: the prelude becomes a pure re-export facade — implementations move to std/core.sx, std/fmt.sx, std/list.sx
std.sx now contains only alias declarations (the re-export mechanism:
own decls carry one flat-import level) over three part-files: core.sx
(builtins, libc escape hatch, Source_Location/Allocator/Context/Into,
the reserved `string` decl — which needs and permits no alias), fmt.sx
(print/format/any_to_string/string ops/cstring/alloc_slice), list.sx
(List). The namespace tail is unchanged; the part-file namespaces
(core/fmt/list) carry alongside it. Consumer surface is byte-identical
— every bare prelude name resolves through the aliases (0120/0121
machinery). 37 .ir snapshots re-pinned: pure string-constant
renumbering from the changed import graph (digit-normalized diff is
empty). Gates: zig build test 426/426, suite 588/588, m3te 23/23,
game SxChess builds + bundles.
2026-06-11 19:25:49 +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
c229f697bd docs: const-aggregate semantics + unchecked-pointer contract (PLAN-CONST-AGG step 6) 2026-06-11 13:54:35 +03:00
agra
330c3aeef7 std: full namespace tail — fs/process/socket/json/cli/hash/test
With 0115's own-wins globals landed, the remaining tail modules join
std.sx: every '#import "modules/std.sx"' now carries mem/xml/log/fs/
process/socket/json/cli/hash/test as namespaces (trace stays a direct
import).

Enablers in the same change:
- emit: dead-global elimination — a plain-data global no instruction
  references is not emitted, so tail modules' data (hash's 64-entry K
  table, OS/ARCH/POINTER_SIZE) stays out of binaries that don't use it.
  Comptime-backed globals keep their #run evaluation. 37 pinned IR
  snapshots regenerated (dead globals dropped + string renumbering from
  the larger module).
- 1055/1056 stop pinning the global error-tag ordinal (it shifts with
  program composition); they assert nonzero + tag identity + name.
- specs/readme/CLAUDE.md tail docs updated.
2026-06-11 10:49:39 +03:00
agra
b8748aee16 docs: alias carry semantics + stdlib layout (step 4)
- specs §9: Namespace Alias Carry section (one level, own-wins, ambiguity,
  no chaining — 0114 noted for the still-ungated bare-call path), the
  three-tier import resolution (file dir -> cwd -> stdlib search path /
  SX_STDLIB_PATH), a Standard Library Layout section, real-layout examples
  replacing the stale modules/std/std.sx ones.
- readme: carry-rule teaser with the std namespace-tail example (verified
  to compile and run as written).
- CLAUDE.md: file-roles rows for std.sx/std//ffi//math//build.sx,
  tests/fixtures, and the PLAN-STDLIB tracker.
2026-06-11 08:46:17 +03:00
agra
698f75d79a lang: reject dir-vs-file ambiguous #import
An extensionless import path that names a directory next to a same-named
.sx file ('modules/std' with both modules/std.sx and modules/std/ present)
no longer silently resolves to the directory — it errors and asks for the
explicit .sx spelling. Exemption: a file importing its own companion
directory (X.sx importing X/, the multi-file test layout) stays legal —
the sibling file is the importer itself, so the directory is the only
sensible target.
2026-06-11 08:37:36 +03:00
agra
12bf61a9fc std: restructure step 3 — ffi/ moves, build.sx, math dir spelling, fixtures
- objc.sx, objc_block.sx (from std/) + sdl3/opengl/raylib/stb/stb_truetype/
  wasm vendor bindings (from modules/ root) -> modules/ffi/
- std/uikit.sx deleted: platform/uikit.sx already declares UIApplicationMain
  and imports objc; '#framework "UIKit"' cannot live in a file imported on
  macOS targets (unconditional link directive, UIKit is iOS-only), so the
  three iOS-only examples carry the 3-line glue inline. 1607/1608/1616 also
  un-rotted (dead ns_string -> 'xx "..."' Into conversions, callconv(.c)
  msgSend fn-ptrs) — all three build for ios-sim/ios again.
- math/math.sx -> math/scalar.sx; one spelling '#import "modules/math"'
  everywhere (4 pinned IR snapshots regenerated: dir import adds Vec2/Mat4
  to the type tables).
- compiler.sx -> build.sx (imports, CLAUDE.md bundling table, specs.md).
- testpkg/ + test_c.sx -> tests/fixtures/ (resolve CWD-relative from repo
  root, same as vendors/).
- library-internal imports use full modules/... paths (std.sx tail,
  platform/bundle.sx, fixtures).
2026-06-11 08:37:22 +03:00
agra
fea5617e4e lang: slice ranges take the same bound markers as for-header ranges
xs[1..=3] (end inclusive), xs[0<..<4] (both exclusive), xs[..=2]
(prefix form with markers, implicit 0 start), xs[2<..] (open end,
exclusive start), and xs[..] (whole collection) — lowered as lo+1 /
hi+1 on the existing subslice op. Strings slice through the same path.
An explicit end marker requires an end expression, matching the
for-header rule.

Regression: examples/0052-basic-slice-range-bounds.sx.
2026-06-10 22:12:45 +03:00
agra
fd14ab5694 lang: range bound markers — '=' inclusive / '<' exclusive on either side of '..'
Each side of '..' takes an optional bound marker, defaulting to
start-inclusive, end-exclusive (a..b == a=..<b; a..=b stays the short
end-inclusive spelling):

    for 0<..<N (i) { }   // 1 .. N-1   (both exclusive)
    for 0=..=N (i) { }   // 0 .. N     (both inclusive)
    for 0<..=N (i) { }   // 1 .. N
    for 0..<N  (i) { }   // 0 .. N-1   (explicit default)
    for xs, 2<.. (x, i)  // open range, exclusive start: i = 3, 4, ...

The nine lexemes are single tokens (maximal munch on '<'/'='/'..'), so
expression parsing never sees the leading marker as a comparison; '<',
'<<', '<=', '==', '=>' lex unchanged. An explicit end marker makes the
end expression mandatory; open forms are a.. / a<.. / a=... Works in
runtime, multi-iterable, and inline-for headers.

Regression: examples/0051-basic-for-range-bounds.sx (full matrix, open
start-marked ranges, comptime unroll, runtime bounds, lexer
non-regression); 1152's pinned message generalized.
2026-06-10 20:55:31 +03:00
agra
116af2359e lang: multi-iterable for loops — drop ':', add '..=', open ranges, arrow bodies
The for header is now a comma-separated list of iterables with a
positional capture group and no ':' separator:

    for xs (x) { }                    // collection
    for 0..n (i) { }                  // range (end exclusive)
    for 1..=5 (a) { }                 // ..= inclusive end
    for xs, 0.. (x, i) { }            // index idiom (replaces (x, i))
    for xs, ys (x, y) { }             // parallel (zip) iteration
    for xs (x) => sum += x;           // arrow body (full statement)

First-iterable-wins: the first iterable's length drives the loop and
must be bounded; the other positions follow by their own cursors (a
non-first range's end is not consulted or evaluated; a shorter
non-first collection is read past its length on mismatch). The old
single-iterable index capture is replaced by the trailing open range.

Capture/call disambiguation is positional: the paren group immediately
before '{' or '=>' is the capture, every earlier top-level group is a
call. 'for zip(a, b) (x, y)' calls zip; 'for f(n) { }' reads (n) as
the capture and errors with a parenthesize/add-capture hint. The old
':' form errors with a migration hint.

Lowering is unified across forms: one cursor slot per position (ranges
start at their start, collections at 0), all advanced together, the
first position's bound terminating. inline for keeps the single
bounded comptime range.

Migrated the full corpus (examples, library modules, issue repros,
in-source test strings). New coverage: examples/0050 (the full feature
surface) and examples/1149-1155 (seven diagnostic faces). specs.md For
Loop section + grammar rewritten; readme teaser updated.
2026-06-10 20:30:55 +03:00
agra
eb7636d0f3 fix(stdlib/E4): qualified generic head ns.Box(..) selects the namespace author
A qualified generic type head `ns.Box(args)` was stripped to its bare name and
read from the last-wins `struct_template_map`, so the namespace qualifier never
selected the template author: `a.Box(s64)` and `b.Box(s64)` (two namespaces each
authoring a same-name `Box($T)` with different layouts) both instantiated the
global same-name template. The documented ambiguity escape hatch ("qualify it as
ns.Box") silently produced the wrong layout.

Select the template via the namespace edge (importer -> alias -> NamespaceTarget)
instead of the bare map, at both the .call and parameterized-type-expr head
sites. Two same-name templates instantiated with the same args would also collide
on the mangled name `Box__s64`, so tag the non-canonical author's mangled name
with its source (the canonical bare-map author keeps the untagged name -> no
churn for single-author generics).

Extract `buildGenericStructTemplate` so the bare registration and the new
namespace-qualified selection share one template builder.

Regression: examples/0772 — two namespaces each authoring Box($T) with different
layouts; ns_a.Box(s64) and ns_b.Box(s64) resolve to their own module's template
(sizes 8 and 16). Fail-before on 566de96 (a=16 b=16), pass-after (a=8 b=16).
2026-06-08 17:19:41 +03:00
agra
cb9ef381b5 fix(stdlib/E4): own-wins at non-leaf bare-type sites + type-fn head ambiguity
attempt-6: address Adi's two in-scope findings (#3 deferred to E6).

#1 E4-own-author-type-arg (silent-wrong): the bare-TYPE gate returned
`.proceed` for the querying source's OWN author, so the non-leaf sites
(reflection / type-arg / array-literal / type-value / match arm) dropped it
and re-resolved a same-name flat import via global `findByName`. headTypeGate
now resolves the own author to ITS per-source TypeId (mirroring
selectNominalLeaf's own-wins, 0754); the type-as-value and type-match sites,
which only consumed the poison bit and re-resolved globally, now route through
the gate and use the `.resolved` author. size_of(Widget) with an own + imported
Widget now yields main's own size, not the import's.

#2 E4-type-fn-head-ambiguity (silent-wrong): headFnLeak only checked
isNameVisible, so two flat same-name type-returning functions both reported
"visible" and one was silently instantiated. It now diagnoses >=2 distinct
direct flat type-fn authors (no own author) as ambiguous before the
isNameVisible short-circuit, consistent with the parameterized struct /
protocol heads and the leaf (0755/0767). Own / single / diamond-collapse
type-fn heads still resolve.

Regressions: 0768 (own-wins at every non-leaf bare-type site, fail-before
reflection=16 -> pass-after 8) and 0769 (two flat Make type-fns -> ambiguity
diagnostic exit 1). README: own-wins + type-fn-head ambiguity at every bare-type
site.
2026-06-08 15:22:10 +03:00
agra
382f78f49b fix(stdlib/E4): carry full author outcome through the bare-TYPE gate (ambiguity at every site)
attempt-4 gated every bare-type-reference site for VISIBILITY via a boolean
leak-check that only caught not-visible and DROPPED the ambiguous outcome, so two
DIRECT flat same-name type authors (the 0755/0105 ambiguity case) fell through to
a global findByName / struct_template_map pick at the non-leaf sites.

Unified author-outcome fix (one path, every site consumes it):

- flatTypeAuthorCount: ≥2 distinct flat authors that do NOT all collapse onto one
  shared TypeId are now `.ambiguous` even when none carries a concrete TypeId yet —
  two same-name GENERIC TEMPLATES (template name registered in no findByName slot)
  are a genuine collision, exactly like two registered structs. Identical-target
  authors (diamond import / two aliases onto the same target) still collapse to
  `.one`, so all valid cases stay byte-identical.

- headTypeGate: the complete source-aware author outcome (.proceed / .resolved /
  .ambiguous / .not_visible) for an unqualified bare TYPE head, emitting the loud
  ambiguity diagnostic (consistent with the leaf / 0755) or the not-visible
  diagnostic. headTypeLeak is now its poison-vs-proceed projection, so every head /
  instantiation / alias-decl / match site poisons on ambiguity with the right
  message. Reflection / type-arg and array/vector-literal identifier heads consume
  `.resolved` to use the source-keyed TypeId, never a global findByName pick.

Regression examples/0767: size_of(Thing) / Nums.[1,2] / Box(s64) / t:Type=Thing /
case Thing: with two direct flat same-name authors each emit the ambiguity
diagnostic, exit 1 (fail-before on bb8f7dc: exit 0 / cascade). 0763/0764/0765/0766
/0755/0706/0544/0105 + FFI byte-identical. README: bare-type ambiguity is enforced
at every reference site.
2026-06-08 14:15:34 +03:00
agra
bb8f7dc5ec fix(stdlib/E4): route reflection/literal/value/match bare-type sites through the non-transitive gate
attempt-3 closed the leaf + parameterized-head leaks but several more
sites still resolved an UNQUALIFIED type name via the global
type_alias_map / findByName / type_bridge.resolveAstType without the
single-hop visibility gate, so a 2-flat-hop bare type leaked through:

  - resolveTypeArg (reflection / size_of / align_of / type_name / type_eq):
    identifier + type_expr leaves now gate via headTypeLeak; the wrapped /
    structural forms (*T, [N]T, []T, ?T, fn-ptr, tuple) route through the
    already-gated resolveTypeWithBindings so each inner leaf recurses the
    source-aware resolveNominalLeaf.
  - resolveTupleLiteralTypeArg: each element leaf is resolved through the
    source-aware resolver before the delegated build, so (COnly, s64) is
    gated.
  - resolveArrayLiteralType (T.[...] typed array/vector-literal head):
    identifier + type_expr leaves gate via headTypeLeak.
  - type-as-value lowerExpr identifier (x: Type = COnly, x == COnly).
  - type-category match arm (case COnly:).

Qualified ns.X / 1-hop / source-pinned library-internal references stay
exempt (the gate falls through for reachable / unauthored names, and
returns the existing "unresolved type" diagnostic for genuinely-undeclared
names). README notes the type gate holds wherever a bare type name is
named. New regressions 0765 (2-hop reject) / 0766 (1-hop pass).
2026-06-08 13:18:51 +03:00
agra
4f99fb0d85 fix(stdlib/E4): gate unqualified parameterized type heads non-transitively
attempt-3: extend the E4 single-hop bare-TYPE gate to parameterized type
HEADS (the constructor-head analog of the bare-leaf gate). Before this, the
head lookup hit the global struct_template_map / protocol_ast_map /
fn_ast_map *before* any source-aware visibility check, so a 2-flat-hop
imported generic struct/protocol/type-fn remained bare-visible (e.g.
`Box(s64)` when main imports only b.sx and b.sx imports c.sx).

- headTypeLeak: generic-struct / parameterized-protocol heads use the same
  type-author single-hop model as the bare-leaf gate (moduleTypeAuthor +
  flatTypeAuthorCount + localTypeInSource + nameAuthoredAsTypeAnywhere).
- headFnLeak: type-returning-function heads use single-hop function
  visibility (isNameVisible), exempting scope-local mangled type-fns.
- Gated at every unqualified head site: resolveParameterizedWithBindings,
  resolveTypeCallWithBindings, the scanDecls alias-decl dispatch (poisoning
  the alias with .unresolved on leak), resolveArrayLiteralType, and the
  generic-static-method call path. Namespaced (`ns.Box(..)`) heads are an
  explicit qualified reach and stay exempt. Source-pinned instantiation
  (E3/E4) is preserved, so library-internal heads still resolve where they
  are visible.

Regression: examples/0764-modules-import-generic-head-non-transitive
(2-hop `Box(s64)` -> "type 'Box' is not visible", exit 1; direct #import
resolves). Fails-before on a250964 (printed 3), passes-after.

README: note the non-transitive rule covers parameterized type heads.

Gate: zig build 0, zig build test 0 (LSP 522, 423/423), run_examples
505/0, FFI 12xx/13xx/14xx green, 0706/0763/0544/0105 green & byte-identical,
m3te ios-sim build+launch exit 0.
2026-06-08 12:37:00 +03:00
agra
33a6f5c650 wip(E4): partial source-pin + non-transitive flip [stdlib E4 attempt-1 WIP checkpoint]
Incomplete WIP from a worker killed at the 55-min wall (large blast radius:
core source-pin + ~8 example migrations + ~10 library module migrations).
Committed so the resumed session continues on a clean tree. May not build.
2026-06-08 11:12:08 +03:00
agra
4bd57c857e fix(resolver): type-author-aware + local-safe bare-TYPE gate; R3 model escalated [stdlib E1 attempt-3]
R1 (type-author-aware gate): the bare-TYPE visibility gate now requires a
flat-import-reachable TYPE author (struct/enum/union/error-set/protocol/foreign
class). A same-name flat VALUE/FUNCTION no longer makes a namespaced-only TYPE
bare-visible — the name-only `m.names.contains` check (attempt-2) is replaced by
`moduleAuthorsType` (kind-checked via `RawDeclRef`). Regression 0745.

R2 (no local false-positive): a block-local type clobbers the global type-table
entry for its name (`registerStructDecl`'s findByName-orelse-intern +
updatePreservingKey), so it IS the resolved type — never a namespaced-only leak.
A new `local_type_names` set, populated at both block-local type-decl paths,
exempts such names from the gate. Regression 0746.

readme.md: drop the false "transitively" claim — flat-import bare visibility for
functions and constants is NON-transitive (0706).

R3 (foundational model consistency) is ESCALATED, not resolved here — see the
attempt-3 worker report. Ground truth: making the TYPE gate single-hop (to match
the value/function model) breaks ~19 tests, ~13 of them library-INTERNAL generic
refs (e.g. `List.append`'s `alloc: Allocator`, lowered in the caller's source
context). That needs source-pinning generic instantiation to the template's
defining module — a separate architectural piece beyond E1's leaf-cut scope, and
proven risky (a `monomorphizeFunction` pin broke 4 FFI objc-block tests and did
not even take, since template method bodies lack a reliable `source_file`). The
TYPE gate therefore stays on the (type-author-aware) transitive flat closure for
E1; the non-transitive reconciliation is a routed follow-up.
2026-06-07 17:51:09 +03:00
agra
7188481761 feat(resolver): complete source-aware nominal-TYPE leaf — bare ns-only types not visible [stdlib E1 attempt-2]
Completes the F1 deliverable the reviewer flagged: the bare TYPE leaf still
returned the global `findByName` match BEFORE any visibility check, so a type
declared only behind a namespaced import leaked bare. Now the registered-type
branch of `selectNominalLeaf` is gated on bare-flat visibility (the type analog
of Phase B's value/function tightening): a bare reference to a namespaced-only
import's TYPE errors ("type 'X' is not visible; #import the module that declares
it") and poisons to `.unresolved` — never the leaked global match, never a
silent empty-struct stub.

Visibility gate is the TRANSITIVE flat-import closure (`typeBareVisible`), not
the single-hop `collectVisibleAuthors`/`isNameVisible`: a flat import is
transitive for resolution, so a type two flat hops away (`CAllocator`, via
`main → std.sx → allocators.sx`) stays bare-visible while a namespaced-only type
(reached solely over a namespace edge) does not. The gate applies ONLY to a
TOP-LEVEL author (`module_decls`) — a LOCAL type / generic-param / fabricated
empty-struct stub is findByName-registered but authored in no module, so it
resolves ungated and byte-identically (its own diagnostics still fire). The
compiler-synthesized default-Context emission falls open (`CAllocator` is
infrastructure, independent of the program's import style). The closure walk
lives in lower.zig, so resolver.zig keeps its single graph-walk.

A namespaced callee's declared return type now resolves in the callee's own
module context (`resolveTypeInSource` over `qualified_fn_source`) — a `Value`
returned by `json.parse` is visible inside `json.sx`, not at the call site
(issue-0100-F1 source-pin analog).

Migrates 0719 (flat-imports `cli` for its types, keeps `cli` namespaced for the
same-name `cli.parse`). Adds 0743 (bare ns-only struct → not visible) and 0744
(bare ns-only enum → not visible) regressions. 0742 (ns-only const) + 0210
(generics stay legacy) unchanged. readme updated.

Gate: zig build / zig build test (LSP sweep, no crash) / run_examples 481/0;
m3te ios-sim exit 0; 0743/0744 fail-before on 7cd12b0 (compiled, no diagnostic)
/ pass-after (clean "not visible").
2026-06-07 16:49:15 +03:00
agra
fc112200c7 docs(readme): stop quoting per-kind ns-import diagnostics (mid-migration) [stdlib B attempt-8]
The namespaced-only bare-visibility behavior is non-uniform and partial during
the resolver migration: runtime const/fn use errors, comptime/array-dim const
positions still compile, const-aliases report 'unresolved', and bare types still
resolve. Rewrite the note to state the durable rule (a namespaced import binds
only its alias; reach members as m.name; bare-name visibility joins over flat
imports only) and that bare references to ns-only members are being phased out
and do not yet resolve uniformly across name kinds. No specific diagnostic
string, no completeness/uniformity claim. Doc-only; no code path touched.
2026-06-07 10:04:40 +03:00
agra
aa2b79683b docs(readme): scope ns-only bare-visibility claim to values/functions [stdlib B attempt-7]
Phase B tightened bare VALUE/FUNCTION visibility through a namespaced-only
import (isNameVisible/isCImportVisible -> 'not visible'). Bare TYPE names
from such an import still resolve today; type-name visibility tightens in a
later resolver phase. Correct the README so it no longer claims all bare
names from a namespaced import error.
2026-06-07 09:51:57 +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
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
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
136484f72f docs(specs,readme): narrow runtime Type-value support to type_name/type_is_unsigned [F0.8]
The 7 type-only builtins doc claimed all of them accept a runtime Type
value, but only type_name and type_is_unsigned do. The other five
(size_of, align_of, field_count, type_eq, is_flags) are compile-time-only
— a runtime Type value (type_of(x)) yields 'unresolved type' since
runtime reflection is deferred. Reword both docs to the accurate scope.

Verified: type_name(type_of(x))=u32, type_is_unsigned(type_of(x))=true;
size_of(type_of(x)) / align_of(type_of(x)) -> error: unresolved type.
2026-06-05 12:31:21 +03:00
agra
b053c64149 fix(ir): reject non-type args to the 7 type-introspection builtins [F0.8]
size_of, align_of, field_count, type_name, type_eq, type_is_unsigned,
and is_flags silently reinterpreted a value argument as a type:
type_is_unsigned(6) read 6 as a TypeId index (types[6] = u8 -> true),
size_of(6)/size_of(true) sized its typeof (8), type_name(6) returned
types[6]'s name. Per Agra's ruling, all 7 now strictly require a type
(compile-time): a value argument is a compile error.

One shared guard (Lowering.reflectionTypeArgGuard, run at the top of
tryLowerReflectionCall) classifies each arg via reflectionArgIsType: a
spelled / compile-time type or generic type parameter (isStaticTypeArg),
or a runtime Type value (static type .any -- type_of(x), a []Type
element list[i], a Type-typed local/field/param) is accepted; anything
else is rejected with "<builtin> expects a type, got '<type>'". The
runtime path for type_name / type_is_unsigned is preserved (the {}
formatter calls type_is_unsigned(type_of(val)) at runtime). The 5
comptime-only builtins stay comptime-only (runtime reflection deferred).

Regression: examples/1144-diagnostics-reflection-builtin-needs-type.sx
(reject cases across all 7, exit 1). Unit test: reflectionArgIsType in
lower.test.zig. specs.md / readme.md document the strict type
requirement (and add the previously-undocumented align_of, type_eq,
type_is_unsigned). issues/0090 RESOLVED banner updated.
2026-06-05 11:22:59 +03:00
agra
64f77e9779 fix(std): render integer formatter extremes — i64::MIN and unsigned all-ones [F0.8]
Resolves issue 0090. The `{}` integer formatter mis-rendered both ends of
the 64-bit range:

- `int_to_string` computed the magnitude as `0 - n`, which overflows for
  `s64::MIN` (its magnitude is unrepresentable as a positive s64) — the
  value stayed negative, the digit loop ran zero times, so only `-`
  printed. It now extracts digits straight from `n` (per-digit
  `|n % 10|`, `n` truncating toward zero), never negating MIN.

- `any_to_string`'s `case int:` formatted every integer as s64, so a u64
  all-ones value printed as `-1`. There was no `uint` type-category to
  distinguish signedness. Added an additive `type_is_unsigned(T)`
  reflection builtin (static fold + dynamic interp/LLVM paths, mirroring
  `type_name`), backed by the new `TypeTable.isUnsignedInt` predicate, and
  a `uint_to_string` formatter (unsigned decimal via long-division over
  four 16-bit limbs). `case int:` routes through `type_is_unsigned(type)`.

The 16-bit-limb split is factored into a shared `decompose_u16x4`, now
reused by `int_to_hex_string` (no second unsigned-math routine).

Regression: examples/0046-basic-int-formatter-extremes pins both extremes
plus a width spread; unit tests cover `isUnsignedInt`. Docs (specs.md
representation note, readme std API) updated for unsigned/extreme `{}`
behavior. IR snapshots refreshed for the two new std functions.
2026-06-05 09:05:37 +03:00
agra
b69ec43ba3 fix(ir): infer mixed int+float arithmetic as the promoted float [F0.7]
`ExprTyper.inferType`'s binary-op arm inferred every non-comparison op
from the LHS alone, so `M + 0.5` (s64 + f64) statically typed as s64
while `0.5 + M` typed as f64 — operand-order-dependent. The value path
(`lowerBinaryOp`) already promoted int×float → float, so static
inference disagreed with the value: `M + 0.5` formatted as a truncated
int and a typed const `BAD : s64 : M + 0.5` was accepted+truncated
(issue 0088 mixed-numeric escape).

Extract the value path's inline promotion into a shared
`Lowering.arithResultType(lhs, rhs)` and reuse it at both sites, so
arithmetic / bitwise / shift inference reports exactly the type the
lowered value carries — int LHS × float RHS → the float, order-
independent. The value-path behavior is unchanged (the block is moved
verbatim into the helper), so no IR shifts; the suite stays green. The
typed-const validation reuses `inferExprType`, so this auto-closes the
escape with no change to the validation logic.

- examples/1143: BAD/BAD2 (`s64 : M + 0.5`, `s64 : 0.5 + M`) rejected
  in both operand orders.
- examples/0162: MF/MFR (`f64 : M + 0.5`, `f64 : 0.5 + M`) fold to 2.5.
- examples/0163 (new): pins the inference fix in a value context
  (`print("{}", n + 0.5)` formats the float, both orders, +-*/, f32).
- expr_typer.test.zig: arithResultType + mixed-arithmetic inference.
- specs.md / readme.md: document the numeric-promotion rule.
- issues/0088: RESOLVED banner notes the inferExprType root fix.
2026-06-05 08:23:59 +03:00
agra
454ea06bd4 fix(ir): validate const-expression typed module-const initializers [F0.7]
Attempt 1 rejected only LITERAL initializers that mismatch a typed module
const's annotation; a const-EXPRESSION initializer escaped, so the same
issue-0088 root remained for `M :: 2; N : string : M + 2` — accepted at exit 0,
folding `[N]s64` to 4 and printing N as an integer.

Root cause: `registerTypedModuleConst` validated only the enumerated literal
node kinds; any other kind fell through to `else => {}`, and pass 0
pre-registers binary_op/unary_op consts as a `.s64` placeholder that was never
reconciled with the annotation.

Fix — validate by TYPE, not by node kind:
- lower.zig: `registerTypedModuleConst` now covers literals AND const-expressions
  (binary_op/unary_op) through one path. `typedConstInitFits` keeps the literal
  arms and routes any non-literal through the new `constExprInitFits`, which
  compares the initializer's INFERRED type (`inferExprType`, the existing
  type-inference facility — no second const evaluator) to the annotation with the
  same integer/float compatibility. A mismatch emits the `type mismatch` diagnostic
  (a const-expression is described by its inferred type, e.g. "an integer
  expression") and evicts the pass-0 placeholder; a match registers the const at
  its resolved annotation type (the same `put` the literal path always did), so a
  const-expression folds and emits at its declared type.
- `literalKindName` → `initializerDescription` (+ `constExprDescription`) so the
  message is accurate for both a literal and a const-expression initializer.

Regression:
- examples/1143: extended with `E : string : M + 2` and `V : string : -M`
  (const-expr mismatches → exit 1, pinned diagnostics).
- examples/0162: extended with `KE : s64 : M + 2` (used as a count + printed) and
  `WE : f32 : M + 2` (over-rejection guard — valid const-exprs still work).
- program_index.test.zig: count-gate test extended with a binary_op value node
  declared `string` (must not fold as a count).

Docs: specs.md §3 + readme.md generalized from "initializer literal" to cover
constant expressions; issues/0088 RESOLVED banner updated.
2026-06-05 07:51:16 +03:00
agra
156edf8e28 fix(ir): reject typed module const whose initializer mismatches annotation [F0.7]
A typed module-level constant whose initializer did not match its
annotation was silently accepted: `N : string : 4` compiled, then
`print(N)` segfaulted (an integer emitted as a `string` const → a bogus
pointer) and `[N]s64` folded `N` to 4 as an integer count. Issue 0088.

Root cause: `registerTypedModuleConst` stored the annotation type but never
validated the initializer literal against it, and
`program_index.moduleConstInt` folded a const into a count by inspecting
the initializer node alone, ignoring `ModuleConstInfo.ty`.

Fix at the declaration (kills both symptoms):
- lower.zig: `registerTypedModuleConst` now validates the initializer via
  `typedConstInitFits` (arms mirror `emitModuleConst`'s faithful-emit
  precondition: int→int/float, float→float, bool→bool, string→string,
  null→pointer/optional, `---`→any). A mismatch emits a `type mismatch`
  diagnostic at the initializer span and does not register the const (also
  evicting the pass-0 placeholder). Not routed through
  `coercionResolver().classify`: that runtime-coercion planner is unsound
  here (null's natural type is void → false-rejects `*T`; bool is 1 bit →
  false-accepts s64).
- program_index.zig: `moduleConstInt` now takes the `TypeTable` and gates
  the fold on `isCountableConstType(ci.ty)` (integer of any width, or a
  float), so a non-numeric typed const can never fold into a count off its
  initializer node. Callers in lower.zig and type_bridge.zig updated.

Regression:
- examples/1143-diagnostics-typed-module-const-mismatch.sx (negative, exit 1)
- examples/0162-types-typed-module-const-roundtrip.sx (positive)
- program_index.test.zig: gate-on-declared-type unit test

Docs: specs.md §3 Constant Binding + readme.md note the compatibility rule.
2026-06-05 07:17:20 +03:00
agra
0fc7a72cbc feat(lang): std.cli exit-code + --json contract helpers [F3.3]
Foundation milestone close — the minimal exit-code / --json contract
`dist` relies on, in pure sx (no compiler change).

- EX_OK (0) / EX_USAGE (64, sysexits.h) / EX_UNAVAILABLE (70) named
  constants in std.cli.
- exit_ok() / exit_usage() terminators routing through the canonical
  process.exit(code: u8) — removes the hand-rolled cli_bail_exit `_exit`
  binding; the unsupported-platform path now uses proc.exit(EX_UNAVAILABLE).
- --json read is parsed.json (already parsed by F3.2); documented as the
  detection point with a stdout-pure / stderr-human convention.
- examples/0718-modules-cli-exit-json.sx exercises the contract: json true
  with --json / false without, EX_USAGE == 64, and a usage path that exits
  64 via exit_usage() (expected .exit = 64).
- readme.md gains a std.cli command-line-interface subsection.
2026-06-05 01:01:25 +03:00
agra
6478ccbe3c fix(lang): numeric-limit shadow guard covers all 3 value sources [NL.2]
The issue-0092 fix guarded the numeric-limit accessor intercept against
raw value shadowing using only lexical Scope.lookup. The ordinary
identifier field-access path resolves a value through THREE sources
(scope / program_index.global_names / program_index.module_const_map),
so a backtick raw identifier bound at module scope — a global
`` `f64 := Box.{…} `` or a module constant `` `f64 :: Box.{…} `` — still
folded `` `f64.epsilon `` to the numeric limit instead of reading the
value's field (issue 0093, plus the module-const variant: same root
cause, same fix).

Fix: a single shared helper Lowering.identifierBindsValue(name) that
returns true when the name resolves through scope OR global_names OR
module_const_map. Used in BOTH lowerNumericLimit (lower.zig) and the
numeric-limit inference arm (expr_typer.zig) so the two resolvers can't
desync (issue-0083 class). A bare `f64.epsilon` / `s32.max` (a
.type_expr receiver) still folds even when a raw value of the same
spelling is bound — the bare receiver is never value-shadowed.

- examples/0161: extended to exercise all three binding kinds — a
  GLOBAL `` `f32 ``, a MODULE-CONST `` `s16 ``, and LOCAL
  `` `f64 ``/`` `s32 ``/`` `u8 `` — each reading its field while the
  bare spelling still folds.
- src/ir/expr_typer.test.zig: unit test pinning the global +
  module-const sources of the shared guard.
- issues/0093: RESOLVED banner (3-source root cause + fix, module-const
  variant folded in).
- specs.md / readme.md: numeric-limit shadow note now source-agnostic
  (local / global / module-const).
2026-06-05 00:21:32 +03:00
agra
b0cc22a8c0 fix(lang): numeric-limit intercept no longer shadows raw value bindings [NL.2]
The numeric-limit accessor intercept (NL.1 integer `.min`/`.max`, NL.2 float
`.epsilon`/`.min_positive`/`.true_min`/`.inf`/`.nan`) treated ANY receiver
whose text matched a builtin numeric type name as a TYPE receiver, without
first checking for an in-scope VALUE binding. An F0.6 backtick raw identifier
(`` `f64 := … ``) binds a local under the stripped name `f64`; field access on
it (`` `f64.epsilon ``) parses as an `.identifier` receiver, which the intercept
silently folded to the type's numeric limit — a silent-wrong-value bug
(issue 0092).

Fix: for `.identifier` receivers, prefer an in-scope value binding
(`Scope.lookup`) over the fold — defer to ordinary field lowering when the
identifier resolves to a value. `.type_expr` receivers are unambiguous types
and are never shadowed, so a bare `f64.epsilon`/`s32.max` still folds even in a
scope where `` `f64 `` is bound (the parser classifies a bare builtin name as a
`.type_expr`). Mirrored in expr_typer.zig so inference matches lowering
(avoids the issue-0083 two-resolver desync). Float-only-on-int and
non-numeric-receiver errors are unchanged.

- src/ir/lower.zig: value-binding guard in lowerNumericLimit.
- src/ir/expr_typer.zig: same guard in the numeric-limit inference arm.
- src/ir/expr_typer.test.zig: unit test pinning the two-resolver agreement.
- examples/0161-types-numeric-limit-value-shadow.sx: regression — raw
  `` `f64 ``/`` `s32 ``/`` `u8 `` value reads coexisting with bare folds.
- issues/0092: RESOLVED banner.
- specs.md / readme.md: receiver-vs-shadowing-value-binding note.
2026-06-04 23:59:11 +03:00
agra
463557990f feat(lang): float numeric-limit accessors — examples, unit tests, docs [NL.2]
Finish NL.2 on top of the WIP compiler impl (2e9e4fe): f32/f64 expose
.min/.max plus the float-only .epsilon/.min_positive/.true_min/.inf/.nan,
folded via the shared lowerNumericLimit intercept + builder.constFloat.

- examples/0159: pins every f32/f64 accessor by untagged-union bit
  reinterpret against exact IEEE-754 hex (true_min read before any
  arithmetic — FTZ/DAZ), plus the defining-property checks
  ((1+eps)!=1 / (1+eps/2)==1, inf>max, min==-max, true_min<min_positive,
  true_min>0, nan!=nan).
- examples/0160: float-only accessor on an int (s32.epsilon/u8.inf/
  s64.true_min) and any accessor on a non-numeric type compile-error
  cleanly (exit 1, pinned stderr).
- type_resolver.test.zig: floatLimitFor bit-pattern + property tests for
  f32/f64, isLimitField coverage, null for non-float/non-limit fields.
- specs.md Numeric Limits: float accessors + the min=-max / min_positive=
  smallest-normal / epsilon=ULP-of-1.0 / true_min=smallest-subnormal
  clarifications, with the mandatory FTZ/DAZ flush-to-zero caveat.
  readme.md overview updated.
2026-06-04 23:30:41 +03:00
agra
b9a29c39c5 docs(lang): fix invalid protocol method-signature snippets — (self) -> () -> s64 [F0.6]
A protocol method signature omits the receiver; a bare `self` has no type, so
`protocol { … :: (self) … }` fails at parse with 'expected :'. Correct the three
member-exemption doc snippets (readme.md, specs.md, issues/0089) to the valid
signature form, matching examples/0158's `Speaker :: protocol { s2 :: () -> s64; }`.
2026-06-04 23:02:09 +03:00
agra
685d3d122b docs(lang): keyword-spelled f32/f64 still need a backtick in member-name positions [F0.6]
The member-name exemption applies only to identifier-classified reserved
spellings (s1..s64, u1..u64, bool, string, void, usize, isize, Any). f32/f64
are lexer keywords (token.zig kw_f32/kw_f64) and member-name slots require an
identifier token, so a bare f32/f64 field/tag/method name is rejected at parse;
the backtick is required there too. specs.md + readme.md corrected.
2026-06-04 22:42:09 +03:00