Compare commits
55 Commits
master
...
fix/0192-q
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22f4719e83 | ||
|
|
7218280bf0 | ||
|
|
95bedf726d | ||
|
|
e52b6c9eae | ||
|
|
493469fd74 | ||
|
|
8d23aad4b9 | ||
|
|
22d5060439 | ||
|
|
69fd76b02c | ||
|
|
cc13700237 | ||
|
|
501399b1a9 | ||
|
|
6b8bce1aba | ||
|
|
df1327e316 | ||
|
|
45e69ac1bb | ||
|
|
f52e16a3fc | ||
|
|
40b5fb5f7e | ||
|
|
1dfc22794e | ||
|
|
989e18b760 | ||
|
|
c882c6c63e | ||
|
|
820cd62fa1 | ||
|
|
468461becc | ||
|
|
6c89a0aa3e | ||
|
|
95c9c0df4c | ||
|
|
097d23d909 | ||
|
|
4ca466fa96 | ||
|
|
fa7c07faf8 | ||
|
|
555ccdc024 | ||
|
|
c41f51aed3 | ||
|
|
8b613af96b | ||
|
|
58f97fff10 | ||
|
|
e5b682e622 | ||
|
|
3c738695dc | ||
|
|
3605165398 | ||
|
|
28bb101a4a | ||
|
|
5a436eddb1 | ||
|
|
2ea25e84ec | ||
|
|
0bc8005b99 | ||
|
|
3e8d003e3d | ||
|
|
2637ae98a5 | ||
|
|
7c21f84151 | ||
|
|
ff9e448f8c | ||
|
|
1b0c857b91 | ||
|
|
9523c29173 | ||
|
|
5cc45a2b38 | ||
|
|
9d3a019670 | ||
|
|
b9311e7de4 | ||
|
|
55ed9a248e | ||
|
|
1e0015d6b4 | ||
|
|
6ee4d066b3 | ||
|
|
5949a88439 | ||
|
|
1b0d640f73 | ||
|
|
62ffea0663 | ||
|
|
02ab077bfb | ||
|
|
8367ad18b1 | ||
|
|
d3944570b9 | ||
|
|
b1e06f21e3 |
@@ -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.0–B1.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`
|
||||||
|
(1800–1817). 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.0–B1.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` 1800–1820 (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 (1800–1820 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 (1800–1817), 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**.
|
||||||
|
|||||||
51
current/CHECKPOINT-LANG.md
Normal file
51
current/CHECKPOINT-LANG.md
Normal 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).
|
||||||
@@ -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 1800–1817, 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.4 — A3: `Io` impls (blocking → deterministic-sim KEYSTONE → event-loop)
|
### B1.5a — M: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.5 — A5: M:1 scheduler
|
### B1.4a — suspending 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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
66
examples/closures/0311-closures-optional-closure.sx
Normal file
66
examples/closures/0311-closures-optional-closure.sx
Normal 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)); }
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
28
examples/closures/0313-closure-inferred-return-early.sx
Normal file
28
examples/closures/0313-closure-inferred-return-early.sx
Normal 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
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
pick 7: 7
|
||||||
|
pick null: -1
|
||||||
|
h 5: 5
|
||||||
|
h null: -1
|
||||||
|
fp 8: 8
|
||||||
|
fp null: -1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
f: 11
|
||||||
|
g+: 100
|
||||||
|
g-: 200
|
||||||
|
h: 3.500000
|
||||||
@@ -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 };
|
||||||
|
|||||||
38
examples/comptime/0643-comptime-run-optional-aggregate.sx
Normal file
38
examples/comptime/0643-comptime-run-optional-aggregate.sx
Normal 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);
|
||||||
|
}
|
||||||
62
examples/comptime/0644-comptime-run-array-aggregate.sx
Normal file
62
examples/comptime/0644-comptime-run-array-aggregate.sx
Normal 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
|
||||||
|
}
|
||||||
28
examples/comptime/0645-comptime-body-local-run-bridgeable.sx
Normal file
28
examples/comptime/0645-comptime-body-local-run-bridgeable.sx
Normal 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]);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
X.a = 7
|
||||||
|
X.b = 11
|
||||||
|
Y = 5
|
||||||
|
N = -1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
1 2 3
|
||||||
|
1 2 3 4
|
||||||
|
1 2 3 4
|
||||||
|
7 8 9
|
||||||
|
10
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
N=42
|
||||||
|
S=3 4
|
||||||
|
A=10 20 30
|
||||||
|
O=2
|
||||||
79
examples/concurrency/1811-concurrency-fiber-scheduler.sx
Normal file
79
examples/concurrency/1811-concurrency-fiber-scheduler.sx
Normal 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;
|
||||||
|
}
|
||||||
64
examples/concurrency/1812-concurrency-fiber-suspend-wake.sx
Normal file
64
examples/concurrency/1812-concurrency-fiber-suspend-wake.sx
Normal 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;
|
||||||
|
}
|
||||||
82
examples/concurrency/1813-concurrency-fiber-async-suspend.sx
Normal file
82
examples/concurrency/1813-concurrency-fiber-async-suspend.sx
Normal 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;
|
||||||
|
}
|
||||||
73
examples/concurrency/1814-concurrency-fiber-sim-timer.sx
Normal file
73
examples/concurrency/1814-concurrency-fiber-sim-timer.sx
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
98
examples/concurrency/1816-concurrency-fiber-io-pipe.sx
Normal file
98
examples/concurrency/1816-concurrency-fiber-io-pipe.sx
Normal 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;
|
||||||
|
}
|
||||||
66
examples/concurrency/1817-concurrency-fiber-m1-end-to-end.sx
Normal file
66
examples/concurrency/1817-concurrency-fiber-m1-end-to-end.sx
Normal 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.0–B1.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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
18
examples/concurrency/1819-concurrency-fiber-double-wait.sx
Normal file
18
examples/concurrency/1819-concurrency-fiber-double-wait.sx
Normal 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;
|
||||||
|
}
|
||||||
124
examples/concurrency/1820-concurrency-fiber-scheduler-deinit.sx
Normal file
124
examples/concurrency/1820-concurrency-fiber-scheduler-deinit.sx
Normal 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
@@ -0,0 +1 @@
|
|||||||
|
{ "target": "macos" }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
sequence: 0 1 2 0 1 2 0 1 2
|
||||||
|
spawned: 3
|
||||||
|
done: 1 1 1
|
||||||
|
all done: 3
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "target": "macos" }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
log: 10 20 21 11
|
||||||
|
suspended-left: 0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "target": "macos" }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
sequence: 1 2 3 42 100 -99
|
||||||
|
spawned: 4
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "target": "macos" }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "target": "macos" }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
log: 2 1
|
||||||
|
clock: 0 n_suspended: 0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "target": "macos" }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
log: wrote read 3 [97 98 99]
|
||||||
|
n_suspended: 0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "target": "macos" }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "target": "macos" }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
134
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
sched: sleep(-5) — negative duration would rewind the virtual clock
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "target": "macos" }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
134
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
sched: wait() — task already has a waiter (one awaiter per task in the M:1 model)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "target": "macos" }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 0173–0175 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
Reference in New Issue
Block a user