Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
92289c6
feat: add SSO tag helpers and slot writer to string runtime
artefactop Sep 11, 2026
0943ac8
fix: write SSO inline tag as byte 23 only, not a full cap-word store
artefactop Sep 11, 2026
1abdd71
feat: make ryo_str_free tag-aware for inline strings
artefactop Sep 11, 2026
8d60355
refactor: extract fat byte access through SSO-aware choke point
artefactop Sep 11, 2026
cc62653
fix: split SSO extraction scratch into per-operand slots
artefactop Sep 11, 2026
6eda3d8
feat: promote-on-view via __ryo_*_ensure_heap for slice/ToView
artefactop Sep 11, 2026
3abd132
feat: slot-out ABI with SSO for string producers
artefactop Sep 11, 2026
e1b80ea
feat: slot-out concat with inline results and growth headroom
artefactop Sep 11, 2026
82ad27c
feat: inline append and promotion in __ryo_str_push
artefactop Sep 11, 2026
a83f137
fix: dispatch inline slots before len-word checked_add in push
artefactop Sep 11, 2026
ee81b6b
feat: record consuming-concat selections in the ownership sidecar
artefactop Sep 14, 2026
a5d7f25
feat: in-place append for provably-consuming reassign concat
artefactop Sep 14, 2026
3eb04c9
test: drop issue-ID citation from integration_sso comment
artefactop Sep 14, 2026
4949d2f
test: bytes-family SSO parity and inline-slice stability
artefactop Sep 14, 2026
2c93a23
test: SSO mixed-representation and view-stability coverage
artefactop Sep 14, 2026
6693f3f
refactor: move runtime tests into tests.rs to meet file-length cap
artefactop Sep 14, 2026
75d0f1e
fix: promote-on-view writes the heap triple back to owner storage
artefactop Sep 14, 2026
2fef77a
fix: key field-slice freeze by field path, not struct root
artefactop Sep 14, 2026
4cef5f9
perf: skip ensure_heap extern call for heap and static slice bases
artefactop Sep 14, 2026
04588a3
test: checkpoint string benchmarks after SSO + consuming concat
artefactop Sep 14, 2026
6715319
docs: re-checkpoint inout/reuse struct benchmarks after SSO
artefactop Sep 14, 2026
a648dea
docs: align doubling_concat checkpoint table with quiet-run numbers
artefactop Sep 14, 2026
7be8f41
docs: pin promotion-repr invariant and heap-cap contracts
artefactop Sep 14, 2026
8862196
docs: record SSO known tradeoffs in eager_destruction/string_slicing
artefactop Sep 14, 2026
e4c4ebf
docs: re-verify eager_destruction crash thresholds, file I-177
artefactop Sep 14, 2026
2678a0f
docs: file I-178/I-179/I-180 from eager_destruction frame analysis
artefactop Sep 14, 2026
a01d553
docs: file I-181 for i128 pair-unpacking noise in the slice/eq path
artefactop Sep 14, 2026
c598378
docs: correct I-181 — the i128 pack is a choice, not an ABI demand
artefactop Sep 14, 2026
acddf12
fix: fresh extraction scratch slot per site — shared slots miscompile…
artefactop Sep 15, 2026
08bbb5b
test: harden review nits — benchmark README accuracy, ownership asser…
artefactop Sep 15, 2026
c6e2bf0
test: match AOT object extension in build_and_link (.obj on Windows)
artefactop Sep 15, 2026
be1271d
docs: distinguish tail position from tail-call optimization in eager_…
artefactop Sep 15, 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
44 changes: 34 additions & 10 deletions ISSUES.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions benchmarks/doubling_concat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ Measured on **macOS 26.6.2 on a MacBook Pro (Apple M3 Pro, 18 GB RAM)**, 2026-09
| **Swift** | 6.3.3 | 4.0 ms ± 0.1 ms | 1.13x slower | 34.03 MB |
| **Ryo (JIT)** | 0.1.0-dev.20260911+b3b7d25 | 4.7 ms ± 0.6 ms | 1.34x slower | 37.03 MB |

### Checkpoint: SSO + consuming concat (2026-09-14)

Re-measured after the string-runtime rework (tagged 24-byte slot: inline ≤ 23 B, heap with growth headroom, static `.rodata`; consuming reassign-concat appends in place via the push path — see `string_building`'s checkpoint). Unchanged by design: `s = s + s` uses the lhs buffer as its own suffix, and the in-place path only fires when the suffix is a *different* owner, so every doubling keeps the fresh-buffer allocating path.

| Candidate | Version | Mean time | vs fastest | Max RSS |
|---|---|---|---|---|
| **Ryo (AOT)** | 0.1.0-dev.20260915+c598378 | 3.7 ms ± 0.3 ms | 1.00x | 33.42 MB |
| **Rust** | 1.98.0 | 3.7 ms ± 0.2 ms | 1.01x slower | 35.64 MB |
| **Swift** | 6.3.3 | 4.2 ms ± 0.6 ms | 1.14x slower | 34.03 MB |
| **Ryo (JIT)** | 0.1.0-dev.20260915+c598378 | 4.8 ms ± 0.8 ms | 1.31x slower | 37.16 MB |

Measurement note: all four rows come from a single full-suite hyperfine run on 2026-09-15 (same protocol for every arm, hyperfine outlier warnings present on Swift/JIT — treat the 1.01x AOT-vs-Rust margin as a tie). The Ryo rows include the scratch-slot fix that followed this checkpoint, which does not touch the doubling path; timings match the 2026-09-14 checkpoint within noise.

## How to Run

Prerequisites: `hyperfine`, `rustc`, `swiftc`, plus a release build of the compiler (`cargo build --release` from the repository root — the script runs it for you).
Expand Down
29 changes: 23 additions & 6 deletions benchmarks/eager_destruction/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ fn recursive(x: int):
```
Because the compiler automatically inserts the cleanup call *before* the recursive call:
1. **$O(1)$ Peak Heap Memory:** Only **one** heap-allocated string is alive in memory at any given point, regardless of the recursion depth.
2. **Infinite Stack-Safety / TCO:** The recursive call is in a true tail-call position. No cleanup remains on unwind, allowing the compiler to optimize the stack frames and execute deep recursion (e.g., 80,000 calls) without crashing.
2. **Deep-Recursion Safety:** The recursive call is in a true tail-call position — no cleanup remains on unwind, so frames stay compact and deep recursion (e.g., 80,000 calls) runs without crashing. Note tail *position* is not tail-call *optimization*: current codegen still emits a normal call + return per frame, so depth remains bounded by frame size × stack size (see the checkpoint numbers below).

---

Expand Down Expand Up @@ -66,7 +66,7 @@ Because `fn1` is called before `fn2`, the string is freed instantly and `fn2` is

To allow direct comparison and capture memory (RSS) metrics across all candidates, the benchmark is configured to run at a recursion depth of **50,000** by default (the limit before Rust's stack frame overhead causes a crash on typical OS configurations).

Measurements executed on **macOS 26.6.2 (Build 25G83) on a MacBook Pro (Apple M3 Pro, 18 GB RAM)**, 2026-08-26, at **50,000** depth:
Measurements executed on **macOS 26.6.2 (Build 25G83) on a MacBook Pro (Apple M3 Pro, 18 GB RAM)**, 2026-08-26, at **50,000** depth (pre-SSO string runtime):

| Benchmark Candidate | Language | Execution Strategy | Max Resident Memory (RSS) | Memory Efficiency (vs Rust Scope-Based) | Result at 50,000 Depth |
|---------------------|----------|--------------------|---------------------------|-------------------|-------------------|
Expand All @@ -75,11 +75,28 @@ Measurements executed on **macOS 26.6.2 (Build 25G83) on a MacBook Pro (Apple M3
| **Rust (Manual Drop)** | Rust 1.98.0 | AOT Compiled (Manual `drop(s)`) | **6.80 MB** | 1.22x more efficient | **Succeeds** |
| **Rust (Scope-Based)** | Rust 1.98.0 | AOT Compiled (Scope RAII) | **8.30 MB** | 1.00x (baseline) | **Succeeds** |

### Checkpoint: SSO string runtime (2026-09-15)

Re-measured on the same machine after the SSO + consuming-concat string rework, at `0.1.0-dev.20260914+7be8f41` (hyperfine `--warmup 3 --shell=none`):

| Benchmark Candidate | Max RSS | Memory Efficiency (vs Rust Scope-Based) | Mean time | vs fastest |
|---------------------|---------|------------------------------------------|-----------|------------|
| **Ryo (AOT, Eager)** | **5.11 MB** | **1.62x more efficient** | **2.0 ms ± 0.1 ms** | **1.00x (fastest)** |
| **Ryo (JIT, Eager)** | 8.56 MB | 0.97x | 3.1 ms ± 0.2 ms | 1.54x slower |
| **Rust (Manual Drop)** | 6.80 MB | 1.22x more efficient | 3.0 ms ± 0.1 ms | 1.50x slower |
| **Rust (Scope-Based)** | 8.30 MB | 1.00x (baseline) | 3.2 ms ± 0.1 ms | 1.57x slower |

**Known tradeoff: inline storage vs deep-recursion stack footprint.** The rework changed both numbers above, in opposite directions:

- **Wall time improved.** Strings of ≤ 22 bytes (every `int_to_str` result here is 1–5 chars) now live inline in their 24-byte slot — the per-frame malloc/free pair is gone entirely. CodSpeed's profiler reads instructions **−47%** and CPU cycles **−29%** for this benchmark, and on bare metal Ryo AOT is now the fastest arm of the suite (2.0 ms).
- **RSS grew** (2.86 → 5.11 MB). `int_to_str` is now a slot-out call, so the string slot is address-taken and cannot ride in registers; each recursion frame is ~45 bytes larger, and with 50,000 frames simultaneously live that is ≈ +2.2 MB of materialized stack. Before SSO the per-frame heap block was freed before recursing and the allocator reused one hot block; now the bytes are spread across 50,000 frames. The memory-efficiency lead over Rust scope-based RAII narrows from 2.90x to 1.62x — still ahead, and still O(1) heap.
- **CodSpeed's instrumented wall-time regression (−31%) does not reproduce on bare metal.** Under CodSpeed's memory-mode environment the first-touch cost of the larger stack (memory R/W +81%, cache misses +400% — one cold line per new frame, plus minor page faults on freshly grown stack pages) dominates; hyperfine shows the opposite sign. Both readings are the same trade: strictly less work, spread over a larger footprint, in the one workload shape (50k simultaneously live frames) where that footprint is the cost.

### Key Takeaways
1. **Unrivaled Memory Performance:** Ryo's Ahead-Of-Time (AOT) compiled binary achieves the **lowest memory footprint** (2.86 MB), outperforming even Rust's manual `drop` version.
2. **Stack Safety under Deep Recursion:** While Rust **crashes with a stack overflow at exactly 74,556 recursive calls** (even with release-level optimizations `-O` and manual `drop` due to conservative LLVM tail call heuristics), **Ryo runs completely clean up to 260,000 recursive calls** (3.5x deeper than Rust) before reaching the OS stack limit.
3. **The Power of Compact Stack Frames:** In recursive scope-based RAII, Rust must keep active references, drop flags, and landing pads in each stack frame until the recursion unwinds. By contrast, Ryo's **Milestone 8.1 Eager Destruction** statically frees the string allocation *before* entering recursion, leaving the stack frame incredibly compact.
4. **Observing the Crash:** To observe the stack overflow in Rust and Ryo's stack-safety first-hand, edit the `main()` function in `eager_destruction.ryo` and `eager_destruction.rs` to change `50000` to `74556` (or higher), then re-run `./run_benchmarks.sh`. To see Ryo's extreme limits, increase its depth to `260000`.
1. **Fastest and leanest-on-heap:** Ryo's AOT binary is the fastest arm of the suite (2.0 ms, 1.50–1.57x over both Rust arms) and keeps O(1) heap — its RSS (5.11 MB) remains below both Rust variants, though SSO's larger stack frames narrowed the margin from 2.90x to 1.62x.
2. **Stack Safety under Deep Recursion:** Rust **crashes with a stack overflow just above 74,556 recursive calls** (re-verified 2026-09-15: depth 74,556 succeeds, 74,600 aborts — both the scope-based and manual-`drop` arms, even with release-level `-O`, due to conservative LLVM tail call heuristics). **Ryo runs completely clean up to ~208,000 recursive calls** (2.8x deeper than Rust) before reaching the OS stack limit. The pre-SSO build reached 260,000; SSO's larger address-taken frames lowered the ceiling, the same tradeoff behind the RSS growth above. The failure modes differ: Rust detects the overflow and aborts cleanly (`thread 'main' has overflowed its stack`, exit 134), while Ryo hits the guard page blind and dies with SIGSEGV (exit 139).
3. **The Power of Compact Stack Frames:** In recursive scope-based RAII, Rust must keep active references, drop flags, and landing pads in each stack frame until the recursion unwinds. By contrast, Ryo's **Milestone 8.1 Eager Destruction** statically releases the string *before* entering recursion — and with SSO, short strings (≤ 22 bytes) never touch the heap at all, so there is no allocation to free. One distinction matters: this puts the recursive call in true tail *position*, but tail position only makes the call eligible for tail-call optimization — current codegen still emits a normal `call` followed by `return` and materializes every frame (no tail-call lowering yet), which is exactly why the depth ceiling in takeaway #2 is finite.
4. **Observing the Crash:** To observe the stack overflow in Rust and Ryo's stack-safety first-hand, edit the `main()` function in `eager_destruction.ryo` and `eager_destruction.rs` to change `50000` to `74600` (or higher), then re-run `./run_benchmarks.sh`. To see Ryo's own limit, increase its depth past `208000`.

---

Expand Down
13 changes: 13 additions & 0 deletions benchmarks/many_small_strings/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ Measured on **macOS 26.6.2 on a MacBook Pro (Apple M3 Pro, 18 GB RAM)**, 2026-09
| **Ryo (AOT)** | 0.1.0-dev.20260911+b3b7d25 | 19.0 ms ± 0.5 ms | 1.86x slower | 1.38 MB |
| **Ryo (JIT)** | 0.1.0-dev.20260911+b3b7d25 | 20.9 ms ± 0.7 ms | 2.04x slower | 4.88 MB |

### Checkpoint: SSO + consuming concat (2026-09-14)

Re-measured after the string-runtime rework: `str` is now a tagged 24-byte slot — inline for ≤ 23-byte strings, heap with growth headroom beyond that, static `.rodata` for literals. `int_to_str(i) + "!"` is at most 8 bytes, so the per-iteration string never touches the heap: no allocation, and its free is a no-op on the inline tag.

| Candidate | Version | Mean time | vs fastest | Max RSS |
|---|---|---|---|---|
| **Ryo (AOT)** | 0.1.0-dev.20260914+75d0f1e | 9.6 ms ± 0.4 ms | 1.00x | 1.34 MB |
| **Rust** | 1.98.0 | 10.5 ms ± 0.3 ms | 1.10x slower | 1.50 MB |
| **Swift** | 6.3.3 | 10.6 ms ± 0.3 ms | 1.10x slower | 1.58 MB |
| **Ryo (JIT)** | 0.1.0-dev.20260914+75d0f1e | 11.4 ms ± 0.6 ms | 1.18x slower | 5.02 MB |

Ryo AOT went from 19.0 ms (1.86x behind Rust) to 9.6 ms — now the **fastest arm**, ahead of both Rust (10.5 ms) and Swift (10.6 ms), at the lightest RSS. A same-day re-run on a busier machine confirmed the ranking (Ryo AOT 10.7 ms vs Rust 11.5 ms, Swift 11.8 ms).

## How to Run

Prerequisites: `hyperfine`, `rustc`, `swiftc`, plus a release build of the compiler (`cargo build --release` from the repository root — the script runs it for you).
Expand Down
24 changes: 19 additions & 5 deletions benchmarks/string_building/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# String Building Benchmark

**Focus:** Runtime string ABI + eager destruction. Concat over 50,000 iterations (`s = s + "x"` — now spelled identically in Rust and Ryo): every iteration allocates a fresh buffer through `ryo_str_concat` and eagerly frees the previous one at the reassign. This is the direct before/after measure for the packed-`u128` string runtime ABI (commit `7d0a047`, return-by-value replacing the per-call-site out-pointer stack slot) — the ABI decision and its rationale are recorded on `pack_pair` in `runtime/src/lib.rs` and pinned by the `clif_string_ops_use_packed_return_no_stack_slots` integration test.
**Focus:** Runtime string ABI + eager destruction. Concat over 50,000 iterations (`s = s + "x"` — now spelled identically in Rust and Ryo). Historically every iteration allocated a fresh buffer through `ryo_str_concat` and eagerly freed the previous one at the reassign; since 2026-09-14 a provably-consuming reassign-concat appends in place with growth headroom, so the loop is amortized O(n) (see the checkpoint below). This is the direct before/after measure for the packed-`u128` string runtime ABI (commit `7d0a047`, return-by-value replacing the per-call-site out-pointer stack slot) — the ABI decision and its rationale are recorded on `pack_pair` in `runtime/src/lib.rs` and pinned by the `clif_string_ops_use_packed_return_no_stack_slots` integration test.

**Languages compared:** Rust, Swift, Ryo (AOT vs JIT), and Python.

## Why Ryo trails here: same source, different allocation policy
## Why Ryo trailed here: same source, different allocation policy

The Rust and Ryo arms are now the *identical* program — both are `s = s + "x"` in a loop — so the ~12x gap is entirely runtime semantics, not algorithm choice. Rust's `impl Add<&str> for String` **consumes the left-hand side and reuses its buffer** (documented std behavior): ownership moves into the operator, uniqueness is proven by the type system, and the append happens in place with amortized capacity growth (~17 reallocs total, O(n)). Ryo's `s = s + "x"` calls `ryo_str_concat`, which constructs a **fresh exact-size buffer every iteration**, copies the whole current string into it, and eager destruction frees the old buffer at the reassign. Iteration *i* copies *i* bytes, so the loop copies ~1.25 GB in total — that O(n²) churn is the entire gap, not codegen quality.

The sharper learning (2026-09-11): Ryo doesn't need COW refcounts to close this. The ownership pass already proves statically what Rust's type system proves — at a reassign concat the old binding is dead, and a reassignable `s` provably has no live views — so in-place append is sound for exactly this pattern. What is missing is purely allocation policy: Ryo buffers are always exact-size (`cap == len`, no growth headroom), and concat never attempts to extend the lhs buffer. This is filed as tracked work in `ISSUES.md` (the consuming-concat in-place-append entry, complementing the small-string entry that redesigns the same slot layout): route a provably-consuming `s = s + suffix` through the `__ryo_str_push`-style growth path — realloc-or-extend, copy the suffix only — turning this loop amortized O(n) with no source change. The SSO/COW roadmap work (`docs/dev/implementation_roadmap.md` → *Standard Library Allocation Optimizations*, `docs/dev/stdlib_optimizations.md`) then generalizes the win beyond the consuming case. This benchmark is the tracking measure: the gap should collapse when the entries land.
The sharper learning (2026-09-11): Ryo doesn't need COW refcounts to close this. The ownership pass already proves statically what Rust's type system proves — at a reassign concat the old binding is dead, and a reassignable `s` provably has no live views — so in-place append is sound for exactly this pattern. What was missing was purely allocation policy: Ryo buffers were always exact-size (`cap == len`, no growth headroom), and concat never attempted to extend the lhs buffer. This landed on 2026-09-14: buffers now carry growth headroom, and a provably-consuming `s = s + suffix` routes through the `__ryo_str_push`-style growth path — realloc-or-extend, copy the suffix only — turning this loop amortized O(n) with no source change. The gap collapsed to Rust parity; see the checkpoint below. The SSO/COW roadmap work (`docs/dev/implementation_roadmap.md` → *Standard Library Allocation Optimizations*, `docs/dev/stdlib_optimizations.md`) then generalizes the win beyond the consuming case.

The amortized fast path also already exists explicitly as `str_push(&s, "x")` (capacity growth via `__ryo_str_push`, `runtime/src/lib.rs:382`); this benchmark intentionally measures the concat + eager-free path (the ABI / eager-destruction measure), not the fastest way to build a string in Ryo.
The amortized fast path also exists explicitly as `str_push(&s, "x")` (capacity growth via `__ryo_str_push`, `runtime/src/lib.rs:521`); this benchmark intentionally keeps the `s = s + "x"` spelling — it measured the concat + eager-free path (the ABI / eager-destruction measure) before 2026-09-14 and now measures the provably-consuming in-place append that the same spelling lowers to, not the explicit-push idiom.

## Benchmarks & Performance Results

Expand All @@ -24,7 +24,21 @@ Measured on **macOS 26.6.2 on a MacBook Pro (Apple M3 Pro, 18 GB RAM)**, 2026-09
| **Ryo (JIT)** | 0.1.0-dev.20260911+f25e95a | 18.6 ms ± 0.4 ms | 12.95x slower | 5.73 MB |
| **Python** | 3.14.7 | 36.1 ms ± 1.4 ms | 25.11x slower | 14.75 MB |

Python (CPython 3.14.7) runs the same `s += "x"` loop interpreted; its ~25x gap over Rust is interpreter overhead, and its ~2x gap over Ryo shows the interpreted baseline is slower than Ryo's compiled O(n²) concat even before any allocation-policy fix lands.
Python (CPython 3.14.7) runs the same `s += "x"` loop interpreted; its ~25x gap over Rust is interpreter overhead, and its ~2x gap over Ryo shows the interpreted baseline is slower than Ryo's compiled O(n²) concat even before any allocation-policy fix landed.

### Checkpoint: SSO + consuming concat (2026-09-14)

Re-measured after the string-runtime rework landed: `str` is now a tagged 24-byte slot — inline for ≤ 23-byte strings, heap with growth headroom beyond that, static `.rodata` for literals — and a provably-consuming `s = s + suffix` reassign appends in place through the push path (realloc-or-extend, copy the suffix only) instead of allocating a fresh exact-size buffer per iteration. The growing string leaves the inline range almost immediately, so the win here is the consuming-concat half: the loop is now amortized O(n) with no source change.

| Candidate | Version | Mean time | vs fastest | Max RSS |
|---|---|---|---|---|
| **Rust** | 1.98.0 | 1.5 ms ± 0.2 ms | 1.00x | 1.61 MB |
| **Ryo (AOT)** | 0.1.0-dev.20260914+75d0f1e | 1.6 ms ± 0.6 ms | 1.03x slower | 1.48 MB |
| **Swift** | 6.3.3 | 2.4 ms ± 0.5 ms | 1.56x slower | 1.81 MB |
| **Ryo (JIT)** | 0.1.0-dev.20260914+75d0f1e | 2.6 ms ± 0.1 ms | 1.71x slower | 5.06 MB |
| **Python** | 3.14.7 | 36.4 ms ± 0.4 ms | 24.03x slower | 14.73 MB |

The ~12x gap is closed: Ryo AOT went from 17.7 ms to 1.6 ms, within noise of Rust (1.5 ms) — parity, as predicted above. Peak RSS dropped 2.25 → 1.48 MB, the lightest arm. A same-day re-run on a busier machine confirmed the ranking (Ryo AOT 1.7 ms vs Rust 1.7 ms).

## How to Run

Expand Down
Loading
Loading