Lands the full VM/compiler-API arc on branch reify (701/0 both gates):
- abi(.compiler) ABI replaces abi(.zig) extern compiler + the fake
#library "compiler"; bodiless decl = compiler-API surface, bodied =
user compiler-domain fn (lowered for VM eval, emit-skipped).
- out is a plain sx fn (libc write) — the out builtin deleted; the VM
handles it via host-FFI. trace_resolve + interp_print_frames ported.
- 4B VM-native diagnostics: 1179/1180 render proper comptime type
construction failed: under strict.
- S5a: build_options/set_post_link_callback on abi(.compiler) with
BuildConfig threaded into the VM (green intermediate).
- 0522 fixed (describe(args: []Type)); regression 0638.
Strict deletion-gate down to 4 compiler_call bails (1609/1614/1615/1616)
+ 1654 (legitimate unresolvable-symbol diagnostic).
Introduce the welded comptime `compiler` library (`#library "compiler"` +
`abi(.zig) extern compiler`), per design/comptime-compiler-api.md, and unify
`callconv(...)` into the new `abi(...)` annotation.
abi(...) replaces callconv(...):
- New ABI enum { default, c, zig, pure }; `abi(.c|.zig|.pure)` parses in the
postfix slot before extern/export (and standalone). `kw_callconv` -> `kw_abi`.
- Migrated 52 sx files, the call-convention-mismatch diagnostic, and docs
(readme/specs) from `callconv(.c)` to `abi(.c)`.
Phase 1 — welded compiler library (parse -> registry -> validation -> bridge):
- `abi(.zig) extern compiler` parses on fn decls (carries abi/extern_lib) and
struct decls (StructDecl.abi/extern_lib).
- `#library "compiler"` is the comptime-only internal surface — never dlopen'd.
- src/ir/compiler_lib.zig: the binding registry (the safety boundary). `Field`
welded to StructInfo.Field with layout baked from the real Zig type
(@offsetOf/@sizeOf); `findType`/`findFn`. Welded structs are layout-validated
at registration (field set + total size) as a header checked against the impl.
- Host-call bridge: a `fn abi(.zig) extern compiler` dispatches under the
comptime interp to its registered Zig handler (intern/text_of round-trip),
never dlsym. IR Function.compiler_welded; validated in declareFunction.
- Comptime-only enforcement: a runtime call to a welded fn is a clean
build-gating error (emitCall), not an undefined-symbol link failure.
Phase 2.1 — byte-layout weld foundation:
- Decision: full byte-layout weld (sx struct laid out byte-identically to the
bound Zig type). Registered StructInfo (first non-natural / Zig-reordered
layout). `computeWeldPlan` — pure offset-ordered element plan + padding +
sx-field->LLVM-element remap; unit-tested. Emit/interp wiring is the next
sub-step (2.2+, see current/CHECKPOINT-COMPILER-API.md).
Examples: 0625/0626 (welded struct + fn round-trip), 1183/1184/1185
(layout-mismatch, unexported-fn, runtime-call diagnostics).
TypeInfo gains a `tuple(TupleInfo) variant (TupleInfo{elements: []Type},
positional/unnamed) — completing the reflect/construct triad with enum
and struct.
- meta.sx: TupleInfo + `tuple TypeInfo variant.
- interp: reflectTypeInfo builds .tuple (tag 2) as bare type_tag elements
(no name pairs); defineType dispatches tag 2 -> defineTuple, which
decodes []Type and completes the declare slot as a structural .tuple
via replaceKeyedInfo (kind change). Tuples are structural so the
declared name is vestigial, but the slot is still completed in place so
define returns the handle (consistent with enum/struct).
- call.zig: the lower-time type_info guard now admits .tuple.
define(declare("P"), .tuple(.{elements=.[i64,f64]})) builds a tuple, and
define(declare("T"), type_info((i64,bool,f64))) round-trips one. Suite
green (683).
TypeInfo gains a `struct(StructInfo) variant (StructField{name,type});
the metatype system now reflects AND constructs structs, not just enums.
- meta.sx: StructField / StructInfo / `struct TypeInfo variant.
- interp: reflectTypeInfo builds .struct (tag 1) for a source @"struct";
define dispatches on the TypeInfo tag (defineType) -> defineEnum (0) /
defineStruct (1). defineStruct mirrors defineEnum (dup-field-name check
included) but completes the declare slot AS a struct via replaceKeyedInfo
(a kind change re-keys the intern map; updatePreservingKey asserts no
key change, true only for the enum path).
- call.zig: the lower-time type_info guard now admits @"struct".
define(declare("P"), .struct(.{ fields = .[ … ] })) builds a struct, and
define(declare("C"), type_info(SrcStruct)) round-trips one. Suite green
(682); enum path (0619) unchanged.
make_enum(name, variants: []EnumVariant) -> Type mints a nominal enum
from a variant list passed as a VALUE, not a hardcoded literal — the
open-ended form the channel-result constructors are special cases of.
Pure sx over declare/define; no compiler machinery.
Because variants is an ordinary comptime value, a non-generic builder
can ASSEMBLE it in a local before minting. examples/0620: build_level
fills a local array, then make_enum mints Level from it — exercising
define decoding a value-arg SLICE (decodeVariantElements' slice branch),
vs. the inline .[ … ] array the 0614-0618 examples pass directly.
No compiler change (locks existing capability). Suite green (678).
declare now takes the type's NAME — `declare(name) -> Type` — because the
compiler needs it at compile time to register the forward type, which is
what makes self-reference resolve. EnumInfo drops `name` (it lives on
declare now); define completes the handle's body in place (the slot is
already named).
Self-reference mechanism (evalComptimeType): before lowering a comptime
type expression, preregisterForwardTypes scans it (and a called ctor fn's
body) for `declare("Name")` calls and registers each as an empty forward
nominal type AND binds it as a type alias. The alias is essential: a
`Name :: ctor()` decl makes `Name` a const_decl author, so a `*Name`
self-reference resolves through the forward-ALIAS path
(type_aliases_by_source), which a bare findByName registration doesn't
satisfy. With both in place `*Name` resolves to the forward slot at lower
time; the interp's declare returns that same slot; define fills it.
List :: make_list();
make_list :: () -> Type {
h := declare("List");
return define(h, .enum(.{ variants = .[
EnumVariant.{ name = "cons", payload = *List },
EnumVariant.{ name = "nil", payload = void } ] }));
}
Verified: cons/nil construct + match (direct and through the pointer),
multi-node list traversal via a recursive `count(*List)`. meta.sx
RecvResult/TryResult + examples 0614/0615/0617 updated to declare(name);
full suite green (673).
The compiler concept is declare/define (comptime type construction); the
old "reify" framing is gone from the entire repo.
- Rename: PLAN-REIFY → PLAN-METATYPE, CHECKPOINT-REIFY → CHECKPOINT-METATYPE,
PLAN-POST-REIFY → PLAN-POST-METATYPE (both rewritten around declare/define);
examples 0614/0615/0617 → comptime-metatype-* (+ their expected/ triplets),
headers rewritten.
- Scrub reify from design/execution-evolution-roadmap.md (§7 step 3 contracts,
§8.1, §9 decisions, §10 gates) → declare/define / comptime type construction.
- core.sx prelude pointer + parser.test.zig surface lock updated to the
declare/define builtins (define(handle, info) -> Type; EnumInfo.name).
No behavior change; renamed examples match their renamed snapshots. Full
suite green (673), all unit tests pass. Zero `reify` tokens remain in
src/docs/sx/examples.
Per the directive to strip reify entirely: the sx `reify(info)` one-shot is
removed. `define(handle, info)` now RETURNS the (completed) handle, so the
one-shot constructor chains as a single expression:
T :: define(declare(), .enum(.{ name = "T", variants = ... }));
- meta.sx: drop reify; RecvResult/TryResult use `define(declare(), …)`.
- interp .define returns the handle type_tag (was void); call.zig lowers it
with `Type` result and sets the info arg's target type to TypeInfo so the
intercepted call still infers the `.enum(…)` literal.
- returnExprMintsType: a type-fn body that returns `define(…)` (or a bodied
non-generic Type-returning sx helper) is comptime-evaluated.
- examples 0614 (direct) + 0615 (type-fn) use `define(declare(), …)`.
Full suite green (673). Files/docs still carry the old reify naming — the
rename sweep is the next commit.
Second slice of the re-architecture — the compiler now has ZERO type-
construction code beyond declare/define.
- instantiateTypeFunction: a type-fn body returning a computed Type (a call
to a non-generic, bodied, Type-returning fn) is comptime-evaluated with the
type bindings active, then renamed to the mangled instantiation name for
identity (renameNominalType). Replaces the old reify-call pattern-matching.
- DELETED: reifyType (lower/nominal.zig), findReturnReifyCall (lower/generic.zig),
and the stale inline-position reify gate in resolveTypeCallWithBindings.
- evalComptimeType (was evalComptimeTypeNamed): pure eval, no rename; the
type-fn caller renames explicitly. renameReifiedType → renameNominalType.
- The TYPE NAME now travels in the data: EnumInfo gains `name`, and define()
names the slot from it (the compiler derives no name from a binding LHS).
examples/0614/0615 carry `name = "..."`; RecvResult/TryResult set it too.
- field_type stays a reflection #builtin (reads a type); only construction
moved out. All reify mentions stripped from compiler source.
examples 0614/0615/0617 run on the floor. Full suite green (673).
First slice of the re-architecture. The compiler gains two comptime
type-construction builtins — declare() (mint an empty/undefined nominal
slot) and define(handle, info) (decode a TypeInfo VALUE + complete the
slot) — executed by the interpreter against a new `mint` TypeTable handle
(setMintTable). reify becomes PLAIN sx in meta.sx:
reify :: (info) -> Type { h := declare(); define(h, info); return h; }
`E :: f(...)` where f is a non-generic Type-returning fn (reify, and later
make_enum) is now comptime-evaluated via evalComptimeTypeNamed: wrap the
call in a throwaway comptime fn, run it through the interp with the mint
table enabled so declare/define mint the type, read back the type_tag, and
rename the anonymous slot to the binding name. The compiler has ZERO reify
knowledge at the decl site — the old `E :: reify` hook is deleted.
examples/0614 (inline reify) now runs on this floor. Full suite green (673).
INTERMEDIATE: reifyType + findReturnReifyCall still serve the type-fn path
(0615/0617) and will be deleted in the next slice (type-fn body
comptime-eval), after which the compiler has no reify code at all.
REIFY Phase 3.1. Add RecvResult($T) and TryResult($T) to meta.sx as
type-fns over reify (value-or-closed; value-or-empty-or-closed). They
need NO new compiler machinery — reify-of-a-literal in a type-fn body is
exactly the Phase 1 path — so the channel result types are pure sx
library code. examples/0617 green (both construct + match, incl.
payload-less .closed / .empty). Suite green (673 examples, 447 unit).
make_enum(variants) (3.2) and type_info (2.2) remain — both blocked on a
generalized reify reader (reifyType currently AST-walks a literal
TypeInfo). Plan/checkpoint updated.
REIFY Phase 0.0. Add the comptime type-metaprogramming surface as the
on-demand module modules/std/meta.sx (NOT the prelude — declaring its
data types in always-loaded core.sx interns them into every module's
type table and shifts every .ir snapshot):
- EnumVariant / EnumInfo / TypeInfo data types. TypeInfo's variant uses
the backtick raw escape `enum so it reads as the keyword.
- reify / type_info / field_type as bodyless #builtin decls.
Each builtin bails LOUDLY when reached unimplemented (no silent default):
- reify(...) in a :: type-alias position -> decl.zig .call branch
(also the Phase 0.2 construction hook); poisons the alias .unresolved.
- reify / field_type in any other type position ->
generic.zig resolveTypeCallWithBindings.
- type_info(...) in expression position -> call.zig tryLowerReflectionCall.
Unit test src/parser.test.zig (registered in root.zig) locks that the
decls parse. zig build test green (447 unit, 669 examples).
The JNI/runtime-class path (Decision 5, Runtime* family). Coordinated across the
hook boundary so the BuildOptions accessor + its registered hook string stay in sync:
- src/: RuntimeClassDecl.foreign_path→runtime_path, splitForeignPath→splitRuntimePath,
foreignPathToJavaName→runtimePathToJavaName, jni_main_foreign_paths→
jni_main_runtime_paths, hookJniMainForeignPathAt→hookJniMainRuntimePathAt, and the
hook string 'BuildOptions.jni_main_foreign_path_at'→'…runtime_path_at'.
- library/: build.sx accessor jni_main_foreign_path_at→jni_main_runtime_path_at +
bundle.sx call sites + the local var → runtime_path + a comment.
- specs.md: the accessor name + <foreign_path_with_dots> doc refs.
- Regenerated 37 .ir snapshots: every program importing build declares the renamed
@BuildOptions.jni_main_runtime_path_at hook stub — symbol-name change only (verified
the .ir diff is ONLY this rename; reverted orthogonal empty-file normalization).
Suite green (646 corpus / 444 unit, 0 failed).
Pure source rename across objc/objc_block/raylib/sdl3/wasm (~51 sites): fn-decl
markers (bare / 'objc' LIB ref) → 'extern …', and objc.sx's 2 import runtime
classes '#foreign #objc_class("X") {' → '#objc_class("X") extern {'. No bare
defined classes. Behavior-preserving. objc + objc_block validated directly by the
50 marked 13xx corpus examples (incl. import classes 1300/1301 + defined classes
1339/1349); raylib/ffi-sdl3/wasm (no marked importers on host) verified by
byte-identical 'sx ir' probes pre/post. Empty snapshot diff; suite green (647
corpus / 444 unit, 0 failed).
Pure source rename across 11 std modules (~60 sites): cli/core/fmt/fs/log/
net/kqueue/process/socket/thread/time/trace. All fn-decl markers — bare
'#foreign;', '#foreign libc;'/'#foreign tlib;' (LIB ref), and
'#foreign libc "csym";' (LIB+rename) → the same 'extern …' tail (extern carries
the identical [LIB] ["csym"] axis). Plus 2 stale comment mentions (fmt/fs).
No class forms in std. These modules ARE host-corpus-exercised, so the empty
snapshot diff is direct validation. Suite green (647 corpus / 444 unit, 0
failed).
Pure source rename across uikit/android/android_jni/sdl3 (~64 #foreign sites):
- 30 fn decls '… #foreign;' → '… extern;'
- 34 import runtime classes '#foreign #objc_class/#jni_class("X") {' →
'#objc_class/#jni_class("X") extern {' (prefix → postfix modifier)
- 4 defined Sx* obj-c classes '#objc_class("X") {' → '… export {'
Behavior-preserving (AST already unified post-Phase-5.0). Verified byte-identical
IR via 'sx ir' on the uikit importers 1610 + 1606 (which compile uikit incl. the
4 defined Sx* classes on host) and an sdl3 probe; android.sx (host-incompatible,
only compiles under OS==.android) verified by an identical 4-error dedup set (the
keyword-neutral 'foreign symbol already bound' message is unchanged). Empty
snapshot diff; suite green (647 corpus / 444 unit, 0 failed).
thread_pool_count = 0 (default) keeps handlers inline on the loop
thread — the measured fast path (BENCH-HTTPZ.md). N > 0 dispatches
each parsed request to a std.thread Pool of N workers, completing the
httpz two-pool shape: the connection freezes as CONN_HANDLING (no
reads, growth, eviction, or recycling — the worker borrows views into
its read buffer), the worker runs the handler under a per-job arena
and serializes into job-owned bytes, the completion queues under the
PoolState mutex, and the loop wakes through the new std.event wake
channel (kqueue EVFILT_USER + EV_CLEAR; the epoll twin maps to
eventfd), attaches the response, compacts the buffer, and resumes
keep-alive/pipeline handling. A full backlog sheds with 503. Stale
completions (generation mismatch after close) are dropped. Pool mode
requires the server's constructing allocator to be thread-safe
(GPA/malloc), documented on the knob.
PoolState lives behind a heap pointer (it embeds a Mutex and is shared
with workers; the Server struct itself is returned by value).
serialize_response/run_handler_job share one serialize_bytes.
examples/1633 gains the pooled section (GET, body echo, 404 across
worker threads) plus the loop-wake path exercised end to end; AOT run
five times. examples/1632 unchanged but the Event struct gains `user`.
pthread bindings with darwin opaque sizes (mutex 64B, cond 48B; glibc
divergence is a C3 per-OS item). Mutex/Cond initialize IN PLACE and
Pool lives behind Pool.create's heap pointer — POSIX sync objects are
address-sensitive, so nothing here moves after setup. Thread.spawn
takes the C2 re-entry contract entry (callconv(.c), fabricates its own
Context); Pool workers do exactly that with a per-worker malloc-backed
GPA, then run default-conv tasks inside it. submit returns false on a
full backlog (httpz thread_pool backpressure); shutdown finishes
queued work and joins every worker.
examples/1637 pins: 4 raw threads x 1000 locked increments, 100 pool
tasks summing exactly once across 4 workers, a held worker + full
backlog refusing the next submit, clean shutdown. JIT + AOT (AOT run
three times). The std.sx barrel carries thread; .ir snapshot regen is
the usual renumbering.
The protocol declares dealloc_bytes(ptr) — the size argument I passed
at three sites was silently accepted and dropped by the compiler
(issue 0131); these calls would stop compiling the moment that
diagnostic gap is fixed.
No conjured GPA: the arena chunks come from own_alloc (captured at
Server.init), so all server memory flows from the allocator the app
constructed it with — the point of the implicit context model.
Handler and serialization allocations through the implicit context die
with the request; response bytes survive via the own_alloc copy made
inside the push scope. Without this every request leaked its render
concats into the loop's long-lived context.
read_buf_cap is now the per-request LIMIT, not a preallocation: slots
start at 16K, double when full (one-step sizing when a Content-Length
declares the body), and keep their grown capacity for slot reuse. At
the limit the refusal distinguishes oversized headers (431) from an
oversized body (413). Unblocks A1: distd accepts multi-hundred-MB
artifact uploads — preallocating that per slot was never an option.
examples/1633 adds a body past the initial capacity echoing intact.
Server.init(cfg, handler, ctx); the handler signature gains a usize
third argument delivered verbatim per dispatch — typically a pointer
to the app's own state, since the server owns the call site. A bare
(req, resp) handler had no way to reach app state without globals.
examples/1633 pins the round trip.
The httpz shape, one worker, handlers inline over the std.event Loop:
nonblocking accept, per-connection state machine (reading -> writing ->
keepalive/close) with incremental parsing (request line, headers,
Content-Length body), partial-write continuation via on-demand write
interest, pipelined-request draining, and timeouts as EVICTION —
request-delivery and keepalive-idle deadlines on the monotonic clock,
checked after I/O each tick. Keep-alive is the HTTP/1.1 default;
Connection header, HTTP/1.0, or the per-connection request_count cap
turn it off. Config mirrors httpz: port/backlog/max_conn/read_buf_cap/
timeout_request_ms/timeout_keepalive_ms/request_count.
API: Server.init(cfg, handler) + tick(max_wait_ms); run() is the
forever-tick loop. tick makes the server drivable single-threaded —
examples/1633 runs a live server and its client sockets in ONE thread,
pinning: GET with keep-alive, actual connection reuse, the request cap
answering Connection: close then EOF, POST body echo, 404 routing, and
a half-header client evicted at the request deadline while a healthy
client keeps being served. Verified under sx run AND sx build.
Connection slots and read buffers are reused across connections
(httpz's min_conn/buffer-pool spirit); response buffers are allocated
per response and freed on completion. Serialization happens while
request views are valid, the served bytes are compacted, and only then
does sending start — write_more's pipelining check must see only the
remainder. The std.sx barrel carries http; .ir snapshot regen is the
usual mechanical renumbering.
S7b adds worker counts + the handler thread pool (needs C2/S6); the
epoll backend activates with the linux target (S4/S7c).
Loop.init/close, add_read/del_read/add_write/del_write with a
per-registration udata word, and wait() normalizing backend events
into Event{fd, udata, readable, writable, eof, err, nbytes}. The epoll
twin (S4) slots in behind this surface when the linux target lands.
No timer registrations by design: request/keepalive eviction is
deadline math — deadline_in/expired/remaining_ms over std.time's
monotonic clock, with remaining_ms feeding wait's timeout. std.sx
barrel carries ; .ir snapshot regen is the usual mechanical
renumbering. examples/1632 pins idle timeout (and that it honors the
deadline), readable with fd/udata/nbytes, immediate writability on an
empty send buffer, and the eof flag on peer close; JIT + AOT.
32-byte darwin struct kevent, EVFILT_READ/WRITE/TIMER, EV_* flags, and
three thin helpers: kev_change (one registration entry), kq_apply
(immediate change, no drain), kq_wait (bounded drain, EINTR reissued,
negative timeout = forever). Off the std.sx barrel by design — the
OS-neutral facade over this and the epoll twin is std.event (S5).
examples/1631 pins zero-cost idle timeout, READ readiness with pending
byte count + udata round-trip, and EV_EOF on peer close; verified under
sx run AND sx build.
set_nonblocking (C-variadic fcntl), errno via __error (darwin; C3
selects per-OS), and accept_nb/read_nb/write_nb returning a typed
SockErr — WouldBlock / Closed / Fault — so readiness-loop callers never
parse -1/errno pairs. EINTR retries internally; accept_nb skips
ECONNABORTED. Adds connect, shutdown, socketpair, AF_UNIX, SHUT_*.
examples/1630 pins the result algebra on a socketpair and a nonblocking
TCP listener (WouldBlock on empty backlog, accept after loopback
connect); verified under sx run AND sx build. The .ir snapshot regen is
mechanical: new std decls shift @str/@tag.str numbering and grow the
type table (179 -> 185).
now_secs (CLOCK_REALTIME, epoch seconds) and mono_ms (CLOCK_MONOTONIC,
process-local milliseconds for deadlines). Clock ids are darwin's; the
per-OS selection mechanism is PLAN-HTTPZ C3. No error channel: with
module-constant clock ids and a stack timespec, clock_gettime is total.
std.sx namespace tail carries the time alias; examples/1629 pins epoch
plausibility, monotone advance, and the alias carry.
kb_text_shape (v2.10, JimmyLefevre) had been LOST from the sx tree —
ffi/stb_truetype.sx referenced repo paths that no longer existed (and
nothing runs glyph_cache, so the dangling unit never fired). The
trimmed copy returns from the m3te project as a proper vendor:
curated c/kbts_api.h decls over the full upstream header, README with
provenance, and examples/1627 pinning context + font creation so the
unit compiles and runs in-suite. file_utils (in-house asset-read
helper with the Android AAssetManager hook) gets the same unit shape.
modules/ffi/stb_truetype.sx is gone: glyph_cache imports the three
vendored units (stb_truetype, kb_text_shape, file_utils) directly.
The stb headers move from the repo-root vendors/ (resolvable only
with CWD = sx repo) into library/vendors/ following the sqlite
convention — bindings module + c/ sources + provenance README — so
'#import "vendors/stb_image/stb_image.sx"' (image v2.30 + image_write
v1.16) and '#import "vendors/stb_truetype/stb_truetype.sx"' (v1.26)
work from any consumer via the stdlib search paths. modules/ffi/stb.sx
dissolves into the stb_image vendor; modules/ffi/stb_truetype.sx keeps
its non-stb text-shaping companions and re-imports the vendored unit.
examples/1625 pins a deterministic in-memory BMP decode; examples/1626
pins font init + metric invariants against the system Helvetica.
cstring is ONE pointer to a null-terminated u8 buffer, C's char*: thin
(8 bytes, no length; cstring_len walks to the terminator), crossing
#foreign boundaries verbatim in both directions, with ?cstring as the
nullable case lowering to the same bare pointer (null = absent).
Conversion discipline mirrors Odin: a string LITERAL coerces implicitly
(its bytes are terminated constants); any other string is rejected with
a diagnostic naming to_cstring (it may be an unterminated view); and
cstring never coerces to string implicitly — from_cstring(c) is the
explicit zero-copy view, pricing the strlen.
Plumbing: TypeId/TypeInfo builtin slot 18 (first_user 19), name
classifiers, size/align/name tables, LLVM ptr lowering, the ?T pointer
niche, the xx pointer ladder, the literal-gated coercion plan
(isConstString + data_ptr), and the reserved-spelling set. std gains
cstring_len/from_cstring/to_cstring (fmt.sx, re-exported); the old
cstring(size) allocator helper is renamed alloc_string everywhere;
getenv migrates to (name: cstring) -> ?cstring as the canonical user
and env() drops its manual strlen/memcpy.
Pinned: examples/1222 (FFI both directions, literal coercion,
?cstring null paths, round trip) and examples/1173 (both coercion
diagnostics); FAIL pre-feature. The alloc_string rename + getenv
signature shift the .ir snapshots — regenerated. zig build test
426/426; run_examples 604/604.
Spec: reserved spelling + cstring section + C-interop rows.
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.
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.
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.
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.
- 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).
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.
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.
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.
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.
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.
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).
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).