Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ac891be
test: add I-176 regression coverage for borrowed-param slice promotion
artefactop Sep 15, 2026
ff2d9e9
feat(core): add PromoFree schedule for borrowed-param view bases
artefactop Sep 15, 2026
f2b91f3
feat(frontend): record borrowed-param view bases as promotion candidates
artefactop Sep 15, 2026
7342762
feat(frontend): schedule promotion frees at borrowed-param view death
artefactop Sep 15, 2026
8fa4866
feat(backend): promote borrowed-param view bases into a scratch slot
artefactop Sep 15, 2026
3f520bd
fix(backend): free borrowed-param promotion buffers at view death
artefactop Sep 15, 2026
4e17a44
chore: drop issue-ID citation from promo-free doc comment
artefactop Sep 15, 2026
923873f
chore: resolve I-176 (borrowed-param promotion leak)
artefactop Sep 15, 2026
e170dde
fix(frontend): defer in-loop promo anchors and cover return epilogues
artefactop Sep 15, 2026
b214168
chore: record conditional-last-use not-taken-path owner leak
artefactop Sep 15, 2026
7271e87
fix(frontend): anchor in-loop fallback promo frees at function end
artefactop Sep 15, 2026
7baf1b7
chore: record view-liveness back-edge misattribution follow-up
artefactop Sep 15, 2026
3ee4a5e
test: cover early-return and in-loop rebind promo paths
artefactop Sep 15, 2026
e63bf06
docs: add substring search example (mutable strview window)
artefactop Sep 15, 2026
80743fc
refactor(backend): fail loudly on missing promotion scratch slot
artefactop Sep 15, 2026
f6212c2
docs: document byte-wise stepping caveat in substring example
artefactop Sep 15, 2026
f2a30af
fix(frontend): free promotion buffers on implicit fallthrough exits
artefactop Sep 15, 2026
d75c05d
fix(backend): zero promotion slots in deterministic emission order
artefactop Sep 16, 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
18 changes: 12 additions & 6 deletions ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,6 @@ Resolved entries are **removed** from this file. Language-visible decisions behi
**Summary:** The parser recovers at statement boundaries by emitting `StmtKind::Error` placeholders (R10), and sema's return-flow analysis already suppresses cascading `MissingReturn` diagnostics for *sema-level* errors via the TIR `Unreachable` sentinel. The parse-error path leaks between those two mechanisms: astgen lowers `StmtKind::Error` to *nothing*, so a function whose only `return` failed to parse reaches sema with a body that genuinely ends without returning, and the user gets a bogus E0036 stacked on the real parse diagnostic (reproduced 2026-09-11: a typo'd `return Person{name=p.name, age=.age + 1}` produced E0100 at the typo *and* E0036 "missing return" on the function signature, pointing the user at the wrong place).
**Resolution:** Lower `StmtKind::Error` to a UIR error/unreachable sentinel (or have sema treat it as one) so the existing TIR `Unreachable` rule suppresses `MissingReturn` for parse-broken bodies, matching the cascade suppression sema tests already enforce for sema-internal errors. Regression test: a function whose only return statement fails to parse yields exactly the parse diagnostic, no E0036.

### I-176 — Slicing a borrowed `str`/`bytes` param whose argument is inline (SSO) leaks the promotion buffer

**Files:** `ryo-backend/src/codegen/views.rs` (`emit_ensure_heap_for_view_base`), `runtime/src/lib.rs` (`__ryo_str_ensure_heap` / `__ryo_bytes_ensure_heap`), `ryo-frontend/src/ownership/` (no free is ever scheduled for borrowed params)
**Summary:** With the tagged-slot string runtime, creating a view from an owner-typed value promotes an inline (≤ 23 B) representation to heap so the view addresses memory that never moves. Plain locals write the promoted triple back into the binding's codegen locals (freed at last use) and struct fields promote in place (the struct drop frees the field) — but a *borrowed* parameter (`fn f(s: str): v = s[0:1]`) has no scheduled free: the callee promotes its by-value copy of the caller's inline triple into a fresh heap buffer that nothing owns. Reproduced under Valgrind (16 bytes definitely lost per call) with `fn scan(s: str): v = s[0:1]; print(v)` called on an `int_to_str` argument. Heap and static arguments are unaffected (promotion no-ops; the view borrows the caller's buffer). The naive fix is unsound: at any free site a callee-promoted buffer is indistinguishable from a caller-owned heap buffer — both are plain `(ptr, len, cap)` triples — so freeing the param would double-free caller memory whenever the argument was heap.
**Resolution:** The ownership pass already computes view liveness for the P2 freeze; use it to schedule a free at the view's last use when a slice/`ToView` base is a borrowed (non-`inout`) `str`/`bytes` param, and make the promotion distinguishable at runtime — e.g. promote through a callee that reports whether it allocated, or route borrowed-param view bases through `ryo_str_from_view`-style materialization as a tracked temporary owner instead of in-place promotion. Regression test: the repro above must come out Valgrind-clean.

### I-177 — AOT binaries die with SIGSEGV on stack overflow; no guard-page detection or diagnostic

**Files:** `ryo-backend/src/codegen.rs` (function prologue emission), `ryo-backend/src/linker.rs` (link-time stack size / guard-page setup), `runtime/src/` (no stack-limit check or signal handler exists)
Expand All @@ -191,6 +185,12 @@ Resolved entries are **removed** from this file. Language-visible decisions behi
**Summary:** When a call sits in true tail position — no pending drops, which is exactly what eager destruction arranges — codegen still emits a normal `call` followed by `return`, so every recursion frame is materialized and maximum depth is frame size × stack size. Measured 2026-09-15 on `benchmarks/eager_destruction`: ~80 B/frame → SIGSEGV at ~208k frames on an 8 MB stack, with the wall time dominated by first-touch cache misses and page faults on the ever-growing stack (CodSpeed: cache misses +400%, memory R/W +81%, while instructions fell −47%). Cranelift supports explicit `return_call` on aarch64/x86_64; emitting it for tail-position calls reuses the frame, giving O(1) stack, unbounded tail recursion, and collapsing those first-touch misses. The benchmark README already claims the tail-position story ("allowing the compiler to optimize the stack frames") — the compiler does not deliver it yet; Cranelift never performs tail-call optimization on its own.
**Resolution:** In codegen, detect calls in tail position with no drops scheduled after them and emit Cranelift `return_call` instead of `call` + `return`. Requires the caller/callee signatures to satisfy `return_call` constraints, the ownership pass to guarantee no frees are pending after the call, and a CLIF-level test: `return_call` present for a self-tail-call after eager destruction, absent when a drop follows. Tail calls remove the overflow only for tail-recursive code — the guard-page diagnostic for non-tail recursion is still needed separately.

### I-182 — Owner free leaks on the not-taken path of a conditional last use when the taken arm returns

**Files:** `ryo-frontend/src/ownership/mod.rs` (the `branch_may_not_return` conditional-last-use re-anchor in the last-use Free pass, ~:519-529)
**Summary:** When an owner's last read sits inside an if-arm that `return`s, the conditional-last-use re-anchor deliberately keeps the in-arm anchor (moving the Free to the branch exit would leave it unreachable on the return path). But the in-arm anchor then never fires on the NOT-taken path — the owner is still alive there and leaks its buffer. Confirmed with a control experiment: a heap-owning local whose only read is inside a returning if-arm leaks 32 B per call on the fall-through path (steady RSS growth), while the taken path frees correctly. The borrowed-param view-promotion frees inherited the same shape (a view whose last use is in a returning arm, or whose anchor is bypassed by an early return, leaked its promotion buffer); that side is covered by the promotion-free return epilogue, but the underlying owner-Free gap this entry tracks is pre-existing and orthogonal.
**Resolution:** For a conditional last use whose anchor arm may return, schedule the Free twice: keep the in-arm anchor (covers the taken path up to the return, alongside the return epilogue) AND add a branch-exit anchor gated to the arms that fall through (the `branch` field / arm-gated emission the `ConditionalDeadDrop` machinery already uses), so the not-taken path frees at the merge. Verify against the existing `last_use_in_if_fallthrough` and conditional-move Valgrind fixtures plus a new fixture pairing a returning arm with a live fall-through path.

---

## 🟢 Cleanup
Expand Down Expand Up @@ -405,6 +405,12 @@ Resolved entries are **removed** from this file. Language-visible decisions behi
**Summary:** Slice results and literal values are packed `(ptr, len)` pairs represented as i128, so extracting one half is a 128-bit shift — which Cranelift legalizes into a ~9-instruction funnel-shift/select sequence (`lsr`/`lsl`/`orr`/`csel`) instead of the register move it already is. Disassembly of `benchmarks/string_slicing`'s `count_fox` (aarch64, 2026-09-15): two such sequences per scan iteration, one to unpack the `__ryo_slice` result and one to unpack the literal — ~18 wasted instructions × 700k iterations ≈ 12.6M instructions, on top of the extern-call overhead tracked separately. Inlining the slice/eq bodies will not remove this if the values keep flowing as i128.
**Resolution:** Stop representing small pair values as packed i128 end to end. The C ABI does not require it: on aarch64/x86-64 SysV a `u128` return and a `#[repr(C)]` two-`u64` struct return occupy the same two registers, so changing the runtime signatures (`__ryo_slice` :427, `ryo_str_from_literal` :358, both currently `-> u128` via `pack_pair` :270) to return a repr(C) pair — and modeling views as two i64 SSA values in TIR/codegen — is machine-identical at the boundary while eliminating the i128 type that triggers the legalization. Verify Cranelift maps the two-register struct return correctly on the Windows x64 target (different struct-return convention there) before committing to the signature change.

### I-183 — View-liveness back-edge merge is one-pass first-wins; reads inside a loop are attributed to the pre-loop slice

**Files:** `ryo-frontend/src/ownership/views.rs` (`collect_view_liveness` / `view_liveness_loop_body` back-edge merge :603-621), `ryo-frontend/src/ownership/mod.rs` (promo scheduling fallback that compensates :700-731)
**Summary:** The view-liveness pre-pass walks a loop body once and merges back-edge bindings first-wins, so a read of a view binding inside or after a loop is attributed to the binding's *pre-loop* slice inst, leaving an in-loop rebinding slice with no recorded last use. Consumers of `view_last_use` that release memory must compensate conservatively: the promotion-free scheduler anchors in-loop bound-never-read candidates at function end (over-liveness, sound) instead of at the true last read. The pre-loop slice's buffer can likewise be kept alive past its real last use.
**Resolution:** Rewrite the loop-body liveness walk as a fixpoint (re-walk until `last_use` assignments converge) so post-loop and in-loop reads attribute to the most recent slice inst. Once attribution is exact, tighten the promotion-free fallback from the function-end anchor back to the true last use.

---

## Cross-References
Expand Down
43 changes: 43 additions & 0 deletions examples/substring_search.ryo
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Substring search — sliding a mutable strview window over a buffer.
# Shows the projection pattern at work: scanning is zero-copy (views
# borrow the owner's storage, never copy), and `mut` lets one name
# track the current position as it advances.
#
# Note: `rest[1:]` steps one byte at a time, which is exact for ASCII
# text. With multibyte UTF-8 the byte index can land inside a
# character, and slicing at a non-char-boundary index panics — proper
# code-point iteration comes with the planned `utf8` module.

# Return the index of the first occurrence of `needle` in `haystack`,
# or -1 when there is no match. An empty needle matches at index 0.
fn find(haystack: strview, needle: strview) -> int:
if needle.len() == 0:
return 0
# `rest` is the unsearched remainder: one mutable view, re-sliced a
# byte at a time. Views are read-only borrows, so the whole scan
# allocates nothing.
mut rest = haystack[:]
mut index: int = 0
while rest.len() >= needle.len():
if rest[0:needle.len()] == needle:
return index
rest = rest[1:]
index += 1
return -1

fn main():
text: str = "the quick brown fox jumps over the lazy dog"

print(int_to_str(find(text, "fox"))) # 16
print("\n")
print(int_to_str(find(text, "dog"))) # 40
print("\n")
print(int_to_str(find(text, "cat"))) # -1
print("\n")
print(int_to_str(find(text, ""))) # 0 — empty needle matches at the start
print("\n")

# Owned str and strview both pass to strview parameters — no copies.
word = text[16:19]
print(int_to_str(find(word, "o"))) # 1
print("\n")
4 changes: 2 additions & 2 deletions ryo-backend/src/codegen/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ impl<M: Module> Codegen<M> {
/// rather than `last() == Some(&b)` so a Free anchored to a
/// parent arm still fires when codegen is inside a nested child
/// arm of that parent.
fn branch_active(
pub(crate) fn branch_active(
branch: Option<ryo_core::ownership::BranchId>,
stack: &[ryo_core::ownership::BranchId],
) -> bool {
Expand Down Expand Up @@ -551,7 +551,7 @@ impl<M: Module> Codegen<M> {
/// (`ryo_bytes_free` when `is_bytes`, else `ryo_str_free`).
/// Resolved only at call sites that survive the cap==0 elision, so
/// an all-static schedule never declares an unused import.
fn free_ref_for(
pub(crate) fn free_ref_for(
builder: &mut FunctionBuilder,
ctx: &mut FunctionContext<'_, M>,
str_free_ref: &mut Option<FuncRef>,
Expand Down
Loading
Loading