Skip to content

Commit 59ec416

Browse files
Haofeiclaude
andcommitted
rss: lower builtin value-type .clone() to Rust clone (fix E0425); document release fast path for package check
#1 (List<T>.clone() etc.): `.clone()` on a builtin value type whose clone the checker resolves (List/Bytes/Buffer) typechecked via the Clone protocol but lowered to a dangling `List_clone`-style call -> rustc E0425. The lowerer now emits Rust's `.clone()` for these (they lower to Clone Rust types). Same frontend-accepts/ backend-fails class as the borrowed-match E0308 fix. Removes the need for manual copy helpers in source-shaped ports. Regression test in checker_lowering/stdlib. #2 (slow package check in debug): documented the release-binary fast path in README (Performance) and cross-referenced from BUGS.md RSS-11 — debug leaves the ~O(n^3) generic-substitution path unoptimized; release is usable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f0f82e1 commit 59ec416

4 files changed

Lines changed: 66 additions & 6 deletions

File tree

BUGS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ $BIN eval file.rss
2828
repro used a *non-generic* `id`, which never enters the substitution recursion and stays flat —
2929
the trigger requires a generic callee.) Not a correctness fault.
3030
- **Fix:** parse type strings into a tree once / memoize substitutions.
31+
- **Fast path (until fixed):** package-scale `rss check` / `rss pkg` is comfortable when the
32+
compiler is built in **release** (debug leaves the hot string-reparse path unoptimized). For
33+
repeated package-wide validation use a release `rss` (`cargo build --release --bin rss`); this is
34+
documented under "Performance" in `README.md`. Observed on generics-heavy ports (e.g. tinygrad-rss):
35+
debug package check slow, release usable.
3136
- **Status — deferred (intentional):** the asymptotic fix means replacing the string-based type
3237
representation in the generic-substitution path with a parse-once tree — a sizeable refactor of
3338
correctness-critical, heavily-relied-upon generics code. The trigger is adversarial, self-authored

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,15 @@ rss test [--all] [--json] [--filter <substring>]
473473
- `rss pkg publish --dry-run` runs pre-publish checks without uploading and reports whether the package is ready.
474474
- `rss run` lowers a single file (or a package with `src/main.rss`) to a temporary Rust package and delegates to `cargo run`; package lowering carries enabled `[native.rust]` wrappers through as generated Cargo path dependencies and maps `native/bindings.rssbind.toml` call bindings into generated Rust calls. `--dry-run` prints the generated `Cargo.toml`, lowered Rust, and cargo invocation without executing it; `--release` delegates to Cargo's release profile, `--out-dir` keeps the generated package, and arguments after `--` reach the program through the core `Args` API.
475475

476+
> **Performance — use a release-built `rss` for package-scale checking.** On large, generics-heavy packages, `rss check` / `rss pkg` can be noticeably slow when run from a **debug** build of the compiler, because generic type-argument substitution currently re-parses type strings at each nesting level (a known, deferred ~O(n³) path — see `BUGS.md` RSS-11). The debug build leaves that path unoptimized; a release build optimizes it enough to be comfortable. For repeated package-wide validation (e.g. an inner edit→check loop on a big codebase), build the compiler once in release and use that binary:
477+
>
478+
> ```sh
479+
> cargo build --release --bin rss
480+
> ./target/release/rss pkg # or: rss check <package-dir>
481+
> ```
482+
>
483+
> This is a temporary fast path; the durable fix is the parse-once refactor tracked in RSS-11.
484+
476485
### Execution backends
477486

478487
These are **not equivalent backends** — they cover different slices of the language. Only Rust lowering executes the full language; it is the semantic reference. The others are progressively narrower fast-feedback/optimization tiers that **fail closed** (or fall back) rather than silently diverging, and the N-way differential (`tests/backend_differential.rs`) gates that they agree on their shared supported subset.

crates/rsscript/src/rust_lower/lowerer.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2511,12 +2511,19 @@ impl<'a> RustLowerer<'a> {
25112511
.map(type_ref_display_name)
25122512
.unwrap_or_else(|| "Unknown".to_string());
25132513
let receiver_type_root = type_root_name(&receiver_type_name).to_string();
2514-
if method == "clone" && self.type_derives_clone(&receiver_type_root) {
2515-
// Explicit `.clone()` on a user struct/sum that derives `Clone` -> Rust's
2516-
// derived Clone (a deep copy). `.clone()` borrows its receiver, so a place
2517-
// behind a `&` (read param / field of one) works without moving. Types that
2518-
// don't derive Clone are rejected earlier (RS0206); builtins (JSON/Map/List/…)
2519-
// keep their own clone lowering below.
2514+
if method == "clone"
2515+
&& (self.type_derives_clone(&receiver_type_root)
2516+
|| Self::is_builtin_clone_value(&receiver_type_root))
2517+
{
2518+
// Explicit `.clone()` -> Rust's `Clone`: a user struct/sum that derives
2519+
// `Clone`, or a builtin value type that lowers to a `Clone` Rust type but has
2520+
// no dedicated clone intrinsic (`List`/`Map`/`Set`/…). Without the builtin
2521+
// case these fell through to a dangling `List_clone`-style call (the checker
2522+
// accepts `.clone()` via the `Clone` protocol, so it never errored at the
2523+
// front end — E0425 at the Rust backend). `.clone()` borrows its receiver, so
2524+
// a place behind a `&` (read param / field of one) works without moving. Types
2525+
// that don't derive Clone are rejected earlier (RS0206). `String`/`Json` keep
2526+
// their own clone intrinsics and are handled below.
25202527
return format!("{}.clone()", self.lower_expr(receiver));
25212528
}
25222529
let receiver_rust_type = receiver_type
@@ -4123,6 +4130,17 @@ impl<'a> RustLowerer<'a> {
41234130
})
41244131
}
41254132

4133+
/// Builtin value types whose `.clone()` the checker resolves (via the `Clone`
4134+
/// protocol) and which lower to a `Clone` Rust type but have no dedicated clone
4135+
/// runtime intrinsic — so `.clone()` must lower to Rust's `.clone()` directly.
4136+
/// Without this they fell through to a dangling `List_clone`-style call (E0425).
4137+
/// `String`/`Json` are excluded (they have their own clone intrinsics);
4138+
/// `Deque`/`Set`/`Map`/resources are rejected at check time, so they never reach
4139+
/// here.
4140+
fn is_builtin_clone_value(name: &str) -> bool {
4141+
matches!(name, "List" | "Bytes" | "Buffer")
4142+
}
4143+
41264144
fn is_string_comparison_operand(&self, expr: &Expr) -> bool {
41274145
matches!(expr, Expr::String(_, _) | Expr::MultilineString(_, _))
41284146
|| self

crates/rsscript/tests/checker_lowering/stdlib.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,3 +1944,31 @@ impl Writer for BufferWriter {
19441944
let diagnostics = analyze_source_with_core("protocol-impl.rss", source);
19451945
assert_eq!(diagnostics, Vec::new());
19461946
}
1947+
1948+
#[test]
1949+
fn rust_lowering_builtin_value_clone_uses_rust_clone() {
1950+
// Regression: `.clone()` on a builtin value type whose clone the checker
1951+
// resolves (List/Bytes/Buffer) typechecks via the `Clone` protocol but used to
1952+
// lower to a dangling `List_clone`-style call (rustc E0425). It must lower to
1953+
// Rust's `.clone()`, which these types support.
1954+
let source = r#"
1955+
features: local
1956+
1957+
fn dup_list(xs: read List<Int>) -> fresh List<Int> {
1958+
return xs.clone()
1959+
}
1960+
1961+
fn dup_bytes(b: read Bytes) -> fresh Bytes {
1962+
return b.clone()
1963+
}
1964+
"#;
1965+
let rust = lower_source_to_rust("builtin-clone.rss", source).expect("source should lower");
1966+
assert!(
1967+
rust.contains(".clone()"),
1968+
"builtin `.clone()` should lower to Rust's clone, got:\n{rust}"
1969+
);
1970+
assert!(
1971+
!rust.contains("List_clone") && !rust.contains("Bytes_clone"),
1972+
"builtin `.clone()` must not emit a dangling `Type_clone` call, got:\n{rust}"
1973+
);
1974+
}

0 commit comments

Comments
 (0)