Skip to content

Commit cd3b63f

Browse files
Haofeiclaude
andcommitted
fix(lowering): sanitize Rust-keyword crate names (async.rss -> rss-async)
A source file named after a Rust keyword (e.g. `async.rss`, `match.rss`) derived a keyword crate/lib name, so the generated harness emitted `async::main()` — a Rust parse error surfaced as RS1102, and both Cargo and rustc reject keyword crate names. The checker accepted such programs; only the backend failed, violating the "valid RSScript -> valid Rust" contract. `cargo_package_name` now prefixes a keyword result with `rss-` (`async` -> `rss-async`), which flows through to the lib name and the `<crate>::main()` reference (`rss_async::main()`), all valid. Extracted the keyword set shared with `rust_ident` into `is_rust_keyword`. Regression test in checker_lowering. Third "valid RSScript -> invalid Rust" bug found while building rss-testgen (the async tier surfaced it via a keyword-named scratch file). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 253f342 commit cd3b63f

2 files changed

Lines changed: 42 additions & 6 deletions

File tree

crates/rsscript/src/rust_lower/helpers.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,18 +1558,21 @@ pub(super) fn rust_path_segment(segment: &str) -> String {
15581558
}
15591559
}
15601560

1561-
pub(super) fn rust_ident(name: &str) -> String {
1562-
let keywords: BTreeSet<&'static str> = [
1561+
/// Whether `name` is a Rust keyword (strict or reserved) that cannot be used as a
1562+
/// bare identifier.
1563+
pub(super) fn is_rust_keyword(name: &str) -> bool {
1564+
const KEYWORDS: &[&str] = &[
15631565
"abstract", "as", "async", "await", "become", "box", "break", "const", "continue", "crate",
15641566
"do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "gen", "if", "impl",
15651567
"in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub",
15661568
"ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "try",
15671569
"type", "typeof", "unsafe", "unsized", "use", "virtual", "where", "while", "yield",
1568-
]
1569-
.into_iter()
1570-
.collect();
1570+
];
1571+
KEYWORDS.contains(&name)
1572+
}
15711573

1572-
if keywords.contains(name) {
1574+
pub(super) fn rust_ident(name: &str) -> String {
1575+
if is_rust_keyword(name) {
15731576
format!("r#{name}")
15741577
} else {
15751578
name.to_string()
@@ -1620,6 +1623,12 @@ pub(super) fn cargo_package_name(name: &str) -> String {
16201623

16211624
if out.is_empty() {
16221625
"rsscript-generated".to_string()
1626+
} else if is_rust_keyword(&out) {
1627+
// A package named after a Rust keyword (e.g. `async.rss`) would derive a
1628+
// keyword crate/lib name, which both Cargo and rustc reject (and the
1629+
// generated `async::main()` harness is a parse error). Prefix it so the
1630+
// package name, lib name, and `<crate>::main()` reference are all valid.
1631+
format!("rss-{out}")
16231632
} else {
16241633
out
16251634
}

crates/rsscript/tests/checker_lowering/misc.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,33 @@
22
#![allow(unused_imports, dead_code)]
33
use super::*;
44

5+
#[test]
6+
fn keyword_named_source_lowers_to_a_valid_crate_name() {
7+
// Regression (found while building rss-testgen's async tier): a source whose
8+
// name is a Rust keyword (e.g. `async.rss`) derived a keyword crate name, so
9+
// the harness emitted `async::main()` — a Rust parse error (RS1102), and both
10+
// Cargo and rustc reject keyword crate names. The package/lib/crate-reference
11+
// names must all be sanitized.
12+
let source = "fn main() -> Unit {\n Log.write(message: read \"ok\")\n return Unit\n}\n";
13+
let package = lower_source_to_rust_package("async.rss", source, "async", "/tmp/runtime")
14+
.expect("keyword-named source should lower");
15+
16+
assert_ne!(
17+
package.package_name, "async",
18+
"package name must not be the bare keyword"
19+
);
20+
let main_rs = package.main_rs.expect("runnable main");
21+
assert!(
22+
main_rs.contains("rss_async::main"),
23+
"main.rs should reference the sanitized crate name, not the keyword:\n{main_rs}"
24+
);
25+
// No bare keyword crate path (e.g. a line starting ` async::`).
26+
assert!(
27+
!main_rs.contains(" async::") && !main_rs.contains("\nasync::"),
28+
"main.rs must not reference a bare keyword crate:\n{main_rs}"
29+
);
30+
}
31+
532
#[test]
633
fn rust_lowering_emits_checked_rust_source() {
734
let source = r#"

0 commit comments

Comments
 (0)