Compare commits

55 Commits

Author SHA1 Message Date
agra
22f4719e83 fix: aarch64-linux port of the M:1 fiber runtime (sched.sx)
Port library/modules/std/sched.sx to run on aarch64-linux alongside
aarch64-macOS, validated byte-identical on both via Apple `container`.

Per-OS bits are comptime-branched:
- MAP_AP (mmap MAP_ANON flag): linux 0x22 / macOS 0x1002.
- fd-readiness backend: epoll on linux, kqueue on darwin (epoll import
  scoped to the linux branch). block_on_fd, the run-loop Mode-2 drain,
  and cancel_io_waiter_for each branch; the epoll paths EPOLL_CTL_DEL on
  fire and on early-wake (EPOLLONESHOT only disables a registration;
  kqueue EV_ONESHOT auto-removes it).
- first-entry trampoline: a per-OS hand-written global-asm symbol becomes
  a naked sx fn fib_tramp (mov x0,x19; br x20) + register-indirect
  dispatch (spawn presets regs[1] == x20 == &fib_dispatch), dropping the
  per-OS .global symbol entirely.

Fixes issue 0193 Bug A: the trampoline redesign bus-errored on the
go/wait/sleep capstone (1817) until `export "fib_dispatch"` was restored.
Without the export, fib_dispatch reverts to sx's internal ABI (x0 =
implicit context, first arg self shifted to x1) while the trampoline
hands self over in x0 (C-ABI); on first entry the body runs (x1 happens
to alias self) but the closure then loads regs[1] == &fib_dispatch as its
first capture and re-invokes fib_dispatch forever -> stack overflow ->
bus error. The export pins fib_dispatch to the C-ABI (self in x0),
matching the trampoline. Root cause found via lldb on an AOT build;
confirmed against the compiler source.

Bug B (a top-level asm block wrapped in inline-if is dropped during the
comptime-conditional flatten) is carved out to issue 0194 (OPEN) -- no
live trigger remains, since the naked-fn trampoline sidesteps it.

1811/1814/1816/1817 run byte-identical on the aarch64-macOS host and in
an aarch64-linux container; full suite green (817/0). Documents the fiber
runtime in readme.md.
2026-06-26 11:32:01 +03:00
agra
7218280bf0 docs: streamline readme into a punchy project overview
Drop experimental/Jai/Zig framing and the Acknowledgments section, trim
the verbose edge-case paragraphs (numeric limits, float narrowing,
reserved names, module visibility) to punchy summaries, and remove the
from-source build section. Describe sx as a programming language.
2026-06-26 11:20:33 +03:00
agra
95bedf726d docs: file issue 0193 — linux fiber-runtime port WIP + wrapped-asm drop
Port of std/sched.sx (the M:1 fiber runtime) to aarch64-linux. The epoll
bindings + std.event.Loop epoll backend are already committed and runtime-
validated (cc137002); this records the SCHEDULER port, which is WIP:

- WORKS, validated in an Apple `container` Linux VM: 1811 (round-robin) and 1816
  (block_on_fd over the epoll fd path) run identically to macOS kqueue.
- Bug A: a register-indirect trampoline (naked fn + `br x20`, to avoid a per-OS
  hand-written global-asm symbol) bus-errors on the 1817 go/wait/sleep capstone
  on both platforms, though 1811/1816 work — unresolved.
- Bug B: wrapping the original global `asm` trampoline in an `inline if`/`case`
  drops it (nm: fib_tramp U) in sched.sx's context, though every minimal repro
  emits fine — a flatten/lowering interaction in src/imports.zig.

The WIP sched.sx port is preserved both in `git stash` and as
issues/0193-linux-fiber-port.patch. Two resolution paths (either suffices)
documented in the issue. sched.sx itself is left at HEAD (macOS green).
2026-06-26 10:50:50 +03:00
agra
e52b6c9eae docs: record epoll Loop runtime validation on real Linux (Apple container)
The std.event.Loop epoll backend is now runtime-validated, not just
lower-verified: a static aarch64-linux build of the 1632-equivalent Loop test
(plus the eventfd wake path) runs 6/6 green inside an Apple `container` Linux VM
(kernel 6.18 aarch64) — add_read, idle-timeout, readable+fd+udata, the MOD-mask
add_write path, the eventfd wake channel, and EPOLLRDHUP/HUP eof all behave
identically to kqueue (lone difference: nbytes is 0 on epoll). Update the
event.sx VALIDATION note (with the re-run recipe) and the fibers checkpoint;
the epoll deliverable is complete.
2026-06-26 09:53:10 +03:00
agra
493469fd74 fix: lambda inferred return type from a block body's early returns (issue 0187)
A `:=`-bound closure with no explicit `-> T` and a BLOCK body inferred its
return type via inferExprType(lam.body), which yields the last statement's
type. A block whose value comes only from early `return`s ends in a return
statement (void/noreturn), so the closure was built with a void return while
the body returned i64 — the call site then fed `i64 undef` and LLVM
verification failed. (A block whose tail referenced a block-local hit the
sibling failure: inferExprType returned .unresolved → an LLVM panic.)

Infer the return type exactly as a named fn does (resolveReturnType in
lower.zig): an arrow body `(params) => expr` uses the expression type; a block
body `(params) { stmts }` takes the first explicit `return <val>` type via
findReturnValueType, else void (the block tail is a discarded statement unless
an explicit `-> R` makes it the value). Regression test:
examples/closures/0313-closure-inferred-return-early.sx.
2026-06-26 09:28:29 +03:00
agra
8d23aad4b9 refactor: compiler.sx imports only std/list.sx, not the whole std barrel
compiler.sx needs only `List` (string is a builtin), so import the std/list.sx
part-file instead of std.sx. Its standalone transitive footprint drops from
~16k to ~50 lines of IR. Enabled by core.sx now self-declaring its libc, so
list.sx → core.sx resolves without the std assembly.

Regenerates 40 .ir snapshots: compiler.sx sits in the std import graph
(std → cli → build → compiler), so narrowing its import shifts the
registration order in every std program, renumbering LLVM symbol suffixes
(@foo.N → @foo.N+1) and adding a redundant `declare void @out` (LLVM dedups
it). Verified the diffs are purely that — no .exit/.stdout/.stderr changed, no
instruction/type/constant changed — and the full suite is green (817/0).
2026-06-26 09:16:38 +03:00
agra
22d5060439 refactor: core.sx self-declares its libc #library
core.sx owns the libc escape hatches (libc_write / libc_malloc / libc_free /
memcpy / memset, all `extern libc "..."`) but never declared the `libc`
#library constant — it free-rode on `#library` being program-global, satisfied
by other std modules (socket/fs/cli) in a full std build. So core.sx — and
list.sx, which imports it for Allocator/List — could not be imported without
assembling the whole std prelude (`extern library 'libc' is not declared`).

Declare `libc :: #library "c"` in core.sx itself. `#library` constants are
program-global and dedup, so this is harmless alongside the other declarers,
and it makes core.sx / list.sx self-contained — importable standalone. No
snapshot drift (a #library decl adds no type/global), full suite green 817/0.
2026-06-26 09:02:38 +03:00
agra
69fd76b02c refactor: home the target facts in a dependency-free std/target.sx
Move OS / ARCH / POINTER_SIZE + the OperatingSystem / Architecture enums out of
build.sx into a new std/target.sx that imports NOTHING, so low-level code can
name the target enum types without dragging the build/std barrel (build.sx
transitively pulls std + compiler + bundle, ~16k lines of IR).

build.sx flat-imports target.sx so the decls stay registered in the standard
import graph (build.sx is reachable from std.sx — dropping it would shift every
std program's type table). This is not a re-export: flat import only splices
into build.sx's own scope. Consumers are unaffected — the compiler resolves
OS / ARCH / POINTER_SIZE by name (comptime constants), so `inline if OS`/value
reads need no import; a module that names the enum type imports target.sx.

No behavior change (full suite green, 817/0); the enum types stay in the same
import graph, so no .ir snapshot drift.
2026-06-26 08:47:07 +03:00
agra
cc13700237 feat: linux epoll backend for std.event.Loop (the kqueue twin)
Add library/modules/std/net/epoll.sx — raw epoll bindings, the linux twin of
std/net/kqueue.sx — and branch std.event.Loop on `inline if OS` so the
OS-neutral readiness Loop runs on linux (epoll) as well as darwin (kqueue);
callers never see the backend.

epoll_event has no packed-struct primitive in sx, so it is modelled as an
arch-branched struct of u32 fields — { events, data_lo, data_hi } → 12 bytes on
x86_64 (matching __attribute__((packed))), { events, pad, data_lo, data_hi } →
16 bytes on aarch64 — every field 4-aligned, so the layout is byte-exact for the
kernel ABI with no packed attribute and no unaligned access. The fd is stashed
in data_lo (epoll echoes one data word, not the fd separately).

epoll.sx is self-contained (libc only, no build.sx): the `inline if ARCH`
selecting the struct is resolved by the compiler's flatten pre-pass, so the
module's IR stays small. The epoll backend is imported INSIDE event.sx's
`inline if OS == .linux` branch (not top level): event.sx rides the std.sx
barrel, so a top-level import would register epoll's types into every std
program's type table on darwin and drift every .ir snapshot.

The epoll Loop keeps a small per-fd registration table (combined EPOLLIN/OUT
mask via EPOLL_CTL_ADD/MOD/DEL), maps the fd back to the caller's udata, arms
EPOLLRDHUP so a peer half-close surfaces as Event.eof (matching kqueue EV_EOF),
and uses an eventfd as the cross-thread wake channel (kqueue's EVFILT_USER).

Validation: the kqueue path runs end-to-end on the macOS host (1632 unchanged);
the epoll bindings + ABI layout are corpus-locked ir-only by
examples/event/1633 (x86_64-linux, both arches probe-verified). The epoll Loop
is verified to lower clean for both linux arches and self-reviewed, but is not
corpus-snapshotted (a Loop example drags the std barrel → ~18k-line brittle IR);
runtime behavior validates on a linux runner.
2026-06-26 08:37:12 +03:00
agra
501399b1a9 fix: resolve qualified-import-member const as a compile-time constant (issue 0192)
A namespaced import's const (`m :: #import "lib.sx"; … m.CAP`) only ever
resolved as a runtime value — the const folders in program_index.zig had no
namespace-member arm, so a qualified const was rejected as an array dimension /
Vector lane / generic value-param and could not seed another const, while the
flat-import form worked everywhere.

Add a `lookupQualifiedConst` (+ float / float-typed twins) ctx hook: resolve
the alias via `namespaceAliasVerdictFrom` to its target module, then fold the
member from that module's per-source const cache (`foldQualifiedConstInt` in
lower/comptime.zig), pinned to the target source so nested const RHSs fold
there. Wire it into evalConstIntExpr / evalConstFloatExpr / isFloatValuedExpr —
both the expression-position field_access arm (`[m.CAP]T`) and the
type-argument dotted-name arm (`Vector(m.LANES, …)`, generic value-params).

Implemented on the source-aware ctxs (Lowering / SourceConstCtx); the
namespace-blind ModuleConstCtx / StatelessInner return null, so a qualified-const
dim reached only via the stateless type-alias path stays a clean unresolved-dim
diagnostic, never a fabricated length. Resolves correctly for array dims,
arithmetic, integral-float dims, Vector lanes, generic value-params, inline-for
bounds, and struct fields.

Regression: examples/modules/0842-modules-qualified-import-const-comptime.sx.
2026-06-26 07:51:27 +03:00
agra
6b8bce1aba docs: file issue 0191 (coerceToType welds type-incompatible value into return slot, no diagnostic) 2026-06-25 22:40:43 +03:00
agra
df1327e316 fix: initialize the error-channel slot on every failable implicit success return (issue 0190)
A failable function that returned by IMPLICIT success (no explicit
`return`) left its error-tag slot uninitialized, so a caller's `catch` /
`or` (or `main`) read a garbage tag and reported a phantom unhandled
error — and for value-carrying failables the success value was dropped.
The "no error" sentinel was only written on the explicit-`return;` path.

Unified all function-body-return lowering so the failable-success slot
is always written:
  - void `-> !` fall-through: `ensureTerminator` (control_flow.zig) now
    emits `ret constInt(0)` for a pure-failable end-of-body.
  - value-failable trailing-expression success: `lowerValueBody`
    (stmt.zig) routes through `lowerFailableSuccessReturn`.
  - generic + pack-fn instances: `monomorphizeFunction` (generic.zig) and
    `monomorphizePackFn` (pack.zig) now DELEGATE their body-return to
    `lowerValueBody` instead of hand-rolling a `coerce`+`ret` that drifted
    (covers generic/pack value-failables).

Also fixes the missing-value diagnostic guard added here: it now counts
`.err`-level diagnostics (new `DiagnosticList.errorCount`) rather than the
total list length, so a warning/note emitted while lowering the body
(e.g. an ObjC selector arity warning) can no longer suppress a genuine
"body produces no value" error — which previously shipped an
uninitialized return at exit 0.

Regressions: examples/errors/1061 (void fall-through), 1062 (value-failable
trailing expr), 1063 (generic value-failable trailing expr).
2026-06-25 22:39:49 +03:00
agra
45e69ac1bb fix: reject non-type expression in type position instead of fabricating {} (issue 0189)
Two type-resolution paths silently resolved a non-type AST node in type
position to a zero-field `{}` struct that reached codegen with no
diagnostic:
  - a dotted `type_expr` / field-access (`g.a`, `g` a runtime value) whose
    prefix is not a namespace alias
  - an `error_type_expr` (`!Name`) whose `Name` is not a declared error set

Now both reject loudly:
  - `resolveTypeWithBindings` (lower.zig): "expected a type, found a value
    '<name>' in type position" + `.unresolved`
  - `checkTypeNodeForUnknown` (semantic_diagnostics.zig): validates a named
    `!E` against the declared error-set names — "unknown error set
    '<name>'" / "expected an error set after '!', found type '<name>'".

A bare `!` (void channel) and a declared `!E` in return position stay
valid; namespace-qualified types (`pkg.Type`) are unaffected.

Regression: examples/diagnostics/1195-diagnostics-non-type-in-type-position.
2026-06-25 20:35:02 +03:00
agra
f52e16a3fc docs: file issue 0190 (void-failable fall-through leaves error slot uninitialized) 2026-06-25 18:41:22 +03:00
agra
40b5fb5f7e docs: tuple syntax cutover — Tuple(...) type, .(...) value, channel-outside-Tuple failables
Rewrite specs.md tuple/failable/pack/UFCS/grammar sections to the new
syntax, update readme.md, and refresh stale tuple references in example
header comments. Also fixes two pre-existing doc inaccuracies surfaced in
review: drop the value-discarding `;` in the tuple-return examples, and
correct the §13 function-type grammar production (optional param list +
optional trailing `!` channel). Optional semantics unchanged.

current/CHECKPOINT-LANG.md logs the cutover.
2026-06-25 18:41:22 +03:00
agra
1dfc22794e docs: file issue 0189 (non-type expr in type position fabricates empty struct) 2026-06-25 17:55:19 +03:00
agra
989e18b760 feat: tuple syntax cutover — Tuple(...) type + .(...) value
Replace the bare-paren tuple grammar with explicit, position-unambiguous
forms, mirroring how structs work:

  type     `(A, B)`        -> `Tuple(A, B)`          (named keeps `:`)
  value    `(a, b)`        -> `.(a, b)`              (named uses `=`)
  typed    (new)           -> `Tuple(A, B).(a, b)`   (like `Point.{...}`)
  failable `-> (T, !)`     -> `-> T !`
           `-> (T1, T2, !)`-> `-> Tuple(T1, T2) !`   (channel outside Tuple)

Bare `(...)` is now grouping only, everywhere; a comma in bare parens is a
hard error with a migration hint. Grouping, function types `(A, B) -> R`,
param lists, lambdas, and match bindings are unaffected.

`Tuple(...)` is strictly a TYPE in every position (including `size_of` /
`type_info` args); a tuple VALUE comes only from `.(...)` (anonymous) or
`Tuple(...).(...)` (explicitly typed). A bare `Tuple(1, 2)` is a tuple
type with non-type elements -> rejected.

The ~110 tuple-bearing corpus files were migrated with a one-shot
AST-aware migrator (the `sx migrate` tool from the prior commit, removed
here). New examples: 0130 (new syntax), 0131 (typed construction), 1060
(named-tuple failable return). 1116 golden updated for the new hint text.
2026-06-25 17:53:57 +03:00
agra
c882c6c63e add sx migrate tuple-syntax migration tool
Temporary scaffolding for the tuple-syntax cutover. Parses old-grammar
.sx and rewrites tuple syntax to the new spelling:
  - tuple TYPES   `(A, B)`        -> `Tuple(A, B)`   (named keeps `:`)
  - tuple VALUES  `(a, b)`        -> `.(a, b)`        (named flips `:` -> `=`)
  - 1-tuples / empty / spread     -> `.(x)` / `.()` / `.(..xs)`, `Tuple(..Ts)`
  - failable returns: the `!` channel stays OUTSIDE Tuple
      `-> (T, !)`        -> `-> T !`
      `-> (T1, T2, !)`   -> `-> Tuple(T1, T2) !`

AST-walk based: rewrites only `tuple_literal` / `tuple_type_expr` nodes
(function types, param lists, match bindings, arrays, struct literals,
Closure sigs, groupings are left untouched). Nested tuples rewrite
recursively as a single non-overlapping edit per outermost tuple.

Value-vs-type ambiguity (call-arg tuples whose elements could be types,
e.g. `size_of((Box, i32))`, empty `()`) is never guessed: such sites go
to a worklist. A non-empty worklist exits nonzero and suppresses the
"looks-done" stdout output unless `--force` is passed.

`sx migrate <f>` prints migrated source; `--dry-run` prints only the
worklist. Built against the old grammar; removed after the cutover.
2026-06-25 15:23:18 +03:00
agra
820cd62fa1 docs: file issues 0185-0188
File four issue write-ups discovered alongside the 0179 work:
- 0185: binary-op operand auto-unwrap silently miscompiles a NULL ?T
- 0186: closure VALUE call does not coerce arg to ?T parameter
- 0187: lambda with inferred return type + block body with early returns
  mis-infers its return type
- 0188: closure-VALUE calls skip argument validation (arity + tuple spread)
2026-06-25 13:57:56 +03:00
agra
468461becc fix: gate implicit optional unwrap on flow narrowing (issue 0179)
Optional (?T) operands were implicitly unwrapped without proof of
presence, silently miscompiling a NULL ?T to garbage. Unwraps in
binary ops and other expression positions are now gated on flow
narrowing: a ?T value is only auto-unwrapped where control flow has
established it is non-null (the narrowed_refs set). Outside a narrowed
region, an implicit unwrap is rejected rather than producing garbage.

Touches the lowering pipeline (lower.zig + lower/{call,closure,coerce,
comptime,control_flow,expr,ffi,generic,pack,stmt}.zig). Adds optionals
examples 0919-0923 and closures example 0312 covering flow narrowing,
binop narrowing, no-implicit-unwrap rejection, and no closure leak of
narrowed state. Updates specs.md and readme.md.
2026-06-25 13:57:48 +03:00
agra
6c89a0aa3e fix: body-local #run of an unbridged shape fails loudly instead of silent garbage (issue 0182)
The body-local #run fold in emitCall was effectively dead (gated on
args.len==0, but the __ct comptime wrapper always carries the implicit
*Context arg), so every body-local #run fell through to a RUNTIME call:
bridgeable shapes lucked into the right value; an unbridgeable shape
(e.g. [2][]i64) ran over --- storage -> garbage, exit 0, no diagnostic.

Fold any is_comptime callee (gated !enclosing.is_comptime so nested
metatype calls in a comptime wrapper's dead body aren't folded). On a
tryEval bail, distinguish a BRIDGE bail (result can't regToValue-
materialize -> error: comptime init of 'X' failed: <reason> +
comptime_failed, build fails, symmetric with the global #run path) from
an EXECUTION bail (VM can't run the body, e.g. NaN/extern -> runtime
fallthrough, preserving types/0150), via comptime_vm.last_bail_was_bridge
(reset at tryEval entry, set only at regToValue). The const name is
threaded onto the wrapper (comptime_display_name) so the diagnostic reads
the source name, not __ct_N.

Regressions: diagnostics/1204 (negative), comptime/0645 (positive).
Verified by 3 adversarial reviews, suite 801/0.
2026-06-23 19:29:11 +03:00
agra
95c9c0df4c fix: diagnose indexing a non-indexable type instead of panicking (issue 0183)
lowerIndexExpr fell through to an index_get with an .unresolved element
type for any non-indexable object (*T, *[]T, struct, scalar), reaching
codegen -> 'unresolved type reached LLVM emission' panic. Add a guard
after all indexable arms: if getElementType(obj_ty) is .unresolved and
obj_ty is itself resolved (genuinely non-indexable, not a prior-error
placeholder), emit a located 'cannot index a value of type <T>'
diagnostic + placeholder (hasErrors aborts before codegen). A single
pointer hints by pointee: ptr-to-scalar -> many-pointer/dereference;
ptr-to-array/slice -> dereference first. No false-positives (generics,
aliases, late-resolved, every indexable shape verified).

Regression: examples/diagnostics/1203-diagnostics-index-non-indexable.sx.
Verified by 3 adversarial reviews, suite 799/0. Filed adjacent pre-existing
panic 0184 (untyped positional .{ } literal with no target type).
2026-06-23 17:29:12 +03:00
agra
097d23d909 fix: presence-preserving optional->optional coercion (issue 0180)
The generic-?? wrong-fallback was not in lowerNullCoalesce: coercing
?A -> ?B (differing payload, e.g. the ?i32->?i64 call-arg coercion when
instantiating unwrap_or(99, ?i32)) routed through .optional_wrap, which
unconditionally unwrapped the source and re-wrapped as ALWAYS-PRESENT, so
a null became present-zero everywhere (args, returns, field init,
var-decl, ??). Add a CoercionPlan.optional_to_optional (conversions.zig)
+ a presence-preserving arm in coerceMode (coerce.zig): has_value ->
present: unwrap+coerce-child+wrap-present; absent: constNull(dst); merge
via a dst_ty block param. lowerVarDecl gains a !src_is_optional guard so
an annotated x : ?B = <?A> routes through the same arm (also makes
aggregate-payload var-decl ?[3]i64->?[]i64 / ?Concrete->?Protocol work).

Alias-optional struct-literal default already works (grouping + 0166);
a 1-tuple default ?(i32,) ?? 5 now emits a clean diagnostic instead of an
LLVM PHI abort (no implicit scalar->1-tuple coercion per spec).

Regressions: optionals/0916 (generic ??), 0917 (alias struct default),
0918 (var-decl optional->optional), diagnostics/1202 (1-tuple default) +
a conversions.test.zig unit test. Verified by 3 adversarial reviews,
suite 798/0.
2026-06-23 16:16:47 +03:00
agra
4ca466fa96 fix: optional-chain index opt?.xs[i] over array/ptr-array field (issue 0181)
opt?.xs[i] typed and lowered the index over the optional CONTAINER
(?[N]T); getElementType returned .unresolved, so index_get reached LLVM
with an unresolved element type and panicked. Mirroring the 0101
!-unwrap fix: add lowerOptionalChainIndex (optional_has_value -> some:
unwrap + index (index_gep+load for ?*[N]T, else index_get) +
optional_wrap; none: const_null; merge -> ?ElemType, element-optional
flattened). The typer + dispatch guard compute the element via
ptrToArrayElem(child) orelse getElementType(child), so value-arrays,
slices, many-pointers, AND pointer-to-array (?*[N]T) children resolve.
Null receivers short-circuit (no null deref).

Regression: examples/optionals/0915-optional-chain-array-field-index.sx.
Verified by 3 adversarial reviews, suite 794/0. Filed broader pre-existing
gap 0183 (indexing a non-indexable type panics instead of diagnosing).
2026-06-23 12:29:29 +03:00
agra
fa7c07faf8 fix: comptime reg->value bridge for array-in-aggregate + clean abort on comptime-init failure (issue 0167)
(C) regToValue (comptime_vm.zig) gained no array arm, so a #run returning
an aggregate containing an array bailed 'reg->value: aggregate shape not
bridged yet'. Add an .array arm: read N elements at stride
typeSizeBytes(elem) from the array address, bridge each recursively via
regToValue -> an .aggregate Value (serializeAggregateValue already emits
arrays). Composes with struct fields, nested arrays, array-of-structs,
and the ?Arr optional payload; unbridgeable elements bail loudly.

(E) A global failing #run proceeded into LLVM emission and panicked
'unresolved type reached LLVM emission' when the unresolved const was
used. Add 'if (self.comptime_failed) return;' in emit() after Pass 0 so
it aborts cleanly (exit 1, the comptime diagnostic) across run/ir/build.

Regression: examples/comptime/0644-comptime-run-array-aggregate.sx.
Verified by 3 adversarial reviews, suite 793/0. Filed separate bugs found
during review: 0181 (optional-chain ?. to array field + index panics),
0182 (body-local #run unbridged silently miscompiles).
2026-06-23 11:34:22 +03:00
agra
555ccdc024 feat: parenthesized type grouping — (T) groups, (T,) is a 1-tuple (issue 0177)
In type position, parentheses now mirror value position: (T) (a single
unnamed element, no trailing comma) is a GROUPING that resolves to the
inner type; (T,) is a 1-tuple; (A, B) a 2-tuple; named (x: T) and spread
(..Ts) stay tuples; (...) -> R stays a function type. This lets a
closure/optional/function type be parenthesized for readability without
silently becoming a 1-tuple:
  [1](Closure(i64,i64) -> i64)   // array of closures (issue 0177) -> 7
  ?(?i64)                        // genuine nested optional (issue 0165 intent)

Parser: src/parser.zig returns the inner node for a single unnamed
non-spread no-trailing-comma parenthesized type. formatTypeName (both
generic.zig diagnostics + types.zig reflection) now render a 1-tuple as
(T,) so the spelling is unambiguous and diagnostics are self-consistent.
The 0165 coerce/stmt note reworded accordingly.

specs.md §Type Syntax updated; basic/0036 wrap return -> (i64,); obsolete
diagnostic 1195 removed (?(?i64) now compiles); regression
examples/types/0201-types-parenthesized-type-grouping.sx added; 0414 .ir
golden regenerated for the (T,) rendering. Resolves 0177; updates
0165/0170. Verified by 3 adversarial reviews; suite 792/0.
2026-06-23 10:43:47 +03:00
agra
c41f51aed3 fix: validate protocol impl method signatures vs the protocol declaration (issue 0178)
The issue-0176 conformance gate was name-only, so an impl P for T with a
mismatched return/param type (or arity) built a wrong-ABI thunk that
silently miscompiled (exit 0, wrong value). firstUnimplementedMethod now
validates arity (after self), each param type, and the return type
against the protocol declaration, substituting protocol Self->concrete
via resolveProtoTypeSubSelf (recurses through pointer/many-pointer/
optional/slice/array so []Self<->[]T match; conservative .unresolved for
Self-in-generic-arg). Comparison is by structural formatTypeName
(alias/module/spelling independent); typesClearlyDiffer skips when either
side has an unresolved leaf at any depth, biasing against false-positives.

Regressions: diagnostics/1201 (negative), protocols/0420 (positive,
[]Self param). Verified by 3+3 adversarial reviews (a mid-fix []Self
false-positive was found and closed); suite 792/0.
2026-06-23 08:48:31 +03:00
agra
8b613af96b docs: close issue 0171 as not-a-bug (wrong casing: any vs Any)
The type-erased value type is spelled Any (capital), per specs.md and
type_resolver.zig. Lowercase 'any' is an undefined name that resolves to
an empty-struct stub, which is why ?any appeared to silently discard the
value. ?Any round-trips correctly (present/absent/unwrap all work), so
there is no Any-TypeId canonicalization bug. Reword the 0165 cross-ref
accordingly.
2026-06-23 08:05:44 +03:00
agra
58f97fff10 fix: diagnose ?? with a non-optional lhs instead of codegen panic (issue 0172)
lowerNullCoalesce fed resolveOptionalInner's .unresolved (returned for a
non-optional lhs) into the merge-block params / optionalUnwrap / RHS
target type, reaching codegen and panicking 'unresolved type reached
LLVM emission'. Guard: when inferExprType(nc.lhs) is a resolved
non-optional type, emit a located diagnostic and bail; an .unresolved
lhs (prior error) is excluded to avoid double-report. ?? is optional-only
per specs.md (error unions use or/catch), so rejecting a failable lhs is
correct; comptime panic closed too.

Regression: examples/diagnostics/1200-diagnostics-null-coalesce-non-optional.sx.
Verified by 3 adversarial reviews, suite 790/0. Filed adjacent bug 0180
(?? lowering defects for generic/alias/tuple optional lhs).
2026-06-23 03:31:58 +03:00
agra
e5b682e622 fix: reject implicit ?T -> bool coercion instead of silent false (issue 0169)
The Optional->Concrete unwrap classify rule treated ?i64 -> bool as
unwrap+narrow (both builtin), silently yielding false for every optional
(present or null). specs.md defines no implicit optional->bool
conversion. Reject it: conversions.zig adds an optional_to_bool_reject
plan (dst == bool, child != bool); coerce.zig emits a located diagnostic
suggesting '!= null'. Covers arg/field-init/return via the shared
coerceMode. The if-opt presence test (issue 0164) is a separate path,
untouched.

Regression: examples/diagnostics/1199-diagnostics-optional-to-bool.sx +
conversions.test.zig unit test. Verified by 3 adversarial reviews, suite
789/0. Filed adjacent issue 0179 (whole implicit ?T->concrete unwrap
family silently miscompiles a null optional; design-touching).
2026-06-23 02:47:51 +03:00
agra
3c738695dc fix: diagnose non-conforming protocol erasure instead of unreachable-thunk SIGABRT (issue 0176)
Erasing a type to a protocol when it conforms only via a free function
(not an explicit impl P for T) built a vtable of unreachable thunks ->
SIGABRT on first dispatch, with no diagnostic. Per specs.md erasure is
impl-driven, not structural, so the erasure was never valid.

Add a conformance gate (firstUnimplementedMethod in buildProtocolValue,
src/ir/lower/protocol.zig): emit a located diagnostic when a protocol
method has no reachable impl, or when an impl method introduces its own
type params (signature mismatch — it bails lazyLowerFunction and would
reach the unreachable thunk). A std.debug.panic tripwire guards the
diagnostics==null path so a non-conforming erasure can never silently
ship as undef. Gate<->thunk equivalence verified bidirectional.

Regressions: protocols/0419 (positive struct-field dispatch),
diagnostics/1197 (no-impl) + 1198 (generic-method signature mismatch).
Updated memory/0808 (it erased a non-conforming type that never
dispatched). Verified by 3+1 adversarial reviews, suite 788/0. Filed
adjacent bug 0178 (protocol impl method type-mismatch silent miscompile).
2026-06-23 02:13:30 +03:00
agra
3605165398 fix: dispatch unwrapped optional-closure call g!() through call_closure (issue 0170)
Calling through an unwrapped optional closure (g!()) crashed with LLVM
'Called function must be a pointer!': the indirect-call catch-all else
arm emitted call_indirect on the whole {fn,env} closure struct with a
hardcoded .i64 return. The else arm now inspects inferExprType(callee):
a .closure callee dispatches through call_closure (threads env + ctx via
the [ctx, env, user_args] ABI, returns closure.ret); a plain fn pointer
uses call_indirect with the callee's real function.ret instead of i64.

The filed repro's ?(() -> void) spelling is a tuple-optional (now
diagnosed by the 0165 fix); the real ?Closure(...) layout was already
correct. Verified load-bearing (HEAD crashes) by 3 adversarial reviews,
suite 785/0. Regression: examples/closures/0311-closures-optional-closure.sx.
Filed adjacent bug 0177 (array-element closure direct call crashes).
2026-06-23 01:02:13 +03:00
agra
28bb101a4a fix: literal element typing — typed-array null element, tuple coercion, positional var element (0173-0175)
0173: resolveArrayLiteralType gained no arm for [N]T/[]T heads, so a
([2]?i64).[...] head lost its ?i64 element type and a bare null reached
LLVM as const_null(.unresolved). Route structural heads through
resolveTypeWithBindings; validate an undefined element name in the head
via UnknownTypeChecker (semantic_diagnostics.zig) instead of a silent
empty-struct stub (no-silent-fallback).

0174: positional .{...} against a TUPLE target now coerces each element
to TupleInfo.fields[i] (was neither struct nor array, so uncoerced).

0175: a positional struct literal with a bare-variable element was
misclassified as a named shorthand (parser puns .{x} -> x=x), zeroing
the fields. has_names now consults the struct definition to reclassify a
punned non-field name as positional; positional coercion uses the
lowered value's real getRefType.

Regressions: optionals/0914, types/0199, types/0200, diagnostics/1196.
Verified by 4 adversarial reviews; suite 784/0. Filed adjacent bug 0176
(protocol-typed struct field method call aborts).
2026-06-23 00:25:28 +03:00
agra
5a436eddb1 fix: coerce array/vector literal elements to element type (issue 0168)
[N]?T arrays were corrupted: a positional literal .{ null, 7 } stored
bare T/null elements into {T,i1} optional slots because array elements
were never coerced (getStructFields is empty for an array, so the
i<struct_fields.len field-coercion gate never fired). A present element
then read back as absent and direct indexing segfaulted.

lowerStructLiteral's positional branch now computes array_elem_ty for
array/vector targets and coerces each element to it; lowerArrayLiteral
generalizes its slice-only coercion to coerce every element via
coerceToType (layout-aware: scalar->{T,i1}, pointer-sentinel->one-word,
array->slice, concrete->protocol). Verified by 3 adversarial reviews,
suite 780/0.

Regression: examples/optionals/0913-optionals-array-of-optionals.sx.
Filed adjacent pre-existing bugs: 0173 (typed .[null,..] element), 0174
(tuple positional-element coercion), 0175 (positional struct literal
variable element zeroed).
2026-06-22 22:50:20 +03:00
agra
2ea25e84ec fix: thread optional child type into ?? struct-literal default (issue 0166)
The RHS of a null-coalesce was lowered with no target type, so a bare
struct literal default (x ?? .{ ... }) produced a struct_init with
.ty == .unresolved that panicked in emitStructInit. lowerNullCoalesce
now saves self.target_type, sets it to the optional's resolved child
before lowering nc.rhs, and restores it (leak-free). Verified across
struct/slice/enum/tuple/protocol/nested-optional/generic child types by
3 adversarial reviews.

Regression: examples/optionals/0912-null-coalesce-struct-literal.sx.
Filed adjacent pre-existing bug 0172 (?? on a non-optional lhs panics).
2026-06-22 22:17:01 +03:00
agra
0bc8005b99 fix: diagnose ?(?T) tuple-payload mismatch instead of malformed IR (issue 0165)
In type position (T) is a 1-tuple (specs.md:843), so ?(?i64) is
optional(tuple(?i64)); assigning a bare ?i64 had coerceToType classify
.none and pass the value through, then optionalWrap built a corrupt
insertvalue that aborted the LLVM verifier. After coercing toward an
optional's child, verify the coerced type equals the child type
(stmt.zig decl-init + coerce.zig .optional_wrap); on mismatch emit a
located diagnostic (tuple-specific note only when the child is a tuple).
formatTypeName now renders tuples as (x: i64, y: i64).

Regressions: optionals/0911 (nested optional via alias, round-trip),
diagnostics/1195 (the mismatch diagnostic). Updated diagnostics/1101 +
protocols/0414 goldens for the improved tuple type-name rendering.
Verified by 3 adversarial reviews. Filed adjacent bug 0171 (?any child
not canonicalized).
2026-06-22 21:54:12 +03:00
agra
3e8d003e3d fix: bindingless if/while/and/or over optional reads has_value (issue 0164)
lowerIfExpr emitted optional_has_value only for the binding form; a bare
'if opt' passed the raw {T,i1} aggregate to condBr, where emitCondBr's
catch-all struct arm silently folded it to 'i1 true' (structs always
truthy) — a silent miscompile that took the present-branch for null
optionals. while / and / or shared the same defect.

Reduce bindingless optional conditions to optional_has_value in
lowerIfExpr/lowerWhile and via a new lowerBoolCondition helper for and/or
operands. Replace the silent-true emitCondBr arm with a lowering-time
diagnostic (checkConditionType/isValidConditionType) rejecting conditions
whose type isn't bool/integer/pointer/optional; the backend @panic is now
an unreachable tripwire.

Regressions: examples/optionals/0908..0910 + diagnostics/1194 (negative).
Verified by 3+3 adversarial reviews.

Filed adjacent bugs found during review: 0168 (array-of-optionals element
load), 0169 (optional->bool coercion), 0170 (closure-optional layout).
2026-06-22 21:04:05 +03:00
agra
2637ae98a5 docs: file issues 0164-0167 (optional/comptime bugs found during 0162 review)
0164 if <optional> no-binding folds has_value to true (silent miscompile)
0165 parenthesized nested optional ?(?T) malformed double-wrap (crash)
0166 ?? .{ } struct-literal default unresolved type (crash)
0167 comptime regToValue array-in-aggregate gap + unclean recovery
2026-06-22 19:43:55 +03:00
agra
7c21f84151 fix: comptime VM reg→value bridge for optional results (issue 0162)
Add an .optional arm to regToValue in comptime_vm.zig: read the
has_value flag at offset sizeof(child), bridge the payload recursively
into a { payload, i1=true } aggregate when set, yield .null_val (zero
{T,i1}) when clear or the bare null sentinel. Matching serialize arm in
serializeAggregateValue (emit_llvm.zig). Pointer/?Closure/?Protocol-child
optionals and array-payload aggregates bail loudly, not silently.

Regression: examples/comptime/0643-comptime-run-optional-aggregate.sx
(present ?T, present ?i64, null ?i64). Verified by 3 adversarial reviews.
2026-06-22 19:42:41 +03:00
agra
ff9e448f8c fix: optional-chain getter/field correctness from 0160 adversarial review
Five adversarial reviews of the issue-0160 fix surfaced three more bugs in the
touched optional-chain / optional-coercion code; all fixed here:

1. A COLD generic-instance getter through `?.` (`?*Vec(i64)` `.getter`, never
   called directly first) panicked with "unresolved type reached LLVM emission":
   a cold instance method is absent from resolveFuncByName, so the getter's
   return type resolved to .unresolved → a ?unresolved merge type. lowerOptionalChain
   and getterReturnTypeOnDeref now warm the monomorph (ensureGenericInstanceMethodLowered)
   before querying its return type. (The 0907 test passed only by luck — List(i64)
   is warmed by stdlib use; 0907 now also exercises a cold user generic.)

2. A real-field read through a `?*T` chain (`op?.field`, op: ?*T) reinterpreted
   the pointer bits as the field (silent garbage) — the some-branch real-field
   path didn't load through the pointer. It now derefs `?*T` before the field
   access. (Pre-existing — the else-branch predates 0160 — but it's the same
   function and a silent miscompile, so fixed here.)

3. `?[]T = array` skipped the array→slice promotion (corrupt .len/.ptr): the
   lowerVarDecl optional arm wrapped the raw array. It now coerces the value to
   the optional's child type (array→slice) before wrapping.

Regression examples 0906/0907 extended to cover all three. Distinct PRE-EXISTING
bugs the reviews surfaced in untouched subsystems are filed as issues 0161
(struct-literal vs scalar), 0162 (#run returning an optional aggregate), 0163
(untagged-union payload-binding match).
2026-06-22 18:55:41 +03:00
agra
1b0c857b91 fix: struct-literal → optional coercion + #get through optional chain (issue 0160)
Two fixes for optional interactions surfaced by the #set/#get review. The
original issue 0160 mis-diagnosed (A) as an optional-chain bug; the chain works
fine for real fields. The actual bugs:

(A) A bare struct literal `.{ ... }` against an optional target `?T` was built
into the optional's {payload, has_value} layout instead of the inner T, then
re-wrapped — corrupting the value (a multi-field payload's first field clobbered
by the has_value flag, or a `?T` arg silently null) or failing LLVM
verification. lowerStructLiteral now builds the inner T, materializes it, and
wraps via coerceToType; lowerVarDecl's previously-UNCONDITIONAL optional wrap is
guarded so an already-`?T` value isn't double-wrapped. Fixed across var-decl,
arg, return, nested field, reassignment, and array-element contexts.

(B) `#get` accessors are now reachable through an optional chain (`obj?.getter`):
lowerOptionalChain dispatches the getter via a synthetic receiver, and
expr_typer types `obj?.getter` through a shared getterReturnTypeOnDeref helper
(handles `?T` and `?*T`, value and pointer optionals, and generic-instance
getters like List.len). The `#set` write side through `?.` is intentionally left
matching real-field behavior (optional-chain assignment unsupported).

Regression tests: examples/optionals/0906 (struct-literal → optional) and 0907
(accessor through chain). issues/0160 marked RESOLVED with the corrected root
cause.
2026-06-22 18:28:57 +03:00
agra
9523c29173 feat: #set property accessors (write counterpart of #get)
A method `name :: (self: *T, value: V) #set { ... }` (or `=> expr;`) is the
write counterpart of a `#get` accessor: `obj.name = rhs` dispatches to it as
`obj.name(rhs)` when no real field matches. Plumbed parallel to `#get`:

- lexer/token `#set`; `FnDecl.is_set` + `Function.is_set`; parsed in the same
  marker slot as `#get` (no return type, exactly self + one value param).
- get+set coexistence: a setter registers/mangles/dispatches under an effective
  `name$set` name (`$` is illegal in sx identifiers, so unmistakable), keeping a
  same-name `#get` under the plain `name`. Resolution is declaration-order-
  independent: a plain read query picks the non-setter, a `name$set` write query
  picks the setter (accessorEffName / accessorNameMatches / structMethodFn).
- write dispatch in lowerAssignment via tryLowerPropertyAssignment: plain assign
  synthesizes `obj.name$set(rhs)`; compound `OP=` is get-modify-set and
  evaluates the receiver EXACTLY ONCE (bound to a synthetic local); read-only
  (#get-only) and write-only (#set-only + compound) emit clear diagnostics; a
  real field of the same name still wins. Multi-assign property targets dispatch
  the setter too (tryLowerPropertyStore, via a pre-lowered-Ref binding).

Payoff: List gains a `len` #set, so `xs.len = n` works; the `.items.len = N`
write workarounds in sched.sx + ui/* + platform/* revert to `xs.len = N`.

issues/0160 records an optional-chain interaction surfaced by the review (a
pre-existing `?T` value-optional read miscompile that blocks getter-through-`?.`).
2026-06-22 17:55:18 +03:00
agra
5cc45a2b38 refactor: List is slice-backed { items: []T; cap } — directly iterable
items is now a []T slice whose .len IS the live element count (cap = allocated
capacity), so a List iterates directly: `for xs.items (e) { ... }`. A
`len :: (self) -> i64 #get => items.len` accessor keeps `xs.len` reads working;
`.len` WRITES become `.items.len`. List stays 24 bytes (`[]T`=16 + cap=8).

- list.sx: append/ensure_capacity/deinit rewritten for the slice backing. deinit
  guards the free on `cap > 0` (true ownership) and resets via explicit
  ptr=null/len=0 (a `.{}` slice assignment yields a garbage len; `.[]` is the
  empty-slice literal but can't be assigned to a generic []T — both worked around).
- Compiler coupling updated: comptime_vm makeStringList/readStringList write/read
  items as a {ptr,len} fat pointer at field 0 + cap at field 1; control_flow
  listView views an `items: []T` slice (keeps the legacy {[*]T,len} shape too).
- Migrated List `.len` writes to `.items.len` in sched.sx + ui/{render,pipeline,
  glyph_cache} + platform/{sdl3,android,uikit}.
- Snapshots: List's type-table layout changed → ~40 .ir + memory/0800 (items now
  prints as a slice) regenerated; diagnostics/1183 retargeted to a genuine
  many-pointer (xs.items is a slice now). Example memory/0840 locks for-each.
2026-06-22 11:55:19 +03:00
agra
9d3a019670 feat: #get property accessors (no-paren method-as-field)
A method declared `name :: (self: *T) -> R #get => expr;` is invoked via
no-paren field syntax (`obj.name`) instead of `obj.name()`. It is an ordinary
method (registered `Type.method`, flagged is_get); field-access lowering and
inference dispatch to it when no real field of that name exists, by synthesizing
a no-arg `obj.name()` call routed through the normal call path (so receiver
address-of and generic binding are reused).

- Lexer/token: `#get`. Parser: parsed after the return type in parseFnDecl;
  hasFnBodyAfterArrow treats it as a body marker so struct-body methods parse.
- Resolution: getAccessorFor handles a generic-struct instance and a plain
  struct. A REAL field of the same name wins (a getter never shadows stored
  data). An explicit postfix-deref receiver (`p.*.getter`) dispatches on the
  inner pointer so it takes the working auto-deref path.
- Works on plain + generic structs (incl. getters returning the type param),
  in expressions/conditions/args/loop-bounds, chained, and via a pointer
  receiver. Examples: types/0196 (basic) + types/0197 (stress).

Known narrow limitations (clean errors / workarounds, not silent): a getter
RESULT used directly as a method/getter receiver (`o.gi.dbl`) errors — bind it
to a local first; a getter named `len`/`ptr` returning non-i64 mis-infers
(the .len/.ptr builtin-field shortcut).
2026-06-22 11:55:01 +03:00
agra
b9311e7de4 fix: slicing a many-pointer yields a correct slice (issue 0159)
emitSubslice handled a struct (slice/string) base and an array base, but a
many-pointer [*]T base is an LLVM pointer kind — it fell through to the else arm
that mapped the result to LLVMGetUndef(slice_ty), so a slice of a many-pointer
(mp[lo..hi]) had a garbage .len/.ptr and iterating it segfaulted.

Add a LLVMPointerTypeKind branch: the base value IS the data pointer, so GEP by
lo and len = hi - lo (the caller supplies the bound; no length is read from the
unbounded pointer). An open-ended mp[lo..] has no resolvable upper bound (a [*]T
carries no length), so lowerSliceExpr now diagnoses it instead of emitting a
.length op that yields garbage.

A List (whose items is [*]T) is now iterable with for items[0..len] (e);
applied in Scheduler.deinit. Regressions: examples/types/0195 (valid slice +
List for-each) + examples/diagnostics/1192 (open-ended rejection).
2026-06-22 10:15:18 +03:00
agra
55ed9a248e fibers: Scheduler.deinit + struct-literal init cleanup
Scheduler.deinit closes the bounded leaks B1 documented: it reaps any leftover
ready fibers, frees every heap Task from go (now tracked via a task_allocs
field), frees the timers/io_waiters/task_allocs List backings, and closes the
lazily-opened kqueue fd. Terminal + idempotent; the per-spawn/go closure env
remains unfreeable (language limitation). Locked by
examples/concurrency/1820-concurrency-fiber-scheduler-deinit.sx, which exercises
every freed resource under a tracking GPA (freed by deinit: 5, kq reset to -1).

Also converts plain-struct '= ---'+field-assign init to '.{ ... }' literal init
where '---' carries no meaning: Scheduler.init, Dock.make, and the fiber
examples 1811/1813/1814/1816 (partial literals zero-fill the index-filled array
fields). Unions, '---'-feature tests, the 0154 regression, documented
generic-pack gaps, and loop/conditional inits are intentionally left on '---'.
2026-06-22 09:45:33 +03:00
agra
1e0015d6b4 fix: union struct-literal init (issue 0158)
A plain union initialized with a struct literal (b : Overlay = .{ f = 3.14 })
silently miscompiled — it fell through the generic struct-literal path
(getStructFields returns empty for a union), building a malformed structInit
whose overlapping zero-fill clobbered the named member, so it read back 0.0
(and a type-pun read segfaulted).

lowerStructLiteral now detects a plain-union target and dispatches to a new
lowerUnionLiteral, which writes each named member into a union-sized slot via
the same lvalue resolver the u.member = v assignment path uses, then loads the
union value back. Validity: the named members must share one arm — a single
direct member, or several promoted members of the same anonymous-struct variant.
Overlapping members, members from different arms, and positional union literals
are rejected with a diagnostic (no silent last-wins); an empty .{} yields an
undefined union (matching the --- form).

specs.md updated. Regressions: examples/types/0194 (valid forms) +
examples/diagnostics/1191 (overlap rejection).
2026-06-22 09:45:17 +03:00
agra
6ee4d066b3 fibers: address adversarial review of the B1 changes (6 findings)
UFCS generic overload resolution (issue 0157 follow-ups):
- P1-a: call planning (calls.zig) used the last-wins fn_ast_map winner
  while lowering reselected by receiver, so the planned result type
  could disagree with the dispatched function and misbox the result.
  Both now share selectUfcsGenericByReceiver(.., fd0).
- P1-b: selection scanned module_decls globally, flagging a
  transitively-hidden same-named overload as a false ambiguity. Now
  two-tier: directly-visible authors first (ambiguity only among
  those), global fallback for receiver-reachable namespaced methods
  (e.g. Task.cancel) that defers to fd0 on a hidden tie.
- P2-b: boolean specificity tied *$T with *Box($T). Now peels pointer
  layers so the structurally-narrower receiver wins.

Scheduler (sched.sx):
- P1-c: a second concurrent Task.wait overwrote the single waiter slot
  -> silent deadlock. Now one-awaiter-per-task loud abort.
- P2-c: sleep(negative) rewound the monotonic virtual clock. Rejected
  loudly.

(P2-a, non-generic-winner-hides-generic, did not reproduce -- the
non-generic arm already falls through.)

Regressions: examples/generics/0218 (receiver specificity +
plan/lowering agreement), examples/concurrency/1818 (negative-sleep
abort), 1819 (double-wait abort). Suite green 758/0.
2026-06-21 22:05:22 +03:00
agra
5949a88439 fibers: end-to-end M:1 capstone (B1.5) — Stream B1 complete
1817 composes the whole colorblind pure-sx async stack: the M:1
scheduler, suspending go/wait async, and deterministic virtual-time
sleep/now_ms, over the naked swap_context on guarded mmap stacks. A
coordinator launches three async tasks (sleep 30/10/20 -> return
100/20/3), awaits all three in spawn order, and sums them; tasks
complete in DEADLINE order (task 2@10, 3@20, 1@30), sum 123, final
virtual clock 30 -- fully deterministic.

Stream B1 (fibers + Io + M:1 scheduler) is feature-complete: examples
1800-1817, suite 755/0. Checkpoint + plan marked COMPLETE; next carve
is Stream B2 (channels / cancel / async stdlib).
2026-06-21 19:43:22 +03:00
agra
1b0d640f73 fibers: event-loop Io — real fd readiness via kqueue (B1.4c)
A fiber can block on a file descriptor and the run loop blocks on
kevent until the kernel reports it ready. Reuses the existing
std/net/kqueue.sx bindings. Scheduler gains a lazy kq fd + an
io_waiters list; block_on_fd arms a one-shot EVFILT_READ registration,
records an IoWaiter, and suspends. Run-loop Mode 2: when the ready
queue drains and no timer is pending, block on kq_wait(-1), match each
fired ident to its waiter, evict it, wake the fiber. wake evicts a
pending fd-waiter (cancel_io_waiter_for) so no stale IoWaiter outlives
a reaped fiber.

Adversarial review found two CRITICALs: (1) two fibers on the same fd
share one kqueue registration (macOS EV_ADD replaces), so one is lost
and the loop hangs -- fixed by enforcing one-waiter-per-fd with a loud
abort; (2) an fd-waiter on a never-ready fd 'hangs' -- reclassified as
correct event-loop semantics (a server idling on a socket), with the
misleading orphan-check comment corrected. UAF parity, ident width,
EINTR handling, timer/io precedence all probed safe.

Example: 1816 (pipe roundtrip -- reader blocks, writer writes, reader
wakes via kqueue). macOS only; linux epoll twin deferred. Suite green 754/0.
2026-06-21 19:39:16 +03:00
agra
62ffea0663 fibers: deterministic virtual-time timers (B1.4b)
Add a virtual clock + sleep timers to the M:1 scheduler so fibers
schedule in reproducible simulated time. Scheduler gains clock_ms (the
virtual clock, advances only as timers fire), a timers list, now_ms(),
sleep(ms) (arm {clock_ms+ms, current} + suspend), and a timer-driven
run (drain ready -> fire earliest timer -> advance clock -> wake ->
repeat; the orphan-suspend deadlock check is preserved for a genuine
no-timer park). Wakes fire in deadline order with a FIFO tiebreak.

Adversarial review found a use-after-free: a fiber woken early (manual
or Task wake) before its sleep timer fired was reaped while its Timer
kept a dangling *Fiber, so a later fire dereferenced freed memory.
Fixed: wake evicts the fiber's pending timer (cancel_timer_for) -- every
re-ready path funnels through wake, so no stale timer outlives its fiber.

Examples: 1814 (sim-timer deadline ordering), 1815 (early-wake timer
eviction regression). Suite green 753/0.
2026-06-21 19:09:22 +03:00
agra
02ab077bfb fibers: checkpoint + plan for B1.5a/B1.4a; next is B1.4b (deterministic-sim Io) 2026-06-21 18:44:11 +03:00
agra
8367ad18b1 fibers: M:1 scheduler core + suspending fiber-task async (B1.5a, B1.4a)
library/modules/std/sched.sx: a generic Fiber + Scheduler over the
proven naked swap_context on guarded mmap stacks --
init/spawn/yield_now/suspend_self/wake/run (B1.5a), then Task($R) +
go/wait/cancel, a truly-suspending nullary-thunk async layer (B1.4a).
go(work) runs a thunk as a real fiber; wait() parks the caller until it
completes. Self-contained in sched.sx (io.sx importing it would
duplicate the _fib_tramp global asm).

Hardened per adversarial review: wake guarded on .suspended (FIFO
corruption), suspend_self/yield_now guard a null current, loud
mmap/mprotect/OOM/deadlock bails, cancel skips not-yet-run work.
Closure-env + heap-Task leaks documented (bounded, default-GPA-invisible).

Examples: 1811 (round-robin), 1812 (suspend/wake + spurious-wake guard),
1813 (async interleave + await-suspend + cancel). Also files issue 0155
(scalar-pointer index panics codegen -- non-blocking, found in review).
2026-06-21 18:44:03 +03:00
agra
d3944570b9 lang: generic $R type-arg resolution + receiver-driven ufcs overload (issues 0156, 0157)
0156 Part 1: a single-type generic $R (parsed as comptime_pack_ref)
used as a type-arg in a pack-fn body (Box($R), size_of(Box($R))) hit a
missing arm in resolveTypeWithBindings -> .unresolved -> LLVM panic.
Fix: mirror resolveTypeArg's comptime_pack_ref arm (look up
type_bindings, else a loud diagnostic). Regression: examples/generics/0216.
(Part 2 -- deferred .. spread crashes -- reframed OPEN/non-blocking.)

0157: a user generic ufcs method whose name collides with a stdlib
re-export resolved via last-wins fn_ast_map with no receiver filtering,
so the wrong overload won, $R never bound, and .unresolved reached LLVM.
Fix: selectUfcsGenericByReceiver enumerates all module authors, keeps
the receiver-binding ones, picks the most receiver-specific (concrete >
bare $T), dedups re-exports, and flags a genuine tie as a deterministic
'ambiguous -- qualify' diagnostic. Regression: examples/generics/0217.
2026-06-21 18:43:49 +03:00
agra
b1e06f21e3 lang: fix struct-field null/undef over-store (issue 0154)
Assigning null/--- to a struct field picked up a leaked enclosing
target_type (the function's return type, set for the whole body), so
constNull/constUndef built a whole-struct-typed value. The oversized
store overran the field's slot and clobbered the saved frame pointer,
so the function returned to 0x0. Surfaced building a by-value-returned
struct whose array field precedes a pointer field (Scheduler.init()).

Fix: add null_literal/undef_literal to the needs_target switch in
lowerAssignment so the field's own type is used. Regression:
examples/types/0193-types-sret-array-before-pointer.sx.
2026-06-21 18:43:33 +03:00
534 changed files with 71158 additions and 59097 deletions

View File

@@ -4,7 +4,242 @@ Companion to [PLAN-FIBERS.md](PLAN-FIBERS.md). Update after every step (one step
per the cadence rule). New corpus category: `18xx` concurrency. per the cadence rule). New corpus category: `18xx` concurrency.
## Last completed step ## Last completed step
**B1.3b-1 — the x86_64 / Win64 `swap_context` sibling — VALIDATED on real hardware.** The **B1.6 — aarch64-LINUX port of the M:1 fiber runtime (sched.sx).** `library/modules/std/sched.sx`
now runs end-to-end on aarch64-linux as well as aarch64-macOS, validated **byte-identical** on both
via Apple `container` (static ELF, no emulation). The per-OS bits are comptime-branched:
- `MAP_AP` (mmap MAP_ANON flag) — `inline if OS == { case .linux: 0x22 case .macos: 0x1002 }`,
exhaustive on the supported OSes (no default → a new target fails loud on use).
- The fd-readiness backend — kqueue on darwin, **epoll on linux**. The `epoll` import is scoped to
the linux branch (`inline if OS == .linux { ep :: #import "modules/std/net/epoll.sx" }`) so darwin
never pulls epoll types into the concurrency examples (the std-barrel-drift rule). `block_on_fd`, the
run-loop Mode-2 drain, and `cancel_io_waiter_for` each branch kqueue/epoll; epoll additionally
`EPOLL_CTL_DEL`s on fire + on early-wake (EPOLLONESHOT only DISABLES, kqueue EV_ONESHOT auto-removes).
- The first-entry trampoline was redesigned from a per-OS hand-written global-asm symbol to a **naked
sx fn** `fib_tramp` (`mov x0, x19; br x20`) + register-indirect dispatch (spawn presets
`regs[1] == x20 == &fib_dispatch`), so no per-OS `.global _fib_tramp`/`fib_tramp` symbol literal is
needed. This sidesteps a compiler bug (wrapped top-level `asm` dropped — now **issue 0194**, OPEN).
**Bug fixed en route (issue 0193 Bug A):** the tramp redesign initially bus-errored on the 1817
go/wait/sleep capstone (both OSes) because the WIP had dropped `export "fib_dispatch"`. Without the
export `fib_dispatch` uses sx's internal ABI (x0 = implicit `context`, `self` shifted to x1), but the
trampoline hands `self` in x0 (C-ABI) → on first entry the body runs (x1 happens to alias `self`) but
the closure then loads `regs[1] == &fib_dispatch` as its first capture and recurses forever → stack
overflow. **Fix: restore `export "fib_dispatch"`** (pins it to C-ABI, `self` in x0). Root cause found
via lldb on an AOT macOS build; confirmed by an adversarial source review (`src/ir/lower/decl.zig`).
The 1817 capstone in the suite guards the fix. Suite GREEN **817/0**; 1811/1814/1816/1817 byte-identical
macOS host ↔ aarch64-linux container.
### Earlier — B1 follow-up — `Scheduler.deinit` (close the bounded leaks). Post-B1 non-blocking cleanup: a
terminal `deinit` on `library/modules/std/sched.sx`'s `Scheduler` releases the resources B1 documented
as leaked. Frees, in order: (1) any fibers still enqueued ready (leak-safety net for `spawn`/`go`
without `run()``munmap` stack + free struct; a suspended off-queue fiber is unreachable, but a clean
`run()` aborts on orphans so none survive it); (2) every heap `*Task` from `go` — newly tracked via a
`task_allocs: List(*void)` field appended in `go` (the scheduler otherwise has no handle on its generic
`Task($R)`s); (3) the three `List` backings (`task_allocs`/`timers`/`io_waiters`, all grown through
`own_allocator`); (4) the lazily-opened kqueue fd (`close`, reset to `-1`). NOT freed (unchanged
language limitation): the per-`spawn`/`go` closure env (sx exposes no env-free). Idempotent (rests on
`List.deinit` nulling `items` + the `kq`/`ready_head` resets); TERMINAL contract — no scheduler-owned
handle (`*Task`, `*Fiber`, the scheduler) is usable after `deinit`.
- Added a canonical `close :: (i32) -> i32 extern libc` (matches the dedupe-canonical signature 1816
already uses) + the `task_allocs` field.
- Locked by `examples/concurrency/1820-concurrency-fiber-scheduler-deinit.sx` (aarch64-macOS `.build
{"target":"macos"}`, runs end-to-end): one run touches every freed resource — a SLEEPER (`timers`), a
pipe READER `block_on_fd` + WRITER (kqueue fd + `io_waiters`), two `go` tasks (`Task`s + `task_allocs`)
— then `deinit`. Verified by a tracking `GPA`: `freed by deinit: 5`, `live after deinit: 5` (the
RESIDUAL = the 5 documented closure envs, not a bug), `kq open after run: true` → `kq after deinit:
-1` (the genuinely-open kqueue fd is closed), `read: 3 [97 98 99]` (the fd path actually ran). Counts
captured into locals BEFORE printing (`print` allocates format temporaries through the same GPA).
- **Adversarially reviewed (worker):** no real memory-safety bug in the supported (deinit-after-`run`)
path — reap-loop reads `f.next` before freeing `f`, the three freed List backings + Tasks + kq are all
disjoint + scheduler-owned, no over-free, idempotent. The one CRITICAL it raised was a DOC contradiction
(step-(1) defensive reap vs step-(2) "post-run only"), reconciled by spelling out the terminal contract.
Its 0154-over-store concern (`.{}`→`List` writes in `init` could clobber `kq`) was PROBED and cleared:
`kq == -1` immediately after `init`, all fields clean. Suite GREEN **759/0**.
### Earlier — B1.5 — END-TO-END M:1 validation — STREAM B1 COMPLETE
A single capstone exercises the whole
colorblind pure-sx async runtime together: the M:1 scheduler (B1.5a) + suspending fiber-task async
`go`/`wait` (B1.4a) + deterministic virtual-time `sleep`/`now_ms` (B1.4b), over the `abi(.naked)`
`swap_context` on guarded `mmap` stacks (B1.0B1.3). `examples/concurrency/1817-concurrency-fiber-m1-end-to-end.sx`:
a coordinator fiber launches three `go` tasks (sleep 30/10/20 → return 100/20/3), awaits all three in
SPAWN order, and sums them. The completion log is the deterministic contract — tasks finish in
DEADLINE order (`task 2@10, task 3@20, task 1@30`), not spawn/await order; `sum: 123`; final virtual
clock 30. Fully reproducible (virtual time, no real clock). Suite GREEN **755/0**.
**Stream B1 is feature-complete.** The pure-sx async runtime exists end-to-end: fibers behind the
`abi(.naked)` context switch (proven on aarch64 + x86_64/Win64), the M:1 cooperative scheduler,
suspending `go`/`wait`/`cancel` async, deterministic virtual-time timers, and real fd-readiness via
kqueue — all in `library/modules/std/sched.sx`, all adversarially reviewed, locked by `18xx`
(18001817). Compiler floor delivered: `abi(.naked)` emission (B1.0) + per-fiber `context` (B1.1,
zero-change). Five compiler bugs fixed en route (0151/0152/0153 in B1.2; 0154 in B1.5a;
0156-Part1 + 0157 in B1.4a). Deferred (documented, non-blocking): issue 0150 (`Future(void)`/`timeout`),
0155 (scalar-pointer index), 0156-Part2 (deferred `..` spread); a linux `epoll` twin of `block_on_fd`;
routing the suspending async through the erased `context.io` (M:N evolution); the heap-Task / closure-env
/ kq-fd leaks (bounded, default-GPA-invisible). Stream B2 (channels/cancel/stdlib) is the next carve.
### Earlier — B1.4c — REAL fd-readiness blocking via kqueue (macOS)
`library/modules/std/sched.sx` now lets a
fiber park on a file descriptor and the run loop block on `kevent` until the kernel reports it ready.
Reuses the existing verified `library/modules/std/net/kqueue.sx` bindings (`Kevent` (32 bytes),
`kqueue`/`kevent`/`kq_apply`/`kq_wait` + the `EVFILT_READ`/`EV_ADD`/`EV_ENABLE`/`EV_ONESHOT`
constants) rather than re-deriving the FFI — sched.sx imports it as `kqb`. Added to `Scheduler`:
- `kq: i32` (LAZY — `-1` in `init`, opened by the first `block_on_fd`, so a pure-compute /
virtual-timer scheduler never opens a kqueue fd; leaks one fd at exit once opened, same class as the
documented spawn-env / go-Task leaks — no deinit yet);
- `io_waiters: List(IoWaiter)` (`IoWaiter :: struct { fd: i32; fiber: *Fiber; }`, grown through
`own_allocator` per the long-lived-container rule);
- `block_on_fd(self, fd, want_read)` — lazily opens `kq`, arms a one-shot `EVFILT_READ` registration,
records an `IoWaiter{fd, current}`, then `suspend_self()`. Guards a null `current` (loud abort, like
`sleep`); `want_read=false` (write-readiness) is not wired yet → loud abort rather than silently
arming a read filter.
- Run-loop: after the ready queue drains, **Mode 1 (virtual time)** fires the earliest pending timer
(takes precedence — a program uses `sleep` OR fds, documented non-unification limitation); **Mode 2
(real fd)** — if `io_waiters` is non-empty, BLOCK on `kq_wait(kq, evbuf, MAXEV=16, -1)` (null
timeout), then for each fired event match `ev.ident` back to its waiter, evict it, and `wake` the
fiber; **else** break. Orphan-deadlock check unchanged in spirit but now correct: an fd waiter is NOT
an orphan (while `io_waiters.len > 0` the loop blocks on kqueue rather than reaching the check), and
a genuine no-timer/no-fd suspend still aborts loudly (verified with a probe: exit 134).
- `wake` now also evicts a pending fd-waiter (`cancel_io_waiter_for`, mirror of `cancel_timer_for`) —
same UAF reasoning: a fiber woken by another path must not leave a stale `IoWaiter` pointing at a
reaped `*Fiber`. The kqueue registration is `EV_ONESHOT` so we never `EV_DELETE` (a never-fired
one-shot lingers harmlessly; the drain ignores an unmatched ident; closing the fd auto-removes it).
- DE-RISK probe (run first, no scheduler): confirmed `size_of(Kevent) == 32`, the pipe roundtrip
(`kq_wait` returned 1 with `out.ident == read_fd`, `out.filter == -1` (EVFILT_READ), `out.data == 1`
byte readable) — the struct layout reads back the fd correctly.
- Locked by `examples/concurrency/1816-concurrency-fiber-io-pipe.sx`: a `pipe`; a reader fiber spawned
FIRST blocks on the empty read end, then a writer fiber writes `a b c` → the run loop blocks on
kqueue, wakes the reader, which reads the 3 bytes. Output `log: wrote read 3 [97 98 99]` /
`n_suspended: 0` (the "wrote" before "read" ordering proves the reader actually blocked then woke via
kqueue readiness). `.build` `{ "target": "macos" }` (matches host arch → runs end-to-end; ir-only on
a mismatch, like 1814/1815 — no `.ir` snapshot needed since it runs here). The example declares its
own `read`/`write`/`close` externs with the CANONICAL signatures std already binds
(`(i32,[*]u8,usize)->isize` / `(i32)->i32`) — a divergent re-binding is rejected by the extern dedupe.
- **Adversarial review (worker) of the run-loop change — found 2 CRITICALs:**
- **(1) two fibers on the SAME fd → lost wakeup + permanent hang.** macOS `EV_ADD` for an existing
`(ident, filter)` REPLACES the registration (doesn't stack), so two waiters share one registration:
the fd fires once, one wakes, the other is stranded in `io_waiters` and the next `kq_wait(-1)` blocks
forever. FIXED: `block_on_fd` now enforces one-waiter-per-fd with a loud abort (the model already
assumed it). Verified: dup-fd → `sched: block_on_fd: fd N already has a waiter`, not a hang.
- **(2) an fd-waiter on a never-ready fd hangs instead of the timer path's loud abort.** Re-examined:
this is CORRECT event-loop semantics — blocking on I/O until ready (possibly forever, like a server
idling on a socket) is the point; the scheduler cannot know an fd will never become ready, so it must
keep waiting. NOT a scheduler deadlock. Fixed the MISLEADING comment that implied the orphan check
covers fd-waiters: it does not, by design (it covers only pure `suspend_self` parks). No code change —
the "hang" is a caller-side logic issue (waiting on input that never arrives), not a bug to abort on.
- Review CLEARED: the IoWaiter UAF parity (early-wake evicts the waiter; a lingering one-shot that later
fires hits no match → clean no-op), ident width/sign, `kq_wait` EINTR/error handling, timer-vs-io
precedence (timer wins; no hang). All probed safe.
- Suite GREEN **754/0** (incl. the dup-fd guard, no new example needed — the abort is host-fragile to
pin like 1809's guard-firing). Next: **B1.5** (end-to-end M:1 validation under the deterministic timers
/ fd readiness); a linux epoll twin of `block_on_fd` (mirror via `std/net/epoll`, the OS-neutral facade
is `std.event`) is future work.
### Earlier — B1.4b — deterministic VIRTUAL-TIME timer scheduling (the KEYSTONE) — landed + adversarially
reviewed (caught a CRITICAL UAF, fixed).
`library/modules/std/sched.sx` gained a virtual clock +
sleep timers so fibers schedule in reproducible simulated time (no real clock): `clock_ms` (advances
ONLY as timers fire), a `timers: List(Timer)` (insertion-order, linear min-scan, FIFO tiebreak),
`now_ms()`, `sleep(ms)` (arm `{clock_ms+ms, current}` + `suspend_self`), and a timer-driven `run`
(drain ready → fire earliest timer → advance clock → wake → repeat; orphan-deadlock check preserved
for a genuine no-timer suspend). Locked by `1814` (5 fibers sleep 30/10/20/15/15 → wake order
B@10, D@15, E@15 (FIFO), C@20, A@30 — deadline order, not spawn order; `now_ms()` reads each virtual
deadline; final clock 30). §8.1.3 calibration note in the header: the deterministic wake ORDER
equals what real `sleep`s produce, reproducing blocking semantics' observable ordering without real
time. The deterministic-sim `Io` is realized at the scheduler level (`sleep`/`now_ms`/timer-`run`),
not as an erased `Io`-protocol impl (same erasure reason as FiberIo).
- **Adversarial review (worker) of the run-loop change: found a CRITICAL use-after-free** — a fiber
that armed a `sleep` timer but was woken EARLY by another path (a manual/`Task` `wake`) ran to
completion + was reaped (stack `munmap`'d, `Fiber` freed) while its `Timer` still held a dangling
`*Fiber`; a later fire would `wake` freed memory (silent-corruption: "passes" only because the
freed slot coincidentally read `state != .suspended`). FIXED: `wake` now evicts the woken fiber's
pending timer (`cancel_timer_for`) — every re-ready path funnels through `wake` (the timer-fire in
`run` already removed the fired timer, so it's a harmless re-scan there), so no stale timer can
outlive its fiber. Regression `1815-concurrency-fiber-timer-early-wake.sx` (early wake → `clock: 0`,
the stale 100ms timer evicted, not fired). Review CLEARED: `n_suspended` accounting,
orphan-deadlock false-positives, timer-list integrity (re-arm during fire), clock monotonicity,
termination — all traced/probed safe.
- Suite GREEN (count below). Next: **B1.4c** (event-loop `Io` — real fd readiness, kqueue/epoll).
### Earlier — B1.4a — a truly-SUSPENDING fiber-task async layer (`go`/`wait`/`cancel`)
landed + adversarially reviewed; cleared two more compiler blockers en route. `library/modules/std/sched.sx`
now carries `Task($R)` + `Scheduler.go(work) -> *Task($R)` + `wait`/`cancel` (a `ufcs` layer over
the M:1 scheduler). `s.go(work)` runs the nullary thunk `work` as a REAL fiber; `t.wait()` SUSPENDS
the caller until it completes (vs io.sx's blocking `context.io.async`, which runs inline). Locked by
`examples/concurrency/1813-concurrency-fiber-async-suspend.sx`: two tasks interleave (A yields
mid-body so B runs first → `1 2 3`), awaited values `42`/`100`, and a canceled task's `wait` raises
`.Canceled` → `or -99` → `sequence: 1 2 3 42 100 -99`.
- **Design: a NULLARY thunk, not `async(worker, ..args)`.** A comptime variadic pack can't cross a
deferred (fiber) boundary — `..args` captured into a closure re-expands from the spawner's
now-gone locals (issue 0156 Part 2). So `go` takes `work: Closure() -> $R`; the user captures
inputs in the lambda at the call site (the `go func(){…}()` idiom). **Self-contained in sched.sx**
(NOT io.sx): io.sx importing sched.sx duplicates the `_fib_tramp` global asm when a program also
imports sched.sx directly (global asm emits per import-path) — so the Io-protocol
`spawn_raw`/`suspend_raw`/`ready` hooks stay reserved for the future M:N model; M:1 uses
`go`/`wait` directly. Heap `*Task` (must outlive `go`'s frame; leak documented). `TaskErr` is
LOCAL (the `!` failable detection doesn't see through io.sx's `IoErr` re-export alias).
- **Two compiler blockers hit + FIXED (user-authorized in-session):**
- **issue 0156 Part 1** — a single-type generic `$R` (parsed as `comptime_pack_ref`) used as a
type-arg (`Box($R)`, `size_of(Box($R))`) inside a pack-fn body hit a missing arm in
`resolveTypeWithBindings` → `.unresolved` → LLVM panic. Fix: mirror `resolveTypeArg`'s
`comptime_pack_ref` arm (look up `type_bindings`, else a loud diagnostic). Regression
`examples/generics/0216-generics-typearg-in-pack-fn-body.sx`. (Part 2 — deferred `..` spread
crashes — reframed OPEN/non-blocking, `issues/0156`.)
- **issue 0157** — a user generic `ufcs` method whose name collides with a stdlib re-export
(`cancel` on `*Task` vs io.sx's `cancel` on `*Future`) resolved via last-wins `fn_ast_map` with
NO receiver filtering → wrong overload → `$R` unbound → LLVM panic. Fix
(`src/ir/lower/call.zig` `selectUfcsGenericByReceiver`): every generic-ufcs dispatch enumerates
ALL module authors (`module_decls`), keeps receiver-binding ones, picks the most
receiver-SPECIFIC (concrete > bare `$T`), dedups re-exports, and flags a genuine 2-specific tie
as a deterministic "ambiguous — qualify" diagnostic (never a silent order-dependent pick).
Regression `examples/generics/0217-generics-ufcs-method-name-collides-stdlib.sx`.
- **Adversarial review (worker) of the 0157 fix + Task layer.** Caught the determinism CRITICAL
(fixed: always-run selection + specificity + ambiguity), `wait`-outside-a-fiber null-deref (fixed:
loud guard in `suspend_self`/`yield_now`), and cancel-doesn't-skip-work (fixed: worker skips
`work()` if already canceled). Lost-wakeup / cancel-after-complete / reap traced safe. Also
simplified `1812` (`**Fiber` shared handle → a `Sh.parked` field; output identical).
- Suite GREEN 751/0 (749 + 1813 + 0217). Next: **B1.4b** (deterministic-sim `Io`).
### Earlier — B1.5a — the M:1 cooperative fiber scheduler CORE — landed + adversarially reviewed
The hand-bootstrapped ping-pong (1807-1810) is now a reusable scheduler API in pure sx:
`library/modules/std/sched.sx` — a generic `Fiber` (`body: Closure() -> void`) + `Scheduler`
with `init`/`spawn`/`yield_now`/`suspend_self`/`wake`/`run` over the proven `swap_context` on
guarded `mmap` stacks. The ONE generic dispatch (`fib_dispatch`, reached from the `_fib_tramp`
trampoline) runs ANY stored closure body on a fresh stack — replacing the fixed `bl _fib_body`.
Reaping `munmap`s the stack + frees the heap `Fiber` on completion; an intrusive FIFO gives
round-robin order.
- **Foundational design de-risked by probe before building:** a fiber can store + call a
`Closure() -> void` on its fresh stack via the generic dispatch; outputs flow OUT through
pointers captured in the closure (capture-by-value does NOT write back — pushed onto the user).
- **Hit + FIXED a blocker compiler bug — issue 0154** (user-authorized in-session fix). `null` /
`---` assigned to a struct field picked up a leaked enclosing `target_type` (the function's
RETURN type, set for the whole body at decl.zig:2691) and built a WHOLE-STRUCT-typed null →
an oversized `zeroinitializer` store through the field's GEP that overran the field's slot and
clobbered the saved x29/x30, so the fn `ret`'d to 0x0. This was EXACTLY the `Scheduler.init()`
by-value-return shape (`sched_ctx: [13]u64` before `current: *Fiber`). Fix: added
`.null_literal, .undef_literal` to the `needs_target` switch in `lowerAssignment`
(`src/ir/lower/stmt.zig`) so the field's type is used. Repro → regression test
`examples/types/0193-types-sret-array-before-pointer.sx`; `issues/0154-*.md` RESOLVED.
- **Adversarial review (worker): asm/bootstrap/lifetime SOUND** (the headline closure-env-lifetime
fear was disproven — envs are heap-promoted, survive the spawn frame). Found **1 CRITICAL** +
robustness gaps, ALL hardened: (CRITICAL) `wake` re-enqueued an already-queued fiber →
FIFO corruption/segfault → now GUARDED on `.suspended` (spurious/double/stale wake = safe
no-op); orphan-suspend leak/deadlock → `n_suspended` accounting + a loud `run()`-drain
diagnostic+abort; `mmap` `MAP_FAILED` (=-1, not null) / `mprotect` / Fiber-OOM → loud bails
(per §8.1.1 the guard is mandatory); the per-fiber closure-env leak (sx exposes no env-free) →
documented as a KNOWN LIMITATION (bounded by spawn count; invisible under the default GPA).
- **Locked two `18xx` examples** (aarch64-macos `.build`-pinned, ir-only on a mismatch):
`1811-concurrency-fiber-scheduler.sx` (3 fibers round-robin via `yield_now` → ordering contract
`sequence: 0 1 2 0 1 2 0 1 2`, all `.done`) + `1812-concurrency-fiber-suspend-wake.sx` (park via
`suspend_self`, resumed by another fiber's `wake`, + the spurious-wake no-op — the CRITICAL-fix
regression → `log: 10 20 21 11` / `suspended-left: 0`).
- **Filed issue 0155 (NON-blocking, NOT fixed)** — found incidentally in the review: indexing a
scalar pointer (`pc[0]`, `pc: *i64`) panics codegen (`.unresolved` reaching LLVM emission). The
scheduler uses array-field indexing + `.*`, never this, so it's filed for its own session.
- Suite GREEN **748/0** (746 base + 1811 + 1812 + 0193 regression). Next: **B1.4a** (FiberIo —
wire `Io.spawn_raw`/`suspend_raw`/`ready` onto the scheduler so `async`/`await` truly suspend).
### Earlier — B1.3b-1 — the x86_64 / Win64 `swap_context` sibling — VALIDATED on real hardware
The
context switch is now proven on a SECOND architecture + ABI. A Win64 `swap_context` saves the context switch is now proven on a SECOND architecture + ABI. A Win64 `swap_context` saves the
COMPLETE Win64 callee-saved set — 8 GP (rbx, rbp, rdi, rsi, r12-r15) + rsp **and xmm6-xmm15** COMPLETE Win64 callee-saved set — 8 GP (rbx, rbp, rdi, rsi, r12-r15) + rsp **and xmm6-xmm15**
(10 XMM, 128-bit via `movups` — Win64 has callee-saved XMM, unlike SysV/aarch64) — plus a Win64 (10 XMM, 128-bit via `movups` — Win64 has callee-saved XMM, unlike SysV/aarch64) — plus a Win64
@@ -178,7 +413,46 @@ body); closed + locked. The review's `.naked`-lambda CRITICAL was a false positi
(unparseable — `isLambda` breaks on the `abi` keyword). (unparseable — `isLambda` breaks on the `abi` keyword).
## Current state ## Current state
**B1.2 COMPLETE.** The full async surface (Io capability on Context + `async`/`await`/`cancel` + **STREAM B1 FEATURE-COMPLETE.** `library/modules/std/sched.sx` is the whole pure-sx M:1 async runtime:
the scheduler core (B1.5a: `spawn`/`yield_now`/`suspend_self`/`wake`/`run`), suspending fiber-task
async (B1.4a: `Task($R)`/`go`/`wait`/`cancel`), deterministic virtual-time timers (B1.4b:
`clock_ms`/`now_ms`/`sleep`, timer-driven `run`), AND real fd readiness via kqueue (B1.4c: lazy `kq`,
`io_waiters`, `block_on_fd`, run-loop Mode 2) — all over the `abi(.naked)` `swap_context` on guarded
`mmap` stacks (B1.0B1.3), reusing `std/net/kqueue.sx`. Every park path (timer sleep, fd block, raw
suspend) is balanced through `wake` (which evicts stale timer + fd waiters — the UAF guards). A terminal
`deinit` (B1 follow-up) closes the previously-documented leaks: heap `Task`s (tracked via `task_allocs`),
the `timers`/`io_waiters`/`task_allocs` List backings, and the kqueue fd; the per-`spawn`/`go` closure
env remains unfreeable (language limitation). Locked by `18xx` 18001820 (naked-asm, context-snapshot,
blocking async, the switch + §10.7 stress gate + guarded stacks + Win64 sibling, scheduler round-robin,
suspend/wake, async go/wait/cancel, sim-timer ordering, timer early-wake eviction, kqueue pipe I/O, the
**1817 end-to-end capstone**, sleep-negative/double-wait guards, and **1820 scheduler-deinit**). Suite
GREEN **817/0**, committed. **B1.6: now also runs on aarch64-linux** (epoll fd-backend + comptime-branched
`MAP_AP` + naked-fn trampoline) — validated byte-identical to macOS in an Apple `container`.
Future work (none blocking B1): routing the suspending async through
the erased `context.io` (forces sched.sx into every std consumer — deferred to the M:N model, where
the `Io` protocol's `spawn_raw`/`suspend_raw`/`ready`/
`arm_timer`/`poll` hooks take over); `Future(void)`/`timeout` (issue 0150); freeing the heap-Task /
closure-env / kq-fd (a Scheduler `deinit` + closure-env-ownership affordance). **Next carve: Stream
B2** (channels / structured cancel / async stdlib) — see PLAN-CHANNELS.md when started.
### Earlier — B1.5a COMPLETE — the M:1 scheduler CORE exists
`library/modules/std/sched.sx` drives N fibers
(generic `Closure() -> void` bodies) cooperatively over the proven `swap_context`, on guarded
`mmap` stacks: `spawn` / `yield_now` (round-robin) / `suspend_self` + `wake` (off-queue park/resume)
/ `run` (drives to drain, reaps on `.done`). Adversarially reviewed + hardened (wake guarded, loud
mmap/mprotect/OOM/deadlock bails, env-leak documented). Locked by `1811` (round-robin ordering
contract) + `1812` (suspend/wake park-resume + spurious-wake guard). Suite GREEN **748/0**.
The remaining B1.4 work wires this scheduler under the `Io` capability: **B1.4a (FiberIo)** makes
`context.io` route `spawn_raw`/`suspend_raw`/`ready` onto the `Scheduler` so `async`/`await` truly
SUSPEND (today's `CBlockingIo` runs the worker to completion inline); **B1.4b** the deterministic-sim
`Io` (virtual clock + timer queue, calibrated against blocking — the KEYSTONE test harness);
**B1.4c** the event-loop `Io` (kqueue/epoll). Then **B1.5** is the end-to-end M:1 validation under
the deterministic `Io`.
### Earlier — B1.2 COMPLETE
The full async surface (Io capability on Context + `async`/`await`/`cancel` +
blocking `CBlockingIo`) works end-to-end. Master GREEN (732/0), installed `sx` clean. All four blocking `CBlockingIo`) works end-to-end. Master GREEN (732/0), installed `sx` clean. All four
B1.2 surface bugs resolved or deferred: B1.2 surface bugs resolved or deferred:
- **0151 fixed** (`362674f`): generic `$T` through generic-struct / pointer / UFCS-pack params. - **0151 fixed** (`362674f`): generic `$T` through generic-struct / pointer / UFCS-pack params.
@@ -252,13 +526,77 @@ fibers/Io/scheduler code yet. Grounded floor facts:
boundary; a sharper sx diagnostic for it is a candidate polish, not a blocker. boundary; a sharper sx diagnostic for it is a candidate polish, not a blocker.
## Next step ## Next step
**→ B1.4 — `Io` impls / the scheduler.** The switch substrate is proven on TWO arch/ABI pairs **Stream B1 is COMPLETE — no next step in this stream.** The pure-sx M:1 async runtime is feature-
(aarch64 native + x86_64/Win64 on the VM), with the §10.7 stress gate, guarded mmap stacks, and complete and committed (18001820 green, 759/0), now WITH a `Scheduler.deinit` closing the bounded
adversarial review. That's enough to build the scheduler on. B1.4 builds the deterministic-sim leaks. Pick up **Stream B2** (channels / structured cancel / async stdlib) as a fresh carve
`Io` (calibrated against blocking `Io` before trusting it — §8.1.3), then **B1.5** (M:1 scheduler) (PLAN-CHANNELS.md), OR one of the remaining non-blocking follow-ups: the linux `epoll` twin of
replaces the hand-bootstrapped ping-pong with real `spawn`/`yield`/`resume` over the switch. The `block_on_fd`, `Future(void)`/`timeout` (needs issue 0150), or routing the suspending async through the
§10.7 gate (1808) + guarded-stack path (1809) + the Win64 sibling (1810) must keep passing as the erased `context.io` for the M:N model. (`Scheduler.deinit` — DONE, see Last completed step.) None of
switch is wrapped into the scheduler. these block B1. The closure-env leak survives `deinit` (no language affordance to free a closure env);
revisit if/when sx grows closure-env ownership.
**Deferred (future B1.4c sibling): the linux epoll twin of `block_on_fd`.** B1.4c wired the **macOS
kqueue** path only (the host is aarch64-macOS). The linux mirror would register interest via
`std/net/epoll` and the OS-neutral facade is `std.event` — keep the two as separate run modes inside
`run`, branching on the platform, exactly as the timer-vs-fd modes are kept separate now. Documented
non-unification: virtual-time timers and real kqueue timeouts are NOT merged — `run` fires a pending
timer before ever blocking on kqueue (a program uses `sleep` OR fds); a true "fd-or-real-timeout" wants
a kqueue `EVFILT_TIMER`, future work.
> **▶ LINUX EPOLL — in progress (2026-06-26), via `std.event.Loop` (the OS-neutral facade).**
> Chosen over the sched.sx `block_on_fd` twin because the facade is the named home for epoll, is pure
> sx + libc (zero compiler change), is consumed by http.sx, and has a runnable darwin sibling. Landed:
> (A) **`library/modules/std/net/epoll.sx`** — raw bindings, the linux twin of `std/net/kqueue.sx`.
> `epoll_event` is modelled as an **arch-branched struct** (`{events, data_lo, data_hi}` u32 fields →
> 12 B x86_64 packed / 16 B aarch64), so layout is byte-exact with NO packed attribute, NO unaligned
> access, NO scalar-pointer indexing (issue 0155) — the struct-per-arch approach the user flagged as
> better than raw byte poking. Self-contained (libc only — NO build.sx import; the top-level `inline if
> ARCH` resolves via the compiler's flatten pre-pass, keeping the IR small). Locked by
> `examples/event/1633-event-epoll-bindings-linux.sx` (ir-only x86_64-linux, durable 244-line .ir;
> aarch64 16 B layout also probe-verified). (B) **`std.event.Loop` branched on `inline if OS`** into two
> top-level OS-selected structs (sx has no conditional struct fields): the kqueue Loop unchanged
> (darwin, runs — 1632 green), a new epoll Loop (linux) with the per-fd registration table (combined
> EPOLLIN/OUT mask via ADD/MOD/DEL), eventfd wake channel, and EPOLLRDHUP→eof. **RUNTIME-VALIDATED on
> real Linux:** a static `aarch64-linux` build of the 1632-equivalent Loop test (+ the eventfd wake path)
> ran **6/6 green inside an Apple `container` Linux VM** (kernel 6.18 aarch64) — add_read, idle-timeout,
> readable+fd+udata, the MOD-mask add_write path, the eventfd wake channel, and EPOLLRDHUP/HUP eof all
> behave identically to kqueue (lone documented difference: `nbytes` is 0 on epoll). Also lowers clean for
> both linux arches; the ABI is corpus-locked by 1633. NOT corpus-snapshotted (the corpus runner is
> host-based, not container-aware; a Loop example drags the std barrel → ~18k-line brittle IR).
> **The epoll deliverable is COMPLETE.** Re-validation recipe in the event.sx VALIDATION note. Optional
> follow-on: route sched.sx `block_on_fd` through `std.event` (still needs the linux sched.sx port — mmap
> consts, tramp symbol, errno, x86_64 SysV switch).
> **✅ issue 0192 FIXED (2026-06-26) — epoll work UNBLOCKED.** A qualified-import-member const
> (`m.EV_SIZE`) now folds as a compile-time constant in every position the bare/flat form does
> (array dim, arithmetic, Vector lane, generic value-param, inline-for) — so the clean
> `[MAXEV * ep.EV_SIZE]u8` event buffer the bindings want will work. Fix: a `lookupQualifiedConst`
> ctx hook resolving the namespace alias → target module's per-source const, wired into the int/float
> const folders (`src/ir/program_index.zig` + `src/ir/lower/comptime.zig`). Regression:
> `examples/modules/0842-modules-qualified-import-const-comptime.sx`. The hint stands for the rebuild:
> **a struct-per-arch `EpollEvent` (arch-branched u32 fields, 12 B x86_64 / 16 B aarch64) beat raw
> byte access** — idiomatic field reads, no issue-0155 scalar-pointer indexing, no unaligned u64.
> Resume: rebuild `std/net/epoll.sx`, branch `std.event.Loop` on `inline if OS`, lock with a darwin run
> + ir-only linux example.
> **⛔ (HISTORICAL) BLOCKED on issue 0192 (filed 2026-06-26).** Started the epoll work: chose the `std.event.Loop`
> backend (pure sx + libc externs, zero compiler change — per "do this in sx as much as possible") as
> the first deliverable, since event.sx already names epoll as its linux backend and it's runnable
> (darwin via kqueue) + ir-only-verifiable (linux). De-risked four landmines by probe — arch-dependent
> layout const via module-scope `inline if ARCH` (folds + validates in linux IR), slice-based byte access
> (sidesteps issue 0155), no unaligned u64 (store the 32-bit fd in epoll `data`), and comptime-dead linux
> externs don't break the darwin corpus (just an unreferenced `declare`). Then hit a compiler bug while
> sizing the event buffer: a **qualified-import-member const is not a compile-time constant** —
> `[m.CAP]u8` / `A :: m.CAP` fail (a *flat*-imported const works). Root cause located:
> `evalConstIntExpr` (`src/ir/program_index.zig:325`) has no namespace-member-const arm. Per the STOP
> rule the half-built `std/net/epoll.sx` (which used a struct-based layout to route around the bug) was
> **removed**, not landed — the unblock session rebuilds it cleanly with the fix in hand. Repro +
> investigation prompt: `issues/0192-qualified-import-const-not-comptime.{md,sx}`.
Design note carried forward: an event-loop `Io` needs a current-`Scheduler` handle. `sched.*` methods
thread it via `self`/the `Task`; if B1.4c wants the capability-threaded `context.io` form it'll need
an ambient current-scheduler accessor in sched.sx (still deferred — the `sched.*`-method form
suffices). The `Io` protocol's `poll`/`arm_timer` map onto this when/if that wiring is built.
**Side thread (optional, low priority): the SysV/Linux x86_64 sibling.** A THIRD switch variant **Side thread (optional, low priority): the SysV/Linux x86_64 sibling.** A THIRD switch variant
for `x86_64-linux`: SysV callee-saved = rbx, rbp, r12-r15 + rsp (6 GP + sp; **no** callee-saved for `x86_64-linux`: SysV callee-saved = rbx, rbp, r12-r15 + rsp (6 GP + sp; **no** callee-saved
@@ -275,6 +613,45 @@ incomplete); a dedicated effort; lambda workers are the idiom meanwhile.
`call.zig:1229`, io last). Io protocol + materializers + push-inherit are LANDED + reviewed. `call.zig:1229`, io last). Io protocol + materializers + push-inherit are LANDED + reviewed.
## Known issues / capability gaps ## Known issues / capability gaps
- **issue 0157 (OPEN, BLOCKING B1.4a)** — a user-defined generic ufcs method whose NAME collides
with a stdlib re-export (`cancel`, re-exported by `std.sx` from `io.sx` as `ufcs (f: *Future($R))`),
called via UFCS on a different generic struct (`*Task($R)`), leaves `$R` unresolved → `.unresolved`
reaches LLVM emission → panic (`src/backend/llvm/types.zig:196`). Renaming → works; the non-UFCS
call form already diagnoses `cannot infer generic type parameter 'R'`, so the UFCS path skips that
diagnostic. Surfaced by `cancel :: ufcs (t: *Task($R))` in `std/sched.sx`. Minimal repro (no
fibers/closures): `issues/0157-ufcs-generic-method-name-collides-stdlib-unresolved.{md,sx}`.
- **✅ issue 0154 — FIXED** (`null`/`---` to a struct field over-stored a whole-struct null when
the function's return type leaked as `target_type`, corrupting the frame → `ret` to 0x0;
surfaced building `Scheduler.init()`'s by-value return). Fix: `.null_literal`/`.undef_literal`
added to `needs_target` in `lowerAssignment` (`src/ir/lower/stmt.zig`). Regression:
`examples/types/0193`.
- **issue 0155 (OPEN, NON-blocking)** — indexing a scalar pointer (`pc[0]`, `pc: *i64`) panics
codegen (`.unresolved` reaching LLVM emission, `src/backend/llvm/types.zig:196`). Found in the
B1.5a review; the scheduler doesn't use it (array-field index + `.*` only). Filed for its own
session: `issues/0155-scalar-pointer-index-llvm-panic.{md,sx}`.
- **✅ issue 0158 — FIXED** — a plain `union` struct-literal (`b : Overlay = .{ f = 3.14 }`) fell
through the generic struct-literal path (`getStructFields` empty for a union → malformed
`structInit`, overlapping zero-fill clobbered the member → silent `0.0`). Fix: `lowerStructLiteral`
detects a plain-union target → new `lowerUnionLiteral` (`src/ir/lower/stmt.zig`) writes each named
member into a union-sized slot via the assignment-path lvalue resolver, then loads it back.
Single-arm only (one direct member, or same-arm promoted members); overlapping/different-arm/
positional literals are diagnosed. specs.md updated. Regressions: `examples/types/0194` +
`examples/diagnostics/1191`.
- **✅ issue 0157 — FIXED** (B1.4a) — a user generic `ufcs` method whose name collides with a
stdlib re-export resolved via last-wins `fn_ast_map` with no receiver filtering → wrong overload →
`$R` unbound → LLVM panic. Fix: `selectUfcsGenericByReceiver` (`src/ir/lower/call.zig`) — most
receiver-specific binding author across ALL module authors, deterministic, ambiguity-diagnosing.
Regression: `examples/generics/0217`.
- **✅ issue 0156 Part 1 — FIXED** (B1.4a) — single-type generic `$R` as a type-arg in a pack-fn
body (`Box($R)`/`size_of(Box($R))`) → `.unresolved` → panic. Fix: `comptime_pack_ref` arm in
`resolveTypeWithBindings`. Regression: `examples/generics/0216`.
- **Part 2 (OPEN, NON-blocking)** — a deferred `..` spread (a comptime pack captured into a
closure, or a tuple `..t` spread) crashes instead of working/diagnosing. The fiber async layer
avoids it by design (nullary thunks), so it's filed for its own session: `issues/0156`.
- **Heap leaks in the fiber runtime (documented limitations, NOT bugs):** `spawn`'s closure env +
`go`'s heap `Task` are never freed (sx exposes no closure-env free; Task ownership is deferred).
Bounded by spawn/go count, invisible under the default GPA. Revisit for a long-running
arena-backed scheduler.
- **✅ issue 0153 — FIXED** (re-exported generic value-failable `($R, !E)` kept its `!` channel: - **✅ issue 0153 — FIXED** (re-exported generic value-failable `($R, !E)` kept its `!` channel:
`inferGenericReturnType` now pins return-type resolution to the fn's defining module). `inferGenericReturnType` now pins return-type resolution to the fn's defining module).
Regression: `examples/1058`. Was the LAST B1.2 surface blocker. Regression: `examples/1058`. Was the LAST B1.2 surface blocker.
@@ -332,6 +709,45 @@ incomplete); a dedicated effort; lambda workers are the idiom meanwhile.
trusted. `18xx` asserts program-emitted ordering contracts, not raw interleaving. trusted. `18xx` asserts program-emitted ordering contracts, not raw interleaving.
## Log ## Log
- **B1.6 — aarch64-linux port of sched.sx.** Comptime-branched the per-OS bits: `MAP_AP` (linux
`0x22` / macOS `0x1002`), the fd-readiness backend (epoll on linux, kqueue on darwin — epoll import
scoped to the linux branch; `block_on_fd` / run-loop Mode-2 / `cancel_io_waiter_for` each branch,
epoll `EPOLL_CTL_DEL`s on fire + early-wake), and the first-entry trampoline (per-OS global-asm
symbol → naked sx fn `fib_tramp` + register-indirect `br x20` to `&fib_dispatch` preset in
`regs[1]`). **Fixed issue 0193 Bug A:** the tramp redesign bus-errored on 1817 (both OSes) until
`export "fib_dispatch"` was restored — without it the fn uses sx's internal ABI (x0 = implicit
`context`, `self` → x1) while the trampoline supplies `self` in x0, so the closure loads
`regs[1] == &fib_dispatch` as its first capture and recurses forever → stack-overflow bus error.
Root cause found via lldb (AOT macOS build) + an adversarial source review. **Bug B** (wrapped
top-level `asm` dropped) carved to **issue 0194** (OPEN; no live trigger — the naked-fn tramp
sidesteps it). Validated byte-identical on aarch64-macOS host AND aarch64-linux Apple `container`
for 1811/1814/1816/1817; full suite GREEN **817/0**.
- **B1 follow-up — `Scheduler.deinit`.** Closes the bounded leaks B1 documented. Added a `task_allocs:
List(*void)` field (appended in `go` so the scheduler can reach its generic `Task($R)`s) + a canonical
`close` extern, then a terminal idempotent `deinit`: reap leftover ready fibers (`munmap` + free) →
free tracked Tasks → `List.deinit` the 3 backings → `close` the lazy kqueue fd (reset `-1`). Closure
envs stay unfreeable (documented). Probe-observed the accounting under a tracking GPA (deinit drives
live allocs 7→3 in a spawn+sleep+2×go run; residual = envs). Locked by
`1820-concurrency-fiber-scheduler-deinit.sx` (one run hits timers + kqueue fd + Tasks; `freed by
deinit: 5`, `live after deinit: 5` (env residual), `kq open after run: true`→`kq after deinit: -1`,
`read: 3 [97 98 99]`), `.build {"target":"macos"}`. Adversarial review: no real UAF/over-free in the
supported deinit-after-`run` path; reconciled a doc contradiction (terminal-contract wording); 0154
over-store concern probed + cleared (`kq == -1` right after `init`). Suite GREEN **759/0**.
- **B1.4c — real fd-readiness blocking via kqueue (macOS).** De-risked first with a no-scheduler probe
(confirmed `size_of(Kevent)==32` and the pipe→kevent roundtrip: `kq_wait` returned 1, `out.ident ==
read_fd`, `out.filter == -1`, `out.data == 1` — the struct layout reads the fd back correctly). Then
added to `library/modules/std/sched.sx` (importing the existing verified `std/net/kqueue.sx` as `kqb`
rather than re-deriving the FFI): a lazy `kq: i32` (-1 until first use), `io_waiters: List(IoWaiter)`,
`block_on_fd(fd, want_read)` (arm one-shot `EVFILT_READ`, record waiter, `suspend_self`), a run-loop
Mode 2 (block on `kq_wait(kq, evbuf, MAXEV=16, -1)` when only fd waiters remain, wake the fiber whose
fd fired), and `wake` now also evicts a stale fd-waiter (`cancel_io_waiter_for`, the same UAF guard as
`cancel_timer_for`). Timers keep precedence over fds (documented non-unification). Orphan-deadlock
check still fires for a genuine no-timer/no-fd suspend (probed: exit 134). Locked by
`1816-concurrency-fiber-io-pipe.sx` (reader blocks on empty pipe → writer writes `a b c` → kqueue
wakes reader → reads 3 bytes; `log: wrote read 3 [97 98 99]`, `n_suspended: 0`), `.build`
`{ "target": "macos" }`, runs end-to-end on host. The example's `read`/`write`/`close` externs use the
canonical signatures std already binds (extern-dedupe rejects a divergent re-binding). Suite GREEN
**754/0**. Next: B1.5 (end-to-end M:1 validation); linux epoll twin deferred.
- **carve** — wrote PLAN-FIBERS.md + CHECKPOINT-FIBERS.md. Grounded the B1 compiler floor: - **carve** — wrote PLAN-FIBERS.md + CHECKPOINT-FIBERS.md. Grounded the B1 compiler floor:
`ABI.naked` inert (type_resolver.zig:237), IR `Function` has no naked flag (inst.zig:605), `ABI.naked` inert (type_resolver.zig:237), IR `Function` has no naked flag (inst.zig:605),
attribute API pattern (emit_llvm.zig:1339 nounwind), `.c` ctx-skip precedent attribute API pattern (emit_llvm.zig:1339 nounwind), `.c` ctx-skip precedent
@@ -476,3 +892,79 @@ incomplete); a dedicated effort; lambda workers are the idiom meanwhile.
The B1.3 context switch is now proven on TWO arch/ABI pairs. Next: **B1.4** (Io impls / M:1 The B1.3 context switch is now proven on TWO arch/ABI pairs. Next: **B1.4** (Io impls / M:1
scheduler) on the proven substrate. (Side thread: the SysV/Linux x86_64 sibling, when a Linux scheduler) on the proven substrate. (Side thread: the SysV/Linux x86_64 sibling, when a Linux
x86_64 host is available.) x86_64 host is available.)
- **B1.5a — M:1 scheduler CORE + a fixed blocker bug.** Built `library/modules/std/sched.sx`: a
generic `Fiber`/`Scheduler` over `swap_context` on guarded `mmap` stacks. `spawn` heap-allocs a
fiber, bootstraps its ctx, enqueues it; the ONE generic dispatch (`fib_dispatch` via `_fib_tramp`)
runs ANY stored `Closure() -> void` on a fresh stack (replacing the fixed `bl _fib_body`);
`yield_now` round-robins, `suspend_self`/`wake` park/resume off-queue, `run` drives to drain +
reaps `.done` fibers (`munmap` + free). **De-risked first by probe** (closure-on-fiber + output
via captured pointer). **Hit blocker bug 0154** (user-authorized fix): `null`/`---` to a struct
field over-stored a whole-struct null when the fn return type leaked as `target_type`, corrupting
the frame (`ret` 0x0) — exactly the `Scheduler.init()` by-value-return shape. Fixed in `stmt.zig`
(`needs_target` += `null`/`undef` literals); regression `examples/types/0193`; `0154` RESOLVED.
**Adversarial review:** asm/bootstrap/lifetime sound (env-lifetime fear disproven — heap-promoted);
1 CRITICAL (`wake` re-enqueue → FIFO segfault) + robustness gaps ALL hardened (wake guarded on
`.suspended`, `n_suspended` deadlock diagnostic+abort, loud mmap/mprotect/OOM bails, env-leak
documented). Locked `1811` (round-robin `0 1 2 ×3`) + `1812` (suspend/wake + spurious-wake guard,
`log: 10 20 21 11`). Filed NON-blocking `0155` (scalar-pointer index panics codegen — review
incidental, unused by sched). Suite GREEN **748/0**. Next: **B1.4a** (FiberIo).
- **B1.4a (truly-suspending fiber-task async, nullary-thunk design) — BLOCKED on issue 0157.**
Implemented the async layer SELF-CONTAINED in `library/modules/std/sched.sx` (kept its lone
`#import "modules/std.sx"` to avoid the duplicate-`_fib_tramp` trap): `TaskState`, a LOCAL
`TaskErr :: error { Canceled }` (the re-exported `IoErr` alias is NOT seen through by the
`raise`/failable-type check — verified), `Task($R)`, and `go`/`wait`/`cancel` ufcs. Design is
the validated nullary-thunk (`.sx-tmp/pnullary.sx` → `log: 1 2 3 42 100`): `work` is a
`Closure() -> $R`, user captures inputs at the call site, NO `..args` crosses the fiber boundary
(deliberately sidesteps 0156). `go`+`wait` run correctly; both wake-orderings traced. Wrote the
example `examples/concurrency/1813-concurrency-fiber-async-suspend.sx` (+ `{ "target": "macos" }`
`.build`) but its `cancel` ufcs surfaced a NEW compiler bug — issue **0157**: a user generic
ufcs whose name collides with a stdlib re-export (`cancel` from io.sx) is mis-resolved on UFCS
call over a different generic struct, leaving `$R` unresolved → LLVM panic. Bisected to a minimal
no-fiber repro (name is the sole trigger; non-UFCS form diagnoses correctly). Example NOT seeded
into the corpus (no `.exit` marker) — do NOT regen its goldens until 0157 lands. Per the STOP
rule: filed `issues/0157-*.{md,sx}`, marked state BLOCKED, paused.
- **B1.4a COMPLETE (this session) — suspending fiber-task async + two compiler fixes.** Built the
`Task($R)` + `go`/`wait`/`cancel` layer in `sched.sx` (nullary-thunk design; self-contained to
avoid the `_fib_tramp` duplicate-symbol trap). Locked `1813` (`sequence: 1 2 3 42 100 -99`).
FIXED the two blockers the worker had filed: **0156 Part 1** (`comptime_pack_ref` arm in
`resolveTypeWithBindings`; regression `0216`) and **0157** (receiver-driven UFCS overload
selection `selectUfcsGenericByReceiver`; regression `0217`). Adversarial review of the 0157 fix +
Task layer found a determinism CRITICAL (always-run selection + specificity + ambiguity
diagnostic), a `wait`-outside-fiber null-deref (loud guard), and cancel-not-skipping-work (skip
if pre-canceled) — all fixed. Simplified `1812` (`**Fiber` → `Sh.parked`). 0156 Part 2 reframed
OPEN/non-blocking. Suite GREEN **751/0**. Next: B1.4b (deterministic-sim `Io`, the KEYSTONE).
- **B1.4b COMPLETE (this session) — deterministic virtual-time timers + a CRITICAL UAF fix.** Added
`clock_ms`/`timers`/`now_ms`/`sleep` + a timer-driven `run` to `sched.sx` (worker-built): fibers
sleep in reproducible simulated time, waking in deadline order (FIFO tiebreak). Locked `1814`
(5 fibers, wake order B@10/D@15/E@15/C@20/A@30). Adversarial review of the run-loop change found a
CRITICAL use-after-free — a fiber woken EARLY (manual/Task `wake`) before its `sleep` timer fired
was reaped while its `Timer` kept a dangling `*Fiber`; a later fire dereferenced freed memory
(silent "pass" only by luck). Fixed: `wake` evicts the fiber's pending timer (`cancel_timer_for`);
regression `1815` (early wake → `clock: 0`, stale timer never fires). Review cleared n_suspended
accounting, deadlock false-positives, timer-list integrity, clock monotonicity, termination.
Suite GREEN **753/0**. Next: B1.4c (event-loop `Io`, kqueue/epoll).
- **B1.4c COMPLETE (this session) — real fd readiness via kqueue + 2 CRITICAL review fixes.** Added a
lazy `kq` + `io_waiters` + `block_on_fd` + a kqueue-blocking run-loop Mode 2 to `sched.sx`
(worker-built, reusing `std/net/kqueue.sx`). Adversarial review found two CRITICALs: same-fd
lost-wakeup hang (FIXED — `block_on_fd` enforces one-waiter-per-fd with a loud abort) and a
never-ready-fd "hang" (RECLASSIFIED as correct event-loop semantics; misleading orphan-check comment
corrected). Locked `1816` (pipe block→kqueue-wake→read). Suite green 754/0.
- **B1.5 COMPLETE → STREAM B1 DONE (this session).** Capstone `1817` composes the whole stack
(`go`/`wait` + `sleep`/`now_ms` + scheduler) — three tasks complete in DEADLINE order
(task 2@10 / 3@20 / 1@30), `sum: 123`, final virtual clock 30. The pure-sx colorblind M:1 async
runtime is feature-complete end-to-end (18001817), all adversarially reviewed. Suite GREEN
**755/0**. Five compiler bugs fixed across the stream (0151/0152/0153/0154/0156-P1/0157 — 0151-3 in
B1.2). Next carve: Stream B2 (channels / cancel / async stdlib).
- **Post-review hardening (this session) — 6 findings from an adversarial review of the B1 commits.**
Fixed: **P1-a** the UFCS generic PLANNER (`calls.zig`) used the last-wins `fn_ast_map` winner while
lowering reselected by receiver → plan/lowering could disagree and MISBOX the result; now both share
`selectUfcsGenericByReceiver`. **P1-b** the selection scanned `module_decls` globally, flagging a
transitively-hidden same-named overload as a FALSE ambiguity; now two-tier — directly-visible authors
first (ambiguity only among those), global fallback for receiver-reachable namespaced methods (e.g.
`Task.cancel`) that defers to `fd0` on a hidden tie. **P2-b** boolean specificity tied `*$T` with
`*Box($T)`; now peels pointer layers so the structurally-narrower receiver wins. **P1-c** a second
concurrent `Task.wait` overwrote the single waiter slot → silent deadlock; now one-awaiter-per-task
loud abort. **P2-c** `sleep(negative)` rewound the virtual clock; now rejected loudly. (**P2-a**
non-generic-winner-hides-generic did not reproduce — the non-generic arm already falls through.)
Regressions: `examples/generics/0218` (receiver specificity + plan/lowering agreement),
`examples/concurrency/1818` (negative-sleep abort), `1819` (double-wait abort). Suite GREEN **758/0**.

View File

@@ -0,0 +1,51 @@
# CHECKPOINT-LANG — user-facing language features
Companion to [PLAN-LANG.md](PLAN-LANG.md). Update after every step (one step at
a time, per the cadence rule).
## Last completed step
**Tuple syntax cutover — `Tuple(...)` type + `.(...)` value (commit 989e18b7).**
The bare-paren tuple grammar was replaced with explicit, position-unambiguous
forms that mirror how structs work:
- type `(A, B)``Tuple(A, B)` (named keeps `:``Tuple(x: A, y: B)`)
- value `(a, b)``.(a, b)` (named uses `=``.(x = a, y = b)`)
- typed (new) → `Tuple(A, B).(a, b)` (like `Point.{...}`)
- failable `-> (T, !)``-> T !`
`-> (T1, T2, !)``-> Tuple(T1, T2) !` (error channel OUTSIDE the Tuple)
Bare `(...)` is now grouping ONLY, everywhere; a comma in bare parens is a hard
error with a migration hint. Grouping, function types `(A, B) -> R`, param lists,
lambdas, match bindings, and `?(?T)` grouping are unaffected. `Tuple(...)` is
strictly a TYPE in every position (incl. `size_of` / `type_info` args); a tuple
VALUE comes only from `.(...)` or `Tuple(...).(...)`. A bare `Tuple(1, 2)`
(non-type elements) is rejected. Field access is unchanged (`.0`/`.1` positional,
`.x` named). Optional semantics are untouched — `??T ≡ ?T` was NOT done; nested
optionals (`?(?i64)`) stay genuine.
The ~110 tuple-bearing corpus files were migrated by a one-shot AST-aware
migrator; new examples landed (0130 new syntax, 0131 typed construction, 1060
named-tuple failable return). Issue **0189** filed (non-type expression in type
position silently fabricates an empty struct — surfaced while validating the
`Tuple(i32, g.a)` rejection path).
Docs updated to the new syntax: `specs.md` (Tuple Types section, function
multi-return note, all error-channel sections, Variadic Heterogeneous Type Packs,
Tuple UFCS Splatting, and the normative Grammar block) and `readme.md` (inline-asm
named-tuple return + the `N → a tuple` rule). Stale old-syntax mentions in example
header comments were corrected (comments only — no code touched). Suite green
(810 ran, 0 failed).
## Current state
Tuple syntax cutover shipped and documented. `Tuple(...)` / `.(...)` are the only
tuple spellings across the corpus, specs, and readme.
## Next step
Pick up the next incomplete LANG step from [PLAN-LANG.md](PLAN-LANG.md).
## Log
- **Tuple syntax cutover** (commit 989e18b7): `(A,B)`/`(a,b)` tuples replaced by
`Tuple(A,B)` type + `.(a,b)` value; failable `!` moved outside the Tuple
(`-> T !` / `-> Tuple(...) !`); bare parens are grouping-only. Docs (specs.md +
readme.md) and stale example-comment mentions migrated to the new syntax. Issue
0189 filed. Suite green (810 ran, 0 failed).

View File

@@ -1,17 +1,27 @@
# PLAN-FIBERS — Stream B1 (fibers + Io + M:1 scheduler) # PLAN-FIBERS — Stream B1 (fibers + Io + M:1 scheduler)
> **STATUS: 🚧 in progress.** B1.0 (`abi(.naked)`) ✅ + B1.1 (per-fiber `context`) ✅. **B1.2** > **STATUS: ✅ COMPLETE.** The pure-sx M:1 async runtime is feature-complete end-to-end
> (`Io` interface) is **UNBLOCKED** — the earlier "blockers" were artifacts of non-idiomatic > (`library/modules/std/sched.sx`, examples 18001817, suite 755/0): `abi(.naked)` context switch
> syntax + a worker's dirty binary. Issue **0151 was INVALID** (the `($A)->$R` bare-fn-ptr > (aarch64 + x86_64/Win64), M:1 scheduler, suspending `go`/`wait`/`cancel`, deterministic virtual-time
> form is not idiomatic sx) and is **removed**. The correct `async` idiom **works today, no > timers (`sleep`/`now_ms`), and real fd readiness via kqueue (`block_on_fd`). Five compiler bugs fixed
> compiler change**: `async :: (io, worker: Closure(..$args) -> $R, ..$args) -> Future($R)` > en route (0151/0152/0153/0154/0156-P1/0157). Deferred non-blocking follow-ups: linux `epoll` twin,
> with a **lambda worker** + the `result : Future($R) = ---; result.v = worker(..args);` build > `Scheduler.deinit`, `Future(void)`/`timeout` (0150), `context.io`-routed async (M:N). Next carve:
> form (mirrors the canonical `examples/0543-packs-canonical-map.sx`). Caveats: lambda params > Stream B2 (channels / cancel / async stdlib). Historical step-status below.
> must be annotated; passing a bare *named* fn as the worker is non-idiomatic (use a lambda). >
> Issue **0150** (`void` struct field SIGTRAP, exit 133) is a **real** bug but only hit by > B1.0 (`abi(.naked)`) ✅ · B1.1 (per-fiber `context`) ✅ · B1.2
> `Future(void)`/`timeout` — **deferred** (avoid void Futures in B1.2; revisit in B1.4). Resume > (`Io` interface + `async`/`await`/`cancel` over blocking `CBlockingIo`) ✅ · B1.3 (fiber
> B1.2 with the corrected idiom (the WIP at `.sx-tmp/b12-wip/` has the Io-protocol/Context/ > runtime: naked `swap_context` + §10.7 stress gate + guarded `mmap` stacks, proven on aarch64
> materializer parts that WORK; rewrite the async layer to the pack-lambda form above). > AND x86_64/Win64) ✅ · **B1.5a (M:1 scheduler CORE — `std/sched.sx`: `spawn`/`yield_now`/
> `suspend_self`/`wake`/`run`) ✅** (fixed blocker 0154) · **B1.4a (suspending fiber-task async —
> `sched.go`/`wait`/`cancel` over `Task($R)`, nullary-thunk) ✅** (adversarially reviewed; fixed
> blockers 0156-Part1 + 0157 en route; locked `1813`).
> **B1.4b (deterministic virtual-time timers — sched.sleep/now_ms/timer-run) ✅** (reviewed; fixed a CRITICAL timer-vs-early-wake UAF; locked 1814/1815).
> **B1.4c (event-loop — real fd readiness via kqueue: `block_on_fd` + run-loop Mode 2) ✅** (reviewed; fixed a CRITICAL same-fd lost-wakeup hang; locked 1816). macOS only — linux epoll twin deferred.
> **B1.5 (end-to-end M:1 capstone — `go`/`wait`+`sleep`+scheduler, deterministic ordering) ✅** (locked 1817). **STREAM B1 COMPLETE.** Detailed progress in [CHECKPOINT-FIBERS.md](CHECKPOINT-FIBERS.md). NOTE: suspending async +
> deterministic timers live as `sched.*` methods (M:1), NOT routed through the erased `context.io` (avoids forcing sched.sx into every std consumer + the `_fib_tramp` dup-symbol
> trap); the `Io` protocol's `spawn_raw`/`suspend_raw`/`ready` stay reserved for M:N. Deferred:
> issue 0150 (`Future(void)`/`timeout`); 0156-Part2 (deferred `..` spread); the `::` callable-param
> feature.
Carved from [PLAN-POST-METATYPE.md](PLAN-POST-METATYPE.md) Stream B (§B1) + the Carved from [PLAN-POST-METATYPE.md](PLAN-POST-METATYPE.md) Stream B (§B1) + the
design-of-record [../design/execution-evolution-roadmap.md](../design/execution-evolution-roadmap.md) design-of-record [../design/execution-evolution-roadmap.md](../design/execution-evolution-roadmap.md)
@@ -198,22 +208,34 @@ API surface. xfail→green via an `18xx` example exercising the blocking `Io` de
suspend lands in B1.3). No compiler change expected; if a protocol-in-context gap appears, suspend lands in B1.3). No compiler change expected; if a protocol-in-context gap appears,
file it. file it.
### B1.3 — A2: fiber runtime (naked switch + bootstrap + guarded `mmap` stacks) ### B1.3 — A2: fiber runtime (naked switch + bootstrap + guarded `mmap` stacks) — ✅ COMPLETE
- **B1.3a (switch-stress harness FIRST)** the standalone 2-fiber ping-pong harness - **B1.3a (switch-stress harness FIRST) — ✅** the §10.7 register/canary-survival gate (1807/1808),
(register + canary survival, deep chains) per §10.7. This is A2's gate and predates the validity proven by negative controls, adversarially reviewed.
scheduler + deterministic `Io`. Arch-gated run test (matching-host run; ir-only elsewhere). - **B1.3b — ✅** fiber bootstrap + guarded `mmap` stacks (1809); the x86_64 sibling landed as Win64
- **B1.3b** — fiber bootstrap + `mmap` stacks **with guard pages** (mandatory — §8.1.1). on a real VM (1810, `0 0 P`). Switch proven on TWO arch/ABI pairs.
- (Cadence inside B1.3 follows lock→green per sub-piece; the asm switch is the highest-risk
artifact — review adversarially, with a worker if authorized.)
### B1.4A3: `Io` impls (blocking → deterministic-sim KEYSTONE → event-loop) ### B1.5aM:1 scheduler CORE (`std/sched.sx`) — ✅ COMPLETE
Blocking first; then the deterministic-sim `Io`, **calibrated against blocking** before any The reusable scheduler wrapping `swap_context`: generic `Fiber`/`Scheduler`,
`18xx` test trusts it; then the event loop. The deterministic `Io` is the test harness for `spawn`/`yield_now`/`suspend_self`/`wake`/`run` over guarded `mmap` stacks, one generic
*all* of B1.5 + Stream B2. `fib_dispatch` running any stored closure body. Adversarially reviewed + hardened; fixed blocker
bug 0154 (struct-field `null`/`---` over-store) en route. Locked by `1811` (round-robin) + `1812`
(suspend/wake). Built BEFORE the deterministic `Io` because FiberIo (B1.4a) needs it as substrate.
### B1.5A5: M:1 scheduler ### B1.4asuspending fiber-task async (`sched.go`/`wait`/`cancel`) — ✅ COMPLETE
End-to-end validation of the colorblind stack. `18xx` corpus under the deterministic `Io`, `Task($R)` + `Scheduler.go(work) -> *Task($R)` + `wait`/`cancel` in `sched.sx` (nullary-thunk;
asserting program-emitted ordering contracts. self-contained). `go` spawns `work` as a fiber, `wait` parks the caller until it completes. Locked
by `1813`. Two compiler blockers fixed (0156-Part1, 0157) + adversarially reviewed/hardened.
### B1.4b/c — A3: `Io` impls (deterministic-sim KEYSTONE → event-loop)
Blocking exists (io.sx `CBlockingIo`). Next the deterministic-sim `Io`, **calibrated against
blocking** before any `18xx` test trusts it; then the event loop. The deterministic `Io` is the
test harness for *all* of B1.5 + Stream B2.
### B1.5 — A5: M:1 scheduler — ✅ COMPLETE
End-to-end validation of the colorblind stack. The `18xx` corpus asserts program-emitted ordering
contracts under the scheduler + deterministic timers; the capstone `1817` composes `go`/`wait` +
`sleep`/`now_ms` + the scheduler (three tasks complete in deadline order, deterministic sum). Stream
B1 is feature-complete.
--- ---

View File

@@ -28,14 +28,14 @@ main :: () {
print("{}\n", 1 |> calc(2, 3, 4)); // same = 3 — pipe UFCS print("{}\n", 1 |> calc(2, 3, 4)); // same = 3 — pipe UFCS
// Tuple return type // Tuple return type
swap :: (a: i64, b: i64) -> (i64, i64) { (b, a) } swap :: (a: i64, b: i64) -> Tuple(i64, i64) { .(b, a) }
s := swap(1, 2); s := swap(1, 2);
a := s.0; a := s.0;
b := s.1; b := s.1;
print("{}\n", a); // 2 print("{}\n", a); // 2
print("{}\n", b); // 1 print("{}\n", b); // 1
wrap :: (x: i64) -> (i64) { (x,) } wrap :: (x: i64) -> Tuple(i64) { .(x) } // 1-tuple type `Tuple(i64)`; bare `(i64)` groups
t := wrap(99); t := wrap(99);
print("{}\n", t.0); // 99 print("{}\n", t.0); // 99
} }

View File

@@ -17,7 +17,7 @@ E :: error { Neg }
const_one :: () -> i64 { return 1; return 99; } const_one :: () -> i64 { return 1; return 99; }
// dead `return x;` after an unconditional raise (the failable closure shape) // dead `return x;` after an unconditional raise (the failable closure shape)
always_raise :: (x: i64) -> (i64, !E) { raise error.Neg; return x; } always_raise :: (x: i64) -> i64 !E { raise error.Neg; return x; }
// guard: a conditional return must still fall through to the trailing return // guard: a conditional return must still fall through to the trailing return
clamp :: (x: i64) -> i64 { if x > 10 { return 10; } return x; } clamp :: (x: i64) -> i64 { if x > 10 { return 10; } return x; }

View File

@@ -8,7 +8,7 @@
#import "modules/std.sx"; #import "modules/std.sx";
pair :: () -> (i32, i32) { (5, 7) } pair :: () -> Tuple(i32, i32) { .(5, 7) }
main :: () -> i32 { main :: () -> i32 {
// destructure decl inside a value-bound block // destructure decl inside a value-bound block

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,66 @@
// Optional closures: `?Closure(...) -> R`.
//
// Runtime repr is the sentinel form — the optional IS the closure struct
// `{fn_ptr, env}`; has_value = `fn_ptr != null`, no separate flag word.
// Covers: present vs null truthiness, `== null` / `!= null`, force-unwrap +
// call-through (with and without captures), `??` coalesce, pass-as-param,
// return, and args + non-void return.
//
// Regression (issue 0170): `g!()` where `g : ?Closure(...)` previously lowered
// to a `call_indirect` that treated the closure `{fn,env}` struct as a bare
// fn pointer (LLVM "Called function must be a pointer!"); the indirect-call
// catch-all also hardcoded an `.i64` return type. The else-arm in
// `src/ir/lower/call.zig` now dispatches to `call_closure` when the callee
// expression's static type is a closure, and uses the real return type.
#import "modules/std.sx";
take :: (g: ?Closure() -> void) {
if g { g!(); } else { print("param-absent\n"); }
}
give :: () -> ?Closure() -> void {
return () => print("from-give\n");
}
main :: () {
// With captures.
n := 7;
a : ?Closure() -> void = () => print("a n={}\n", n);
if a { print("a-present\n"); } else { print("a-absent\n"); }
a!();
// Without captures.
b : ?Closure() -> void = () { print("b-called\n"); };
if b { b!(); }
// Null tests absent; == / != null.
c : ?Closure() -> void = null;
if c { print("c-present\n"); } else { print("c-absent\n"); }
print("c==null: {}\n", c == null);
print("c!=null: {}\n", c != null);
// Reassign: null -> value -> null.
c = () { print("c-reassigned\n"); };
if c { c!(); }
c = null;
if c { print("c2-present\n"); } else { print("c2-absent\n"); }
// Coalesce: null falls back, present uses self.
fallback := () { print("fallback\n"); };
(c ?? fallback)();
e : ?Closure() -> void = () { print("e-real\n"); };
(e ?? fallback)();
// Pass as param (present + null).
take(a);
take(c);
// Return an optional closure.
r := give();
if r { r!(); }
// Args + non-void return.
add : ?Closure(i64, i64) -> i64 = (x: i64, y: i64) => x + y;
if add { print("add: {}\n", add!(3, 4)); }
}

View File

@@ -0,0 +1,33 @@
// Calling a closure VALUE whose parameter is `?T` coerces the argument to the
// param type, just like a call to a top-level function does: a concrete arg
// wraps to a present optional, and `null` becomes an absent optional.
//
// Regression (issue 0186): the closure-value call path lowered args without
// coercing to the closure's declared param types, so a concrete `7` arrived as
// a bare payload (read ABSENT) and `null` reached a `{T,i1}` slot as a bare
// pointer (LLVM verifier failure). Fixed by typing args against the closure's
// params (`resolveCallParamTypes`) AND coercing them at the call site
// (`coerceClosureCallArgs`).
#import "modules/std.sx";
main :: () {
pick := (p: ?i64) -> i64 => {
if p == null { return -1; }
return p; // narrowed inside the lambda body
};
print("pick 7: {}\n", pick(7)); // 7 (concrete arg wraps present)
print("pick null: {}\n", pick(null)); // -1 (null arg → absent)
// also via a closure stored in a struct field
Holder :: struct { f: Closure(?i64) -> i64; }
h := Holder.{ f = pick };
print("h 5: {}\n", h.f(5)); // 5
print("h null: {}\n", h.f(null)); // -1
// and via a plain function-pointer VALUE (same coercion contract)
fp : (?i64) -> i64 = target;
print("fp 8: {}\n", fp(8)); // 8
print("fp null: {}\n", fp(null)); // -1
}
target :: (p: ?i64) -> i64 { if p == null { return -1; } return p; }

View File

@@ -0,0 +1,28 @@
// A block-body closure (`closure((params) { ... })`) with an INFERRED return
// type whose value is produced via early `return`s must infer the return type
// from the `return` operands — not fall through to void.
//
// Regression (issue 0187): `closure(() { if c { return 11; } return 22; })`
// used to infer `void` (the block's last stmt is the `return`, not a value), so
// the call site fed an `i64 undef` to `print` and LLVM verification failed. The
// closure return-type inference now mirrors the function-decl path, scanning
// the body's `return` statements.
//
// Syntax note: a block body is the `closure((params) -> R? { ... })` form; the
// bare `(params) => expr` lambda is arrow + EXPRESSION only (no block).
#import "modules/std.sx";
main :: () {
// early returns only — inferred return, no optionals
f := closure(() { if 1 > 0 { return 11; } return 22; });
print("f: {}\n", f());
// early return + trailing-expression block value
g := closure((n: i64) { if n > 0 { return 100; } 200 });
print("g+: {}\n", g(1));
print("g-: {}\n", g(-1));
// inferred float return via early returns
h := closure((n: i64) { if n > 0 { return 3.5; } return 1.5; });
print("h: {}\n", h(1));
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,14 @@
a-present
a n=7
b-called
c-absent
c==null: true
c!=null: false
c-reassigned
c2-absent
fallback
e-real
a n=7
param-absent
from-give
add: 7

View File

@@ -0,0 +1,6 @@
pick 7: 7
pick null: -1
h 5: 5
h null: -1
fp 8: 8
fp null: -1

View File

@@ -0,0 +1,4 @@
f: 11
g+: 100
g-: 200
h: 3.500000

View File

@@ -3,7 +3,7 @@
// tuple here). Tuples are POSITIONAL, so `TupleInfo` is just a `[]Type` (no field // tuple here). Tuples are POSITIONAL, so `TupleInfo` is just a `[]Type` (no field
// names). Two paths: // names). Two paths:
// 1. Programmatic build: `define(declare("Pair"), .tuple(.{ elements = … }))`. // 1. Programmatic build: `define(declare("Pair"), .tuple(.{ elements = … }))`.
// 2. Round-trip: `define(declare("TripleCopy"), type_info((i64, bool, f64)))` // 2. Round-trip: `define(declare("TripleCopy"), type_info(Tuple(i64, bool, f64)))`
// reflects a source tuple type INTO a `.tuple(TupleInfo)` value and // reflects a source tuple type INTO a `.tuple(TupleInfo)` value and
// reconstructs it — no literal element list. // reconstructs it — no literal element list.
#import "modules/std.sx"; #import "modules/std.sx";
@@ -11,7 +11,7 @@
Pair :: define(declare("Pair"), .tuple(.{ elements = .[ i64, f64 ] })); Pair :: define(declare("Pair"), .tuple(.{ elements = .[ i64, f64 ] }));
TripleCopy :: define(declare("TripleCopy"), type_info((i64, bool, f64))); TripleCopy :: define(declare("TripleCopy"), type_info(Tuple(i64, bool, f64)));
main :: () -> i32 { main :: () -> i32 {
p : Pair = .{ 3, 2.5 }; p : Pair = .{ 3, 2.5 };

View File

@@ -0,0 +1,38 @@
// `#run` of a function returning an OPTIONAL value must bridge the comptime
// VM register → host Value through the reg→value optional arm.
//
// Regression (issue 0162): a `#run` whose function returned `?T` / `?i64`
// previously failed comptime evaluation with
// "reg→value: aggregate shape not bridged yet"
// because the VM's regToValue bridge handled scalars/structs/slices/tuples
// but bailed on an OPTIONAL-typed result. The fix reads the optional's
// has_value flag (at offset sizeof(child)); when set it bridges the payload
// recursively into a `{ payload, i1=true }` aggregate (the host serializes
// that to `{T, i1}`), and when clear (or the value is the bare null sentinel)
// it yields `.null_val` (serialized to a zero `{T, i1}` = absent).
//
// Exercises: present `?T` (optional of struct), present `?i64` (optional of
// scalar), and a NULL-returning `?i64`. The values are read via `!` (unwrap)
// and `??` (coalesce), which read the has_value flag correctly.
#import "modules/std.sx";
T :: struct { a: i64 = 0; b: i64 = 0; }
mk_struct :: () -> ?T { t : T = .{ a = 7, b = 11 }; return t; }
mk_scalar :: () -> ?i64 { return 5; }
mk_null :: () -> ?i64 { return null; }
X :: #run mk_struct(); // present ?T
Y :: #run mk_scalar(); // present ?i64
N :: #run mk_null(); // null ?i64
main :: () {
// Present optional-of-struct: payload bridged field-by-field.
print("X.a = {}\n", X!.a);
print("X.b = {}\n", X!.b);
// Present optional-of-scalar.
print("Y = {}\n", Y ?? -1);
// Null optional: coalesce takes the default.
print("N = {}\n", N ?? -1);
}

View File

@@ -0,0 +1,62 @@
// A `#run` (comptime const init) whose function returns an aggregate that
// CONTAINS AN ARRAY — or an array directly — evaluates: the comptime VM's
// reg→value bridge reads the array's elements out of comptime memory and
// produces a `Value` array the LLVM serializer emits as a constant.
//
// Regression (issue 0167 C): the array-in-aggregate shape used to bail with
// `reg→value: aggregate shape not bridged yet`.
// Covers: struct-with-array-field, array-of-structs, nested array `[2][2]`,
// a direct `[N]T` return, and the `?Arr` optional payload (composes with the
// optional bridge arm) unwrapped via `!`.
#import "modules/std.sx";
Arr3 :: struct { xs: [3]i64; }
Pt :: struct { x: i64; y: i64; }
Box :: struct { items: [2]Pt; }
Mat :: struct { g: [2][2]i64; }
mk_struct :: () -> Arr3 {
r : Arr3 = ---;
r.xs[0] = 1; r.xs[1] = 2; r.xs[2] = 3;
return r;
}
mk_aos :: () -> Box {
r : Box = ---;
r.items[0].x = 1; r.items[0].y = 2;
r.items[1].x = 3; r.items[1].y = 4;
return r;
}
mk_nested :: () -> Mat {
r : Mat = ---;
r.g[0][0] = 1; r.g[0][1] = 2;
r.g[1][0] = 3; r.g[1][1] = 4;
return r;
}
mk_direct :: () -> [3]i64 {
r : [3]i64 = ---;
r[0] = 7; r[1] = 8; r[2] = 9;
return r;
}
mk_opt :: () -> ?Arr3 {
r : Arr3 = .{ xs = .[10, 20, 30] };
return r;
}
G :: #run mk_struct(); // struct { [3]i64 }
B :: #run mk_aos(); // struct { [2]Pt }
M :: #run mk_nested(); // struct { [2][2]i64 }
D :: #run mk_direct(); // [3]i64
A :: #run mk_opt(); // ?Arr3
main :: () {
print("{} {} {}\n", G.xs[0], G.xs[1], G.xs[2]); // 1 2 3
print("{} {} {} {}\n", B.items[0].x, B.items[0].y, B.items[1].x, B.items[1].y); // 1 2 3 4
print("{} {} {} {}\n", M.g[0][0], M.g[0][1], M.g[1][0], M.g[1][1]); // 1 2 3 4
print("{} {} {}\n", D[0], D[1], D[2]); // 7 8 9
print("{}\n", A!.xs[0]); // 10
}

View File

@@ -0,0 +1,28 @@
// A body-local `#run` const of a BRIDGEABLE shape — a scalar, a struct, an
// array, or an `?Array` optional — evaluates and produces its const value.
// These are the common cases that must keep working alongside the issue-0182
// fix (which fails ONLY the unbridgeable-result case, e.g. `[2][]i64`).
//
// Regression (issue 0182): the body-local `#run` fold must not regress the
// bridgeable cases when it learned to fail loudly on an unbridgeable result.
#import "modules/std.sx";
Pt :: struct { x: i64; y: i64; }
mk_scalar :: () -> i64 { return 42; }
mk_struct :: () -> Pt { return .{ x = 3, y = 4 }; }
mk_arr :: () -> [3]i64 { r : [3]i64 = ---; r[0] = 10; r[1] = 20; r[2] = 30; return r; }
mk_opt :: () -> ?[3]i64 { r : [3]i64 = ---; r[0] = 1; r[1] = 2; r[2] = 3; return r; }
main :: () {
N :: #run mk_scalar();
S :: #run mk_struct();
A :: #run mk_arr();
O :: #run mk_opt();
print("N={}\n", N);
print("S={} {}\n", S.x, S.y);
print("A={} {} {}\n", A[0], A[1], A[2]);
v := O!;
print("O={}\n", v[1]);
}

View File

@@ -0,0 +1,4 @@
X.a = 7
X.b = 11
Y = 5
N = -1

View File

@@ -0,0 +1,5 @@
1 2 3
1 2 3 4
1 2 3 4
7 8 9
10

View File

@@ -0,0 +1,4 @@
N=42
S=3 4
A=10 20 30
O=2

View File

@@ -0,0 +1,79 @@
// Stream B1 (fibers) B1.5a — the M:1 cooperative fiber scheduler core, in pure
// sx over `swap_context` (proven in 1807-1809). `Scheduler` drives N fibers,
// each running a `body: Closure() -> void` on its own guarded `mmap` stack;
// fibers cooperate by calling `yield_now`, which round-robins control back
// through the scheduler loop.
//
// Round-robin demo: 3 fibers (A=0, B=1, C=2) each append their id to a shared
// sequence buffer, yielding between each of 3 rounds. Because the scheduler
// re-enqueues a yielding fiber at the TAIL (FIFO), the interleaving is the
// deterministic round-robin order:
//
// round 1: A B C (0 1 2)
// round 2: A B C (0 1 2)
// round 3: A B C (0 1 2)
//
// → sequence: 0 1 2 0 1 2 0 1 2
//
// Outputs flow OUT of each fiber through pointers captured in its closure (the
// shared `Shared` struct), since closure capture-by-value does not write back.
// Every fiber must reach `.done` (asserted via a per-fiber done flag).
//
// aarch64-macOS-pinned (the scheduler's asm + guard-page mmap constants are
// per-arch / Apple-specific): runs end-to-end on a matching host, ir-only on a
// mismatch.
#import "modules/std.sx";
sched :: #import "modules/std/sched.sx";
Shared :: struct {
seq: [16]i64; // appended interleaving sequence
n: i64; // count appended
done: [3]i64; // per-fiber done flag (set right before the body returns)
}
append :: (sh: *Shared, v: i64) {
sh.seq[sh.n] = v;
sh.n = sh.n + 1;
}
main :: () -> i64 {
sh : Shared = .{ n = 0 }; // seq[] + done[] zero-filled
s := sched.Scheduler.init();
ps := @s;
psh := @sh;
// Three DIFFERENT fiber bodies (distinct captured ids), interleaving via
// yield_now. Each appends its id once per round for 3 rounds.
spawn_worker :: (ps: *sched.Scheduler, psh: *Shared, my_id: i64) {
ps.spawn(() => {
r := 0;
while r < 3 {
append(psh, my_id);
if r < 2 { ps.yield_now(); } // cooperate between rounds
r = r + 1;
}
psh.done[my_id] = 1;
});
}
spawn_worker(ps, psh, 0);
spawn_worker(ps, psh, 1);
spawn_worker(ps, psh, 2);
s.run();
// Ordering contract: round-robin FIFO interleaving.
print("sequence:");
i := 0;
while i < sh.n {
print(" {}", sh.seq[i]);
i = i + 1;
}
print("\n");
print("spawned: {}\n", s.n_spawned);
print("done: {} {} {}\n", sh.done[0], sh.done[1], sh.done[2]);
print("all done: {}\n", sh.done[0] + sh.done[1] + sh.done[2]);
return 0;
}

View File

@@ -0,0 +1,64 @@
// Stream B1 (fibers) B1.5a — fiber park/resume via `suspend_self` + `wake`,
// the off-queue half of the M:1 scheduler that FiberIo [B1.4] builds on.
//
// A running fiber that has nothing to do parks itself with `suspend_self`: it
// leaves the round-robin queue entirely (unlike `yield_now`, which re-enqueues)
// and only runs again when another fiber (or an I/O completion) calls `wake` on
// it. Here fiber A records 10, parks, and is resumed by fiber B to record 11:
//
// A: rec 10, suspend_self ──park──┐
// B: rec 20, wake(A), wake(A), rec 21
// A: ──resume──> rec 11
// → log: 10 20 21 11
//
// `wake` is GUARDED on `.suspended`: B's SECOND `wake(A)` is spurious (A is
// already re-queued by then). An unguarded enqueue would re-link an
// already-listed node and corrupt the FIFO (segfault); the guard makes a
// double/spurious/stale wake a safe no-op. `suspended-left: 0` confirms every
// park was balanced by a wake (an orphaned park would abort the scheduler).
//
// aarch64-macOS-pinned (the scheduler's per-arch asm + Apple mmap constants):
// runs end-to-end on a matching host, ir-only on a mismatch.
#import "modules/std.sx";
sched :: #import "modules/std/sched.sx";
// The shared state both fibers reach through (passed as `*Sh`). `parked` holds
// the fiber-A handle that B wakes — kept here (rather than a separate
// `**Fiber`) so the one `*Sh` carries everything the helper fns share.
Sh :: struct { log: [16]i64; n: i64; parked: *sched.Fiber; }
rec :: (sh: *Sh, v: i64) { sh.log[sh.n] = v; sh.n = sh.n + 1; }
main :: () -> i64 {
sh : Sh = ---; sh.n = 0; sh.parked = null;
s := sched.Scheduler.init();
ps := @s; psh := @sh;
// Fiber A: record 10, park, then (after wake) record 11. Store A's handle in
// the shared state so B can wake it.
mk_a :: (ps: *sched.Scheduler, psh: *Sh) {
psh.parked = ps.spawn(() => {
rec(psh, 10);
ps.suspend_self();
rec(psh, 11);
});
}
// Fiber B: record 20, wake A (legit) + a spurious second wake (safe no-op),
// record 21.
mk_b :: (ps: *sched.Scheduler, psh: *Sh) {
ps.spawn(() => {
rec(psh, 20);
ps.wake(psh.parked); // legitimate: A is parked
ps.wake(psh.parked); // spurious: A is now .ready/queued — must no-op
rec(psh, 21);
});
}
mk_a(ps, psh);
mk_b(ps, psh);
s.run();
print("log:");
i := 0; while i < sh.n { print(" {}", sh.log[i]); i = i + 1; }
print("\n");
print("suspended-left: {}\n", s.n_suspended);
return 0;
}

View File

@@ -0,0 +1,82 @@
// Stream B1 (fibers) B1.4a — a truly-SUSPENDING fiber-task async layer
// (`go` / `wait` / `cancel`) over the M:1 scheduler, in pure sx. In contrast
// with 1805's `context.io.async` (which runs each worker INLINE to completion
// before returning a `.ready` future — no interleaving), here `s.go(work)` runs
// `work` as a REAL fiber and `t.wait()` SUSPENDS the caller until that fiber
// finishes, so a task that yields mid-body lets a sibling task run before the
// first completes — genuine cooperative interleaving.
//
// `work` is a NULLARY thunk: any inputs are captured in the lambda at the call
// site (no `..args` pack crosses the fiber boundary — that would hit issue 0156
// Part 2). Outputs flow OUT through pointers captured in the thunk (the shared
// `Log` struct), since closure capture-by-value does not write back.
//
// What this proves:
// - REAL suspend + interleave: task A records 1, YIELDS; task B then records 2
// and completes; A resumes, records 3, completes → interleave order 1 2 3.
// - awaited VALUES: A returns 42, B returns 100 (recorded after both waits).
// → sequence: 1 2 3 42 100.
// - cancel rides the `!` channel (model (a), like 1806): a canceled task's
// `wait()` raises `.Canceled`, taken by the `or` default → -99.
//
// `wait` must run inside a fiber (it parks `self.current`), so the "main task"
// is itself a `s.spawn(...)` fiber that drives the two `go` tasks.
//
// aarch64-macOS-pinned (the scheduler's asm + guard-page mmap constants are
// per-arch / Apple-specific): runs end-to-end on a matching host, ir-only on a
// mismatch.
#import "modules/std.sx";
sched :: #import "modules/std/sched.sx";
Log :: struct { seq: [16]i64; n: i64; }
rec :: (l: *Log, v: i64) { l.seq[l.n] = v; l.n = l.n + 1; }
main :: () -> i64 {
lg : Log = .{ n = 0 }; // seq[] zero-filled
s := sched.Scheduler.init();
ps := @s;
pl := @lg;
// The "main task" fiber: drives two real tasks, waits both, then exercises
// cancel. It runs as a fiber so `wait` has a `self.current` to park.
s.spawn(() => {
// Task A yields mid-body so B interleaves before A completes.
a := ps.go(() -> i64 => {
rec(pl, 1);
ps.yield_now(); // suspend A; B (already spawned) runs to completion
rec(pl, 3);
42
});
// Task B runs straight through (no yield).
b := ps.go(() -> i64 => {
rec(pl, 2);
100
});
// Wait both — suspends the main-task fiber until each completes.
va := a.wait() or { -1 };
vb := b.wait() or { -1 };
rec(pl, va);
rec(pl, vb);
// Cancel case: cancel before the worker runs; `wait` raises .Canceled,
// the `or` default (-99) is taken.
c := ps.go(() -> i64 => 7);
c.cancel();
rec(pl, c.wait() or { -99 });
});
s.run();
// Interleaving + value contract: 1 2 3 42 100, then the cancel default -99.
print("sequence:");
i := 0;
while i < lg.n {
print(" {}", lg.seq[i]);
i = i + 1;
}
print("\n");
print("spawned: {}\n", s.n_spawned);
return 0;
}

View File

@@ -0,0 +1,73 @@
// Stream B1 (fibers) B1.4b — deterministic VIRTUAL-TIME timer scheduling (the
// KEYSTONE), in pure sx over the M:1 scheduler. A fiber `sleep(ms)`s in
// SIMULATED time; the scheduler wakes fibers in DEADLINE order, advancing a
// virtual clock that moves only when the ready queue drains and the earliest
// timer fires. No real wall clock is ever read — the wake ORDER and the
// observed timestamps are fully reproducible, which is exactly what a
// deterministic-sim Io test harness needs.
//
// HOW IT WORKS. `s.sleep(ms)` arms a timer `{ clock_ms + ms, current }` and
// parks the fiber off-queue. `s.run` drives ready fibers to quiescence, then
// fires the earliest pending timer: it advances `clock_ms` to that deadline and
// `wake`s the sleeper (re-readying it), and repeats until both the ready queue
// AND the timer set are empty. So a fiber that just woke reads `now_ms()` equal
// to its own deadline.
//
// WHAT THIS PROVES.
// - Deadline-ordered wake (NOT spawn order): spawn A, B, C in that order;
// A sleep(30), B sleep(10), C sleep(20). Wakes fire B(10), C(20), A(30) —
// reordered by deadline, not by spawn order.
// - Virtual timestamps: each fiber on wake reads `now_ms()` == its deadline
// (10, 20, 30) — the virtual clock landed exactly on the firing deadline.
// - FIFO tiebreak: two fibers D, E both sleep(15) — they wake in spawn
// (insertion) order D then E, the deterministic equal-deadline contract.
//
// §8.1.3 CALIBRATION NOTE. The deterministic virtual-time wake ORDER equals
// what real `sleep`s would produce: under real blocking sleeps the OS would
// also wake the shortest sleeper first, i.e. in deadline order. The sim
// reproduces blocking semantics' OBSERVABLE ordering (and the relative
// timestamps) without consuming real time or admitting nondeterminism — so a
// harness can assert exact orderings that a wall-clock test could only
// approximate. (No real-time variant is run here; the equivalence is the
// contract the deterministic test relies on.)
//
// aarch64-macOS-pinned (the scheduler's `swap_context` asm + guard-page mmap
// constants are per-arch / Apple-specific): runs end-to-end on a matching host,
// ir-only on a mismatch.
#import "modules/std.sx";
sched :: #import "modules/std/sched.sx";
// Shared wake log, captured by pointer into each fiber's thunk (closure
// capture-by-value does not write back, so outputs flow through `*Log`).
Log :: struct { ids: [16]i64; ts: [16]i64; n: i64; }
rec :: (l: *Log, id: i64, t: i64) { l.ids[l.n] = id; l.ts[l.n] = t; l.n = l.n + 1; }
main :: () -> i64 {
lg : Log = .{ n = 0 }; // ids[] + ts[] zero-filled
s := sched.Scheduler.init();
ps := @s;
pl := @lg;
// Spawn order A, B, C, D, E — but the WAKE order is set by deadline.
ps.spawn(() => { ps.sleep(30); rec(pl, 1, ps.now_ms()); }); // A: latest
ps.spawn(() => { ps.sleep(10); rec(pl, 2, ps.now_ms()); }); // B: earliest
ps.spawn(() => { ps.sleep(20); rec(pl, 3, ps.now_ms()); }); // C: middle
// Same-deadline FIFO pair: D before E, both at t=15 → wake D then E.
ps.spawn(() => { ps.sleep(15); rec(pl, 4, ps.now_ms()); }); // D
ps.spawn(() => { ps.sleep(15); rec(pl, 5, ps.now_ms()); }); // E
s.run();
// Ordering contract: deadline order with a FIFO tiebreak → B, D, E, C, A
// at virtual times 10, 15, 15, 20, 30.
print("wake order (id @ virtual-ms):\n");
i := 0;
while i < lg.n {
print(" id={} @ {}ms\n", lg.ids[i], lg.ts[i]);
i = i + 1;
}
print("final virtual clock: {}ms\n", s.now_ms());
print("spawned: {}\n", s.n_spawned);
return 0;
}

View File

@@ -0,0 +1,47 @@
// Stream B1 (fibers) B1.4b — a fiber's pending `sleep` timer is EVICTED when it
// is woken early by another path, so a stale timer can never outlive (and
// dereference) a reaped fiber.
//
// Scenario: a "sleeper" fiber arms `sleep(100)` and parks; a "waker" fiber wakes
// it EARLY (at virtual t=0) via `wake`. The sleeper resumes, finishes, and is
// reaped (its stack `munmap`'d + `Fiber` freed). Its 100ms timer must already be
// gone — otherwise, when the run loop later fired that stale timer, it would
// `wake` a freed `*Fiber` (use-after-free) and wrongly advance the virtual clock
// to 100. Here `wake` evicts the timer, so the clock stays at 0 and nothing
// dereferences freed memory.
//
// Regression: the timer-vs-early-wake use-after-free found reviewing B1.4b.
// Contract: `log: 2 1` (waker records 2, then the early-woken sleeper records 1),
// `clock: 0` (no stale timer fired), `n_suspended: 0` (balanced).
//
// aarch64-macOS-pinned (the scheduler's per-arch asm + Apple mmap constants):
// runs end-to-end on a matching host, ir-only on a mismatch.
#import "modules/std.sx";
sched :: #import "modules/std/sched.sx";
S :: struct { sleeper: *sched.Fiber; log: [8]i64; n: i64; }
rec :: (s: *S, v: i64) { s.log[s.n] = v; s.n = s.n + 1; }
main :: () -> i64 {
st : S = ---; st.n = 0; st.sleeper = null;
s := sched.Scheduler.init();
ps := @s; pst := @st;
// Sleeper: arm sleep(100), park; when woken (early), record 1 and finish.
mk_sleeper :: (ps: *sched.Scheduler, pst: *S) {
pst.sleeper = ps.spawn(() => { ps.sleep(100); rec(pst, 1); });
}
// Waker: record 2, then wake the sleeper BEFORE its 100ms timer fires.
mk_waker :: (ps: *sched.Scheduler, pst: *S) {
ps.spawn(() => { rec(pst, 2); ps.wake(pst.sleeper); });
}
mk_sleeper(ps, pst);
mk_waker(ps, pst);
s.run();
print("log:");
i := 0; while i < st.n { print(" {}", st.log[i]); i = i + 1; }
print("\n");
print("clock: {} n_suspended: {}\n", s.now_ms(), s.n_suspended);
return 0;
}

View File

@@ -0,0 +1,98 @@
// Stream B1 (fibers) B1.4c — REAL fd-readiness blocking via kqueue. A fiber can
// `block_on_fd(read_fd, true)`; the scheduler's run loop blocks on `kevent` when
// nothing else is runnable and wakes that fiber when the kernel reports the fd
// readable.
//
// Scenario: a unix `pipe` (read_fd, write_fd). A READER fiber is spawned FIRST,
// so it runs while the pipe is EMPTY — it calls `block_on_fd(read_fd)` and parks
// (genuinely blocked: there is no data yet, the writer has not run). A WRITER
// fiber, spawned second, then writes 3 bytes to write_fd. Now the ready queue is
// drained and the only parked fiber is the reader's io-waiter, so the run loop
// BLOCKS on `kevent`, which reports read_fd ready; the reader wakes and reads the
// bytes. The ordering ("wrote" recorded before "read") proves the reader blocked
// on the empty pipe and was woken by kqueue readiness, not by data already
// present.
//
// Contract:
// log: wrote read 3 [97 98 99]
// n_suspended: 0 (the reader's park was balanced by the kqueue wake)
//
// aarch64-macOS-pinned: kqueue/kevent is Apple/BSD, and the scheduler's
// per-arch asm + Apple mmap constants. Runs end-to-end on a matching host,
// ir-only on a mismatch. Like 1809 (mmap), JIT `sx run` resolves the libc
// extern calls fine — no AOT build needed.
#import "modules/std.sx";
sched :: #import "modules/std/sched.sx";
// Raw libc fd primitives. read/write/close MUST match the canonical signatures
// already bound by std (socket.sx / core.sx), or the extern dedupe rejects a
// divergent re-binding of the same C symbol. `pipe` is ours alone.
pipe :: (fds: *i32) -> i32 extern libc "pipe";
read :: (fd: i32, buf: [*]u8, count: usize) -> isize extern libc "read";
write :: (fd: i32, buf: [*]u8, count: usize) -> isize extern libc "write";
close :: (fd: i32) -> i32 extern libc "close";
// Shared log: a tiny ledger of what happened, in order.
S :: struct {
wrote: bool;
read_n: i64;
bytes: [8]u8;
read_done: bool;
}
main :: () -> i64 {
st : S = .{ wrote = false, read_n = 0, read_done = false }; // bytes[] zero-filled
fds : [2]i32 = ---;
if pipe(@fds[0]) != 0 {
print("1816: pipe() failed\n");
return 1;
}
read_fd := fds[0];
write_fd := fds[1];
s := sched.Scheduler.init();
ps := @s; pst := @st;
// Reader: block on the (empty) pipe until it is readable, then read 3 bytes.
mk_reader :: (ps: *sched.Scheduler, pst: *S, rfd: i32) {
ps.spawn(() => {
ps.block_on_fd(rfd, true); // parks until read_fd is readable
n := read(rfd, xx @pst.bytes[0], xx 3);
pst.read_n = xx n;
pst.read_done = true;
});
}
// Writer: write 3 bytes ('a','b','c') to the write end.
mk_writer :: (ps: *sched.Scheduler, pst: *S, wfd: i32) {
ps.spawn(() => {
buf : [3]u8 = ---;
buf[0] = xx 97; buf[1] = xx 98; buf[2] = xx 99; // 'a' 'b' 'c'
write(wfd, xx @buf[0], xx 3);
pst.wrote = true;
});
}
mk_reader(ps, pst, read_fd); // spawned first → runs + parks on empty pipe
mk_writer(ps, pst, write_fd); // spawned second → writes, then kqueue wakes reader
s.run();
print("log: ");
if st.wrote { print("wrote "); }
if st.read_done {
print("read {} [", st.read_n);
i := 0;
while i < st.read_n {
if i > 0 { print(" "); }
print("{}", st.bytes[i]);
i = i + 1;
}
print("]");
}
print("\n");
print("n_suspended: {}\n", s.n_suspended);
close(read_fd);
close(write_fd);
return 0;
}

View File

@@ -0,0 +1,66 @@
// Stream B1 (fibers) B1.5 — the M:1 colorblind async stack, end-to-end.
//
// One program exercises the whole pure-sx runtime together: the M:1 scheduler
// (B1.5a), the suspending fiber-task async layer `go`/`wait` (B1.4a), and the
// deterministic virtual-time timers `sleep`/`now_ms` (B1.4b) — all over the
// `abi(.naked)` `swap_context` on guarded `mmap` stacks (B1.0B1.3).
//
// A coordinator fiber launches three async tasks; each `sleep`s a different
// duration, records its completion (id @ virtual-ms) into a shared log, then
// returns a value. The coordinator `wait`s on all three (in SPAWN order) and
// sums their results. Because tasks complete in DEADLINE order — not spawn
// order, not await order — the completion log is the deterministic contract:
//
// task A: sleep 30 → returns 100
// task B: sleep 10 → returns 20
// task C: sleep 20 → returns 3
// completion order (by deadline): B@10, C@20, A@30
// coordinator awaits A,B,C → sum = 123, final virtual clock = 30
//
// `wait(A)` parks the coordinator until A finishes at t=30; B and C finish
// earlier (at 10 and 20) and are already `.ready` by the time their `wait`s run,
// so they return without re-parking — the values are correct regardless of await
// order, while the timer-driven schedule fixes the completion ORDER. Fully
// deterministic + reproducible (virtual time, no real clock).
//
// aarch64-macOS-pinned (the scheduler's per-arch asm + Apple mmap constants):
// runs end-to-end on a matching host, ir-only on a mismatch.
#import "modules/std.sx";
sched :: #import "modules/std/sched.sx";
Log :: struct { id: [8]i64; at: [8]i64; n: i64; }
rec :: (l: *Log, id: i64, at: i64) { l.id[l.n] = id; l.at[l.n] = at; l.n = l.n + 1; }
main :: () -> i64 {
lg : Log = ---; lg.n = 0;
s := sched.Scheduler.init();
ps := @s; pl := @lg;
// The coordinator runs as a fiber so `wait` has a `current` to park.
s.spawn(() => {
// Launch three async tasks; each sleeps, logs its completion, returns.
a := ps.go(() -> i64 => { ps.sleep(30); rec(pl, 1, ps.now_ms()); 100 });
b := ps.go(() -> i64 => { ps.sleep(10); rec(pl, 2, ps.now_ms()); 20 });
c := ps.go(() -> i64 => { ps.sleep(20); rec(pl, 3, ps.now_ms()); 3 });
// Await in SPAWN order; results come back correct regardless.
va := a.wait() or { -1 };
vb := b.wait() or { -1 };
vc := c.wait() or { -1 };
sum := va + vb + vc;
rec(pl, 9, sum); // sentinel row: id=9 carries the sum in `at`
});
s.run();
print("completion order (id @ virtual-ms):\n");
i := 0;
while i < lg.n {
if lg.id[i] == 9 { print("sum: {}\n", lg.at[i]); }
else { print(" task {} @ {}ms\n", lg.id[i], lg.at[i]); }
i = i + 1;
}
print("final virtual clock: {}ms\n", s.now_ms());
print("tasks: {}\n", s.n_spawned);
return 0;
}

View File

@@ -0,0 +1,15 @@
// `sleep(ms)` rejects a NEGATIVE duration loudly — the virtual clock is
// monotonic (advances only as timers fire), so a negative deadline would rewind
// it and break every ordering contract. Regression (B1.4b review, P2-c): the
// guard aborts instead of silently arming a past deadline.
//
// aborts (exit 134) after the diagnostic — aarch64-macOS-pinned.
#import "modules/std.sx";
sched :: #import "modules/std/sched.sx";
main :: () -> i64 {
s := sched.Scheduler.init(); ps := @s;
ps.spawn(() => { ps.sleep(10); ps.sleep(-5); }); // -5 → loud abort
s.run();
print("unreachable\n");
return 0;
}

View File

@@ -0,0 +1,18 @@
// A `Task` allows ONE awaiter — a second concurrent `wait` on the same pending
// task would overwrite the single `waiter` slot, and completion would wake only
// the second, stranding the first forever. Regression (B1.4a review, P1-c): the
// guard aborts loudly instead of silently deadlocking.
//
// aborts (exit 134) after the diagnostic — aarch64-macOS-pinned.
#import "modules/std.sx";
sched :: #import "modules/std/sched.sx";
S :: struct { t: *sched.Task(i64); }
main :: () -> i64 {
st : S = ---; st.t = null;
s := sched.Scheduler.init(); ps := @s; pst := @st;
mkprod :: (ps: *sched.Scheduler, pst: *S) { pst.t = ps.go(() -> i64 => { ps.yield_now(); 42 }); }
mkw :: (ps: *sched.Scheduler, pst: *S) { ps.spawn(() => { x := pst.t.wait() or { -1 }; print("got {}\n", x); }); }
mkprod(ps, pst); mkw(ps, pst); mkw(ps, pst); // second waiter → loud abort
s.run();
return 0;
}

View File

@@ -0,0 +1,124 @@
// Stream B1 (fibers) — `Scheduler.deinit` releases the scheduler's owned heap
// + fd resources, closing the documented bounded leaks (kq fd / heap Tasks /
// List backings). Verified by a tracking `GPA`: deinit drives the live
// allocation count DOWN, and resets the kqueue fd to -1.
//
// Scenario (one run that touches every freed resource):
// - a SLEEPER fiber `sleep(5)`s → exercises the `timers` List
// - a READER fiber `block_on_fd`s a pipe → exercises the kqueue fd + the
// `io_waiters` List
// - a WRITER fiber writes 3 bytes → makes the pipe readable
// - two `go` tasks compute 42 / 7 → exercise the heap `Task`s +
// the `task_allocs` List
// After `run()` drains all of it, `deinit()` frees: the 2 heap Tasks, the
// `timers` / `io_waiters` / `task_allocs` List backings, and CLOSES the kqueue
// fd (resetting `kq` to -1). The Fibers were already reaped during `run()`.
//
// WHAT IT PROVES (the contract; numbers below are the snapshot):
// - `freed by deinit: N` — live allocations reclaimed by `deinit` (> 0).
// - `live after deinit` — the RESIDUAL. This is NOT zero and NOT a bug: it is
// exactly the documented closure-env leak — one heap env per `spawn`/`go`
// that sx cannot free (the runtime has no name for the env pointer). deinit
// reclaims everything it CAN; the env residual is a language limitation.
// - `kq open after run: 1` then `kq after deinit: -1` — the lazily-opened
// kqueue fd was genuinely open after the fd round and is closed by deinit.
// - `read: 3 [97 98 99]` — the fd path actually ran (reader blocked, woke via
// kqueue, read 'a' 'b' 'c'), so the kq we close is a real, used fd.
//
// Counts are captured into locals BEFORE any `print` — `print` itself allocates
// format temporaries through the same GPA, which would otherwise pollute the
// reading.
//
// aarch64-macOS-pinned (`.build {"target":"macos"}`, matches host → runs
// end-to-end): sched.sx's switch asm + the kqueue path are per-arch/Apple.
#import "modules/std.sx";
sched :: #import "modules/std/sched.sx";
// Raw libc fd primitives — canonical signatures (the extern dedupe rejects a
// divergent re-binding of the same C symbol). `close` matches sched.sx's own.
pipe :: (fds: *i32) -> i32 extern libc "pipe";
read :: (fd: i32, buf: [*]u8, count: usize) -> isize extern libc "read";
write :: (fd: i32, buf: [*]u8, count: usize) -> isize extern libc "write";
close :: (fd: i32) -> i32 extern libc "close";
S :: struct {
read_n: i64;
bytes: [8]u8;
read_done: bool;
}
main :: () -> i64 {
st : S = .{ read_n = 0, read_done = false }; // bytes[] zero-filled; read() fills it
fds : [2]i32 = ---;
if pipe(@fds[0]) != 0 {
print("1820: pipe() failed\n");
return 1;
}
read_fd := fds[0];
write_fd := fds[1];
// Captured under the GPA scope; printed after it closes.
after_run : i64 = 0;
after_deinit : i64 = 0;
kq_open_run : bool = false;
kq_after : i32 = 0;
gpa := mem.GPA.init();
push Context.{ allocator = xx gpa, data = null } {
s := sched.Scheduler.init();
ps := @s; pst := @st;
// SLEEPER — arms a virtual-time timer, then parks.
ps.spawn(() => { ps.sleep(5); });
// READER — blocks on the empty pipe until kqueue reports it readable.
mk_reader :: (ps: *sched.Scheduler, pst: *S, rfd: i32) {
ps.spawn(() => {
ps.block_on_fd(rfd, true);
n := read(rfd, xx @pst.bytes[0], xx 3);
pst.read_n = xx n;
pst.read_done = true;
});
}
// WRITER — writes 'a' 'b' 'c', making the pipe readable.
mk_writer :: (ps: *sched.Scheduler, wfd: i32) {
ps.spawn(() => {
buf : [3]u8 = ---;
buf[0] = xx 97; buf[1] = xx 98; buf[2] = xx 99;
write(wfd, xx @buf[0], xx 3);
});
}
mk_reader(ps, pst, read_fd);
mk_writer(ps, write_fd);
// Two async tasks — heap Tasks tracked for deinit to free.
ps.go(() -> i64 => 42);
ps.go(() -> i64 => 7);
ps.run();
after_run = gpa.alloc_count;
kq_open_run = s.kq >= 0;
ps.deinit();
after_deinit = gpa.alloc_count;
kq_after = s.kq;
}
print("read: {} [", st.read_n);
i := 0;
while i < st.read_n {
if i > 0 { print(" "); }
print("{}", st.bytes[i]);
i = i + 1;
}
print("]\n");
print("freed by deinit: {}\n", after_run - after_deinit);
print("live after deinit: {}\n", after_deinit);
print("kq open after run: {}\n", kq_open_run);
print("kq after deinit: {}\n", kq_after);
close(read_fd);
close(write_fd);
return 0;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -0,0 +1,4 @@
sequence: 0 1 2 0 1 2 0 1 2
spawned: 3
done: 1 1 1
all done: 3

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -0,0 +1,2 @@
log: 10 20 21 11
suspended-left: 0

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -0,0 +1,2 @@
sequence: 1 2 3 42 100 -99
spawned: 4

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -0,0 +1,8 @@
wake order (id @ virtual-ms):
id=2 @ 10ms
id=4 @ 15ms
id=5 @ 15ms
id=3 @ 20ms
id=1 @ 30ms
final virtual clock: 30ms
spawned: 5

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -0,0 +1,2 @@
log: 2 1
clock: 0 n_suspended: 0

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -0,0 +1,2 @@
log: wrote read 3 [97 98 99]
n_suspended: 0

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -0,0 +1,7 @@
completion order (id @ virtual-ms):
task 2 @ 10ms
task 3 @ 20ms
task 1 @ 30ms
sum: 123
final virtual clock: 30ms
tasks: 4

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -0,0 +1 @@
sched: sleep(-5) — negative duration would rewind the virtual clock

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -0,0 +1 @@
sched: wait() — task already has a waiter (one awaiter per task in the M:1 model)

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -0,0 +1,5 @@
read: 3 [97 98 99]
freed by deinit: 5
live after deinit: 5
kq open after run: true
kq after deinit: -1

View File

@@ -1,7 +1,7 @@
// Out-of-range tuple index produces a clear // Out-of-range tuple index produces a clear
// `error: field 'N' not found on type 'tuple'` diagnostic and exit 1. // `error: field 'N' not found on type '(i64, i64)'` diagnostic and exit 1.
main :: () -> i32 { main :: () -> i32 {
t := (10, 20); t := .(10, 20);
return xx t.42; return xx t.42;
} }

View File

@@ -1,6 +1,6 @@
// A tuple literal used in a type position (`(i32, i32)` reinterpreted as a tuple // A tuple type (`Tuple(i32, i32)` at a type-demanding site like `size_of`) must
// type at a type-demanding site like `size_of`) must list only types. A non-type // list only types. A non-type
// element — here the `1` in `(i32, 1)` — is rejected with a user-facing // element — here the `1` in `Tuple(i32, 1)` — is rejected with a user-facing
// diagnostic instead of silently fabricating an `i64` field for that slot. // diagnostic instead of silently fabricating an `i64` field for that slot.
// Regression (issue 0067). // Regression (issue 0067).
// Expected: a clean "tuple type element is not a type" error at the `1`; exit 1. // Expected: a clean "tuple type element is not a type" error at the `1`; exit 1.
@@ -8,6 +8,6 @@
#import "modules/std.sx"; #import "modules/std.sx";
main :: () -> i32 { main :: () -> i32 {
print("bad tuple type size = {}\n", size_of((i32, 1))); print("bad tuple type size = {}\n", size_of(Tuple(i32, 1)));
0 0
} }

View File

@@ -11,7 +11,7 @@
// offending name; exit 1 — NOT an LLVM verifier abort. // offending name; exit 1 — NOT an LLVM verifier abort.
#import "modules/std.sx"; #import "modules/std.sx";
pair :: () -> (i64, i64) { (1, 2) } pair :: () -> Tuple(i64, i64) { .(1, 2) }
maybe :: () -> ?i64 { return null; } maybe :: () -> ?i64 { return null; }
main :: () -> i32 { main :: () -> i32 {

View File

@@ -1,6 +1,6 @@
#import "modules/std.sx"; #import "modules/std.sx";
pair :: () -> (i64, i64) { (1, 2) } pair :: () -> Tuple(i64, i64) { .(1, 2) }
run :: () -> i32 { run :: () -> i32 {
i2, rest := pair(); // destructure name in an IMPORTED module i2, rest := pair(); // destructure name in an IMPORTED module

View File

@@ -6,7 +6,7 @@
E :: error { Bad }; E :: error { Bad };
f :: () -> (i64, !E) { raise error.Bad; } f :: () -> i64 !E { raise error.Bad; }
main :: () { main :: () {
v := f() catch e { 0 }; v := f() catch e { 0 };

View File

@@ -15,10 +15,9 @@ sum :: (s: []i64) -> i64 {
} }
main :: () -> i32 { main :: () -> i32 {
xs : List(i64) = .{}; a : [4]i64 = .[10, 20, 30, 40];
xs.append(10); mp : [*]i64 = xx @a[0]; // a genuine many-pointer (carries no length)
xs.append(20); r := sum(mp); // [*]i64 → []i64 — rejected; needs mp[0..len]
r := sum(xs.items); // [*]i64 → []i64 — needs xs.items[0..xs.len]
print("{}\n", r); print("{}\n", r);
return 0; return 0;
} }

View File

@@ -0,0 +1,13 @@
// A union struct-literal may set only ONE arm — a single direct member, or
// several promoted members of the same anonymous-struct arm. Naming two
// members that overlay the same storage is a compile error (a later store
// would otherwise silently clobber an earlier one). This guards that
// diagnostic. (Companion: examples/types/0194 covers the valid forms.)
#import "modules/std.sx";
Overlay :: union { f: f32; i: i32; }
main :: () {
o : Overlay = .{ f = 3.14, i = 7 }; // ERROR: f and i overlay the same bytes
print("{}\n", o.i);
}

View File

@@ -0,0 +1,12 @@
// Slicing a many-pointer `[*]T` requires an explicit upper bound — it carries
// no length, so an open-ended `mp[lo..]` has no bound to resolve and would
// otherwise build a garbage-length slice. This guards that diagnostic.
// (Companion: examples/types/0195 covers the valid explicit-bound form.)
#import "modules/std.sx";
main :: () -> i64 {
a : [4]i64 = .[5, 6, 7, 8];
mp : [*]i64 = xx @a[0];
s := mp[1..]; // ERROR: many-pointer slice needs an explicit hi
return s.len;
}

View File

@@ -0,0 +1,17 @@
// Writing to a `#get`-only property (no matching `#set`) is rejected with a
// clear "read-only" diagnostic — not the generic "field not found" the bare
// struct-store path would emit. (The write counterpart, a `#set`-only
// property, accepts plain assignment but rejects compound `+=` because there is
// no `#get` to read the current value.)
#import "modules/std.sx";
Reading :: struct {
raw: i64 = 0;
doubled :: (self: *Reading) -> i64 #get => self.raw * 2;
}
main :: () -> i64 {
r : Reading = .{ raw = 5 };
r.doubled = 10; // ERROR: property 'doubled' is read-only (no '#set')
return 0;
}

View File

@@ -0,0 +1,16 @@
// A branch condition (`if` / `while` / `and` / `or`) must reduce to an i1:
// its type must be a bool, integer, pointer, or optional. A struct (or float,
// etc.) has no truthiness — it used to be silently folded truthy at lowering
// then `@panic` in the LLVM backend (issue 0164). It must instead be a clean,
// located compile-time TYPE error.
//
// Negative test: locks the new diagnostic. `if <struct>` is rejected.
#import "modules/std.sx";
S :: struct { x: i64; }
main :: () -> i64 {
s : S = .{ x = 1 };
if s { return 1; } // ERROR: condition must be a bool, integer, pointer, or optional
return 0;
}

View File

@@ -0,0 +1,35 @@
// Regression (issue 0189): a non-type expression used in TYPE position must be
// rejected with a clear diagnostic — never silently resolved to a fabricated
// zero-field empty struct `{}` that ships to codegen as a real type.
//
// Two fabrication paths are covered:
//
// 1. A dotted `type_expr` / field-access (`g.a`, `g` a runtime VALUE, `a` a
// field) in type position — both the bare annotation `x : g.a = ---;` and
// the `Tuple(i32, g.a)` element form hit the same `resolveTypeWithBindings`
// dotted-name guard. A dotted name whose prefix is not a namespace alias is
// a value field access, not a qualified `pkg.Type` path → "expected a type,
// found a value".
//
// 2. A named `!E` (error-set type) whose `E` is not a declared error set —
// an undeclared name or a value name after `!` silently fabricated a `{}`
// stub via `resolveErrorType` -> `resolveNominalLeaf`. The
// `error_type_expr` arm of `checkTypeNodeForUnknown` now validates it →
// "unknown error set" (undeclared / value) or "expected an error set"
// (a declared non-error-set type).
//
// A bare `!` (the void failable channel) and a DECLARED `!E` in return position
// stay valid — exercised in examples/errors and not flagged here.
#import "modules/std.sx";
S :: struct { a: i32; }
g : S = .{ a = 1 };
main :: () -> i32 {
x : g.a = ---; // field-access value in type position
y : Tuple(i32, g.a) = ---; // same, as a tuple element
z : !Nonexistent = ---; // `!` of an undeclared name
w : Tuple(i32, !Nonexistent) = ---; // nested in a tuple
v : Closure(!Nonexistent) -> i32 = ---; // nested in a closure param
0
}

View File

@@ -0,0 +1,21 @@
// A typed array/slice literal head (`([N]T).[…]` / `([]T).[…]`) names its
// element type exactly like a declaration annotation, so an UNDEFINED element
// type name must be rejected with the same `unknown type '<name>'` diagnostic
// the declaration path emits — NOT silently compiled.
//
// Regression (issues 01730175 adversarial review): the 0173 fix taught the
// lowering's `resolveArrayLiteralType` to resolve a structural `[N]?T` head,
// but for an UNDEFINED element name the resolver returned a forward-reference
// empty-struct STUB instead of `.unresolved`. So `([2]?Undefined).[…]`
// compiled silently (exit 0, "ok") with a wrong empty-struct element, where
// `x: [2]?Undefined = ---` correctly errored. The unknown-type checker
// (`semantic_diagnostics.zig` `walkBodyTypes`) now validates the array
// literal's `type_expr` head through the same `checkTypeNodeForUnknown` walk a
// declaration uses, so a genuinely-undeclared head element name is a loud,
// located error (exit 1) — never a silent empty-struct compile or a raw panic.
#import "modules/std.sx";
main :: () {
arr := ([2]?Undefined).[ null, null ];
print("ok\n");
}

Some files were not shown because too many files have changed in this diff Show More