Commit Graph

113 Commits

Author SHA1 Message Date
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
b3b78e25c0 std: organise the facade — thematic groups, aligned columns
Same 47 re-exports, regrouped: compiler-resolved types, type-system/
reflection builtins, output + libc escape hatch, formatting (print/
format up front, the *_to_string family together), string ops +
allocation helpers, fmt internals marked as legacy-surface, List, tail.
Zero output-stream diffs; 37 .ir snapshots re-pinned (renumbering from
decl reordering). Gates: suite 588/588, zig build test 0, m3te 23/23,
game builds + bundles.
2026-06-11 21:46:03 +03:00
agra
c75cd9c63c std: drop the redundant flat mem.sx import from the facade
The flat #import of mem.sx predated the namespace tail — the tail's
mem :: #import already puts mem.sx in the program graph, which is all
the ufcs helpers (context.allocator.create/alloc/free/clone) and the
CAllocator default-context machinery need; std.sx itself references no
mem name. Probe-verified the full mem surface + all gates: suite
588/588, zig build test 0, m3te 23/23, game builds + bundles. The
double import was also duplicating lowered IR — the 37 re-pinned .ir
snapshots net ~2.5k lines smaller; output streams byte-identical.
2026-06-11 21:25:32 +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
51194a26d8 mem: BufAlloc.init returns the state by value — full buffer usable, no header carve 2026-06-11 17:31:20 +03:00
agra
a47ea1416e lang: opt-in UFCS — ufcs-marked fns + alias dot-dispatch, generic binding via receiver; one binding builder for plan-side generic returns 2026-06-11 17:04:51 +03:00
agra
84e0fb0752 mem: typed allocation helpers + drop bare malloc/free (Phase 2.2); resolve 0119 as |>-contract clarification 2026-06-11 16:17:39 +03:00
agra
88bae3c9f5 mem: rename Allocator primitives to alloc_bytes/dealloc_bytes (Phase 4 naming pulled forward, Agra-approved) 2026-06-11 15:33: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
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
59f0aa7716 std: restructure — std/ modules, namespace tail, std/xml.sx
allocators/fs/process/socket/log/trace/test move under modules/std/
(allocators.sx becomes std/mem.sx; the Allocator protocol moves into
the std.sx prelude, impls stay in mem.sx). New std/xml.sx holds
xml_escape as xml.escape. std.sx gains the carried namespace tail —
flat-importing std.sx now also provides mem./xml./log. — with the
remaining modules (fs/process/socket/json/cli/hash/test) deferred from
the tail until the global last-wins maps are fully own-wins (pulling
them into every closure collides bare names corpus-wide; they stay
direct imports: modules/std/fs.sx etc.). log.sx's internal emit
renamed log_emit (it clobbered consumer fns named emit program-wide).
bundle.sx uses xml.escape via the carried alias. Consumer import paths
swept mechanically; .ir snapshots recaptured for the larger std
closure. m3te + game build unchanged.
2026-06-11 06:10:59 +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
d98ad5c14f feat(stdlib): per-decl nominal identity + same-name shadows — close 0105 [stdlib E2]
Make same-name top-level types in different sources DISTINCT nominal types
instead of collapsing last-wins in the type table (issue 0105).

Registration:
- internNamedTypeDecl assigns a per-decl nominal_id and populates
  type_decl_tids. The first author of a name keeps nominal_id 0 (byte-identical
  to pre-E2); a genuine cross-module shadow (>=2 distinct normalized-path
  authors per the import facts) gets a fresh id -> a distinct TypeId.
- mergeFlat/addOwnDecl stop first-wins-dropping per-source decls (named types +
  non-fn const_decls) so every same-name author reaches registration; functions
  and var_decls (incl. #foreign extern globals) keep first-wins.

Resolution (selectNominalLeaf):
- own-author wins; else flatTypeAuthorCount over the transitive flat closure:
  >=2 distinct -> .ambiguous (loud diagnostic + poison); exactly one -> resolved;
  a flat author not yet findByName-registered -> .undeclared stub (not a leak).
- struct-literal type names route through the same source-aware leaf.
- lazyLowerFunction pins the function's own source before resolving its return
  type, so a shadowed signature type resolves in its module, not the caller's.

Codegen:
- mangleTypeName appends __n<id> for nonzero nominal_id so same-name shadows get
  distinct monomorph symbols (struct_to_string__Box vs __Box__n1).

Library hygiene:
- rename trace.sx's compiler-contracted Frame -> TraceFrame (+ the two compiler
  findByName sites) so it never collides with a UI/geometry Frame; the layout is
  structural (getFrameStructType / SxFrame), name-independent.

Examples: 0752-0756 pin the five 0105 cases (distinct fields / same fields /
own-wins / ambiguous / alias per-source); 0170 pins the folded anon-struct-field
regression.
2026-06-07 22:57:28 +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
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
17b437ecfb F3.2: std.cli minimal subcommand + flag parser over explicit []string
Extend std/cli.sx with a zero-heap argument parser that the caller drives
over a logical argv ([]string), separate from the F3.1 os_args accessor.

Grammar: <group> <command> [--flag VALUE | --bool]... [--json] [-- rest...]
  - (group, command) dispatched against a caller-provided Command table;
    no match -> error.UnknownCommand.
  - value-taking vs boolean flags fixed by each command's FlagSpec list;
    --json is a reserved global boolean surfaced as parsed.json.
  - `--` or the first bare operand ends flag parsing; the remainder is
    parsed.rest (operand views).

Heap discipline (heap-discipline.md): zero heap, zero copy. group/command/
flag values/rest are all VIEWS into args. Parsed is a by-value stack struct;
flag presence/values live in a fixed [16]FlagValue inline array indexed by
spec position (no per-flag allocation, no context.allocator). The flag-spec
list and command table are caller storage passed as views.

Failure surfacing (no silent skip): unknown command, unknown flag, a
value-flag missing its value, and an absent required flag each raise a
specific CliError variant; a caller-owned Diag records the offending token
(index + view) before each raise, since error tags carry no data.

examples/0717 drives the parser over explicit []string vectors: a valid
group/command/--flag/--bool/--json case (asserting parsed values + that
values are views into argv), subcommand dispatch, `--`/bare-operand
separators, and the five failure variants each asserted via destructure +
Diag. zig build && zig build test && run_examples.sh green (385 passed).
2026-06-04 06:13:09 +03:00
agra
e7f5bd7aaa F3.1: std.cli os_args — real OS argv accessor via #foreign _NSGetArgv (examples/0716)
Add library/modules/std/cli.sx: a pure-sx command-line argument accessor
backed by the macOS C runtime (_NSGetArgv/_NSGetArgc), no compiler change.

  os_argc() -> s64
  os_args(buf: []string) -> []string

Zero heap, zero per-arg allocation: os_args fills a caller-provided buffer
(stack array) with string VIEWS over the process's own argv block, which
lives for the whole process. The returned slice header is a by-value stack
return; nothing touches context.allocator.

Documents the `sx run` reality: under `sx run <prog.sx> ...` the process
argv is the interpreter's argv (sx, run, prog.sx, ...), not a program's
logical args. This accessor reports the real process argv truthfully;
mapping to logical args is a later consumer concern (distribution P3.1).

Non-macOS platforms bail loudly (message + _exit) rather than returning a
silent empty.

examples/0716-modules-cli-argv.sx asserts only deterministic structural
invariants (argc >= 1, argv[0] non-empty, os_argc() == filled length).
2026-06-04 03:21:41 +03:00
agra
2871342c0a F2.2: reject raw control bytes (U+0000..U+001F) in JSON strings
parse_string scanned for `"` and `\` but accepted every other byte,
including raw control characters. RFC 8259 §7 requires those bytes to be
escaped inside a string; an unescaped one is invalid JSON and must surface
a parse error, not be silently accepted.

Add `BadControlChar` to JsonParseError and reject any unescaped byte < 0x20
in the string body scan (which gates the decode path too, so escaped forms
like \t/\n/	 still decode correctly; 0x20 and 0x7F are not over-rejected).

Regression test in examples/0714: raw 0x09/0x0A/0x00 each raise
BadControlChar via `?`/`!`; a positive case proves the escaped forms still
decode to the right bytes. All prior assertions kept.
2026-06-04 02:32:32 +03:00
agra
88be541778 F2.2: std/json reader — explicit-alloc parse with error surfacing
Add the JSON reader (parser) to library/modules/std/json.sx, the inverse
of the F2.1 writer over the same value model: insertion-ordered objects,
arrays, strings (full unescaping incl. \uXXXX + surrogate pairs), s64
integers, bool, null.

Heap discipline (binding): exactly two allocation kinds, both through the
EXPLICIT `alloc` parameter, never the implicit context allocator —
composite backing stores (Array/Object.items via add/put) and decoded
escaped-string buffers (bounded by the raw span). Un-escaped string
values are zero-copy VIEWS into the input buffer (valid only while it
lives); scalars carry no heap.

Failure surfacing (hard contract): malformed input raises a meaningful
JsonParseError variant (UnexpectedToken / UnexpectedEnd / BadEscape /
BadNumber / TrailingGarbage) on the error channel, never a bogus value.
Trailing non-whitespace is TrailingGarbage; fractions/exponents,
out-of-s64 magnitudes, and leading zeros are BadNumber. Number
accumulation runs in negative space so s64 MIN parses exactly.

examples/0714-modules-json-reader.sx asserts the parsed structure
(insertion order, every kind), proves the view-vs-decoded heap split by
pointer containment, round-trips back through the writer byte-for-byte,
decodes a surrogate-pair into 4 UTF-8 bytes, and checks every malformed
variant.

Filed issues/0078: a string `==` (or any sub-CFG operand) used in a
short-circuit `and`/`or` emits invalid LLVM IR (stale PHI predecessor),
hit while writing the example's assertions and worked around there by not
combining comparisons with `and`/`or`. src/ untouched.
2026-06-04 01:41:33 +03:00
agra
4552ed61f6 std/json: value model + zero-alloc writer with stable key order
Add library/modules/std/json.sx — the JSON value model and writer
(reader lands in a later step).

Value model: a tagged union over null/bool/integer(s64)/string/array/
object. Objects are an ORDERED list of (key,value) pairs preserving
INSERTION ORDER (no hash map, never sorted/deduped). Integers only — no
fraction/exponent this milestone.

Heap discipline:
  - Scalars carry no heap; string values are VIEWS into caller memory
    (never copied into the node).
  - Composite nodes (Array/Object) own growable child storage, allocated
    through an EXPLICIT allocator parameter on the builder methods
    (arr.add(v, alloc) / obj.put(key, val, alloc), mirroring List.append)
    — never the implicit context allocator.
  - The writer adds ZERO output allocations: it emits into a caller-
    provided Sink, either a fixed []u8 buffer (overflow raises, never
    truncates) or streaming straight to an fs.File through a small caller
    staging buffer (no whole-document string; peak memory O(staging)).
    Integer digits format in a stack [20]u8; s64 MIN is handled by
    formatting in negative space. Sink/IO/overflow surface on the !
    error channel.

examples/0713-modules-json-writer.sx builds a nested object + array +
string with every escape kind + negative int + bool + null, then asserts
the EXACT bytes (insertion order, escaping) from both the buffer sink and
the file-streaming sink, plus the overflow-raises path.
2026-06-04 00:47:30 +03:00
agra
f9bc593bb8 F1.2: std.hash zero-heap [64]u8 hex API + chunked file + pinned vectors
Make the SHA-256 digest path allocation-free (foundation heap-discipline):

- final() and sha256_hex() now return the 64-char lowercase hex digest as
  a [64]u8 by value on the stack; the cstring(64) heap allocation is gone.
- sha256_file() streams the file in fixed 64KB stack chunks via open_file/
  File.read/File.close (defer-closed on every path) instead of slurping it
  with read_file; peak memory is O(chunk), not O(filesize).

Tests (compare via a zero-copy string view over the [64]u8):
- 0710 updated to the by-value API (output unchanged).
- 0711 known-answer vectors: "", "abc", NIST-56/112, padding boundaries
  {0,55,56,57,63,64,65,119,120}, and 1000 / 1,000,000 'a' repeats, each
  pinned to its published digest (cross-checked with shasum -a 256).
- 0712 streaming equivalence (one-shot == byte-at-a-time == split-mid-block
  == split-on-boundary) plus sha256_file(temp) == in-memory digest.

src/ untouched. zig build && zig build test && tests/run_examples.sh green.
2026-06-04 00:08:46 +03:00
agra
8f9691c206 F1.1: std.hash — streaming SHA-256 in library/modules/std/hash.sx
Add a pure-sx streaming SHA-256 (FIPS 180-4) stdlib module, importable
as `#import "modules/std/hash.sx";`. All 32-bit word arithmetic is done
in s64 and masked back with `& MASK32`, so digests are deterministic and
platform-independent — no shelling out, no native crypto.

API:
- init() -> Sha256          (by-value *self pattern)
- update(*Sha256, string)   (multi-block + partial-block buffering)
- final(*Sha256) -> string  (32-byte digest as lowercase hex)
- sha256_hex(string) -> string             (one-shot)
- sha256_file([:0]u8) -> ?string           (digest of a file via fs.read_file)

Verified against FIPS/NIST known-answer vectors and `shasum -a 256`:
"" , "abc", the 56- and 112-byte multi-block vectors, 1000×'a', and the
64/65-byte block boundaries; chunked update() matches the one-shot call.

examples/0710-modules-sha256.sx pins the KAT vectors + the streaming
invariant; gate green (zig build, zig build test, run_examples 370/0/0/0).
2026-06-03 22:38:58 +03:00
agra
df6e830bec fix(diagnostics): reject reserved type-name bindings in every module (issue 0077)
The issue-0076 reserved-type-name binding diagnostic only ran over main-file
decls, so an imported module (or the stdlib) could still declare `s2 := ...`
and reach lowering, where the address-of family loads the whole aggregate and
passes it by value to a `ptr` param — LLVM verifier abort.

Extend coverage to every compiled module: a dedicated `checkBindingNames` walk
(in semantic_diagnostics.zig) visits every var/`:=`/typed-local binding name and
function/lambda/struct-method parameter at any depth, with NO main-file filter,
descending the `namespace_decl` that a `mod :: #import` wraps so imported-module
decls are reached. It tracks each module's source_file (save/restore per node)
so the diagnostic renders against the imported module's text. Rejection still
defers to the parser's `Type.fromName` classifier; the unknown-type check (0064)
stays main-file-only. No lowering special-case; `.identifier`-only address-of
paths are unchanged.

Stdlib audit: the only reserved-name bindings under library/ were two `u1`
locals in ui/renderer.sx (UV coords) — renamed to u_min/u_max/v_min/v_max.

Regression test: examples/1120-diagnostics-imported-reserved-type-name.sx (+
companion mod.sx) — an imported `s2 := ...` now emits the clean diagnostic at
the import's declaration site (exit 1), not an LLVM abort.

Resolves issues 0076 (coverage extension) and 0077.
2026-06-03 19:32:49 +03:00
agra
bdd0e96d78 feat(lang): block value requires no trailing ; (Rust-style)
A block's value is now its last statement ONLY when that statement is a
trailing expression with no `;`. A trailing `;` discards the value,
leaving the block void. This makes value-vs-statement explicit and lets
the compiler reject "this block was supposed to produce a value".

Compiler:
- Parser records `Block.produces_value` (last stmt is a no-`;` trailing
  expression) + `Block.discarded_semi` (the `;` that discarded a value),
  via `expectSemicolonAfter`. A trailing expression before `}` may now
  omit its `;` (previously a parse error). Match-arm and else-arm bodies
  are built value-producing regardless of the arm `;` (arms are exempt —
  the `;` is an arm terminator).
- Lowering: `lowerBlockValue` / the block-expr path / `inferExprType`
  respect `produces_value`. A value-position block that discards its value
  is a hard error (`lowerValueBody` for function bodies; the value-context
  `.block` path for if/else branches, `catch` bodies, value bindings,
  match arms). Pure-failable `-> !` bodies (value rides the error channel)
  and a value-if whose branches are void are handled without false errors.
- `defer`/`onfail` cleanup bodies lower as statements (void), so a
  trailing `;` there is fine.

Migration (behavior-preserving — output unchanged):
- stdlib + ~210 examples: dropped the trailing `;` on value-position last
  expressions. `format` now ends with an explicit `#insert "return
  result;"` (it relied on `#insert`-as-block-value, which `;` discards).
- Two `main :: () -> s32` examples that relied on the old silent
  default-return got an explicit trailing `0`.
- Rejection snapshots 0412 / 1013 regenerated (their quoted source lines
  lost a `;`); the diagnostics themselves are unchanged.

Docs/tests: specs.md "Block values" section; examples 0040 (rules) + 0041
(rejection); 3 parser unit tests. Filed issue 0066 (pre-existing
match-arm negated-literal phi-width quirk, surfaced not caused here).

Gates: zig build, zig build test, run_examples.sh -> 343 passed,
cross_compile.sh -> 7 passed (also refreshed its stale example names).
2026-06-02 09:23:50 +03:00
agra
485b4fa618 issues: file 0060 — closure-literal composition miscompiles (blocks ERR/E5.1)
Probing ERR/E5.1 (composition with closures) surfaced pre-existing closure-
literal lowering bugs: a closure literal passed as a function-type argument and
called inside the callee returns wrong values (block-body 192, arrow-body 20,
want 10 — non-failable too; the working contrast passes the value as a separate
arg, examples/0302). On top of that, failable closure returns don't parse
(isLambda omits .bang — one-line fix in the issue) and arrow-body failable
closures miscompile (return 0); block-body failable closures called directly
work. Runnable repro + parser patch + investigation prompt in the issue.

E5.1 paused per the impassable rule rather than built on miscompiling closures;
the parser fix + a regression example were reverted to avoid landing silently-
miscompiling failable closures on master.
2026-06-01 20:18:25 +03:00
agra
178449b548 ERR/E3.0 (slice 3c): source snippet + caret in traces
Each trace frame now shows the offending source line with a `^` caret
under the column — in the catch-handler formatter, the failable-main C
reporter, and the comptime path.

The source line is embedded at compile time as a 5th Frame field
(line_text), not read from disk at runtime: the file field is a
basename and a runtime read would add a filesystem dependency that
fails under the test harness and on locked-down targets.

- errors.lineAt(src, offset): shared helper for the whole source line.
- Frame gains line_text (mirrored in emit_llvm getFrameStructType,
  trace.sx Frame, sx_trace.c SxFrame). emitTraceFrame embeds it; the
  interp .trace_resolve extracts it from the source map.
- trace.sx (new spaces helper) and the C reporter render the line +
  a col-aligned caret, guarded on a non-empty line_text.

Snapshots 243/244/247/253 regenerated. Gates: zig build, zig build
test, run_examples.sh -> 291 passed.
2026-06-01 15:43:22 +03:00
agra
b5241243e6 ERR/E3.0 (slice 3b): comptime trace resolution
#run failures now print the same `func at file:line:col` trace as
runtime, resolved in-process via the interpreter's IR/source tables.

- Read-side context-split op `.trace_resolve` (mirror of .trace_frame),
  lowered from a name-recognized `__trace_resolve_frame(u64) -> Frame`.
- emit_llvm: inttoptr the operand to *Frame + load (the value
  .trace_frame stamped in).
- interp: unpack (func_id << 32 | span.start); resolve func/file from
  module.functions and line/col via SourceLoc.compute over a new
  source_map (setSourceMap wired at every production interp site).
- trace.sx: frame_at -> u64; to_string routes each frame through
  __trace_resolve_frame, so one source works in both machines.

Compiled path behavior unchanged (243/244/247 identical; it now loads
via the op). New examples/253-comptime-trace.sx exercises the comptime
path. Gates: zig build, zig build test, run_examples.sh -> 291 passed.
2026-06-01 15:33:50 +03:00
agra
1b6cbc17e7 ERR/E3.0 (slice 3a): embedded Frame trace resolution
Return-trace frames now resolve to real `func at file:line:col`
in-process — no DWARF, no symbolizer.

- New niladic, span-stamped `.trace_frame` IR op (mirrors is_comptime):
  carries no operands; each backend derives the frame from context.
  lower.zig's placeholderTraceFrame emits it; the existing
  sx_trace_push call consumes it.
- emit_llvm: resolve the op's span + current function to
  {file(basename), line, col, func}, build an interned Frame global
  ({string,i32,i32,string}, strings cached by content), push its
  address (ptrtoint).
- interp: pack (func_id << 32 | span.start) for the comptime resolver
  (slice 3b); never a pointer.
- sx_trace.c report_unhandled derefs SxFrame; trace.sx gains the Frame
  struct, frame_at -> *Frame, and field-reading to_string. Layout
  mirrored in 3 places with cross-ref comments.

Verified JIT + AOT. Snapshots 243/244/247 regenerated (placeholder ->
func at file:line:col). Gates: zig build, zig build test,
run_examples.sh -> 290 passed.
2026-06-01 15:10:46 +03:00
agra
d67fb7b9b3 ERR/E4.1: trace.print_interpreter_frames() — Phase E4 complete
The last E4 item: a comptime call-frame dump.

- New nullary `interp_print_frames` IR op (inst/print). The interpreter
  maintains a `call_chain` side-stack (push/pop a FuncId around each sx-bodied
  `call`, freed in deinit) and `printInterpFrames` appends the chain to its
  output — most-recent-last, with the dump frame itself skipped. emit_llvm
  makes the op a no-op: compiled code has no interpreter stack, and the only
  caller is `process.exit`'s dead `is_comptime()` branch.
- Lowered from a name-recognized `__interp_print_frames()` builtin
  (tryLowerReflectionCall + inferExprType → void).
- `trace.print_interpreter_frames()` wraps the builtin; wired into
  `process.exit`'s comptime branch (process.sx now imports trace.sx).
- Frame source locations await IR-offset resolution (the comptime analog of
  DWARF), so only function names print today.

examples/252-interp-frames.sx (top-level `#run` drives the dump; exit 0).
Phase E4 (entry-point + stdlib error story) is now 100% complete.
2026-06-01 12:22:23 +03:00
agra
e04bec488b ERR/E4.1b: #caller_location + Source_Location (+ namespaced default fix, comptime flush)
Finishes Phase E4. `process.exit` / `assert` now report the caller's location.

#caller_location + Source_Location:
- new `hash_caller_location` token (lexer) + a leaf `caller_location` AST node
  (parser primary-expr; sema + lsp arms).
- `Source_Location :: struct { file; line; col; func }` in std.sx.
- expandCallDefaults rewrites a `#caller_location` param default to a marker
  carrying the CALL site's span + source_file.
- lowerCallerLocation synthesizes the struct: file + line:col via
  errors.SourceLoc.compute over the diagnostics file→source map, stamped with
  the enclosing (caller) function name. inferExprType resolves it to
  Source_Location. Explicitly forwarding a Source_Location through an inner
  call preserves the outermost site.

namespaced default-param expansion (pre-existing crash): expandCallDefaults
bailed on field_access callees, so `mod.fn(args)` with an omitted defaulted
param passed too few args → LLVM "incorrect number of arguments". Now resolves
the namespace fd (by field / qualified name); method-on-value calls (where
`self` shifts the count) stay excluded. Prerequisite for process.exit/assert
(always called namespaced) taking `loc = #caller_location`.

comptime flush: interp.callForeign flushes the interpreter's buffered print
output before invoking any host symbol, so a comptime diagnostic emitted just
before a terminating `_exit` (process.exit at comptime) survives.

process.exit/assert take `loc: Source_Location = #caller_location`; assert
prints `ASSERTION FAILED at <file>:<line>: <msg>`. examples 250 (assert
file:line), 251 (caller-location + forwarding). The two ffi-objc *.ir
snapshots are regenerated — adding Source_Location to std.sx renumbers the
global string pool the type/field-name tables index (benign, identical IR).
2026-06-01 12:00:03 +03:00
agra
6f77c55613 ERR/E4.1 (slice 1): log + is_comptime + process.exit/assert (+ noreturn codegen)
Stdlib slice of Phase E4, plus the noreturn codegen fix that enables it.

noreturn codegen (the enabling bug): E1.4c made `noreturn` type-system-only;
this is its first backend consumer and it crashed LLVM verification. Fixed:
- lower.zig: a `-> noreturn` body lowers as statements ending in `unreachable`
  (ensureTerminator emits unreachable; the two body-lowering sites no longer
  treat the last expr as a `ret`).
- emit_llvm.zig: a `void`/`noreturn` call result stays unnamed (direct +
  foreign call sites) — LLVM rejects a named void value.
- finishCatchHandler: a `noreturn` value-carrying catch body (which is not an
  IR terminator) closes the handler with `unreachable` instead of feeding a
  bad value into the merge phi. Shared by lowerCatch + lowerCatchOverChain.

is_comptime(): new nullary `.is_comptime` IR op (inst/print/interp/emit_llvm) —
interp evaluates true, emit_llvm emits constant false, so `if is_comptime()`
dead-codes out of compiled binaries. Recognized by name in
tryLowerReflectionCall + inferExprType (no std.sx decl, which would emit a
spurious `declare @is_comptime` into every module).

library/modules/log.sx: warn/info/debug/err — interpolate like print, write
`LEVEL: <msg>` to stderr. (`error` is reserved → the level is `log.err`.)

process.exit(code) -> noreturn + assert(cond, msg) in process.sx. `exit` is
POSIX `_exit(2)` (immediate, no cleanup; sx print is unbuffered so nothing is
lost), bound to "_exit" which also avoids a link-level clash with the sx `exit`
function's own name.

examples 248 (exit 0), 249 (exit 42), 250 (exit 1). #caller_location, the
comptime-exit diagnostic flush, and trace.print_interpreter_frames deferred to
E4.1b.
2026-06-01 11:11:56 +03:00
agra
bb20339691 ERR/E3.3: trace formatting (library/modules/trace.sx) + catch-clear timing fix
The trace formatter, unblocked now that 0057 is fixed.

- library/modules/trace.sx: to_string() walks the trace buffer (sx_trace_len /
  frame_at / truncated) and renders "error return trace ..." with one line per
  frame; print_current() writes it to stderr (libc write(2, ...)). Frame
  locations are "<location pending DWARF>" until E3.0 resolves PCs; count +
  ordering + the overflow note are already meaningful.

- Catch-clear timing fix (lowerCatch): move the absorption clear from
  runCatchBody ENTRY to the handler's non-diverging EXIT (both the pure and
  value-carrying paths). This reconciles the two PLAN-ERR statements that
  conflicted — §clear-points "buffer cleared before the catch body" vs
  §catch-over-or "frames still in the buffer when the body runs". Exit-clear
  satisfies both: the handler can inspect the trace (trace.print_current()
  shows the chain), and the buffer is empty once the handler completes. A
  diverging body (raise/return) keeps/discards on its own path.

- examples/243-trace-format.sx: catch handler prints the tag + the 2-frame
  trace, then shows the buffer is empty after. examples/241 updated: the
  handler now observes len=2 (was 0 under the buggy entry-clear).

Gates: zig build, zig build test, bash tests/run_examples.sh (280 passed; lone
failure is the user's uncommitted 213-canonical-map pack WIP).
2026-06-01 08:58:57 +03:00
agra
a3ff503f47 ERR/E3: error-tag {} interpolation via an always-linked tag-name table
`{}` on an error-set value printed `<?>` (any_to_string had no error_set
category). Now it renders the tag name (`BadDigit`), reusing the existing
any_to_string dispatch.

Pieces:
- New `error_tag_name_get` IR op (UnaryOp): tag id -> name. Lowered from a new
  `error_tag_name(e) -> string #builtin` (std.sx). Handled across inst.zig
  (op def), print.zig, interp.zig (comptime: tags.getName), and emit_llvm.zig.
- emit_llvm getOrBuildTagNameArray: an always-linked `[N x {ptr,i64}]` global
  of tag names indexed by global tag id (the TagRegistry namespace, slot 0 =
  ""). error_tag_name_get zext's the u32 tag value and GEPs into it. Built once;
  not trace-gated, so it works in release too (per the spec's "tag-name table
  always shipped").
- resolveTypeCategoryTags gains an `error_set` category so the
  `case error_set:` arm in any_to_string matches; that arm coerces the Any to
  u32 (`xx val`) and calls error_tag_name. (cast(type) didn't recover the tag
  id for error-set values; the u32 coercion does.)

examples/240-error-tag-interpolation.sx: bound tags + a catch-bound tag print
their names. Regenerated ffi-objc-call-06-sret-return.ir — pure block-renumber
drift from adding one if-arm to the shared any_to_string (verified
semantically identical after collapsing block numbers).

Gates: zig build, zig build test, bash tests/run_examples.sh (277 passed; lone
failure is the user's uncommitted 213-canonical-map pack WIP).
2026-06-01 07:47:32 +03:00
agra
c08433b345 ui: drop redundant .* on pointer match subjects
event_position and translate_sdl_event matched on e.* / sdl.*; lowerMatch now auto-derefs a pointer subject, so 'if e ==' / 'if sdl ==' are equivalent (same load + tag-switch in IR). Pure cleanup.
2026-05-31 10:46:51 +03:00
agra
cd2ab1c4b6 ui: platform-neutral Keycode enum; map SDL keycodes once
KeyData.key was a raw u32 carrying SDL_Keycode values, so app code had to reinterpret it as SDL_Keycode (xx e.key) — a leaky, unchecked cross-platform cast only valid because the backend happened to be SDL. Add a neutral Keycode enum; translate_sdl_event maps SDL_Keycode to it via keycode_from_sdl. App code compares e.key == .escape with no platform type and no cast; a new backend maps its own native codes in one place.
2026-05-31 10:42:55 +03:00
agra
fb8a5399f1 objc: remove ns_string/c_string helpers
ns_string's only caller was impl Into(*NSString) for string, so +stringWithUTF8String: is inlined there. c_string's one use (NSBundle.resourcePath in uikit) becomes rsrc.UTF8String() with resourcePath retyped *NSString. ffi-objc-call-06 and ffi-objc-dsl-07 .ir snapshots regenerated — they only drop the now-absent extern declares.
2026-05-30 18:01:27 +03:00
agra
a29ede0383 objc: migrate remaining ns_string call sites to xx NSString
NSLog's fmt, addObserver's name, UIApplicationMain's principal-class, CADisplayLink's run-loop mode, and metal's newLibraryWithSource/newFunctionWithName string args are retyped *NSString, so their call sites read xx "..." instead of ns_string("...".ptr). ns_string is now used only by impl Into(*NSString) for string.
2026-05-30 17:54:23 +03:00
agra
8e3c3ae981 objc: NSString type + Into(*NSString) for string
Adds an NSString foreign class and impl Into(*NSString) for string so a string literal flows into any *NSString slot via xx. uikit's keyboard userInfo lookups now read objectForKey(xx "...") instead of ns_string("...".ptr), and objectForKey's key param is retyped *NSString.

ffi-objc-call-06 .ir snapshot regenerated: declaring the NSString type adds its reflection thunks (struct_to_string/pointer_to_string), same as the existing NSObject/NSDictionary. Runtime output unchanged.
2026-05-30 17:39:38 +03:00
agra
b31fbae757 platform/sdl3: chdir to .app bundle on macOS so CWD-relative assets resolve
A macOS .app launched with CWD=/ (Finder/open) could not find CWD-relative
assets (read_file_bytes("assets/...")) and crashed in stbtt with a null font.
SdlPlatform.init now chdirs to SDL_GetBasePath() when running from inside a
.app bundle (detected by ".app" in the base path), mirroring uikit.sx s iOS
chdir_to_bundle. Gated so the sx run dev flow (binary not bundled) keeps the
project CWD. Verified: direct-exec with CWD=/ now stays alive (was: instant
stbtt segfault). Filed issue 0051 with the analysis.

Note: launching via Finder/open additionally triggers Gatekeeper App
Translocation for the dev-signed bundle (separate code-signing concern, not
the asset path).
2026-05-30 01:10:13 +03:00
agra
3ac13b7442 ir: Type as first-class value (Any-shaped {tag, value})
Previously, `t : Type = f64` stored a boxed string carrying the literal
name "f64"; comparisons and `type_of`/`type_name` round-trips lost the
underlying TypeId. This switches `Type` to a runtime-representable Any
pair: `{ tag = .any.index() (meta-marker), value = TypeId.index() }`.

Mechanism:
- `const_type` emits a 16-byte Any aggregate via insertvalue.
- `TypeId.any` advertises 16 bytes / 8-byte alignment so structs that
  embed `t: Type` size correctly under verifySizes.
- `lowerBinaryOp` folds `==`/`!=` between static type-refs to a
  `const_bool`, and decomposes runtime Any-vs-Any compares via
  `unbox_any` so LLVM doesn't see icmp on aggregates.
- `lowerMatch`'s `is_type_match` path unboxes Any-typed subjects to
  the i64 type tag before the switch, so `case type:` etc. fire.
- `lowerRuntimeDispatchCall` (used by `case T: ... cast(t) val`) does
  the same unbox for the type-tag arg.
- `type_of(val: Any)` rebuilds an Any with `{.any, tag_of(val)}` so
  the result is itself a `Type` value, not a bare i64.
- `buildPackSliceValue` stops re-boxing const_type — the value is
  already canonical Any.
- `__sx_type_names` now indexes by TypeId across the whole table
  using the new `types.formatTypeName` (structural names for `*T`,
  `[]T`, `[N]T`, `?T`, `Vector(N,T)`, function/closure/tuple) so
  runtime `type_name(t)` works for compound types.
- `interp.zig`'s comptime `type_name` accepts either the bare
  `.type_tag` Value or the Any-boxed aggregate it now sees.
- `scanDecls` registers `Vec4 :: Vector(4, f32)` style aliases in
  `type_alias_map` (before the `fn_ast_map` check; `Vector` IS a
  `#builtin` fn). Lets `Vec4` in expression position lower as
  `const_type(<vector tid>)`.
- `isStaticTypeArg` becomes scope-aware: a name shadowed by a runtime
  local is not static. `isStaticTypeRef` is the symmetric helper for
  the eq fold.
- `inferExprType` returns `.any` for bare type names (identifier and
  type_expr) so pack arg types are correct.

Side effect: `print("{}", Vec4)` now prints the structural name
`Vector(4,f32)` rather than the alias literal `Vec4` — 12-meta's
expectation updated. Aliases stay pointer-equal to their target
(`Vec4 == Vector(4, f32)` is true).

Tests:
- examples/189-type-all-interactions.sx: 12-section comprehensive
  coverage — literal `==`, `type_of(value) == T`, `Type` var storage,
  `type_name` (static + runtime), printing Type values, generic
  dispatch via `$T: Type`, `identity($T, val)`, `Wrap($T)`, reflection
  builtins (`size_of`, `align_of`, `field_count`, `type_eq`),
  `..$args` pack walking, `Type` in struct field, compound type
  literals (`*Point`, `[4]s32`, `[]bool`, `?f64`).
- examples/12-meta.sx: expected output updated to reflect structural
  name for the Vec4 alias path.
- ffi-objc-call-06-sret-return.ir: regenerated to absorb the new
  type-name strings now emitted globally.

223/223 examples pass.
2026-05-28 14:02:10 +03:00
agra
11eef8a6b1 ffi step 6: print / format migrate to ..\$args (comptime per-position pack)
`format` and `print` move from `..args: []Any` to `..$args`. The
pack-fn machinery monomorphises each call shape, so the
build_format-emitted body's `any_to_string(args[i])` substitutes
to the i-th concrete-typed call arg via packArgNodeAt — no more
runtime Any-boxing for static args. The Any boxing path still
fires for arg positions whose types collapse to `.any` (already
Any-typed inputs).

Net effect:
- Calls with statically-typed args produce per-shape monos
  (`print__ct_<fmt_hash>__pack_s64_string_bool` etc). The mono
  cache key now reflects both the format string AND the arg
  types, so different shapes get distinct emit paths.
- Compile-time arity errors are now possible (callers passing
  the wrong number of args mismatch the mono's positional
  binding instead of silently mis-boxing).
- Optionals flow through the new `case optional:` arm in
  `any_to_string` (commit ce77867); the variadic auto-unwrap
  in `packVariadicCallArgs` stays as a fast-path but is no
  longer load-bearing.

IR snapshots regenerated for 13 tests where the print/format
mono shape changed the string-constant pool: 142, the ffi-jni
test cluster, ffi-objc-call-03/06, ffi-objc-dsl-07. Test
08-types' undef-memory-read snapshot also shifted (the test
exercises `field = ---` reads from a print call's stack
neighbours; the new pack-mono lays out its stack frame
differently, so the previously-stale 1s now read as 0s — same
undefined behaviour, different garbage).

218/218 example tests + `zig build test` green.
2026-05-28 08:04:12 +03:00
agra
ce77867566 ffi any_to_string handles optionals — make-green
Closes the optional-through-Any gap that test 178 pinned.

Stdlib (`library/modules/std.sx`):
- New `optional_to_string :: (o: $T) -> string` returns `"null"`
  when the optional is None, otherwise recurses through
  `any_to_string` on the unwrapped inner value. Per-shape
  monomorphisation re-emits this for each concrete `?T`.
- `any_to_string` grows a `case optional:` arm that dispatches
  through `cast(type) val` (same shape as `case struct:` etc.).
  The cast picks up the dynamic optional type from the Any tag.

Compiler (`src/ir/lower.zig`):
- `resolveTypeCategoryTags` recognises "optional" as a dynamic
  category, scanning the TypeTable for `info == .optional`. The
  type-switch dispatch then routes any ?T tag into the optional
  arm.

IR snapshots regenerated where the optional addition shifted
constant pool / string numbering: 142, ffi-objc-call-06,
ffi-objc-dsl-07. 218/218 (test 178 included).

The variadic auto-unwrap in `packVariadicCallArgs` stays in
place — direct `print(opt)` calls still flow through it. The new
arm closes the gap for struct fields, slice elements, and any
other path that boxes an optional before stringifying.
2026-05-28 07:51:44 +03:00
agra
2eaf932fcf ffi M5.A.next.5.3: delete hand-rolled __block_invoke trampolines
Removes `__block_invoke_void` / `__block_invoke_bool` and their
companion `Into(Block)` impls from
`library/modules/std/objc_block.sx`. The generic
`Into(Block) for Closure(..$args) -> $R` impl from step 5.2 now
covers both shapes (and every other closure shape) via per-mono
`#insert build_block_convert($args, $R)` source emission.

Net stdlib shrinkage: ~52 lines, two trampolines + two per-shape
impls down to zero. Adding a new block-shape consumer no longer
requires touching stdlib — the impl emits per-call-shape on
demand.

`examples/95-objc-block-noop.sx` (zero-arg closure) and
`examples/96-objc-block-multi-arg.sx` (user-declared per-shape
impl for `Closure(s32, *void) -> void`) still pass: 95 routes
through the new generic, 96 keeps its in-file impl as a
documentation example of the user-declares-their-own path.
Suite at 217/217.
2026-05-27 21:59:57 +03:00
agra
165b621ab3 ffi M5.A.next.5.2.B: generic Into(Block) impl — make-green
Adds the generic `impl Into(Block) for Closure(..$args) -> $R`
in `library/modules/std/objc_block.sx` alongside the existing
hand-rolled `Closure() -> void` and `Closure(bool) -> void`
impls. The convert body is a single
`#insert build_block_convert($args, $R);` — per-call-shape
monomorphisation re-runs the builder so each closure shape gets
its dedicated nested `callconv(.c)` trampoline + Block literal.

The impl-mono path threads pack types through
`pack_bindings[args]` and the single-type return through
`type_bindings[R]`. Both need to be visible to the body's
`$args` / `$R` expression-position references — the existing
lowering only consulted `pack_arg_types` (set by pack-fn mono,
not by tryPackImplMatch). Two small extensions:

- `lowerExpr`'s `.comptime_pack_ref` arm now consults
  `pack_arg_types` → `pack_bindings` → `type_bindings` in order,
  treating a `type_bindings` hit as a single `const_type(T)`
  value rather than the slice form.
- `resolveTypeArg` grows a `.comptime_pack_ref` arm that maps
  the same name through `type_bindings` so type-arg positions
  (e.g. inside `type_name(...)` in the builder body) resolve
  the bound single Type.
- `type_bridge.isTypeShapedAstNode` lists `comptime_pack_ref`
  and `pack_index_type_expr` as type-shaped so
  `buildTypeBindings`'s strategy-1 explicit-arg path picks
  them up when calling a `$T: Type`-generic fn.

`examples/177-generic-into-block.sx` flips green: a
`Closure(s64, s64) -> void` (no hand-rolled impl) is converted
through the generic impl, its block invoked via a typed
`callconv(.c)` fn-pointer, and the closure's side effects land
in the host globals. Hand-rolled impls remain for `()` and
`(bool)` shapes; 5.3 deletes those once a focused test covers
their behaviour through the generic path. Suite at 217/217.
2026-05-27 21:58:33 +03:00
agra
aeb950b86f ffi M5.A.next.5.1.B: build_block_convert added to stdlib — make-green
`build_block_convert(args: []Type, $ret: Type) -> string` emits
the convert-body source for the generic `Into(Block) for
Closure(..$args) -> $R` impl (step 5.2):

  1. A nested `__invoke :: (block_self: *Block, arg0: T0, ...) ->
     R callconv(.c) { ... }` trampoline matching the per-shape
     Apple Block ABI.
  2. A `return Block.{ ... };` literal whose `invoke` slot points
     at the nested trampoline via `xx @__invoke`.

Void-returning shapes emit `typed_fn(block_self.sx_env, args...)`;
non-void emits `return typed_fn(...)`. Per-position arg names
follow `arg0`, `arg1`, ... in declaration order; the typed-fn
cast reconstructs the closure's call signature so the trampoline
hands control back to `sx_fn` with the right argument layout.

`examples/176-build-block-convert.sx` flips green (216/216).
2026-05-27 21:48:45 +03:00
agra
5b3d86440b ffi: migrate remaining variadic decls to new ..name: []T form
Stdlib:
- `format` / `print` in std.sx — both move from `args: ..Any` to
  `..args: []Any`. The post-issue-0049 lowering makes this safe
  across module boundaries.
- `open` in fs.sx — `args: ..s32` → `..args: []s32`. Foreign
  C-variadic semantics are preserved (the trailing `, ...` lands
  in the generated `declare` regardless of which surface form is
  used).

Examples:
- `19-varargs.sx` — `sum` / `print_all` migrated.
- `20-any-varargs.sx` — `print_any` / `count` migrated.
- `50-smoke.sx` — `typed_sum` migrated.
- `120-interp-variadic-any.sx` — comment-only update referencing
  the new form.
- `ffi-foreign-cvariadic.sx` — three C-variadic foreign decls
  migrated; header comment refreshed.

Suite stays at 214/214. The legacy `name: ..T` surface form is
still accepted by the parser; rejection follows in a later commit
once specs.md catches up.
2026-05-27 21:30:48 +03:00
agra
64dcbca06a ffi issue-0049: new-form variadic cross-module LLVM crash — xfail lock-in
Migrating stdlib's `path_join` to the new variadic syntax
(`(..parts: []string) -> string`) surfaces a latent compiler bug:
`resolveParamType` and `packVariadicCallArgs` treat the new-form
declaration the same as the legacy `parts: ..string` and wrap the
element type in `sliceOf` regardless of whether it already is one.
The new form's `[]string` becomes `[][]string`; the call-site
marshal pack emits `[N x string]` (correct) but the callee stores
its slice param into a `[]([]string)`-typed slot. The shape
mismatch propagates as null/undef Refs that crash
`LLVMBuildExtractValue` inside `emitStrCmp` during emission.

`examples/121-ios-sim-bundle.sx` (existing) and the new focused
`examples/174-new-form-variadic-cross-module.sx` both fail today
with the segfault. The next commit fixes `resolveParamType` +
`packVariadicCallArgs` so both flip green. Stdlib's `format` /
`print` / `open` and the example fixtures stay on the legacy form
in this commit — they migrate in the follow-up cleanup commit.
2026-05-27 21:29:08 +03:00
agra
07f25689ff ffi M5.A revert: drop compiler synthesis, require explicit Into(Block) impls
Reconsidered the M5.A.2 cleanup. The compiler-synthesised trampoline
path was hidden behaviour — a user reading their code couldn't tell
how `xx my_closure : Block` worked without reading lower.zig. That's
exactly the kind of magic sx's design has been pushing against.

New design (strict mode):

1. Stdlib's modules/std/objc_block.sx hand-rolls
   `__block_invoke_void` + `Into(Block) for Closure() -> void` and
   the same pair for `Closure(bool) -> void` (restored from M5.A.2).
   These are readable reference implementations of the bridge ABI.

2. The compiler intercept fires NO synthesis — instead, when
   `tryUserConversion` can't find a reachable `Into(Block)` impl for
   the closure's signature, it emits a focused diagnostic:
     "no `Into(Block) for <Closure-sig>` impl — add a per-signature
      `__block_invoke_<sig>` trampoline + Into impl alongside the
      existing ones in modules/std/objc_block.sx, or declare it in
      your own code"

3. Per-signature declarations live in stdlib (for common signatures)
   or in user code (for app-specific ones). 96-objc-block-multi-arg
   now demonstrates the user-side pattern in-file — it declares its
   own `__block_invoke_void_s32_p` + `Into(Block) for Closure(s32,
   *void) -> void` impl alongside its main().

Net effect:
- Every block bridge is source-visible. No hidden compiler magic.
- Users see exactly how the Apple ABI shape is constructed in sx
  source — stdlib serves as the reference implementation.
- Compiler enforces the discipline: missing impl → clear diagnostic
  pointing at the template.
- Coverage for arbitrary signatures requires conscious user opt-in,
  not silent fallthrough.

Removed from lower.zig: `tryClosureToBlockConversion`,
`emitBlockInvokeTrampoline`, `mangleClosureSigForBlock`,
`mangleTypeForBlock`, and the `block_invoke_trampolines` dedup
state field. Net: the synthesis machinery is gone; only the
detection helper `isClosureToBlockCast` remains, used by the
diagnostic.

190/190 example tests pass; chess on iOS-sim green.
2026-05-27 00:34:26 +03:00
agra
556e4e12ea ffi M5.A.2: drop hand-rolled __block_invoke_* impls + Into(Block) per-sig boilerplate
The compiler-synthesised trampoline path (previous commit) covers
every closure signature on demand; the hand-rolled stdlib impls
were only for two specific shapes (`Closure() -> void`,
`Closure(bool) -> void`) and are now strictly redundant.

Kept: the `Block` struct, `BlockDescriptor`, the
`_NSConcreteStackBlock` extern decl, and the shared
`__sx_block_descriptor` global. The compiler-emitted code
references all four; users still need to `#import
"modules/std/objc_block.sx";` to bring them into the module.

Removed: `__block_invoke_void`, `__block_invoke_bool`, and both
`impl Into(Block) for Closure(...) -> void` blocks. Replaced with
a comment block explaining how the compiler now handles the cast.

After this commit, `xx my_closure : Block` works for ANY closure
signature with no per-signature stdlib boilerplate. 189/189
example tests pass; chess on iOS-sim green.
2026-05-27 00:24:36 +03:00
agra
5c1d00a877 ffi M4.B helpers: objcPropertyKind + ARC runtime decls + xfail tests
Three pieces, no behavior change yet:

1. `ObjcPropertyKind` enum (strong/weak/copy/assign) + `objcPropertyKind`
   helper in lower.zig. Reads `field.property_modifiers`, applies the
   default rule (`*<ObjC-class>` → strong; primitives → assign), and
   emits loud diagnostics for the silent-error budget:
   - unknown modifier name (typo) → "expected one of: strong, weak, copy, ..."
   - conflicting modifiers (e.g. `strong,weak`) → "mutually exclusive"
   - `weak` on non-object slot → "requires a pointer-to-Obj-C-class type"
   - `copy` on non-object slot → same
   - `strong` (default or explicit) on `*void` → "ambiguous: specify
     #property(strong|weak|copy|assign) explicitly"
   Called from `emitObjcDefinedClassPropertyImps` for validation; the
   returned kind isn't wired into setter/getter/dealloc yet — that's
   the next three commits.

2. `ensureArcRuntimeDecls` lazily declares libobjc's ARC helpers:
   objc_retain, objc_release, objc_storeWeak, objc_loadWeakRetained,
   objc_initWeak, objc_destroyWeak. Uses the existing
   `ensureCRuntimeDecl` pattern; idempotent.

3. Fix existing NSObject method names in std/objc.sx — `isEqual_`,
   `isKindOfClass_`, `respondsToSelector_` had trailing underscores
   that the selector mangling turned into double-colon selectors
   (`isEqual::`). Removed the trailing underscore so the selectors
   come out as `isEqual:`, `isKindOfClass:`, `respondsToSelector:`
   as Apple's runtime expects.

4. Two xfail regression tests:
   - ffi-objc-arc-02-strong-property: assigns child to parent's strong
     property, releases the original child reference. Midpoint check:
     child's dealloc should NOT have fired (strong setter retained).
     Pre-M4.B-setter: child dealloc fires immediately → "FAIL: child
     dealloc'd at midpoint" snapshot. Exit code 1.
   - ffi-objc-arc-03-weak-property: assigns target to holder's weak
     property, releases target. Reads holder.target → should be null
     (auto-niled). Pre-M4.B-getter/setter: reads stale pointer →
     "FAIL: weak property didn't auto-nil" snapshot.

These will turn green as M4.B setter (commit 2), getter (commit 3),
and dealloc-cleanup (commit 4) land. Each subsequent commit updates
the snapshot to reflect the now-passing output.

189/189 example tests pass; chess on iOS-sim green.
2026-05-26 22:58:30 +03:00