21 Commits

Author SHA1 Message Date
agra
c92d11e748 docs(asm): Phase 0.2 — document <name>.build sidecar; Phase 0 complete
CLAUDE.md §Testing + §Test-layout now describe the optional `<name>.build` JSON
config (aot + target keys, ir-only arch-gating, unknown-key-is-error) and list
it among the `expected/` files, replacing the stale standalone `.aot` marker
prose. Closes Phase 0 (corpus target-gating); next is Phase A (kw_asm keyword).
2026-06-15 18:20:33 +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
6a539ca057 test(ffi-linkage): xfail export fn called from C via AOT (Phase 2.0)
Phase 2 of the extern/export stream verifies `export` (define + expose a
C-ABI sx symbol) end-to-end. C->sx-by-name linkage cannot work under the
corpus's `sx run` JIT mode — a JIT-resident symbol is invisible to a
dlopen'd C dylib's flat-namespace lookup — so this lands a new AOT
execution mode for the corpus: an `expected/<name>.aot` marker switches an
example from JIT `sx run` to a `sx build` + execute flow, linking the sx
object with its C `#source` companions into a native binary.

example/1226 defines `sx_square :: (n: i32) -> i32 export { ... }` and a
companion .c that declares `extern int sx_square(int)` and calls it back.
RED: with `export` not yet lowered, the AOT link fails with an undefined
`_sx_square` (the define path still emits it `internal` + with an implicit
ctx slot, and lazy lowering leaves an uncalled export fn as a bodiless
declare). Phase 2.1 greens it.

Also retires the standalone `tests/run_examples.sh` runner — `zig build
test` (src/corpus_run.test.zig) is now the sole corpus runner, and the
shell mirror would have needed its own AOT-mode port to stay in lockstep.
verify-step.sh drops its redundant step (zig build test already runs the
corpus); CLAUDE.md documents the `.aot` mode.
2026-06-14 14:41:33 +03:00
agra
ab3c9202ff test: run example corpus in zig build test; sx ir → stdout
`zig build test` now runs the full examples/ + issues/ regression corpus
alongside the Zig unit tests, driven by a pure-Zig test
(src/corpus_run.test.zig) — no shell script in the build path. It spawns
the installed `sx` per example (subprocess-isolated, per-run timeout),
diffs stdout/stderr/exit and optional `sx ir` snapshots, and fails the
build on any mismatch. The file list is enumerated at runtime, so new
examples are covered with no test edit.

- `sx ir` / `ir-dump` now write to stdout (fd 1) instead of stderr, so
  the dumps can be piped/redirected.
- `zig build test -Dupdate-goldens` regenerates snapshots in-build,
  byte-identical to the legacy `run_examples.sh --update`; on mismatch
  the runner prints how to regenerate.
- run_examples.sh kept (still used by tools/verify-step.sh) and made
  portable to a bare macOS: timeout/gtimeout fallback, bash 3.2-safe
  empty-array handling.
- CLAUDE.md: document the new workflow.
2026-06-13 09:41:56 +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
a2bd5b0438 plan: expand PLAN-CONST-AGG scope — struct-const migration, full pointer const-ness, const decay 2026-06-11 11:40:08 +03:00
agra
4f1a9738c7 plan: aggregate consts (PLAN-CONST-AGG) + file 0116 const-write hole
Array-typed '::' consts as immutable globals (approved design: storage
global + untyped inference + comptime-fold layer + const-write rejection
covering the pre-existing struct-const write crash, issue 0116).
2026-06-11 11:18:43 +03:00
agra
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
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
bc9777d81f docs: readme.md is now a maintained user-facing doc (lift freeze)
Per Agra ruling: user-facing sx changes must update all relevant docs
including readme.md. Replaces the prior 'Original syntax sketches. Do
not modify.' rule so the docs-track-changes review criterion is
enforceable from the next step onward.
2026-06-04 15:45:28 +03:00
agra
9e660f30c2 docs: update CLAUDE.md testing workflow to the new test layout
Reflect the migrated layout: XXXX-category-test-name naming with per-category
100-blocks; expected output in an expected/ dir next to each test, split into
.exit/.stdout/.stderr (+ optional .ir); runner scans examples/ and issues/.
Replace the old 50-smoke / tests/expected / examples/issue-* workflow with:
add a feature as a focused example, file open bugs as issues/NNNN-slug.{md,sx}
co-located, and resolve an issue by moving its repro into examples/ as a
regression test + marking the .md RESOLVED. Update stale test count (29 -> 324).
2026-06-01 19:40:39 +03:00
agra
ce7a4862a5 docs: rename planned errdefer keyword to onfail in CLAUDE.md 2026-05-31 12:34:55 +03:00
agra
8be1deea93 claude.md: register LANG and ERR workstreams
Add LANG (already had files in current/ but missing from the workstream
list) and ERR (new error-handling design, plan + checkpoint in current/
PLAN-ERR.md and CHECKPOINT-ERR.md — gitignored).

Updates the "On every session start" enumeration, the per-step
checkpoint-update guidance, and the File roles table to reference all
five streams.
2026-05-31 09:40:52 +03:00
agra
55e62694d1 ir: dedicated TypeId.unresolved sentinel; kill inferred_type => .s64
An unannotated param resolving to a plausible .s64 was the classic
silent-default trap (root of the 2.5 multi-param-closure bug). Replace it
with a dedicated TypeId.unresolved at slot 0, so a zero-initialised or
forgotten TypeId trips the sentinel instead of masquerading as a real type.

- types.zig: TypeId.unresolved = 0 (void moves to 17); TypeInfo.unresolved;
  sizeOf/toLLVMType @panic on it (codegen tripwire); hash/eql/printer cover it.
- type_bridge: inferred_type => .unresolved (was .s64).
- resolveParamType: emit "parameter 'x' has no type annotation" for a
  genuinely-unannotated value param (comptime/variadic/pack params exempt --
  they resolve via per-call substitution).
- lowerLambda: resolve unannotated params from the target closure signature;
  otherwise emit "cannot infer type of lambda parameter".
- CLAUDE.md: .void documented as an UNACCEPTABLE failed-type sentinel (it
  conflates with a real, heavily-checked type); prescribe a distinct
  .unresolved-style value + codegen tripwire.

Snapshot churn: one .ir (ffi-objc-call-06) -- the runtime type-name table and
typeof match arms renumber by the new builtin slot; program output unchanged.
2026-05-29 22:25:45 +03:00
agra
35c63a92d4 docs: scratch files go in .sx-tmp/, not /tmp (avoids approval prompts) 2026-05-29 19:34:43 +03:00
agra
da1063f1bb mem: allocator init returns state by value (drops state-struct heap alloc)
Building on the Option 3 lvalue-borrow rule, the long-lived allocators
in `library/modules/allocators.sx` (GPA, Arena, TrackingAllocator) now
return their state by value instead of via a heap-allocated `*T`. The
caller binds the result to a local; the local IS the allocator state.
`xx local` borrows that storage under Option 3, so the `Allocator`
protocol value's `ctx` points at the local — no heap allocation for
the state struct, no `free` of the state needed.

```sx
gpa     := GPA.init();                          // GPA (value)
arena   := Arena.init(xx gpa, 4096);            // Arena (value)
tracker := TrackingAllocator.init(xx gpa);      // TrackingAllocator (value)

push Context.{ allocator = xx tracker, data = null } { ... }
```

Why by-value:
- One fewer `libc_malloc` per allocator instance.
- No state-struct leak. The local is reclaimed at scope exit; `deinit`
  only handles downstream resources (chunks, etc.) — not its own struct.
- Owning structs can embed allocators as value fields directly.

Callsite changes:

- `library/modules/ui/pipeline.sx`: `arena_a: Arena;` / `arena_b:
  Arena;` (was `*Arena;`). The `build_arena: *Arena` local takes
  `@self.arena_a` / `@self.arena_b`.
- `examples/126-xx-recover-then-dispatch.sx`: `recovered == @gpa`
  instead of `recovered == gpa` (gpa is a value now).
- `examples/135-xx-lvalue-borrows.sx`: drop the `tracker_ptr.*`
  deref — `init` already returns the value.
- `examples/50-smoke.sx`: Arena alloc counts dropped by 1 (no
  state-struct allocation). Comments + snapshot updated.

`Arena.deinit` drops the trailing `parent.dealloc(xx a)` — the
caller's local owns the storage.

FFI IR snapshots regenerated to reflect the new signatures:
`@GPA.init` returns `i64` (was `ptr`); `@Arena.init` and
`@TrackingAllocator.init` use sret returns (was `ptr`).

CLAUDE.md "Allocator construction" rule rewritten around the
by-value convention. The forbidden caller-provides-storage and
redundant-pointer-rename patterns are still forbidden but for the
right reasons now (verbose, fragile) rather than as a workaround
for the old `init() -> *T` shape.

157/157 example tests pass; chess clean on macOS, iOS sim, and
Android via `tools/verify-step.sh`.
2026-05-25 15:33:28 +03:00
agra
f75b7caad1 docs: rewrite the long-lived-container rule as a self-contained principle
Previous version leaned on chess-specific terminology (GlyphCache, render,
frame arena) and made the rule read like a project memo. Replaced with a
generic `LongLived` example, a two-question test for when to apply, and
no incident-specific narrative. The "field name is by convention" line
removes the implicit prescription of `parent_allocator` so projects can
follow their own naming.

Also drops the explicit cross-reference list of existing examples — those
already drift with the code; the principle is enough to recognise the
shape when it appears.
2026-05-25 14:42:20 +03:00
agra
72593db953 mem: List(T) mutations gain optional alloc: Allocator = context.allocator
The chess panel-text regression (text vanished after the first move on
macOS) had a single root cause: GlyphCache's entries List, hash table,
and shaped_buf grew through `context.allocator` — which during render
is the per-frame arena. On the next arena reset the backing died, and
subsequent glyph lookups read garbage / wrote into freshly-allocated
view-tree memory.

Fix is shaped as the user proposed: `List(T)`'s mutations take an
optional trailing `alloc: Allocator = context.allocator` argument. No
allocator stored on the container, no init ceremony, every existing
`list.append(item)` callsite keeps working unchanged. Long-lived
owners now write `list.append(item, self.parent_allocator)` and the
arena-leak bug becomes impossible to write accidentally.

Default-arg substitution previously only fired for identifier callees
(`expandCallDefaults` at lower.zig:7978). Extended to the generic
struct-method dispatch path (`list.append(...)` lands here) via a new
`appendDefaultArgs` helper that lowers fd.params[i].default_expr in
the caller's scope and appends to the lowered args slice.

Long-lived owners updated to capture `parent_allocator: Allocator` at
init and use it for every internal growth:

- GlyphCache (the chess bug) — entries, shaped_buf, hash_keys,
  hash_vals, atlas bitmap.
- DockInteraction — drops the existing `push Context` workaround in
  `ensure_capacity` for the explicit-arg form.
- StateStore — entries list + per-entry data buffer.
- Gles3Gpu, MetalGPU — shaders, buffers, textures (atlas-grow during
  render would otherwise leak resources into the frame arena).

Also kept: an operator-precedence fix in pipeline.sx
(`(self.frame_index & 1) == 0` instead of
`self.frame_index & 1 == 0`, which parses as
`self.frame_index & (1 == 0)` = always 0). That was a stealth
single-arena-only bug that masked the GlyphCache one for a long time.

Docs:
- specs.md §11 documents `param: T = expr` default parameter values.
  The parser already supported it — formalised in the spec now.
- current/CHECKPOINT-MEM.md logs the change.
- CLAUDE.md REJECTED PATTERNS gains a "Long-lived containers growing
  through context.allocator" section with the `parent_allocator`
  capture template and the list of existing examples to mirror.

155/155 example tests pass — zero-diff against snapshots since every
existing callsite still resolves to `context.allocator`.
2026-05-25 14:41:17 +03:00
agra
4de565b7da docs: forbid silent unimplemented arms in REJECTED PATTERNS
Sibling to the silent-fallback-defaults rule. Catch-all `else`
branches that pass a value through unchanged, write a default width,
or swallow errors into a zero-init are the same class of bug — a
case the implementer didn't think of corrupts data silently.

Both bites this session:

- `storeAtRawPtr` writing 8 bytes regardless of IR type (fixed by
  threading val_ty through inst.Store).
- `.deref` else-arm returning val unchanged (now errors loudly for
  raw pointers).
- comptime init catch swallowing the error into `.void_val`.

Preferred order: implement the arm in the same step. If plumbing is
out of scope, bail loudly with `bailDetail(comptime msg)` and leave a
one-line comment about what's needed. Width/type/layout info that's
ambiguous from the Value tag belongs in the IR op struct, not in a
"this case is usually 8 bytes" shortcut.
2026-05-25 11:45:45 +03:00