Skip to content

Commit 2d2fe51

Browse files
Haofeiclaude
andcommitted
docs+stdlib: backend matrix, JIT helper contract, File effect annotations; trim BUGS.md to open items
- README: add an "Execution backends" matrix (frontend check / Rust lowering / register-VM / tier-0 JIT / native JIT), stating they are not equivalent and that the VM tiers fail closed; correct `rss eval` description (register VM, not tree-walk). - docs/jit-semantics-ledger: document the host-helper contract — what helpers may read, handle validity/lifetime, float parity, and the fallback observational- equivalence proof. - stdlib/fs/file.rssi: add `effects(native)` to File handle reads/writes (read_all, read_into, write, write_string, …) to match DbConnection/TempDir. - BUGS.md: remove fixed items (RSS-1…10, 12, 13, 15, borrowed-match), leaving only the open deferred RSS-11 (cubic generic substitution) with its scoping note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c577d29 commit 2d2fe51

4 files changed

Lines changed: 75 additions & 275 deletions

File tree

BUGS.md

Lines changed: 14 additions & 271 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,9 @@
11
# RSScript (rss) — known compiler bugs
22

3-
Found by a white-box audit of the compiler on **2026-06-10**, reproduced against the
4-
release binary built from commit `f5fab92` (`target/release/rss`). Every item below was
5-
reproduced; the HIGH items were additionally re-verified by hand.
3+
Open issues only. Fixed items have been removed once verified across `eval`, `run`,
4+
and `run --release` with the test suite green; see git history for their write-ups.
65

7-
> **Status (2026-06-10, re-audit + fix):** all 11 items were re-verified against a fresh
8-
> build. **RSS-1 … RSS-10 are fixed** and confirmed across `eval`, `run`, and
9-
> `run --release`, with the full `cargo test` suite still green. **RSS-11** (LOW,
10-
> compile-time perf only) is confirmed valid but **deferred** — see its entry.
11-
>
12-
> Repro notes found during the re-audit:
13-
> - **RSS-5 / RSS-10:** the original single-line struct bodies (`{ first: T second: T }`)
14-
> additionally tripped a field-separator quirk that masked the documented symptom; with
15-
> idiomatic newline-separated fields both bugs reproduce exactly as described and are fixed.
16-
> - Separately discovered (not one of the 11): matching on a borrowed
17-
> `read Option<T>`/`Result<…>` param and using the bound payload by value
18-
> (`match o { Some(s) => return s }`) lowered to `&T` and failed rustc E0308.
19-
> **FIXED:** the lowerer now rebinds a by-ref match's single payload to an owned
20-
> value — `*x` for a `Copy` payload, `x.clone()` for any other cloneable type
21-
> (resources, which aren't `Clone`, stay borrowed and are rejected by the resource
22-
> move rules). Covers built-in `Option`/`Result` and single-field user variants.
23-
> Regression test: `vm_eval_parity::parity_borrowed_match_payload_used_by_value`.
24-
25-
Run recipe used for all repros:
6+
Run recipe used for repros:
267

278
```sh
289
cd rsscript
@@ -34,172 +15,6 @@ $BIN run --release file.rss # AOT: lower to Rust -> cargo build -> run
3415
$BIN eval file.rss
3516
```
3617

37-
The recurring theme: **the interpreter, `check`, and the AOT Rust-lowering path disagree.**
38-
The green cargo suite does not exercise the `eval` / `run` / `run --release` triad, operator
39-
precedence trees, or borrow/clone coercion on assignment — which is where every HIGH bug lives.
40-
41-
---
42-
43-
## HIGH
44-
45-
### RSS-1 — [FIXED] — Prefix `!` and `~` bind looser than every binary operator (silent wrong result)
46-
- **Class:** wrong result (no error; both backends agree on the wrong parse)
47-
- **Source:** `crates/rsscript/src/syntax/parser.rs:2464` (`parse_unary_expr` is tried before
48-
`parse_binary_expr`), `:2960-2968` (`unary_operand_range` extends the operand to the rest of
49-
the expression)
50-
- **Root cause:** prefix `!`/`~` are parsed before binary operators and their operand is
51-
extended to the entire remaining expression, so they bind *looser* than every binary op — the
52-
inverse of all C-family languages. `!a && b` parses as `!(a && b)`; `~a + b` as `~(a + b)`.
53-
54-
```rust
55-
// not_and.rss
56-
fn main() -> Unit {
57-
let a = false
58-
let b = false
59-
let r = !a && b
60-
if r { Log.write(message: read "T") } else { Log.write(message: read "F") }
61-
}
62-
```
63-
- **Expected:** `(!a) && b` = `true && false` = `F`. **Actual:** prints `T` (parsed `!(a && b)`).
64-
Also `~a + b` with a=0,b=1 prints `-2` (correct `(~0)+1` = `0`). Wrapping the prefix operand in
65-
parens gives the right answer, proving the mis-grouping.
66-
- **Fix:** give prefix `!`/`~` precedence above all binary operators.
67-
68-
### RSS-2 — [FIXED] — Integer overflow silently wraps in release AOT but traps everywhere else
69-
- **Class:** soundness / cross-backend divergence
70-
- **Source:** `crates/rsscript/src/rust_lower/lowerer.rs` Add/Sub/Mul emit bare `+`/`-`/`*`; the
71-
generated release profile sets no `overflow-checks = true`. Sibling: `crates/runtime/src/math.rs`
72-
`Math.pow` uses `i64::pow` not `checked_pow`.
73-
- **Root cause:** reg_vm uses `checked_*` and traps; the AOT lowerer emits plain Rust arithmetic
74-
and ships a release profile with overflow checks off.
75-
76-
```rust
77-
// ovf.rss
78-
fn bump(x: Int) -> Int { return x + 1 }
79-
fn main() -> Unit {
80-
let a = 9223372036854775807
81-
let b = bump(x: a)
82-
Log.write(message: read Int.to_string(value: read b))
83-
}
84-
```
85-
- **Expected:** all modes agree (trap). **Actual:** `eval` → "integer addition overflow…";
86-
`run``panic: attempt to add with overflow`; **`run --release` → prints `-9223372036854775808`
87-
(exit 0)**. Breaks the documented `interp == compiled` invariant; release builds compute wrong
88-
finite values where every other mode errors.
89-
- **Fix:** lower `+ - *` to `wrapping_*`/`checked_*` (as `<<` already uses `wrapping_shl`), or set
90-
`overflow-checks = true` in the generated release profile; route `Math.pow` through `checked_pow`.
91-
92-
### RSS-3 — [FIXED]`read`-param / borrowed RHS not cloned on assignment → non-compiling Rust (E0308)
93-
- **Class:** miscompile (valid program won't compile under AOT)
94-
- **Source:** `crates/rsscript/src/rust_lower/lowerer.rs:1865-1869` (`Stmt::Assign` emits
95-
`target = lower_expr(value)` with no clone coercion; the `let`-init path at `:1601-1613` *does*
96-
clone). Aliased-view variant: `let_init_is_clonable_read_param_ref` at `:3915-3941` (returns
97-
false for `read_view_bindings`, `:3922`).
98-
- **Root cause:** the clone-coercion fix that lets `let mut x = <read-param>` own a `T` was applied
99-
only to the `let`-initializer path, not to plain assignment (`x = b`) nor to read params reached
100-
through an alias. Those lower to `chosen: Vec<i64> = b /*&Vec<i64>*/`, which rustc rejects.
101-
102-
```rust
103-
// pick.rss
104-
fn pick(a: read String, b: read String, useB: read Bool) -> String {
105-
let mut s = a
106-
if useB { s = b }
107-
return s
108-
}
109-
fn main() -> Unit {
110-
Log.write(message: read pick(a: read "AA", b: read "BB", useB: read true))
111-
}
112-
```
113-
- **Expected:** prints `BB`. **Actual:** `check` ok, `eval` prints `BB`, **`run` fails** with
114-
`error[RS1101] … mismatched types` (rustc E0308 at `s = b`). Hits `String`, `List<T>`, any
115-
non-Copy struct. (This is the "known open" let-mut bug from the port TODO — still live, plus the
116-
new aliased-view variant.)
117-
- **Fix:** apply the same `.clone()` coercion in `Stmt::Assign` lowering; also clone when the RHS is
118-
a read-view alias (`:3922`).
119-
120-
### RSS-4 — [FIXED] — Untyped `Some(...)` / `Ok(...)` local bypasses argument type checking (false-accept)
121-
- **Class:** soundness (false-accept → backend E0308)
122-
- **Source:** `crates/rsscript/src/hir.rs:2431-2459` (`infer_hir_expr_type``None` for
123-
`CallResolution::EnumVariant`), `:1422-1446` (`Stmt::Let` records `type_name=None`),
124-
`crates/rsscript/src/checks/calls.rs:1330-1345` (arg check skipped when actual type is `None`).
125-
- **Root cause:** `let o = Some(5)` gets no inferred type, so when `o` is passed where
126-
`Option<String>` is required, the arg check is skipped and `Option<Int> → Option<String>` is
127-
accepted. rustc then rejects the lowered Rust. The inline form `describe(o: read Some(5))` *is*
128-
caught (RS0207) — only the untyped let escapes.
129-
130-
```rust
131-
// optbug.rss
132-
fn describe(o: read Option<String>) -> String {
133-
match o { Some(s) => { return s } None => { return "none" } }
134-
}
135-
fn main() -> Unit {
136-
let o = Some(5)
137-
Log.write(message: read describe(o: read o))
138-
}
139-
```
140-
- **Expected:** rejected at `check`. **Actual:** `check` ok, then `run` → E0308 (`expected
141-
&Option<String>, found &Option<i64>`).
142-
- **Fix:** infer `Option<Int>` / `Result<Int,_>` for enum-variant constructors so the local carries
143-
a type.
144-
145-
### RSS-5 — [FIXED] — Turbofish struct constructor miscompiles to a positional tuple-struct call (E0423)
146-
- **Class:** miscompile
147-
- **Source:** `crates/rsscript/src/rust_lower/lowerer.rs:2342` (named-field path looked up via the
148-
raw callee string), fall-through at `:2646`; `type_kinds` keyed by bare name at `:58`.
149-
- **Root cause:** the named-field constructor path is gated by `type_kinds.get(name)` where `name`
150-
is the raw callee `"Pair<Int>"` (turbofish embedded), but `type_kinds` is keyed by the bare
151-
`"Pair"`. The lookup misses, so the call falls through to a positional emission
152-
`Pair(11i64, 22i64)` against a named-field struct → rustc E0423.
153-
154-
```rust
155-
// turbofish_ctor.rss
156-
struct Pair<T> derives(Clone, Eq, Hash) { first: T second: T }
157-
fn main() -> Unit {
158-
let p = Pair<Int>(first: 11, second: 22)
159-
Log.write(message: read Int.to_string(value: read p.first))
160-
}
161-
```
162-
- **Expected:** prints `11`. **Actual:** `check` ok, `run` → E0423. Non-turbofish
163-
`Pair(first:…, second:…)` lowers correctly.
164-
- **Fix:** use `type_root_name(name)` for the `type_kinds` lookup at `:2342`.
165-
166-
---
167-
168-
## MEDIUM
169-
170-
### RSS-6 — [FIXED]`<<` / `>>` share the comparison precedence tier
171-
- **Source:** `crates/rsscript/src/syntax/parser.rs:2883-2891` (`ShiftLeft`/`ShiftRight` grouped
172-
with `Equal`/`Less`/`Greater` in `find_top_level_operator`).
173-
- `4 == 1 << 2` parses as `(4 == 1) << 2` (shift on a bool). In C/Rust shift binds tighter than
174-
comparison, so the correct parse is `4 == (1 << 2)` = `4 == 4` = true. `check` passes; both
175-
backends then fail (`no method named wrapping_shl for type bool`, rustc E0599). False-reject.
176-
- **Fix:** move `<<`/`>>` to a tier between additive and relational.
177-
178-
### RSS-7 — [FIXED] — Lexer accepts malformed multi-dot number literals (`1.2.3`, `5.`)
179-
- **Source:** `crates/rsscript/src/lexer.rs:114-134` (`lex_number` consumes any run of digit-or-`.`).
180-
- `1.2.3` and `5.` tokenize as one Float; `check` reports `ok`; the lowerer emits the verbatim
181-
token `let x = 1.2.3;` → rustc E0610. False-accept that defers to a confusing backend error.
182-
- **Fix:** reject more than one `.` (and trailing `.`) in `lex_number`.
183-
184-
### RSS-8 — [FIXED] — Mixed numeric operands skip operand-type checking
185-
- **Source:** `crates/rsscript/src/checks/forbidden.rs:377-381` (empty arm), `:341-358`.
186-
- `Float + Int` and `Float < Int` are not type-checked (the `==` path correctly rejects), so they
187-
pass `check` then fail the backend with E0277/E0308.
188-
- **Fix:** require matching numeric roots in the arithmetic/relational arms.
189-
190-
### RSS-9 — [FIXED] — reg_vm compares floats bitwise, diverging from AOT's IEEE `==`
191-
- **Source:** `crates/rsscript/src/.../vm_value.rs:308` (Float `PartialEq` via `to_bits`).
192-
- `NaN == NaN` → true and `0.0 == -0.0` → false in the interpreter; AOT uses IEEE `==` and gives
193-
the opposite. A real interp-vs-compiled divergence.
194-
- **Fix:** use IEEE `==` in the Float equality arm.
195-
196-
### RSS-10 — [FIXED] — Generic struct constructor drops its type argument
197-
- **Source:** `crates/rsscript/src/hir.rs:3391`, `:2483` (`constructor_sig_from_type` returns the
198-
bare `"Wrap"`).
199-
- `let w: Wrap<Int> = Wrap(item: 7)` is wrongly rejected (RS0207) because the constructor's return
200-
type loses `<Int>`. False-reject.
201-
- **Fix:** synthesize `Wrap<T>` and substitute the type arg.
202-
20318
---
20419

20520
## LOW
@@ -219,86 +34,14 @@ fn main() -> Unit {
21934
source (the compiler only processes local source, so there is no remote DoS vector), real code
22035
never nests generics more than a handful of levels, and the item is rated LOW / non-correctness.
22136
The risk of regressing generics resolution was judged disproportionate to the benefit, so the fix
222-
is left for a dedicated change rather than bundled with the RSS-1…RSS-10 correctness fixes.
223-
224-
---
225-
226-
## Added during tinygrad-port work (2026-06-11)
227-
228-
### RSS-12 — [FIXED] — Nested generic type-arguments in method calls parsed as comparisons
229-
- **Source:** `crates/rsscript/src/syntax/parser.rs` `find_top_level_operator` (~:3031).
230-
- `List.new<List<Int>>()`, `List.get<List<Int>>(...)`, `List.len<List<Int>>(...)` all failed with
231-
RS0015 "unsupported syntax". The binary-operator splitter tracked `angle_depth` asymmetrically:
232-
it decremented on a nested inner `>` but never incremented on the nested inner `<` (the inner
233-
`<` of `List<Int>` is not a generic-open per `is_generic_angle_open`, which requires the matching
234-
`>` to be followed by `.`/`(`), so `angle_depth` hit 0 one `>` early and the outer `>` was parsed
235-
as a `Greater` comparison → the call collapsed to `Expr::Unknown`.
236-
- **Fix:** inside a generic argument list (`angle_depth > 0`), also `angle_depth += 1` on `<`. Inside
237-
a type-argument list `<` is unambiguously a nested generic open, never a comparison.
238-
- **Verified:** `List<List<Int>>` build/index/len/param all parse, typecheck, and run.
239-
240-
### RSS-13 — [FIXED] — User sum payload-variant construction did not resolve/lower
241-
- Previously documented in fixtures as "user payload-variant construction is not yet executable":
242-
declaring and `match`ing payload variants worked, but constructing one (`ArgInt(value: 5)`,
243-
`Shape.Circle(radius: 5)`) failed with RS0206 "does not resolve" / a backend "cannot find tuple
244-
variant" error.
245-
- **Sources / fix (two sites):**
246-
1. `crates/rsscript/src/hir.rs` `resolve_call` (~:762): also resolve a call as
247-
`CallResolution::EnumVariant` when `self.sum_type_for_variant(name).is_some()` (not only the
248-
builtins Ok/Err/Some/None). The reg-VM already lowered `MakeVariant` via `sum_variant_fields`.
249-
2. `crates/rsscript/src/rust_lower/lowerer.rs` constructor lowering (~:2412): emit the qualified,
250-
struct-style form `Enum::Variant { field: value, ... }` (nullary: `Enum::Variant`) for user sum
251-
variants, matching the lowered enum (whose payload variants use named fields), instead of the
252-
bare/tuple fall-through.
253-
- **Verified:** construct + `match`/discriminate + extract (via owned/`take` scrutinee) all run.
254-
- **Known follow-up:** extracting a *Copy* payload (Int/Float) directly from a *borrowed* (`read`)
255-
match still needs the binding deref'd (use an owned `take`/cloned scrutinee for now).
256-
257-
### RSS-15 — [ADDED] — Explicit, callable `.clone()` for `derives(Clone)` types
258-
- Previously only implicit clone existed (e.g. `read` args retained into a collection); there was no
259-
surface syntax to deep-copy a user value, and `derives(Copy)` on a sum is rejected. This blocked
260-
rebuilding immutable graph nodes (e.g. a UOp simplifier).
261-
- **Added (kept implicit clone):**
262-
- `hir.rs` `resolve_receiver_call`: when no method candidate resolves and the method is `clone`,
263-
synthesize a builtin sig `(self: read T) -> fresh T` so `x.clone()` resolves and types as `T`.
264-
- `rust_lower/lowerer.rs` receiver-call lowering: for a user struct/sum receiver, emit
265-
`{receiver}.clone()` (Rust's derived Clone = deep copy). Gated to user types so builtins
266-
(JSON/Map/List) keep their own clone lowering (`json_clone`, …).
267-
- `rust_lower/lowerer.rs` `lower_call_arg_for_expected_type`: exclude `clone` from the
268-
"`read` receiver-call result is re-borrowed" rule — `.clone()` yields an owned value, so passing
269-
it to a by-value param must not add `&` (was emitting `&x.clone()` into an owned param).
270-
- **Verified:** clone of struct/sum/field; clone passed to a by-value param; whole rss suite green.
271-
272-
### RSS-13 follow-ups — [FIXED] — payload-variant construction is now typed & checked
273-
- (Audit found the initial RSS-13 resolved variant calls but did not type/check them.)
274-
- **P1 type:** `hir.rs` `infer_enum_variant_type` now returns the variant's sum type for user variants
275-
(was only Some/Ok/Err), so `Number(value: 5)` has type `Token` and misuse (e.g. passing it to a
276-
`read String` param) is caught (RS0207) instead of emitting invalid Rust.
277-
- **P1 check / no panic:** `checks/calls.rs` `check_enum_variant_form` validates user-variant
278-
construction against declared fields — unknown field name (RS0203), wrong arity (RS0203/RS0204) —
279-
so a malformed constructor (`Number(1, 2)`, `Number(bad: 5)`) is a checker error, not an
280-
out-of-bounds panic in the lowerer.
281-
- **P2 field types:** `rust_lower/lowerer.rs` variant construction lowers each arg against its declared
282-
field type (mirroring struct ctors) and resolves fields by name bounds-safely. Note: once round-2
283-
value-type checking landed, `Tiny(value: 1)` with `value: Int32` is *rejected* at check (RS0207, an
284-
`Int` literal is not `Int32`) — same as struct constructors; the field-type lowering applies to
285-
values that already have the field's type (e.g. an `Int32`-typed binding), which lower without a cast.
286-
287-
### RSS-13 follow-ups (round 2) — [FIXED] — duplicate fields & field-value types
288-
- (Second audit: the round-1 variant checker tracked names/counts but not duplicates or value types.)
289-
- **Exactly-once coverage:** `check_enum_variant_form` now tracks per-field coverage — duplicate field
290-
(RS0205), unknown field (RS0203), too many (RS0203), and any unfilled field (RS0204). So
291-
`Both(left: 1, left: 2)` reports duplicate `left` + missing `right` (was: passed check, then E0062).
292-
- **Value types:** each variant field value is type-checked against its declared field type (reusing
293-
`argument_type_matches` + the JSON/Map/List literal acceptances, like binding-payload checks). So
294-
`Number(value: "x")` for `value: Int` reports RS0207 (was: passed check, then E0308).
295-
296-
### RSS-15 / RSS-13 follow-ups (round 3) — [FIXED] — clone gated on `Clone`; variants are named-only
297-
- **`.clone()` only for `Clone`-deriving types:** the synthesized clone previously resolved for *any*
298-
user type, so `struct Boxy derives(Eq)` passed `check` then failed Rust with E0599. Now `Hir` tracks
299-
`clone_types` (declared types whose derive list includes `Clone`); `resolve_receiver_call` synthesizes
300-
`clone` only for those (non-user receivers keep prior behavior), and rust_lower emits `.clone()` only
301-
when `type_derives_clone(root)`. A declared type without `Clone` now reports RS0206 at check time.
302-
- **Variants are named-field-only:** `check_enum_variant_form` rejects any unnamed payload arg
303-
(RS0201), matching the v0.6 spec (variants use the same named-field construction form as structs).
304-
`Number(5)` is now a checker error instead of lowering to `Token::Number { value: 5i64 }`.
37+
is left for a dedicated change rather than bundled with correctness fixes.
38+
- **Scoping note (for the eventual parse-once change):** memoization alone does **not** help — a
39+
strictly-nested type has all-distinct substrings, so there are no repeated cache keys; the win has
40+
to come from parsing each level once. The two hot functions also do **not** parse identically:
41+
`substitute_type_params` takes its `Fn(...)` branch on `fn_return_type(..).is_some()` (requires a
42+
`->`), while `collect_type_param_substitutions` takes it on `is_fn_type(..)` (does not). So an
43+
arrow-less `Fn(A)` is an opaque leaf to `substitute` but a recursed fn-type to `collect`. A
44+
parse-once tree must preserve each function's branch rules (or the divergence must be deliberately
45+
unified) and be guarded by a differential/fuzz test comparing old vs new output across many type
46+
strings — otherwise it risks silently changing which programs typecheck. That verification, not the
47+
tree itself, is the bulk of the work and the reason this stays a dedicated, separately-reviewed change.

0 commit comments

Comments
 (0)