Commit Graph

461 Commits

Author SHA1 Message Date
agra
fbbfcb268c fix(0114): gate alias-qualified calls to one-level carry, pin to target
The lowerCall namespace branch routed alias.fn() through the global
qualified registration (first-wins) at any import depth, and through the
global last-wins bare map for comptime/generic members. Plain-identifier
alias roots now resolve via the carry-aware namespaceAliasVerdict:

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

Regressions: examples 0832 (two-hop), 0833 (carried collision),
0834 (own-target pin / first-wins repair).
2026-06-11 09:16:03 +03:00
agra
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
ee00db849c lang: qualified namespace members in value position + alias carry
Two coupled capabilities on the road to the std restructure
(current/PLAN-STDLIB.md, issue 0114):

1. alias.Type.method() / alias.Type as a call head, alias.CONST, and
   alias.Enum.variant now resolve — previously only alias.fn() and
   type-position alias.Type worked. objectIsValue treats an
   alias-rooted field_access as a type head; the call path strips the
   alias to the existing Type.method machinery; lowerFieldAccess
   resolves alias.CONST pinned to the target module and alias.Enum.x
   as a typed enum literal; resolveTypeWithBindings resolves qualified
   type_exprs pinned to the target.

2. The carry rule: namespaceAliasTarget resolves an alias from the
   file's own edges first, then from DIRECT flat imports (one level),
   diagnosing two distinct carried targets as ambiguous. All qualified
   shapes work through a carried alias — the std.sx namespace tail
   (mem.GPA.init() etc.) is now expressible.

Regression: examples/0831-modules-namespace-alias-carry.sx (direct +
carried, all seven shapes).
2026-06-11 05:52:10 +03:00
agra
83ec2536af lang: catch/onfail error bindings take parens
try foo() catch (e) { }   // legal
try foo() catch e { }     // parse error with a migration hint

Same capture style as the for-loop. All four catch shapes keep working
with the parenthesized binding — block, bare-expression body, and the
== match sugar — and the no-binding forms are unchanged. onfail follows
the same rule (onfail (e) { }); its expression-cleanup form is
disambiguated by the paren-group-before-brace lookahead, so
onfail (f()); stays an expression cleanup.

AST unchanged; the printer renders the parens; the #run escape help
text updated. Corpus migrated (57 catch + 3 onfail bindings, in-source
parser test strings, specs incl. grammar rules, readme untouched —
no catch examples there).

Regression: examples/1157-diagnostics-catch-binding-needs-parens.sx;
re-captured stderr for 1010/1013/1037/1123 (migrated source echoed in
carets + help text).
2026-06-10 23:05:02 +03:00
agra
12149eb548 fix(0113): negated-literal global initializers fold as constants
globalInitValue had no unary_op arm, so g : s64 = -1; fell into the
catch-all 'must be initialized by a compile-time constant' even though
constExprValue already folds negate(literal) for the module-const
identifier route. The new arm routes through constExprValue and applies
the direct-literal rules to the folded value: checkIntLiteralFits on
ints (g : s8 = -300 gets the range diagnostic), and a negated float at
an integer global narrows only when integral (-4.0 folds to -4, -4.5
errors). Binary-op initializers keep the specific non-constant
diagnostic.

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

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

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

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

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

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

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

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

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

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

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

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

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

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 16:34:17 +03:00
agra
5b304a29c1 fix(KB-9): move Lowering/ProgramIndex maps off page_allocator defaults
16 Lowering map fields and 8 ProgramIndex map fields were declared with
`= ....init(std.heap.page_allocator)` field defaults that init() never
replaced — every instance really allocated page-at-a-time outside the
compilation allocator, invisible to leak checking and never reclaimed.

All 24 now init explicitly with the compilation allocator (module.alloc
/ the init alloc param), which is arena-backed in both the driver
(main's arena) and the test suites (per-test arenas), so backing is
reclaimed at teardown. ProgramIndex's struct doc no longer claims the
page_allocator defaults.

Six lower.test.zig tests that constructed Module with bare
std.testing.allocator leaked once the checker could finally see these
maps; they now use the same per-test ArenaAllocator idiom as the rest
of the file and the facade test suites.

Gate: zig build OK; zig build test 426/426 (6/6 steps, leak-clean);
run_examples 541/0; zero expected/ snapshot churn.
2026-06-10 16:19:52 +03:00
agra
82500931ce cleanup(B-review): fix stale section banners, merge alias block, prune imports
Review follow-up to the ARCH-B split (comment/import hygiene only, no
code changes):

- Section banners that travelled to the wrong file with the B1-B8 cuts
  are reworded to describe the section that actually follows (e.g.
  stmt.zig's trailing "Expression lowering", expr.zig's "Control flow"
  before lowerChainedComparison) or deleted where nothing follows
  (4 trailing-at-EOF banners). ffi.zig's facade note no longer claims
  the IMP builders "stay here" (they live in lower/objc_class.zig);
  protocol.zig's namespace-lookup banner now points at
  pack.zig:resolvePackProjection for the orchestrator.
- lower.zig's two lower/expr.zig alias blocks (B8.1 + B8.2 appends)
  merged into one.
- 448 unused header decls pruned from the 15 lower/*.zig files (each
  had inherited lower.zig's full import block; pruned to fixpoint so
  cascading type-extraction consts went too).

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 16:09:24 +03:00
agra
89b3789973 refactor(B8.3): move lambdas + captures to lower/closure.zig
Verbatim relocation of the 5-method closure cluster (lowerLambda,
bare-fn trampoline, closure-to-bare-fn adapter, capture collection, env
sizing) into src/ir/lower/closure.zig. 5 aliases on Lowering keep all
call sites unchanged. Method pub-flip: typeAlignBytes.

Resolves the B7.1 flag: CaptureInfo relocates from lower/call.zig to
lower/closure.zig (its domain home, next to collectCaptures); the
Lowering type alias is repointed so external references are unchanged.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 14:43:04 +03:00
agra
35e35adb0a refactor(B8.2): move expr core + operators to lower/expr.zig
Verbatim relocation of the 8-method core expression cluster (lowerExpr
dispatch, ref-capture pointee, binary ops, tuple ops/lex-compare/
membership, chained comparison, emitCmp) appended to
src/ir/lower/expr.zig. 8 aliases on Lowering keep all call sites
unchanged.

Method pub-flips: isArithOperand, isBitwiseOperand, isOrderingOperand,
lowerLambda, binOpSymbol. expr.zig reaches arithResultType,
exprIsFailable, binOpSymbol via Lowering-namespace alias consts.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 14:40:44 +03:00
agra
0592c9dc97 refactor(B8.1): move expression literals/field/index to lower/expr.zig
Verbatim relocation of the 30-method expression cluster (struct/array/
tuple/enum/tagged-enum literals, init blocks, field access on values and
types, optional chains, numeric limits, indexing, slicing, deref, force
unwrap, null coalesce) into src/ir/lower/expr.zig — one contiguous
1,372-line cut. 30 aliases on Lowering keep all call sites unchanged.

Nested StructConstInfo stays on Lowering (field type of
struct_const_map), flipped pub and reached via an alias const, alongside
headNameOfCallee.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 14:38:11 +03:00
agra
ef4059bdde refactor(B7.3): move generics emission to lower/generic.zig
Verbatim relocation of the 39-method generics cluster (function/struct/
union monomorphisation + instantiation, type-arg resolution and
matching, type-name formatting/mangling, type-category tags, head-gate
author selection, value-param resolution + diagnostics) plus four
single-home nested types (HeadTemplate, HeadName, HeadTypeGate,
GenericStructMethod) into src/ir/lower/generic.zig. 39 aliases on
Lowering keep all call sites unchanged.

Method pub-flips: packResolver, resolveVectorLane. generic.zig reaches
five relocated/static helpers via Lowering-namespace alias consts
(inferExprType, isNamedTypeKind, resolveBuiltin, structMethodFn,
typeFnAuthor).

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 14:35:52 +03:00
agra
6b07346d98 refactor(B7.2): move pack/variadic lowering to lower/pack.zig
Verbatim relocation of the 19-method pack cluster (pack element/value
projection, spread refs, pack indexing + diagnostics, pack-to-slice,
variadic arg lowering/packing, pack-slice materialisation, pack-fn
calls + monomorphisation, pack projection resolution, isPackFn/
isPackParam predicates) into src/ir/lower/pack.zig. 19 aliases on
Lowering keep all call sites unchanged.

ProjectionPosition and PackProjection move from lower/protocol.zig to
lower/pack.zig (their domain home, next to resolvePackProjection);
the Lowering type aliases are repointed, so external references
(lower.test.zig) are unchanged.

Method pub-flip: findReturnValueType.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 14:30:34 +03:00
agra
54db29e60a refactor(B7.1): move call lowering to lower/call.zig
Verbatim relocation of the 18-method call cluster (lowerCall moved
whole, context diagnostics, foreign-call helper, builtin/function
resolution, generic + runtime-dispatch calls, reflection calls + guards,
default-arg expansion, call param typing) into src/ir/lower/call.zig.
18 fn aliases keep all call sites unchanged.

CaptureInfo (closure-domain type that sat inside the run) travelled and
is re-exposed via a Lowering type alias; candidate to relocate to
lower/closure.zig in B8.3.

Method pub-flips: callResolver, createBareFnTrampoline,
ensureGenericInstanceMethodLowered, fixupMethodReceiver,
getStructTypeName, isStaticTypeArg, lowerPackFnCall, packSpreadRefs,
packVariadicCallArgs, refCapturePointee, resolveParamTypeInSource,
typeSizeBytes, headNameOfCallee.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 14:27:20 +03:00
agra
b163c4a3fc refactor(B6.2): move Obj-C defined-class emission to lower/objc_class.zig
Verbatim relocation of the 23-method defined-class cluster (IMP/property
emission: class/alloc/static/dealloc IMPs, property getters/setters +
ARC runtime decls, defined-state field access, property/method chain
lookup, string-constant globals) plus the single-home
ObjcDefinedStateField type into src/ir/lower/objc_class.zig. 23 aliases
on Lowering keep all call sites (incl. expr_typer.zig facade and
lower/stmt.zig) unchanged. Zero pub-flips — all callees were already
public from earlier steps.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; all 37
.ir snapshots byte-identical, zero expected/ churn.
2026-06-10 14:14:49 +03:00
agra
638a048cc5 refactor(B6.1): move Obj-C/JNI call lowering to lower/ffi.zig
Verbatim relocation of the 24-method FFI cluster (selector/class-object
interning, FFI intrinsic + JNI calls, foreign instance/static method
lowering, super calls, foreign-class registration, Obj-C defined-class
method registration, JNI env TLS fids, JNI main-stub synthesis) plus
the file-scope jniMapParamType (no alias needed — all callers moved)
into src/ir/lower/ffi.zig. 24 aliases on Lowering keep all call sites
unchanged.

Method pub-flips: emitObjcDefinedAllocAndInit, findForeignMethodInChain.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; all 37
.ir snapshots byte-identical, zero expected/ churn.
2026-06-10 14:03:21 +03:00
agra
e884e87f80 refactor(B5.2): move coercions to lower/coerce.zig
Verbatim relocation of the 19-method coercion cluster (lowerXX, user
conversions, protocol erasure, default-value construction, zero values,
coerceToType implicit/explicit ladder, C-variadic promotion, call-arg
coercion) plus the nested single-home CoerceMode enum into
src/ir/lower/coerce.zig. 19 aliases on Lowering keep all call sites
unchanged.

Method pub-flip: prependCtxIfNeeded. ParamImplEntry stays a Lowering
nested type (field type of param_impl_map) and is reached via an alias
const.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 13:58:09 +03:00
agra
8990bd4978 refactor(B5.1): move protocol emission to lower/protocol.zig
Verbatim relocation of the 13-method protocol cluster (protocol decl
registration, param-protocol instantiation, thunk creation, vtable
globals, protocol-value construction, dispatch emission, impl lookup)
into src/ir/lower/protocol.zig. 13 fn aliases on Lowering keep all call
sites unchanged.

Two pub nested types travelled with the run (ProjectionPosition,
PackProjection) and are re-exposed via Lowering type aliases; they are
pack-domain types and may relocate to lower/pack.zig in B7.2.

Method pub-flips: allocViaContext, callForeign, genericInstanceMethod,
monomorphizeFunction.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 13:54:59 +03:00
agra
5928d9f067 refactor(B4.2): move nominal-type registration to lower/nominal.zig
Verbatim relocation of the 23-method nominal-type cluster (struct/enum/
union/error-set registration, anon-type qualification, nominal-id
stamping, shadow-slot reservation, named-type interning, generic struct
templates + alias registration) plus the nested ShadowTypeDecl union
into src/ir/lower/nominal.zig. 23 aliases on Lowering keep all call
sites unchanged.

Method pub-flip: instantiateGenericStruct. nominal.zig reaches
VisibleStructAuthor and structDeclOfRaw (both relocated to decl.zig in
B4.1) via Lowering-namespace alias consts.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 13:51:15 +03:00
agra
13f5fc57c1 refactor(B4.1): move decl scan + fn-lowering entry to lower/decl.zig
Verbatim relocation of the 52-method declaration cluster (lowerRoot,
scanDecls, entry-point checks, const/global registration, forward-alias
resolution, declareFunction + qualified registration, visibility,
lazy/deferred function lowering, same-name author selection) plus four
nested types (BareCallee, SelectedFunc, TypeHeadResolution,
VisibleStructAuthor) and the file-scope isExportedEntryName (all callers
moved with it) into src/ir/lower/decl.zig. 52 fn aliases + 3 type
aliases on Lowering keep all call sites (incl. calls.zig facade)
unchanged.

Method pub-flips: assertInstanceMapsCoincide, emitDefaultContextGlobal,
headFnLeak, initializerDescription, instantiateTypeFunction,
lookupObjcDefinedClassForMethod, lowerObjcDefinedClassMethods,
registerForeignClassDecl, registerGenericStructAlias,
registerNamespacedForeignClasses, reserveShadowSlot,
resolveTypeCallWithBindings, selectGenericStructHead,
synthesizeJniMainStubs, typeResolver, hasComptimeParams, isPlainFreeFn,
topLevelTypeDecl, isPackFn. Nested-type exposure: FnBodyReentry (+
enter/restore), ShadowTypeDecl.key/name/isGeneric.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 13:47:41 +03:00
agra
8adfc1dd50 refactor(B3.2): move control flow to lower/control_flow.zig
Verbatim relocation of the 14-method control-flow cluster (if/while/
for/match lowering incl. comptime-inline variants, break/continue, block
plumbing: freshBlock, freshBlockWithParams, currentBlockHasTerminator,
ensureTerminator) into src/ir/lower/control_flow.zig. 14 aliases on
Lowering keep all call sites unchanged.

Method pub-flips: computeHasImpl, headTypeGate, inferMatchResultType,
resolveTypeCategoryTags, isTypeCategoryMatch. Unqualified references in
moved bodies (ComptimeValue nested type, isTypeCategoryMatch static)
resolved via file-scope alias consts — bodies stay verbatim.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 13:18:07 +03:00
agra
3dbd3ce072 refactor(B3.1): move statement lowering to lower/stmt.zig
Verbatim relocation of the 24-method statement cluster (block/stmt
dispatch, var/const/local-fn decls, return, assignment + compound ops,
multi-assign/destructure, push, defer/onfail/cleanup) plus the nested
single-home FieldLvalue type into src/ir/lower/stmt.zig. 24 aliases on
Lowering keep all call sites unchanged.

Method pub-flips: buildDefaultValue, buildProtocolErasure,
diagPackAsValue, diagnoseMissingContext, emitError, emitFieldError,
ensureTerminator, getExprAlloca, getJniEnvTlFids, isPackName,
lazyLowerFunction, lowerObjcDefinedStateForObj, lowerObjcPropertySetter,
recordLocalTypeName, registerEnumDecl, registerErrorSetDecl,
registerStructDecl, registerUnionDecl, zeroValue.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 13:14:51 +03:00
agra
856299ce36 refactor(B2.1): move comptime hooks + const folding to lower/comptime.zig
Verbatim relocation of the 26-method comptime cluster (comptime eval
hooks, #insert, comptime calls/deps/substitution, source-const folding
and module-const selection) plus the three nested const-selection types
(SelectedConst, ConstAuthor, ConstSourcePin) into src/ir/lower/
comptime.zig. 26 fn aliases + SelectedConst type alias on Lowering keep
all call sites unchanged.

Shared file-scope helpers stay in lower.zig per the helpers-stay-home
rule, now pub: ConstFoldFrame, constFoldFrameContains, SourceConstCtx.
Method pub-flips: findVariantIndex, putGlobal, tryLowerAsExpr,
lowerVariadicArgs, resolver, setCurrentSourceFile, diagNonIntegralNarrow,
lowerStmt, stampCallerSource, resolveParamType, resolveReturnType.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 13:11:33 +03:00
agra
b240810b6f refactor(B1.1): move error-channel lowering to lower/error.zig
Verbatim relocation of the 53-method error cluster (error typing,
raise/failable, try/catch/or, inferred-set convergence, trace runtime
hooks) out of the Lowering struct into src/ir/lower/error.zig as free
functions taking *Lowering. Each gets a pub-const alias on Lowering, so
every call site compiles unchanged (decl-alias method resolution).

Pub-flips (callees now referenced cross-file): lowerExpr, coerceToType,
freshBlock, freshBlockWithParams, emitErrorCleanup,
currentBlockHasTerminator, lowerBlock, lowerBlockValue.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 12:58:46 +03:00
agra
2d68beb053 refactor(R3): migrate headTypeGate + bareVisibleStructDecl to single walk
headTypeGate and bareVisibleStructDecl were using the same
moduleTypeAuthor + flatTypeAuthorCount pattern that selectNominalLeaf
used before R2. Migrated both to a single collectVisibleAuthors call
with inline type-specific resolution, matching the R2 pattern.

Deleted now-unused helpers: moduleTypeAuthor, FlatTypeAuthor,
moduleTypeAuthorTid, FlatTypeAuthorCount, flatTypeAuthorCount.
Net: -76 lines.

541/541 regression tests pass. 426/426 unit tests pass.
2026-06-09 22:37:18 +03:00
agra
3b4df4ab8d refactor(R2): selectNominalLeaf uses single collectVisibleAuthors walk
Replaces the 3 separate author-collection calls inside selectNominalLeaf
(moduleTypeAuthor + ownConstDeclIsPendingAlias + flatTypeAuthorCount +
forwardAliasOrUndeclared) with a single collectVisibleAuthors call plus
inline type-specific resolution. The flat walk now handles:
- own named type: resolved or forward (slot not yet interned)
- own const_decl: resolved alias or pending (own wins over flat)
- flat named types: ambiguous / resolved / forward
- flat const_decl pending aliases: pending (for forward aliases in imports)

Deletes 3 now-unused helpers: forwardAliasOrUndeclared, constAuthor,
ownConstDeclIsPendingAlias. Net: -17 lines.

541/541 regression tests pass. Issue 0107 repro still outputs 300.
2026-06-09 22:31:12 +03:00
agra
010e644897 fix(R1): own const_decl pending alias wins over flat imports (issue 0107)
When a module declares `A :: B; B :: u64;` and both a flat import and a
namespaced import export `B :: u8`, the flat import's B was discovered by
flatTypeAuthorCount before the own B :: u64 was processed — binding A to
u8 and silently truncating values.

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

Regression: examples/0830-modules-flat-ns-same-name-forward-alias.sx
(x : A = 300 prints 300, not 44). 541/541 tests pass.
2026-06-09 22:16:47 +03:00
agra
5ed54a08ee refactor(R0): delete ResolvedProgram pre-pass; add resolveBare/resolveQualified
Removes the S2.x pre-pass and its 10 NodeRefTable maps — 1934 net lines
deleted. The Resolver gains two lazy functions: resolveBare(name, from,
domain) and resolveQualified(target, name), each returning ResolvedAuthors
(verdict + author set). verdictOver and authoredAsDomainAnywhere move from
ResolvePass to Resolver as private methods. All domain-predicate helpers
(eligibleKind, structDeclOf, fnDeclOf, etc.) are promoted to pub.

Test file trimmed from 1352 to 396 lines; old pre-pass population tests
replaced by focused resolveBare / resolveQualified verdict tests.

540/540 regression tests pass. Zero behavior change.
2026-06-09 21:55:53 +03:00
agra
2ae0ab1cff additive: compute resolver type-demanded verdicts 2026-06-09 15:55:33 +03:00
agra
a8d57521ac feat(stdlib/S2.2): selection verdicts in the resolver [additive]
Move the author-SELECTION SEMANTICS (the verdicts) into the resolver. Every
`.authors` ResolvedRef now carries the verdict the resolver COMPUTES above the
collector — own-wins / single-flat-visible / ≥2-ambiguous / not-visible /
type-vs-value domain-filtered — evaluated over the DOMAIN-ELIGIBLE subset of the
collected author set (`eligibleKind`). This folds the per-kind selection the old
lower-side selectors carried (selectNominalLeaf / flatTypeAuthorCount /
selectModuleConst / selectPlainCallableAuthor / selectGenericStructHead /
headTypeGate / headFnLeak) into ONE uniform computation, closing the
protocol / error-set / foreign per-kind surfaces (E6c/d/e) as resolver behavior.

Template/pack grammar stays carried as `.template` / `.pack` refs — NO
`sig_registration_mode`.

ADDITIVE / PARALLEL / UNCONSUMED: lowering still reads the old selectors, so the
verdict changes no generated byte. No file outside resolver.zig reads
ResolvedRef, so byte-identity is structural. ResolvedRef.authors is wrapped into
{ set, verdict } (the RAW set is preserved; the verdict filters).

Resolver unit tests prove the verdicts on real Phase A facts: the five bare-type
outcomes incl. the type-vs-value filter, and the resolver-target classes the old
selectors get WRONG — 0811 error-set / 0821 protocol-head / 0829 generic-struct
all → ambiguous (two flat authors, none own) and → own-wins (own author present).
The resolver-target corpus stays xfail in run_examples (unconsumed until S3.9);
verdicts asserted via the harness, not by flipping goldens.

Gate: zig build && zig build test (430) && tests/run_examples.sh (540 byte-identical),
all exit 0; tests/resolver-target 18 xfail unchanged.
2026-06-09 15:29:17 +03:00
agra
3b1415a287 fix(stdlib/S2.1c): preseed top-level UFCS aliases [additive] 2026-06-09 14:50:51 +03:00
agra
59681e0a09 feat(stdlib/S2.1c): foreign-class + struct-const + UFCS — final 3 domains on the owning pass [additive]
On the S2.1a owning traversal, populate the last three ResolvedProgram side
tables, closing planspec S2.1's full-population acceptance (all ten domains):

  - foreign_class_refs: a bare reference whose collected author is a
    foreign_class_decl is routed here (its own domain) instead of the bare
    type/value/callable table.
  - struct_const_refs: a Type.CONST field access whose base resolves to a
    struct author carrying that const member (mirrors lowering's struct_const_map).
  - ufcs_refs: a ufcs_alias decl (alias -> target author) plus its UFCS-rewrite
    call sites (alias(args), incl. pipe-desugared), via a global traversal-ordered
    alias map mirroring lowering's flat ufcs_alias_map.

Still PARALLEL / UNCONSUMED / RAW: lowering reads the OLD selectors, no consumer
cut over, ResolvedRef stays raw. Byte-identical vs baseline (540 examples).

Population proof extended: a new resolver.test fixture exercises all ten domains
at once and asserts each side table non-empty + node-keyed for the three new ones.
2026-06-09 14:32:27 +03:00
agra
1f10036a1a fix(stdlib/S2.1b): namespace-qualified parameterized heads [additive] 2026-06-09 14:01:23 +03:00
agra
6b41d113f2 feat(stdlib/S2.1b): namespace-qualified + 3 head domains on the owning pass [additive]
On the S2.1a exhaustive traversal, populate four more ResolvedProgram side
tables, still RAW / PARALLEL / UNCONSUMED:

- namespace-qualified references: an `alias.member` field_access whose base
  alias is a NamespaceEdges[ambient_source] target resolves via
  collectNamespaceAuthors into namespace_refs, keyed by the access node.
- the three HEAD domains at parameterized_type_expr heads, binned by the
  resolved author's decl kind: a struct with type params -> generic_struct_heads,
  a fn/const-wrapped fn with type params -> type_fn_heads, a protocol ->
  protocol_heads. RAW: the whole author set is recorded with no winner picked;
  a name authored as >1 head kind lands a distinct entry in every matching table.

Lowering still reads the old selectors and resolved_program has no consumer, so
generated output is byte-identical. ResolvedRef stays RAW (selection is S2.2);
generics stay symbolic. S2.1c (foreign-class / struct-const / UFCS) owns the
remaining three tables.

Extends the population proof: a resolver unit test asserting all four tables are
non-empty + node-keyed with the expected RAW authors.

Gate (all exit 0): zig build; zig build test (All 427 mod + exe + LSP sweep 574);
tests/run_examples.sh (540 passed, byte-identical); tests/resolver-target
(18 xfail, 0 leaked); m3te ios-sim via the main sx binary.
2026-06-09 13:38:59 +03:00
agra
1fb65e9e75 additive: complete resolver traversal gaps 2026-06-09 13:05:23 +03:00
agra
b46ad8b7a7 feat(stdlib/S2.1a): resolver.zig owning pass + ResolvedProgram scaffold + 3 bare-name domains [additive]
Turn src/ir/resolver.zig from a raw author-collection facade into the OWNING
resolution pass: one exhaustive recursive AST walk (exhaustive switch over
ast.Node.Data with NO else arm, so a new node kind is a compile error here
rather than a silently unvisited subtree) populating a ResolvedProgram.

- ResolvedProgram: all 10 node-keyed side tables declared as
  AutoHashMap(*const ast.Node, ResolvedRef) + symbolic TemplateParamId/
  PackParamId registries. ResolvedRef is the S2.1 RAW form — collected author
  identity (AuthorSet, own ∪ flat), NO verdict (own-wins/ambiguity is S2.2).
- Populate the 3 bare-name domains (type / value-const / callable heads) via
  collectVisibleAuthors(.user_bare_flat); record $T / ..$Ts / $pack[i] as
  SYMBOLIC template/pack refs, never TypeIds. The 7 head/qualified/foreign
  domains stay declared-but-empty (S2.1b/c own them).
- Slot via Compilation.resolveProgram() after the program_index facts are
  wired and before lowerRoot; ResolvedProgram owned on Compilation, borrowed
  *ResolvedProgram lent to ProgramIndex (lowerToIR signature unchanged).
- Population proof unit test over real Phase A facts: the 3 tables are
  non-empty, keyed by node identity, and carry symbolic template/pack refs.

ADDITIVE / PARALLEL / UNCONSUMED: lowering still reads the OLD selectors, so
single-author output is byte-identical. Gate green: zig build; zig build test
(425/425, LSP smoke 574 files no crash); run_examples (540 passed, 0 failed,
byte-identical incl. FFI 12xx-14xx + 1615 ios-sim); resolver-target (18 xfail
unchanged).
2026-06-09 12:29:27 +03:00
agra
cd7510067f refactor(stdlib/S1.2): delete module_fns; dissolve legacy_direct_any [additive]
Delete module_fns as a separate function-author fact source. Its authors
already live in the module_decls raw facts, so lowerRetainedSameNameAuthors now
reads function authors straight out of module_decls (filtered to *FnDecl via
fnDeclOfRaw) — the same path → name → RawDeclRef store, fn-filtered. Remove
imports.ModuleFns / FnIndex / indexModuleFns / buildModuleFns / fnDeclOf, the
Compilation.module_fns field + its build + wiring, and ProgramIndex.module_fns.

Remove VisibilityMode.legacy_direct_any (the quarantined own-scope-plus-full-
import_graph mode): no production caller passed it, so the collectVisibleAuthors
and isVisible switch arms that handled it are dead and go too, collapsing
VisEdgeSet to the single flat-import walk. No semantic fallback is introduced;
import_graph stays the transitive-visibility source for findVisibleImpls.

Additive: the old maps stay active and lowering still consumes them — no
lowering consumer is cut over to the DeclTable (that is S3), and no resolution
behavior changes. Tests that drove the removed symbols are rerouted through
module_decls / the flat-edge walk.

Gate over the baseline-green corpus: zig build, zig build test (424/424),
bash tests/run_examples.sh (540 passed) — all exit 0; single-author output
byte-identical; multi-author 0722–0740 stdout/exit unchanged.
2026-06-09 11:36:04 +03:00
agra
8058be2538 feat(stdlib/S1.1): DeclId for every declaration — additive DeclTable [additive]
Build a DeclTable in parallel with the import facts: every RawDeclRef
(source / imported / namespaced / C-imported) gets a stable DeclId carrying
source path, display name, AST node identity, span, and DeclKind. Namespace
targets record their members' DeclIds (NamespaceTarget.member_ids). A generic
struct's template is keyed by DeclId in a parallel struct_template_by_decl
store, written alongside the live name-keyed struct_template_map.

A Debug-only round-trip cross-check (RawDeclRef -> DeclId -> AST node ptr)
asserts the table identifies the same node across the corpus, run from
buildDeclTable and pinned by a unit test.

Additive (S0.1 class: mirror): the old maps stay active and lowering still
consumes them; nothing reads the DeclTable / struct_template_by_decl for
selection yet (the S4 cutover does). Generated IR + output bytes are unchanged
by construction.

Gate over the baseline-green corpus: zig build, zig build test (424/424),
bash tests/run_examples.sh (540 passed) — all exit 0; single-author output
byte-identical (37 .ir snapshots unchanged).
2026-06-09 11:25:04 +03:00
agra
b9a67d1042 fix(stdlib/E6a): adopt forward struct stub for recursive enum/union (E6A-1)
attempt-1's per-decl enum/union register path panicked on any valid
self- or mutually-referential top-level enum/union: a `*Name` field in
the body is resolved through the stateless `type_resolver.resolveNamed`,
which has no kind context and forward-stubs an as-yet-unregistered name
as a STRUCT. `internNamedTypeDecl` then `findByName`-adopted that struct
stub and called `updatePreservingKey`, whose kind-stability assert tripped
on struct -> enum/union (types.zig:446). The corpus had no recursive
enum/union, so the gate missed it.

Fix: when the slot `findByName` returns is a wrong-kind forward struct
placeholder (empty-fields struct) for an enum/union/tagged_union
registration, re-key it in place (`replaceKeyedInfo`) under the same
TypeId instead of `updatePreservingKey`. This mirrors how a self-ref
struct adopts its own (same-kind) forward stub; the new helper
`adoptsForwardStructStub` gates the re-key precisely to that case, so a
struct adopting a struct stub and every non-recursive enum/union stay on
the byte-identical `updatePreservingKey`/fresh-intern path.

Regression 0799 (single-author): self-ref union linked cells
(`next: *Node`), self-ref enum/tagged-union (`branch: *Tree`), and a
mutual-ref pair (A holds *B, B holds *A); builds and walks each recursive
link. Fail-before: panic at registerUnionDecl on eed2f99. Pass-after:
exit 0, "union=7 enum=42 mutual=99".

Gate: zig build && zig build test && run_examples.sh all exit 0
(538 passed, 0 failed; 0795-0798 + 0752-0794 + FFI byte-identical);
m3te ios-sim build via the main binary exit 0.
2026-06-08 23:55:46 +03:00
agra
eed2f99f76 feat(stdlib/E6a): per-decl nominal identity for enum + union decls
Give top-level ENUM and UNION decls per-decl nominal identity so two
same-name flat enums/unions intern DISTINCT nominal TypeIds instead of
collapsing to one global last-wins entry. Establishes the reusable
non-struct register path the later E6 kind-steps (E6b error-set, E6c
protocol, E6d foreign-class) extend.

Registration side (was: stateless `type_bridge.resolveInlineEnum/Union`
`findByName` last-wins short-circuit, no Lowering access):
- Split the type_bridge inline builders into a body-BUILDER
  (`buildEnumInfo` / `buildUnionInfo`) + the existing thin interner
  wrappers (field-type positions keep the legacy single-slot path).
- Add `Lowering.registerEnumDecl` / `registerUnionDecl` mirroring
  `registerStructDecl`: build the TypeInfo, intern via
  `internNamedTypeDecl(decl_key, name_id, info, nominal_id)` under the
  per-decl nominal identity (reserved slot id, else `shadowNominalId`).
- Reroute all six enum/union registration dispatch sites (scanDecls
  const-wrapped + top-level, lowerDecls/comptime, block-local, local
  const) to the new path.

Shared infra generalized ONCE:
- Pass-0b genuine-shadow pre-pass now reserves struct/enum/union shadow
  slots of the MATCHING kind, grouped by (kind, name), via a kind-generic
  `topLevelTypeDecl` / `reserveShadowSlot`. A forward/self/mutual ref to a
  shadow name binds to the reserved nominal TypeId.
- `namedRefTid` consults `type_decl_tids` for `.enum_decl`/`.union_decl`
  before the global `findByName`.

No new per-kind resolution path: selectNominalLeaf / headTypeGate /
flatTypeAuthorCount already gate every kind. Single-author /
phantom-double-spelling names keep nominal_id 0 (byte-identical corpus).

Regressions 0795-0798 (enum + union: ambiguity over every bare-type form,
and own-wins with distinct nominal TypeIds), fail-before/pass-after:
0795/0797 exit 0 -> exit 1 with the loud "type is ambiguous" diagnostic;
0796 silently printed `own=.east` -> correct `own=.north`; 0798 hard
`field 'm' not found` error -> correct `own=5 dep=9`.

Gate: zig build && zig build test (423/423) && run_examples.sh (537/537)
all exit 0; m3te ios-sim build via the main binary exit 0.
2026-06-08 23:18:29 +03:00
agra
919c7bd855 fix(stdlib/E5): source-aware value-const TYPE inference (F4)
Value-const SELECTION was source-aware for emission/folding (F2/R1/F1), but
expression TYPE inference still read the global last-wins `module_const_map`,
so an inferred return type / coercion on a same-name const borrowed another
module's const TYPE (mixed-type same-name consts were never exercised by the
attempt-1 same-typed goldens).

- expr_typer.zig: the `.identifier` const path now selects via the source-aware
  `selectModuleConst` (own-wins / one-flat-visible) instead of the global
  `module_const_map`. The global map still gates "is this a const name?"; an
  unpartitioned registration-only author emits its global type, and an ambiguous
  bare reference yields `.unresolved` (the emission path diagnoses loudly).
- lower.zig: expose `selectModuleConst` so the type-inference path shares the one
  author selector emission/folding already use.

Audited every `module_const_map` read: emission (4102) and global-init copy
(1447) were already source-aware (attempt-1); the binds-a-value predicate (6400)
is a boolean, not a type read; the in-`selectModuleConst` read (13842) is the
unwired fallback. No sibling inference site leaks.

examples: 0793 mixed-type own-wins inference (A's `K:s32` yields `1`, not the
global `f64`'s `1.000000`); 0794 mixed-type bare → loud ambiguous (exit 1), the
inference change does not mask the ambiguity. Prior E5 surfaces (0786-0792), the
0105 set (0752-0758), E1-E4 type surfaces (0763-0785) and FFI byte-identical;
533 markers green.
2026-06-08 22:07:12 +03:00
agra
5df4ac61a7 fix(stdlib/E5): source-aware same-name VALUE consts (own-wins / ambiguous / cross-module expr-chains)
Re-land the value-const analog of the E1-E4 type work, reconciled onto the
current source-keyed resolver and hardened. A same-name VALUE const declared in
multiple flat-imported modules is now resolved per declaring source, not the
global last-wins `module_const_map`.

- imports.zig: `isPerSourceDecl` retains every non-function `const_decl`
  per-source (value consts + type aliases), so each same-name author reaches
  registration as a distinct author of its own module. Functions and var_decls
  keep first-wins.
- lower.zig:
  * `selectModuleConst` over `module_consts_by_source` — own-wins; exactly one
    flat-visible resolves; >=2 flat-visible bare -> loud ambiguous (consistent
    with the 0755 type / 0724 fn / 0782 generic ambiguities). Rewires every
    consumer: `comptimeIntNamed`, the runtime-id read, the global-init read,
    and the float-name path (`lookupFloatName` / `nameIsFloatTyped`).
  * `SourceConstCtx` + `foldSourceConstInt`/`Float` + `sourceConstIsFloatTyped`
    fold a selected const's RHS with nested same-name leaves re-selected in
    their own author source, so VALUE and array-DIMENSION results are coherent.
  * `pinConstAuthorSource` pins each fold level to the SELECTED const's author
    (F1), including multi-level cross-module chains.
  * cycle guard keyed on (name, author-source), not name alone (F3), so
    same-name nested consts across modules do not trip a false cycle.
  * `emitModuleConst` takes the author source and pins while folding/lowering.
  Registration-time struct/inline-type field dimensions route through the now
  source-aware stateful reader; the type-alias dimension path resolves each
  alias against its own author's consts.
- program_index.zig: expose `isFloatConstType` / `isCountableConstType` for the
  source-aware folds.

examples: 0786 own-wins, 0787 ambiguous (exit 1), 0788 expr-chain value+dim
coherent, 0789 leaf-author-pin, 0790 cross-module cycle-guard (F3), 0791
multi-level cross-module chain, 0792 struct-field registration-time dim.
Single-author corpus byte-identical (524 prior markers green); 531 total.
2026-06-08 21:29:31 +03:00