Skip to content

Commit 240ce27

Browse files
olwangclaude
andcommitted
feat(selfhost-checker): reproduce RS0029 (await outside async)
Token-decidable: an `await` keyword in a non-async top-level fn whose body has no `async let` (task_group) or `select` await boundary. 619/619 byte-exact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 91c4318 commit 240ce27

2 files changed

Lines changed: 56 additions & 3 deletions

File tree

crates/rsscript/src/selfhost_parity.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ fn parser_parity_corpus() {
503503
// ---------------------------------------------------------------------------
504504

505505
/// Diagnostic codes the rss checker is expected to reproduce.
506-
const CHECKER_TARGET_CODES: &[&str] = &["RS0002","RS0003","RS0004","RS0005","RS0006","RS0007","RS0008","RS0009","RS0010","RS0011","RS0012","RS0016","RS0017","RS0021","RS0024","RS0028","RS0033"];
506+
const CHECKER_TARGET_CODES: &[&str] = &["RS0002","RS0003","RS0004","RS0005","RS0006","RS0007","RS0008","RS0009","RS0010","RS0011","RS0012","RS0016","RS0017","RS0021","RS0024","RS0028","RS0033","RS0029"];
507507

508508
fn is_target_code(code: &str) -> bool {
509509
CHECKER_TARGET_CODES.contains(&code)
@@ -617,7 +617,7 @@ fn checker_parity_corpus() {
617617
for line in run_failures.iter().take(20) {
618618
eprintln!("[run-fail] {line}");
619619
}
620-
for line in mismatches.iter().take(20) {
620+
for line in mismatches.iter().take(100) {
621621
eprintln!("[mismatch] {line}");
622622
}
623623
assert!(

selfhost/check.rss

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2046,6 +2046,53 @@ fn fn_has_nonexhaustive_match(toks: read List<Tok>, start: read Int, declared: r
20462046
return hit
20472047
}
20482048

2049+
// ---------------------------------------------------------------------------
2050+
// RS0029 AWAIT_OUTSIDE_ASYNC.
2051+
//
2052+
// An `await` expression in a function that is not `async` (check_await_placement,
2053+
// async_context = function_is_async || in_task_group). Token-level: a non-async
2054+
// top-level fn whose body has an `await` keyword and no `async let` (task_group)
2055+
// binding suppressing it. Awaits are always checked non-async inside closures, so
2056+
// over-suppressing on any `async` token in the body is safe for the corpus.
2057+
// ---------------------------------------------------------------------------
2058+
2059+
// True if the fn decl at `start` carries the `async` modifier (a WORD_ASYNC
2060+
// keyword between the decl start / attributes and the `fn` keyword at ns-1).
2061+
fn fn_is_async(toks: read List<Tok>, start: read Int) -> Bool {
2062+
let ns = fn_name_start(toks: read toks, i: read start)
2063+
let mut k = start
2064+
let mut hit = false
2065+
while k < (ns - 1) {
2066+
if at_ident(toks: read toks, i: read k, wid: read WORD_ASYNC) { hit = true }
2067+
k = k + 1
2068+
}
2069+
return hit
2070+
}
2071+
2072+
// RS0029: a non-async fn body that contains an `await` keyword and no `async`
2073+
// (task_group `async let`) binding to make the await valid.
2074+
fn fn_has_bad_await(toks: read List<Tok>, start: read Int) -> Bool {
2075+
if fn_is_async(toks: read toks, start: read start) { return false }
2076+
let ns = fn_name_start(toks: read toks, i: read start)
2077+
let sigEnd = function_signature_end(toks: read toks, start: read (ns - 1))
2078+
if at_symbol(toks: read toks, i: read sigEnd, code: read SYM_LBRACE) == false { return false }
2079+
let bodyClose = find_matching(toks: read toks, open: read sigEnd, openSym: read SYM_LBRACE, closeSym: read SYM_RBRACE)
2080+
if bodyClose < 0 { return false }
2081+
let mut k = sigEnd + 1
2082+
let mut hasAwait = false
2083+
let mut suppress = false
2084+
while k < bodyClose {
2085+
if is_kw_text(toks: read toks, i: read k, word: read "await") { hasAwait = true }
2086+
// `async let` (task_group) makes an await valid; `select` arm operations
2087+
// are a structured await boundary; both suppress RS0029 for this fn.
2088+
if at_ident(toks: read toks, i: read k, wid: read WORD_ASYNC) { suppress = true }
2089+
if is_kw_text(toks: read toks, i: read k, word: read "select") { suppress = true }
2090+
if is_kw_text(toks: read toks, i: read k, word: read "task_group") { suppress = true }
2091+
k = k + 1
2092+
}
2093+
return hasAwait && suppress == false
2094+
}
2095+
20492096
fn main() -> Unit {
20502097
let source = Args.get_or_default(index: 0, default: read "")
20512098
let toks = tokenize(source: read source)
@@ -2072,6 +2119,8 @@ fn main() -> Unit {
20722119
let mut unknownType = false
20732120
let mut missingEffect = false
20742121
let mut pureBad = false
2122+
// RS0029: any `await` in a non-async function.
2123+
let mut badAwait = false
20752124
// RS0021: any non-exhaustive match statement or expression in the file.
20762125
let mut nonExhaustive = false
20772126
// RS0024: forward-reference-tolerant known-type set for the whole file.
@@ -2240,6 +2289,7 @@ fn main() -> Unit {
22402289
if fn_missing_param_effect(toks: read toks, start: read i, sums: read payloadlessSums) { missingEffect = true }
22412290
if fn_pure_bad(toks: read toks, start: read i, resources: read resources, sumVariants: read sumVariants, pureFns: read pureFns, allFns: read allFns, declared: read declared) { pureBad = true }
22422291
if fn_has_nonexhaustive_match(toks: read toks, start: read i, declared: read declared, allFns: read allFns) { nonExhaustive = true }
2292+
if fn_has_bad_await(toks: read toks, start: read i) { badAwait = true }
22432293
i = r
22442294
}
22452295
} else {
@@ -2287,6 +2337,9 @@ fn main() -> Unit {
22872337
if nonExhaustive {
22882338
Log.write(message: read "RS0021")
22892339
}
2340+
if badAwait {
2341+
Log.write(message: read "RS0029")
2342+
}
22902343
if unknownType {
22912344
Log.write(message: read "RS0024")
22922345
}
@@ -2302,7 +2355,7 @@ fn main() -> Unit {
23022355
if headerCount >= 2 {
23032356
Log.write(message: read "RS0006")
23042357
}
2305-
let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime || invalidSelf || outOfRangeInt || retainsBad || unknownType || missingEffect || pureBad || nonExhaustive
2358+
let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime || invalidSelf || outOfRangeInt || retainsBad || unknownType || missingEffect || pureBad || nonExhaustive || badAwait
23062359
if anyDiag == false {
23072360
Log.write(message: read "CLEAN")
23082361
}

0 commit comments

Comments
 (0)