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
289cd 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