Commit Graph

1016 Commits

Author SHA1 Message Date
agra
235f74a8c9 test(ffi-linkage): xfail example for extern data global (Phase 1.2c)
Add examples/1225-ffi-extern-global.sx — '__stdinp : *void extern;'
references libSystem's stdin pointer via the bare 'extern' modifier on
a typed var decl (the extern-named counterpart of the #foreign global
in examples/1205). Hand-authored snapshot expects the success output.

RED: 1225 is the sole corpus failure (636 ran, 1 failed) — parse error,
'extern' after a type annotation is not yet accepted in the var-decl
path. Phase 1.2d parses it and lowers the extern global.

xfail commit per the cadence rule.
2026-06-14 13:33:50 +03:00
agra
5777ff62ad feat(ffi-linkage): consume extern LIB "csym" rename for fns (Phase 1.2b)
parseFnDecl parses the optional [LIB] ["csym"] tail after the
extern/export keyword into FnDecl.extern_lib/extern_name (mirrors
'#foreign LIB "csym"'). declareFunction unifies the symbol-name
override: rename_c_name = foreign_expr.c_name (for #foreign) OR
fd.extern_name (for extern) -> declare under the C name and map sx->C
in foreign_name_map; the dedupe guard now covers extern too.

examples/1224 green: 'c_abs :: (n) -> i32 extern "abs";' resolves
c_abs to libc abs -> c_abs(-42) = 42. 1223 (bare extern) unregressed.
Suite green (635 corpus / 443 unit).

extern_lib is parsed + stored but not a linking driver — like
'#foreign libc', it references a lib; the #library decl + build flags
remain the separate linking axis (decision 4). green commit.
2026-06-14 13:30:59 +03:00
agra
5f946a3d44 test(ffi-linkage): xfail example for extern fn rename (Phase 1.2a)
Add examples/1224-ffi-extern-fn-rename.sx — 'c_abs :: (n) -> i32
extern "abs";' binds C's abs via the optional symbol-name override.
Hand-authored expected captures the success output (c_abs(-42) = 42).

RED: 1224 is the sole corpus failure (635 ran, 1 failed) — parse error,
the '"abs"' string after 'extern' is not yet accepted. Phase 1.2b
parses the optional [LIB] ["csym"] tail and consumes the rename.

xfail commit per the cadence rule.
2026-06-14 13:26:57 +03:00
agra
18c43984e1 feat(ffi-linkage): lower extern fns as C imports (Phase 1.1)
Route a bare 'extern' fn declare-only, exactly like a lib-less #foreign
import. Six edits in decl.zig, each mirroring an existing foreign_expr
guard so the empty-block placeholder body is never lowered:

  1. funcWantsImplicitCtx: suppress the implicit __sx_ctx for .extern_
  2. declareFunction: add is_extern_decl
  3. ...and include it in the C-ABI calling-convention promotion
  4. lazyLowerFunction: .extern_ -> declareFunction (declare-only)
  5. lowerFunction: .extern_ in the declare-only guard
  6. lowerFunctionBodyInto: never promote/lower an extern stub

examples/1223 now green: 'extern' abs lowers to 'declare i32 @abs(i32)'
(external linkage, C ABI, no ctx param) and the call resolves against
the default-linked libc -> abs(-7)=7, abs(42)=42. The 1.0b hand-authored
snapshot matched byte-exact (no regen). Suite green (634 corpus / 443
unit). green commit (makes the 1.0b xfail pass; adds no new test).
2026-06-14 13:17:00 +03:00
agra
78e304f552 test(ffi-linkage): xfail example for extern fn binding (Phase 1.0b)
Add examples/1223-ffi-extern-fn.sx — binds libc 'abs' via bare 'extern'
(sx name = C symbol, no rename). Hand-authored expected/ captures the
SUCCESS output (abs(-7)=7 / abs(42)=42, exit 0).

RED: 1223 is the sole corpus failure (634 ran, 1 failed) — it parses
then errors at sema ('body produces no value') because lowering does
not yet route extern fns through declareExtern. Phase 1.1 wires the
lowering and turns this green.

xfail commit per the cadence rule (no commit both adds a test and makes
it pass).
2026-06-14 13:06:54 +03:00
agra
df6b675e67 feat(ffi-linkage): fn-path accepts postfix extern/export + lib/name fields (Phase 1.0a)
parseFnDecl now calls parseOptionalExternExport() after the callconv
slot and stores the modifier on FnDecl.extern_export. For 'extern' the
body is ';' (an empty-block placeholder — the modifier carries the
linkage, no *_expr node, per the naming constraint). Both fn-decl
lookahead predicates (isFunctionDef, hasFnBodyAfterArrow) now treat
kw_extern/kw_export as fn-body markers beside kw_callconv, so
'(...) -> R extern;' is recognized as a fn def rather than a fn-type
const.

Per user feedback, decision 4 ("library separate") is REVISED: extern
carries an optional LIB + "csym" axis mirroring '#foreign LIB "csym"',
so it is a true #foreign superset (Gate A->B requirement — the Part B
migration of 466 #foreign uses across 6 libs must preserve each
symbol's library). Added FnDecl.extern_lib/extern_name and
VarDecl.extern_lib (beside is_extern/extern_name).

All unconsumed by lowering: extern parses, but a fn still errors at
sema (body produces no value). Suite green (443 unit / 633 corpus).
lock commit.
2026-06-14 13:02:42 +03:00
agra
62a3b46f6e feat(ffi-linkage): extern/export parser+AST plumbing, unconsumed (Phase 0.1)
Add ast.ExternExportModifier { none, extern_, export_ } beside
CallingConvention; FnDecl.extern_export and VarDecl.is_extern/extern_name
fields (all defaulting to absent); and Parser.parseOptionalExternExport()
mirroring parseOptionalCallConv.

None of this is consumed by a decl path yet — no user-facing behavior
change, corpus diff empty. Two inline parser unit tests pin the helper's
keyword mapping and the field defaults. Phase 1.0 wires the helper into
the fn-decl path. lock commit.
2026-06-14 12:48:56 +03:00
agra
bf6ef8370f feat(ffi-linkage): add kw_extern/kw_export tokens (Phase 0.0)
Lex 'extern' and 'export' as keywords beside 'callconv': new token.Tag
variants + keywords StaticStringMap entries + LSP semantic-token keyword
classification. Adds a 'lex linkage keywords' unit test.

Tokens only — parser/AST plumbing and lowering land in later phases.
Corpus sweep confirmed no .sx identifier collides with the new reserved
words. lock commit per the cadence rule.
2026-06-14 12:40:35 +03:00
agra
78f7bb7857 ... 2026-06-14 12:21:37 +03:00
agra
c562fe236d docs(plans): inline-asm design + ASM and FFI-linkage plans/checkpoints
Two new workstreams:
- ASM: inline assembly — asm { "tmpl", "=r" -> T, "r" = expr, clobbers(.…) },
  multi-return tuples; lowers via the existing llvm_api.c (no shim).
- FFI-linkage: add extern/export postfix keywords, migrate every #foreign onto
  them, then purge 'foreign' from the tree (end-state invariant).

Drop current/ from .gitignore so plans + checkpoints are tracked normally
(the dir was ignored; only checkpoints had been force-added). Includes
docs/inline-asm-design.md. specs.md change left uncommitted.
2026-06-14 12:16:10 +03:00
agra
e386a0d0b4 fix: reject direct assignment to a tagged-union variant member
A tagged union (enum-with-payload) is laid out { tag, payload }, but a
direct member write `s.rect = payload` lowered to a payload-only store
(union_gep into field 1) with no tag store — the discriminant went stale,
so a later match/== took the wrong arm with no diagnostic (issue 0136).
The read path already distinguishes tagged unions (enum_payload/enum_tag);
the write path treated them like plain unions.

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

Resolves issue 0136. Tests: examples/0185 (rejected), 0186 (nested write +
construction still work). specs.md / readme.md updated.
2026-06-13 21:18:40 +03:00
agra
4d32a4d4fb fix: propagate union-member type to a struct-literal RHS
Assigning a struct literal to a named-struct member of a plain union
(`u.b = .{ ... }`) lowered the RHS as .unresolved and tripped the
LLVM-emission tripwire: lowerAssignment's .field_access target-type
path used getStructFields, which returns nothing for a union, so the
literal never received its target type.

Unify the lvalue field matcher into a pure fieldLvalueResolve consumed
by both fieldLvaluePtr (GEP builder) and the target-type path, so the
store slot and the RHS target type can't diverge (covers union direct +
promoted members, tuple/vector lanes, and structs).

Resolves issue 0133 (depended on 0135). Regression test: examples/0184.
Notes the now end-to-end union path in issue 0132.
2026-06-13 18:55:41 +03:00
agra
8c47268539 fix: xx pack[i] to a protocol target heap-copies the element
Erasing a single comptime-pack element to a protocol value
(`xx sources[0]` with a protocol target) tripped the pack-as-value
error: buildProtocolErasure treated the index_expr as an lvalue and
took its address via lowerExprAsPtr, whose .index_expr arm lowers the
bare pack as a value (a pack is comptime-only with no runtime storage).

isLvalueExpr now reports a comptime pack index as an rvalue, decided
via the same packArgNodeAt predicate the value path uses — so the value
and lvalue paths can't diverge on what counts as a pack element — and
erasure heap-copies the already-materialized element instead.

Resolves issue 0135. Regression tests: examples/0547, 0548.
2026-06-13 18:55:10 +03:00
agra
d3f5cb20cb fix: visibility-aware type resolution for protocol method signatures
`registerProtocolDecl` resolved each method's param/return type NAME
through the flat, visibility-unaware `type_bridge.resolveAstType`, so a
type name colliding across modules bound to the wrong author. In the
repro the user's `Event` enum collides with the stdlib `event.Event`
struct (pulled in by `modules/std.sx`): the protocol grabbed the stdlib
struct, typed an inferred `g_plat.one_event()` as a fieldless struct,
bound the `case .key_up:(e)` payload to `.unresolved`, and emitted
"enum literal '.escape' has no destination type to resolve against".

Resolve both param and return types through
`resolveTypeInSource(pd.source_file, …)` — the visibility-aware resolver
pinned to the protocol's own declaring module, keeping the `Self → *void`
short-circuit. Brings the non-parameterized path to parity with
`instantiateParamProtocol` and concrete-fn signatures. No silent default:
not-visible / ambiguous names still diagnose and poison with `.unresolved`.

Closes issue 0132 — the protocol-return case left open by f13f4ab (which
fixed the enum/union/inline/error-set registration class). Regression
test: examples/0417-protocols-protocol-return-name-collision.sx.
2026-06-13 15:44:11 +03:00
agra
45befed698 docs(issues): correct 0132 root cause; file 0133 and 0134
- 0132: rewrite to the verified root cause -- protocol method signature
  registration resolves type names via flat findByName and picks the wrong
  same-name author. Original payload-field hypothesis kept as superseded;
  repro switched to canonical `impl ... for` syntax. Still open (the
  protocol path is unchanged).
- 0133: assigning a struct literal to a union member panics ("unresolved
  type reached LLVM emission"); pre-existing, surfaced while testing.
- 0134: a same-name `error` set collapses into a namespaced import's set --
  error-set declarations lack per-decl nominal identity (E6a gap); this is
  what keeps the 0132-class error-ref resolution dormant.
2026-06-13 13:41:30 +03:00
agra
f13f4abfb1 fix: visibility-aware type-name resolution at registration time
Enum payloads, union fields, inline struct/enum/union field types, and
named error-set references now resolve through the visibility-aware
`inner` recursion hook (the same seam `resolveCompound` uses) instead of
the flat `findByName`. A bare type name in any of these positions now
selects the querying module's OWN author over a same-name namespaced
import -- the own-wins rule already applied to top-level named references
and struct fields.

- buildEnumInfo / buildUnionInfo / resolveInlineEnum / resolveInlineStruct
  / resolveInlineUnion / resolveErrorType take the `inner: anytype` seam;
  registerEnumDecl / registerUnionDecl and the struct-const annotation
  pass `self` (visibility-aware); resolveAstType passes the stateless `si`.
- resolveTypeWithBindings routes inline type decls and named error refs
  through `self` instead of delegating to flat resolveAstType.

Regression tests: examples/0781 (top-level enum payload over a namespaced
import), examples/0784 (inline struct field). Addresses issue 0132's
broader latent class; the protocol-return case (0132 primary) is a
separate registerProtocolDecl fix and stays open. The error-set reference
path is in place but dormant pending error-set per-decl nominal identity
(issue 0134).
2026-06-13 13:41:11 +03:00
agra
ab3c9202ff test: run example corpus in zig build test; sx ir → stdout
`zig build test` now runs the full examples/ + issues/ regression corpus
alongside the Zig unit tests, driven by a pure-Zig test
(src/corpus_run.test.zig) — no shell script in the build path. It spawns
the installed `sx` per example (subprocess-isolated, per-run timeout),
diffs stdout/stderr/exit and optional `sx ir` snapshots, and fails the
build on any mismatch. The file list is enumerated at runtime, so new
examples are covered with no test edit.

- `sx ir` / `ir-dump` now write to stdout (fd 1) instead of stderr, so
  the dumps can be piped/redirected.
- `zig build test -Dupdate-goldens` regenerates snapshots in-build,
  byte-identical to the legacy `run_examples.sh --update`; on mismatch
  the runner prints how to regenerate.
- run_examples.sh kept (still used by tools/verify-step.sh) and made
  portable to a bare macOS: timeout/gtimeout fallback, bash 3.2-safe
  empty-array handling.
- CLAUDE.md: document the new workflow.
2026-06-13 09:41:56 +03:00
agra
39488133c9 fix: vendored sqlite builds SQLITE_THREADSAFE=1 — std.thread exists now
THREADSAFE=0 was correct when sx had no threads; with std.thread (S6)
and std.http's pooled dispatch (S7b), concurrent connections corrupted
sqlite's unprotected globals (caught live: distd under ab -c20 died
with free-of-unallocated inside yy_reduce). Serialized mode is
sqlite's own default and safe for every consumer; per-connection use
across threads is the supported pattern.
2026-06-12 22:38:43 +03:00
agra
e57a27205e feat: std.http pooled handler dispatch (PLAN-HTTPZ S7b)
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`.
2026-06-12 22:31:27 +03:00
agra
7f23bb7530 feat: std.thread — Thread, Mutex/Cond, bounded worker Pool (PLAN-HTTPZ S6)
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.
2026-06-12 22:21:40 +03:00
agra
4fa12853ed test: pin C function pointers into sx (PLAN-HTTPZ C2 — pre-existing)
Both halves of the C2 contract already work in JIT and AOT; these
examples pin them. 1635: libc qsort drives an sx callconv(.c)
comparator passed by name as a typed fn-pointer param. 1636: a real
pthread enters sx through a callconv(.c) entry, fabricates its own
Context (push Context with a local GPA), and runs default-conv sx code
that allocates through it — the re-entry contract std.thread (S6)
stands on. Also unblocks the sqlite callback APIs (hooks/UDFs) left
unbound by design in P5.1.
2026-06-12 22:14:14 +03:00
agra
fe4f059a52 issue 0131: RESOLVED (d7808f6) 2026-06-12 21:52:11 +03:00
agra
d7808f69a3 fix: protocol method calls arity-check (issue 0131)
emitProtocolDispatch now requires the user-arg count to equal the
protocol method's parameter list — exact, since protocol signatures
have no defaults, packs, or variadics — and emits the same
"expects N arguments, but M were given" diagnostic plain calls get.
Previously extra args were silently dropped (and missing args left the
thunk reading garbage). The dispatch gains the call-site span for the
diagnostic. examples/1634 pins the rejection; full sweep confirms no
existing code relied on the leniency.
2026-06-12 21:51:56 +03:00
agra
ff94b004c4 issue 0131: protocol method calls silently drop extra arguments 2026-06-12 21:44:07 +03:00
agra
81fa50c77d fix: std.http dealloc_bytes calls match the Allocator protocol arity
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.
2026-06-12 21:43:19 +03:00
agra
f3aa2716ae fix: std.http per-request arenas are backed by the server's own allocator
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.
2026-06-12 21:37:39 +03:00
agra
0db4262833 fix: std.http dispatch + error responses run under a per-request arena
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.
2026-06-12 21:34:22 +03:00
agra
8641441fad feat: std.http read buffers grow on demand toward read_buf_cap
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.
2026-06-12 21:29:13 +03:00
agra
3a97019aa7 feat: std.http handler carries an opaque ctx word (PLAN-HTTPZ A1 prep)
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.
2026-06-12 21:22:42 +03:00
agra
721793b4bf feat: std.http — single-worker HTTP/1.1 server core (PLAN-HTTPZ S7a)
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).
2026-06-12 21:16:56 +03:00
agra
92e220ee24 feat: std.event — OS-neutral readiness Loop over kqueue (PLAN-HTTPZ S5)
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.
2026-06-12 21:05:56 +03:00
agra
1017657c90 feat: std/net/kqueue — raw kqueue/kevent bindings, darwin (PLAN-HTTPZ S3)
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.
2026-06-12 20:57:25 +03:00
agra
659c43c8d6 feat: std.socket nonblocking surface — fcntl, errno, typed _nb wrappers (PLAN-HTTPZ S2)
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).
2026-06-12 20:53:35 +03:00
agra
da2f76b383 feat: std.time — wall + monotonic clocks over clock_gettime (PLAN-HTTPZ S1)
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.
2026-06-12 20:33:01 +03:00
agra
f6b0029249 fix: AOT build cache disabled for programs with top-level #run
The JIT path already guards its object cache with hasTopLevelRun (the
#run interp executes during codegen; a cache hit skips codegen and
loses its effects). The build path had no such guard, so a second
'sx build --cache' of any app with a '#run configure_build()' block
linked WITHOUT the build.sx config — no link flags (m3te: undefined
SDL3 symbols), and on a binary-level hit the output path and bundling
would have been wrong too. Both cache levels and both save sites now
share the guard; #run-free programs keep full cache behavior
(verified: second build hits the binary cache in <1ms; m3te's
build/--cache/build sequence now links and bundles both times).
2026-06-12 19:11:18 +03:00
agra
9fb7290861 feat: duplicate C exports across #import c units are a compile diagnostic
All units share one link namespace (per-unit isolation is PLAN-C C3.2,
deferred), so a symbol defined by two units previously died inside the
JIT dylib link or the AOT link with raw linker spew. The clang shim
gains sx_clang_object_exported_symbols (llvm::object scan: defined +
global, format-specific excluded) and compileCToObjects cross-checks
every unit object — collisions name both source files. Scan failures
are non-fatal; the linker remains the backstop. Covers JIT and native
AOT; the emcc path still relies on wasm-ld's own error.
2026-06-12 19:01:43 +03:00
agra
6114b51073 feat: emcc C compiles go through the object cache
compileCWithEmcc now probes/saves .sx-cache/c-<key>.o with the same
content key as the native path (source + declared headers + transitive
deps + defines/flags/incdirs), keyed by the emcc --version line and the
wasm triple so emsdk upgrades and wasm32/64 variants never collide with
each other or with native objects. Cache hits hand the linker the cache
path directly. objectMagicOk accepts the wasm magic. Verified: warm
wasm build of a c-unit drops 1.85s -> 0.61s (emcc -c skipped).
2026-06-12 18:52:43 +03:00
agra
4b9324e585 feat: transitive quoted includes participate in the c-object cache key
The key previously covered the #source bytes + the block's DECLARED
headers, so a unit whose impl is a thin wrapper over an undeclared
header (vendors/kb_text_shape: two-line impl.c, all code in
kb/kb_text_shape.h) would serve STALE cached objects after an
upstream upgrade. collectIncludeDepBytes now walks the transitive
closure of quoted #include lines (includer-dir first, then -I dirs;
angle/system includes never participate; unresolvable names skip) and
the dep contents fold into the key — no sidecar, no compare logic, a
changed header is just a different key. Verified live: appending to
kb_text_shape.h mints a new cache entry; reverting hits the old one.
2026-06-12 18:48:56 +03:00
agra
b06776d6e9 library: vendors/kb_text_shape + vendors/file_utils; modules/ffi/stb_truetype.sx retired
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.
2026-06-12 17:58:23 +03:00
agra
58af806b7a library: vendors/stb_image + vendors/stb_truetype — stb ships with sx
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.
2026-06-12 17:50:21 +03:00
agra
2097772ec9 library: vendors/sqlite — SQLite ships with sx
The vendored amalgamation (3.53.2, public domain) plus the curated
bindings move from the distribution repo into the sx library:
'#import "vendors/sqlite/sqlite.sx"' gives any sx program SQLite
with no system dependency and no build flags — the bindings declare
the C as a named #import c unit (pinned defines + -O2), compiled
through the object cache and shadow-proof via unit-first resolution.
examples/1624 pins the version and a typed round trip in-suite.
2026-06-12 17:39:26 +03:00
agra
8dedacb23f fix(C4): dedup c-import units by normalized content, not node identity
One module imported through several aliased chains materializes one
c_import_decl copy per chain, each carrying a differently-spelled
relative path to the same file (src/app/../repo/../db/../../vendor/x.c
vs src/db/../../vendor/x.c). Dedup now keys on lexically-normalized
sources/includes + defines + flags, so the unit compiles and links
exactly once — pointer-identity dedup linked it once per chain and
died with duplicate symbols at AOT link.
2026-06-12 17:27:06 +03:00
agra
e9b500a232 fix(C4): collectCImportSources recurses into nested namespaces
A named #import c unit declared inside an aliased module sits two
namespace levels deep in the merged tree; the one-level walk (the
extractLibraries/0130 pattern in c_import form) never collected it,
so the unit silently never compiled and its symbols resolved from
whatever process image carried the same names — surfaced by C4's
sqlite migration, where only the version pin could tell the OS copy
from the vendored one.
2026-06-12 17:16:03 +03:00
agra
44f4aab51c test(C3.3): #define/#flags reach a unit consumed only by hand-curated #foreign decls 2026-06-12 17:09:07 +03:00
agra
1bad29e72e feat(C3.1): #foreign refs are validated — must name a #library or a named #import c unit
validateForeignRefs walks the merged tree (libraries + named c units,
nested namespaces included) and diagnoses any #foreign whose ref names
neither — a typo'd ref previously compiled and resolved silently
through whatever image carried the symbol. Decls synthesized from
#include headers carry no ref and are exempt. Flips the C0.2b pin;
zero collateral across the 608 other examples.
2026-06-12 17:09:07 +03:00
agra
0bd8f3e5ce feat(C2): unit-first JIT symbol resolution — program-owned dylibs beat process images
runJITFromObject now takes priority dylibs (the #import c unit's
linked objects first, then #library deps in declaration order) and
attaches a per-path search generator for each AHEAD of the
process-wide fallback, so a vendored symbol can never lose to a
same-named export of an image the host process happens to carry
(libz via LLVM, libsqlite3 via CoreServices). loadLibrary reports
the name dlopen succeeded on; the c-import handle records its dylib
path; temp link inputs are per-pid so concurrent runs can't clobber
each other. Flips the C0.3 shadowing pin to from_unit: true.
2026-06-12 16:56:35 +03:00
agra
2a2f43eada test(C1.4): objectMagicOk — corrupt/truncated cache entries recompile, never poison the link 2026-06-12 16:48:27 +03:00
agra
053314b3ea feat(C1.2): persistent content-addressed cache for compiled #source units
compileCToObjects now probes .sx-cache/c-<key>.o before invoking the
embedded clang and writes fresh objects back (per-pid temp + copy, the
main object cache's pattern). Default on for both JIT and AOT — the
temp-compile-and-delete behavior it replaces was strictly worse. A
cached entry must carry an object-file magic (Mach-O/ELF) or it falls
back to a fresh compile; no cache failure can fail a build. Cold/warm
verified via --time: the object compile disappears on the warm run.
2026-06-12 16:48:02 +03:00
agra
10f5a4318d test(C1.1): cSourceCacheKey — content key for compiled #source units
Source bytes, declared-header CONTENT (header edits invalidate),
defines/flags/include dirs in order, LLVM version, and target
triple/sysroot all participate; section tags keep equal strings in
different roles distinct. Pure function + variance property tests;
nothing consumes it yet.
2026-06-12 16:43:42 +03:00
agra
1009181638 test(C0.3): pin JIT shadowing — a #source unit's zlibCompileFlags loses to the process-loaded OS libz (xfail for C2.1) 2026-06-12 16:38:02 +03:00