Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2301d97
fix(codegen): P0 — case-arm rebinding the scrutinee's name emitted C …
claude Jun 9, 2026
31208a0
fix(codegen): P0 — compiled tail loops never yielded; spawn storms di…
claude Jun 9, 2026
273cd78
fix(runtime): P1 — allocation failure now panics loud instead of SIGS…
claude Jun 9, 2026
9c0a409
docs: Round-7 audit report (Fable, June 2026) + two new known issues …
claude Jun 9, 2026
d8bf46f
fix(lang): converge the two execution paths + make every failure loud
claude Jun 9, 2026
47e2015
test: dual-path conformance gate — every conform program must agree a…
claude Jun 9, 2026
bb52969
docs: error-model + new-surface reference; KNOWN_ISSUES — clear try/c…
claude Jun 9, 2026
48bd46f
feat(std): Std.map / Std.filter / Std.join — make the Elixir-shaped g…
claude Jun 9, 2026
fda588a
docs: README — conformance gate + current test counts + Std aliases; …
claude Jun 9, 2026
a47f14d
feat(gc): Phase 2.1 — LeakSanitizer lifecycle gate (make lsan-gate + …
claude Jun 9, 2026
2239288
feat(diag+lang): real source paths, module-qualified did-you-mean, st…
claude Jun 9, 2026
0e4337e
test+docs: Phase 2.2 scheduler-count matrix — bounded tests, S<3 skip…
claude Jun 9, 2026
76c44e0
perf(sched): O4 — bounded spin-before-park; cross-scheduler ping-pong…
claude Jun 9, 2026
df57d3c
docs: roadmap — Phase 2.1 + 2.2 DONE with findings; Round-7 table — O…
claude Jun 9, 2026
3f3b2bf
perf(boot): O5 — watchdog shutdown via condvar; hello wall time 110-1…
claude Jun 10, 2026
35283f6
test: spin-wedge reproducer + gc_slope probe timeout armor
claude Jun 10, 2026
78931d0
docs: P1 spin-gated deadlock entry; roadmap 2.2 amended; README start…
claude Jun 10, 2026
8603514
fix(runtime): three C11 data races flagged by TSan — volatile flags b…
claude Jun 10, 2026
92813f0
test: Phase 2.3 — make tsan-gate (msg ping-pong, msg+spin, 80k storm …
claude Jun 10, 2026
831d6ce
docs: roadmap — Phase 2.3 (TSan) DONE: gate + findings + the no-fiber…
claude Jun 10, 2026
437adc1
test+docs: O8 host-limit prints in the stress gate; Phase 2.4 audit r…
claude Jun 10, 2026
e8ddea1
fix(sched): P1 deadlock root-caused — Dekker StoreLoad bug in the rec…
claude Jun 10, 2026
4d2a692
docs: P1 deadlock -> Recently cleared with the full Dekker story; roa…
claude Jun 10, 2026
c635b5e
fix(parser): untrusted .sw can no longer OOM swc — receive-clause spi…
claude Jun 10, 2026
38a817a
fix(runtime): Phase 2.4 — race-free proc->state, timer peek, monitor-…
claude Jun 10, 2026
30e5497
fix(runtime): Phase 2.4 COMPLETE — supervisor/registry/monitor races …
claude Jun 10, 2026
f98288d
feat(gc): Phase 2.5 — allocation-failure injection gate (make alloc-f…
claude Jun 10, 2026
a72e32a
feat(soak): Phase 2.6 — mixed-workload soak harness (make soak); Phas…
claude Jun 10, 2026
ec1ded6
fix(json): Phase 3 — fuzz the JSON decoder; fix two heap-overflows + …
claude Jun 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ examples: swc libswarmrt
# pass/fail counts across all files. See tests/sw/run_tests.sh.
test-sw: swc libswarmrt
@./tests/sw/run_tests.sh
@./tests/sw/run_conform.sh

# Security regression: the curl-backed HTTP builtins must not pass
# caller-supplied URLs / headers through a shell. Builds the injection
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ The [`lib/`](lib/) directory ships modules that auto-resolve via `import` — no

| Module | What it gives you |
|---|---|
| `Std` | List / map / string helpers (range, take, drop, nth/at, zip, partition, sort, unique, find, any, all, sum, product, group_by, chunk_every, intersperse, …) — see `lib/Std.sw` for the full list. |
| `Std` | List / map / string helpers (map, filter, join, range, take, drop, nth/at, zip, partition, sort, unique, find, any, all, sum, product, group_by, chunk_every, intersperse, string_join, …) — see `lib/Std.sw` for the full list. `map`/`filter`/`reduce` also exist as global builtins; `Std.map`/`Std.filter`/`Std.join` work too. |
| `Mcp` | Model Context Protocol client + server (JSON-RPC over stdio) |
| `Embed` | Embeddings client for any OpenAI-compatible `/v1/embeddings` endpoint |
| `Vec` | ETS-backed cosine-similarity vector store (`Vec.new / add / search / size`) |
Expand Down Expand Up @@ -351,7 +351,7 @@ make test-full # the comprehensive gate: core + OTP + phases 2-10 + search
- **Compiled** — each `test_*.sw` is compiled with `swc build` and the resulting binary is run.
- **Interpreter** — `tests/sw/repl/test_*.sw` files are run via `swc test` (tree-walking interpreter). Guards against the REPL/codegen builtin drift that the May 2026 marathon closed.

Together the suite reports `all sw tests passed — 53 files, 475 assertions`.
Together the suite reports `all sw tests passed — 56 files, 493 assertions`, and `make test-sw` then runs the **dual-path conformance gate**: every program in `tests/sw/conform/` executes under BOTH `swc run` (interpreter) and `swc build` (compiled) and must produce byte-identical stdout and exit codes — the structural guard against the two paths drifting apart.

Add a `test_<topic>.sw` file in either directory and it'll be picked up automatically.

Expand Down Expand Up @@ -428,7 +428,7 @@ Stable enough to be the substrate for [swarm-code](https://github.com/skyblanket
**What CI gates on, every push:**
- README quickstart (`counter.sw`) + a few more example programs (`hello.sw`, `lambda.sw`)
- `bash scripts/check_sw_docs.sh` — **doc-compile tripwire**: every complete ```sw block in the docs and every runnable `examples/*.sw` must still compile with this `swc`
- `make test-sw` — **53 files, 475 assertions** (`.sw` language: compiled + interpreter + `swc run` paths)
- `make test-sw` — **56 files, 493 assertions** (`.sw` language: compiled + interpreter + `swc run` paths) **plus the dual-path conformance gate** (`tests/sw/conform/` — interpreter and compiled output must be byte-identical per program)
- `make test-phase$p` for `p` in **2 through 10** — C-side runtime tests: GenServer/Supervisor (phase 2), ETS (phase 3), Agent/App/DynSup (phase 4), StateMachine/ProcessGroup (phase 5), TCP (phase 6), hot reload (phase 7), GC scaffolding (phase 8), distribution (phase 9), language frontend (phase 10); the **deadlock watchdog** runs automatically in every test (active by default in the runtime)
- `make stress` — high-process-count race guard (multi-scheduler + single-scheduler spawn storm); every run must complete
- `make gc-stress` — GC v1 copy-on-escape correctness: the value-arena stress harness compiled with ASAN + `-DSW_ARENA_POISON`; a missed deep-copy on any send/spawn/ETS boundary surfaces as a use-after-free or a `0xDE`-garbage content assert
Expand Down
32 changes: 28 additions & 4 deletions docs/SW_LANGUAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ fun greet(name) {
- Body is one or more expressions; the last expression's value is the return value.
- Parameters are positional; default values: `fun greet(name = "world") { ... }`.
- No explicit `return` — Erlang-style trailing-expression-is-the-value.
- Recursion is the loop construct (no `for`/`while`). Tail calls are detected and optimised by the codegen, so unbounded tail recursion doesn't blow the stack.
- Recursion is the loop construct (no `for`/`while`). **Self**-tail-calls are detected and optimised by the codegen, so unbounded `f -> f` tail recursion doesn't blow the stack. **Mutual** tail recursion (`a -> b -> a`, e.g. two state functions calling each other) is NOT yet optimised — each hop costs a C stack frame and deep chains overflow the 128KB process stack. Keep the recursive call in the same function (dispatch on an argument instead of bouncing between functions) for unbounded loops.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix loop wording to avoid contradicting supported syntax.

Line 106 says there is no for/while, but for is supported and documented in this reference. Please narrow this to “no while” (or equivalent) so users don’t get incorrect guidance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/SW_LANGUAGE.md` at line 106, Update the sentence that currently reads
"Recursion is the loop construct (no `for`/`while`)" to accurately reflect
supported syntax by removing `for` from the negation—e.g. change it to
"Recursion is the loop construct (no `while`)"—and keep the rest of the
paragraph about self-tail-call optimisation and mutual tail-recursion unchanged;
target the sentence containing "Recursion is the loop construct (no
`for`/`while`)" so the docs no longer incorrectly state that `for` is
unsupported.


```sw
fun count_down(n) {
Expand Down Expand Up @@ -256,7 +256,7 @@ receive {
- Each arm is `pattern -> body`.
- `_` matches anything; specific tuples / atoms / values match exactly.
- Bound names (`prompt`, `reply_to`) capture parts of the message.
- `after MS { body }` fires if no message arrives within MS milliseconds.
- `after MS { body }` fires if no message arrives within MS milliseconds. The Erlang-style arrow form `after MS -> body` is accepted too (the body runs to the receive's closing brace — `after` is always the last clause).
- Selective receive: messages that don't match any arm STAY in the mailbox for the next `receive`.

Inside receive arm bodies, `;` DOES separate statements (it's a recognised statement separator within arm bodies).
Expand All @@ -267,6 +267,20 @@ Inside receive arm bodies, `;` DOES separate statements (it's a recognised state

Patterns appear in `receive` arms, `case` expressions, and some other binding contexts. Supported:

### Tuple-destructuring assignment

A statement may bind several names from a tuple at once:

```sw
{a, b} = {1, 2} # a=1, b=2
{x, _, z} = three_tuple() # `_` skips an element
{'ok', body} = http_fetch(url) # literal positions ASSERT-match (panic on
# {'error', _}), ident positions bind
{200, html} = fetch_page() # ints/strings/floats assert too
```

Statement position only (not inside a larger expression). Elements may be identifiers (bind), `_` (skip), or literal atoms/ints/floats/strings (match-assert with a `destructuring mismatch` panic — the Erlang `=` contract for the `{'ok', v}` idiom). A too-short or non-tuple right side panics through `elem`'s existing range/type check; extra trailing elements are tolerated. It desugars at parse time (temp + `elem()` binds + `expect()` asserts), so `swc run` and compiled binaries behave identically. List and nested-tuple left sides are not supported — use `case` for those.

| Pattern | Matches |
|---|---|
| `42`, `"foo"`, `'ok'`, `nil` | exact literal |
Expand Down Expand Up @@ -346,6 +360,16 @@ fun main() {

`with` desugars at parse time into nested `case`, so it has the same pattern-matching power (tuples, maps, lists, literals) and the exact same behavior in `swc run` and a compiled binary. There are no per-bind `when` guards — match a pattern instead. A single bind (`with p <- e { ... } else { o -> ... }`) is fine; it's just a one-arm chain.

The `else` block takes one or more `pattern [when guard] -> body` arms, exactly like `case` — the first non-`{'ok', _}` value falls through them in order:

```sw
} else {
{'error', k} when k == 'host' -> "no host configured"
{'error', k} -> f"missing: {k}"
_ -> "unknown failure"
}
```

---

## 8. Processes and message passing
Expand Down Expand Up @@ -801,7 +825,7 @@ r = try {
}
```

`error(msg)` sets a thread-local error sentinel that `try { ... } catch e { ... }` catches. Outside a `try`, `error()` is silent (the calling code continues with `nil`), which makes try/catch the explicit "I want to handle failure" marker.
`error(msg)` aborts the rest of the `try` body and lands in the nearest enclosing `catch` — through function calls too: an `error()` raised inside a callee unwinds to the caller's `catch`, and the statements after the raise do not run. Outside any `try`, `error()` is silent (the calling code continues with `nil`), which makes try/catch the explicit "I want to handle failure" marker. Identical in `swc run` and compiled binaries (gated by `tests/sw/run_conform.sh`).

### Unrecoverable failures: `panic` + `expect`

Expand All @@ -825,7 +849,7 @@ panic: hit the bottom
[3] Trace.main at src/Trace.sw:16
```

`expect(value, msg)` is the idiomatic "unwrap" pattern — passes the value through if non-nil, otherwise panics with `msg`. Saves the explicit `if (x == nil) { panic(...) }` boilerplate.
`expect(value, msg)` is the idiomatic "unwrap" pattern — passes the value through unless it is **nil or `'false'`**, in which case it panics with `msg`. Saves the explicit `if (x == nil) { panic(...) }` boilerplate, and `expect(a == b, msg)` works as an assert (comparisons return `'true'`/`'false'`).

### Builtins that panic (instead of returning nil silently)

Expand Down
52 changes: 48 additions & 4 deletions docs/notes/KNOWN_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,30 @@ the divergence is the issue.
when compiled. **Workaround:** add an explicit `after MS -> ...` clause to any
`receive` that might not match, so compiled and interpreted runs behave the same.

### Interpreter recursion depth is bounded (no TCO in the tree-walker)

The interpreter (`swc run` / REPL / `swc test`) evaluates on the C stack with
a stack-margin guard and no tail-call optimisation; deep recursion raises a
clean, uncatchable `interpreter recursion depth exceeded` panic (exit 1). The
exact ceiling is environment-dependent (real stack headroom is measured);
assume a few hundred frames. Compiled binaries TCO self-tail-calls to
unbounded depth (gated by `tests/sw/test_tco_depth.sw`).

**Impact:** recursion-heavy programs must be compiled. **Workaround:**
`swc build` — the interpreter is for short scripts, tests, and the REPL.

### Mutual tail recursion is not TCO'd; deep chains overflow silently

Only **self** tail calls are optimised. Two functions tail-calling each other
(the natural state-machine shape) consume a C stack frame per hop and overflow
the 128KB fiber stack at depth ~10^4–10^5 — currently as a raw SIGSEGV without
even the crash banner (the handler runs on the overflowed fiber stack; needs
`sigaltstack`). See [REVIEW_FABLE_2026-06.md](REVIEW_FABLE_2026-06.md), O2.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

**Impact:** mutually recursive FSMs die at depth in compiled binaries.
**Workaround:** keep the loop in one function and dispatch on an argument
(`fun fsm(state, n) { case state { ... } }`).

### No static type or shape checking

`sw` is dynamically typed by design — there is no compile-time type or arity
Expand All @@ -35,12 +59,32 @@ model), recorded here so the behavior is not a surprise.

## Recently cleared

### `try/catch` caught builtin panics in the interpreter but not compiled

Cleared on 2026-06-09 (Round-7 follow-up). The error model is now identical
on both paths and gated by the dual-path conformance runner
(`tests/sw/run_conform.sh`, wired into `make test-sw`):

- `error()` unwinds the full dynamic extent to the nearest `try` — an
`error()` raised inside a callee lands in the caller's `catch`, and the
statements after the raise do NOT run. (Compiled: per-process
setjmp/longjmp chain, `sw_self_try_chain`; the old codegen ran the whole
try body and tested the sentinel once at the end, so the statement after
an `error()` still executed — and could panic the process before the
catch was consulted.)
- `error()` outside any `try` stays the documented silent
continue-with-nil on both paths (the interpreter previously unwound the
entire rest of the program, silently).
- Panics (builtin panics, `panic()`, failed `expect()`) are uncatchable on
both paths: `try/catch` does not absorb them, the run exits 1.
`assert_raises` remains the sanctioned test-only interceptor.

### No `swc run` subcommand

Cleared. `swc run file.sw` exists (parse → codegen → `cc` → run the resulting
binary in one shot; see `run_file` in `src/swc.c`). The interpreter path
(`swc run`, REPL, `swc test`) shares the runtime's builtins with the compiled
path.
Cleared. `swc run file.sw` exists and runs the tree-walking interpreter
(parse → merge imports → interpret `main()`; see `run_file` in `src/swc.c`).
The interpreter path (`swc run`, REPL, `swc test`) shares the runtime's
builtins with the compiled path.

### Multi-head cons patterns are unimplemented

Expand Down
Loading
Loading