@@ -869,7 +869,7 @@ fn fn_pure_bad(toks: read List<Tok>, start: read Int, resources: read Set<String
869869
870870// Pre-pass filling RS0009 lookup sets: user resource type names, sum variant
871871// names, pure fn (simple) names, and all fn (simple) names.
872- fn collect_rs0009(toks: read List<Tok >, resources: mut Set<String >, sumVariants: mut Set<String >, pureFns: mut Set<String >, allFns: mut Set<String >, noBlockFns: mut Set<String >, noPanicFns: mut Set<String >) -> Unit {
872+ fn collect_rs0009(toks: read List<Tok >, resources: mut Set<String >, sumVariants: mut Set<String >, pureFns: mut Set<String >, allFns: mut Set<String >, noBlockFns: mut Set<String >, noPanicFns: mut Set<String >, asyncFns: mut Set< String > ) -> Unit {
873873 let ntok = List.len(list: read toks)
874874 let mut i = 0
875875 let mut running = true
@@ -957,6 +957,9 @@ fn collect_rs0009(toks: read List<Tok>, resources: mut Set<String>, sumVariants:
957957 if fn_has_named_effect(toks: read toks, start: read i, effName: read "no_panic") {
958958 Set.insert<String >(set: mut noPanicFns, value: read fname)
959959 }
960+ if fn_is_async(toks: read toks, start: read i) {
961+ Set.insert<String >(set: mut asyncFns, value: read fname)
962+ }
960963 }
961964 i = r
962965 }
@@ -2169,6 +2172,44 @@ fn fn_has_fd_surface(toks: read List<Tok>, start: read Int) -> Bool {
21692172 return hit
21702173}
21712174
2175+ // ---------------------------------------------------------------------------
2176+ // RS0022 ASYNC_CALL_NOT_CONSUMED (signature-table decidable).
2177+ //
2178+ // A call whose callee resolves to a same-file `async fn` must be consumed: it is
2179+ // the direct operand of `await` or `spawn`, or the RHS of an `async let` binding.
2180+ // A bare/`let`-bound async call is unconsumed (check_async_call_consumed). Only
2181+ // same-file async fns register a signature; stdlib/native fns are never async, so
2182+ // qualified/receiver calls (`Ns.method(...)`) never resolve to an async signature
2183+ // and are skipped. Consumption threads through the AST, but the corpus only ever
2184+ // consumes an async call as the immediate `await`/`spawn` operand or `async let`
2185+ // RHS, so a token-adjacency probe is exact here.
2186+ // ---------------------------------------------------------------------------
2187+
2188+ fn has_unconsumed_async_call(toks: read List<Tok >, asyncFns: read Set<String >) -> Bool {
2189+ let n = List.len(list: read toks)
2190+ let mut i = 0
2191+ let mut hit = false
2192+ while i < n {
2193+ let isCall = is_ident_tok(toks: read toks, i: read i) && at_symbol(toks: read toks, i: read (i + 1), code: read SYM_LPAREN)
2194+ if isCall && Set.contains<String >(set: read asyncFns, value: read tk_text(toks: read toks, i: read i)) {
2195+ // A method call (`x.name(...)`) or the fn's own declaration (`fn name(`)
2196+ // is not a resolvable unqualified async call site.
2197+ let prevDot = at_symbol(toks: read toks, i: read (i - 1), code: read SYM_DOT)
2198+ let prevFn = at_ident(toks: read toks, i: read (i - 1), wid: read WORD_FN)
2199+ if prevDot == false && prevFn == false {
2200+ let awaited = is_kw_text(toks: read toks, i: read (i - 1), word: read "await")
2201+ let spawned = is_kw_text(toks: read toks, i: read (i - 1), word: read "spawn")
2202+ let asyncLet = at_symbol(toks: read toks, i: read (i - 1), code: read SYM_EQ) && is_kw_text(toks: read toks, i: read (i - 3), word: read "let") && at_ident(toks: read toks, i: read (i - 4), wid: read WORD_ASYNC)
2203+ if awaited == false && spawned == false && asyncLet == false {
2204+ hit = true
2205+ }
2206+ }
2207+ }
2208+ i = i + 1
2209+ }
2210+ return hit
2211+ }
2212+
21722213// A param segment's type region [segStart+2, segEnd) names `Fd`.
21732214fn seg_type_has_fd(toks: read List<Tok >, segStart: read Int, segEnd: read Int) -> Bool {
21742215 if (segStart + 2) >= segEnd { return false }
@@ -2488,7 +2529,11 @@ fn main() -> Unit {
24882529 let mut allFns = Set<String >.new()
24892530 let mut noBlockFns = Set<String >.new()
24902531 let mut noPanicFns = Set<String >.new()
2491- collect_rs0009(toks: read toks, resources: mut resources, sumVariants: mut sumVariants, pureFns: mut pureFns, allFns: mut allFns, noBlockFns: mut noBlockFns, noPanicFns: mut noPanicFns)
2532+ // Signature table (cross-function): same-file async fn names, for RS0022.
2533+ let mut asyncFns = Set<String >.new()
2534+ collect_rs0009(toks: read toks, resources: mut resources, sumVariants: mut sumVariants, pureFns: mut pureFns, allFns: mut allFns, noBlockFns: mut noBlockFns, noPanicFns: mut noPanicFns, asyncFns: mut asyncFns)
2535+ // RS0022: an unconsumed call to a same-file async fn.
2536+ let asyncNotConsumed = has_unconsumed_async_call(toks: read toks, asyncFns: read asyncFns)
24922537 // RS0033: any decimal integer literal in the file that overflows i64.
24932538 let outOfRangeInt = has_out_of_range_int(toks: read toks)
24942539 let mut i = 0
@@ -2731,6 +2776,9 @@ fn main() -> Unit {
27312776 if badAwait {
27322777 Log.write(message: read "RS0029")
27332778 }
2779+ if asyncNotConsumed {
2780+ Log.write(message: read "RS0022")
2781+ }
27342782 if fdSurface {
27352783 Log.write(message: read "RS0023")
27362784 }
@@ -2764,7 +2812,7 @@ fn main() -> Unit {
27642812 if headerCount >= 2 {
27652813 Log.write(message: read "RS0006")
27662814 }
2767- let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime || invalidSelf || outOfRangeInt || retainsBad || unknownType || missingEffect || pureBad || nonExhaustive || badAwait || fdSurface || lowerConflict || unknownProtocol || noallocAlloc || noBlockCall || noPanicCall
2815+ let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime || invalidSelf || outOfRangeInt || retainsBad || unknownType || missingEffect || pureBad || nonExhaustive || badAwait || fdSurface || lowerConflict || unknownProtocol || noallocAlloc || noBlockCall || noPanicCall || asyncNotConsumed
27682816 if anyDiag == false {
27692817 Log.write(message: read "CLEAN")
27702818 }
0 commit comments