Skip to content

Commit e559c1f

Browse files
olwangclaude
andcommitted
feat(selfhost-checker): add RS0008 (missing parameter effect)
Mirrors analyzer/signatures.rs: an effect-less param is flagged unless its type is share/noescape/owned/bare-Closure/surface-ref/contains-Fd/Copy-scalar/ payload-less-sum. Adds a payload-less-sum pre-pass. Corpus-green over 619 files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent fd688f0 commit e559c1f

2 files changed

Lines changed: 199 additions & 2 deletions

File tree

crates/rsscript/src/selfhost_parity.rs

Lines changed: 1 addition & 1 deletion
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","RS0010","RS0011","RS0012","RS0016","RS0017","RS0024","RS0028","RS0033"];
506+
const CHECKER_TARGET_CODES: &[&str] = &["RS0002","RS0003","RS0004","RS0005","RS0006","RS0007","RS0008","RS0010","RS0011","RS0012","RS0016","RS0017","RS0024","RS0028","RS0033"];
507507

508508
fn is_target_code(code: &str) -> bool {
509509
CHECKER_TARGET_CODES.contains(&code)

selfhost/check.rss

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,196 @@ fn fn_retains_bad(toks: read List<Tok>, start: read Int) -> Bool {
632632
return hit
633633
}
634634

635+
// ---------------------------------------------------------------------------
636+
// RS0008 MISSING_PARAMETER_EFFECT.
637+
// ---------------------------------------------------------------------------
638+
639+
// Does any token in [s,e) have the given identifier text?
640+
fn region_has_text(toks: read List<Tok>, s: read Int, e: read Int, word: read String) -> Bool {
641+
let mut k = s
642+
let mut hit = false
643+
while k < e {
644+
if tk_text(toks: read toks, i: read k) == word { hit = true }
645+
k = k + 1
646+
}
647+
return hit
648+
}
649+
650+
// Pre-pass: user `sum` names whose EVERY variant is payloadless (no `(...)`).
651+
fn collect_payloadless_sums(toks: read List<Tok>) -> Set<String> {
652+
let ntok = List.len(list: read toks)
653+
let mut sums = Set<String>.new()
654+
let mut i = 0
655+
let mut running = true
656+
while running {
657+
if i >= ntok {
658+
running = false
659+
} else if tk_kind(toks: read toks, i: read i) == TOK_EOF {
660+
running = false
661+
} else {
662+
let cls = classify_top(toks: read toks, i: read i)
663+
let isPub = cls.isPub
664+
if cls.isFeatures {
665+
i = declaration_line_end(toks: read toks, start: read i)
666+
} else if cls.isProfile {
667+
i = declaration_line_end(toks: read toks, start: read i)
668+
} else if cls.isSum {
669+
let r = parse_sum_decl(toks: read toks, i: read i)
670+
if r < 0 {
671+
running = false
672+
} else {
673+
let ns = sum_name_start(toks: read toks, i: read i)
674+
let name = dotted_name_text(toks: read toks, i: read ns)
675+
let ob = find_body_open(toks: read toks, i: read i, end: read r)
676+
if ob >= 0 {
677+
if region_has_paren(toks: read toks, s: read (ob + 1), e: read (r - 1)) == false {
678+
Set.insert<String>(set: mut sums, value: read name)
679+
}
680+
}
681+
i = r
682+
}
683+
} else if starts_type_decl(toks: read toks, i: read i, isPub: read isPub) {
684+
let r = parse_type_decl(toks: read toks, i: read i)
685+
if r < 0 { running = false } else { i = r }
686+
} else if at_ident(toks: read toks, i: read i, wid: read WORD_PROTOCOL) {
687+
let r = parse_protocol_decl(toks: read toks, i: read i)
688+
if r < 0 { running = false } else { i = r }
689+
} else if at_ident(toks: read toks, i: read i, wid: read WORD_IMPL) {
690+
let r = parse_impl_decl(toks: read toks, i: read i)
691+
if r < 0 { running = false } else { i = r }
692+
} else if cls.isNativeMod {
693+
let r = parse_native_module_decl(toks: read toks, i: read i)
694+
if r < 0 { running = false } else { i = r }
695+
} else if cls.isTypeAlias {
696+
let r = parse_type_alias_decl(toks: read toks, i: read i)
697+
if r < 0 { running = false } else { i = r }
698+
} else if cls.isConst {
699+
let r = parse_const_decl(toks: read toks, i: read i)
700+
if r < 0 { running = false } else { i = r }
701+
} else if at_ident(toks: read toks, i: read i, wid: read WORD_MODULE) {
702+
let r = parse_module_decl(toks: read toks, i: read i)
703+
if r < 0 { i = i + 1 } else { i = r }
704+
} else if at_ident(toks: read toks, i: read i, wid: read WORD_USE) {
705+
let r = parse_use_decl(toks: read toks, i: read i)
706+
if r < 0 { i = i + 1 } else { i = r }
707+
} else if starts_fn_like(toks: read toks, i: read i, isPub: read isPub) {
708+
let r = parse_function_decl(toks: read toks, i: read i)
709+
if r < 0 { running = false } else { i = r }
710+
} else {
711+
i = i + 1
712+
}
713+
}
714+
}
715+
return sums
716+
}
717+
718+
// Any `(` token in [s,e)?
719+
fn region_has_paren(toks: read List<Tok>, s: read Int, e: read Int) -> Bool {
720+
let mut k = s
721+
let mut hit = false
722+
while k < e {
723+
if at_symbol(toks: read toks, i: read k, code: read SYM_LPAREN) { hit = true }
724+
k = k + 1
725+
}
726+
return hit
727+
}
728+
729+
// One param segment: RS0008 iff no data-effect and the type demands one.
730+
fn param_seg_missing_effect(toks: read List<Tok>, segStart: read Int, segEnd: read Int, sums: read Set<String>) -> Bool {
731+
if segStart >= segEnd { return false }
732+
if is_ident_tok(toks: read toks, i: read segStart) == false { return false }
733+
if at_symbol(toks: read toks, i: read (segStart + 1), code: read SYM_COLON) == false { return false }
734+
let typeStart = segStart + 2
735+
if is_effect_kw(toks: read toks, i: read typeStart) { return false }
736+
// Type region ends at the first top-level `=` (default), else the segment end.
737+
let mut typeEnd = segEnd
738+
let mut k = typeStart
739+
let mut depth = 0
740+
let mut found = false
741+
while k < segEnd && found == false {
742+
if at_symbol(toks: read toks, i: read k, code: read SYM_LPAREN) || at_symbol(toks: read toks, i: read k, code: read SYM_LANGLE) || at_symbol(toks: read toks, i: read k, code: read SYM_LBRACKET) {
743+
depth = depth + 1
744+
} else if at_symbol(toks: read toks, i: read k, code: read SYM_RPAREN) || at_symbol(toks: read toks, i: read k, code: read SYM_RANGLE) || at_symbol(toks: read toks, i: read k, code: read SYM_RBRACKET) {
745+
if depth > 0 { depth = depth - 1 }
746+
} else if depth == 0 && at_symbol(toks: read toks, i: read k, code: read SYM_EQ) {
747+
typeEnd = k
748+
found = true
749+
}
750+
k = k + 1
751+
}
752+
// Base name: skip surface-ref `&`, prefixes and (defensive) data effects.
753+
let mut p = typeStart
754+
let mut sawFresh = false
755+
let mut sawNoOwn = false
756+
let mut going = true
757+
while going {
758+
let t = tk_text(toks: read toks, i: read p)
759+
if p < typeEnd && t == "fresh" {
760+
sawFresh = true
761+
p = p + 1
762+
} else if p < typeEnd && (t == "noescape" || t == "owned") {
763+
sawNoOwn = true
764+
p = p + 1
765+
} else if p < typeEnd && (t == "handle" || t == "weak" || t == "&" || t == "read" || t == "mut" || t == "take") {
766+
p = p + 1
767+
} else {
768+
going = false
769+
}
770+
}
771+
if p >= typeEnd { return false }
772+
let base = tk_text(toks: read toks, i: read p)
773+
if base == "share" { return false }
774+
let hasArgs = at_symbol(toks: read toks, i: read dotted_end(toks: read toks, i: read p), code: read SYM_LANGLE)
775+
// NOT noescape / owned anywhere in the type region.
776+
if region_has_text(toks: read toks, s: read typeStart, e: read typeEnd, word: read "noescape") { return false }
777+
if region_has_text(toks: read toks, s: read typeStart, e: read typeEnd, word: read "owned") { return false }
778+
// NOT bare `Closure` (no args).
779+
if base == "Closure" && hasArgs == false { return false }
780+
// NOT a surface reference (`&` anywhere in the segment).
781+
if region_has_text(toks: read toks, s: read segStart, e: read segEnd, word: read "&") { return false }
782+
// NOT containing type name `Fd` anywhere in the type region.
783+
if region_has_text(toks: read toks, s: read typeStart, e: read typeEnd, word: read "Fd") { return false }
784+
// NOT a Copy scalar (base ∈ scalars, no generic args).
785+
if is_scalar_name(name: read base) && hasArgs == false { return false }
786+
// NOT a bare payload-less sum.
787+
if hasArgs == false && sawFresh == false && sawNoOwn == false && Set.contains<String>(set: read sums, value: read base) { return false }
788+
return true
789+
}
790+
791+
// RS0008: any parameter of the fn that must declare an effect but doesn't.
792+
fn fn_missing_param_effect(toks: read List<Tok>, start: read Int, sums: read Set<String>) -> Bool {
793+
let ns = fn_name_start(toks: read toks, i: read start)
794+
let sigEnd = function_signature_end(toks: read toks, start: read (ns - 1))
795+
let open = find_param_open(toks: read toks, from: read ns, end: read sigEnd)
796+
if open < 0 { return false }
797+
let close = find_matching(toks: read toks, open: read open, openSym: read SYM_LPAREN, closeSym: read SYM_RPAREN)
798+
if close < 0 { return false }
799+
let mut k = open + 1
800+
let mut depth = 0
801+
let mut angle = 0
802+
let mut segStart = open + 1
803+
let mut hit = false
804+
while k < close {
805+
let opener = at_symbol(toks: read toks, i: read k, code: read SYM_LPAREN) || at_symbol(toks: read toks, i: read k, code: read SYM_LBRACKET) || at_symbol(toks: read toks, i: read k, code: read SYM_LBRACE)
806+
let closer = at_symbol(toks: read toks, i: read k, code: read SYM_RPAREN) || at_symbol(toks: read toks, i: read k, code: read SYM_RBRACKET) || at_symbol(toks: read toks, i: read k, code: read SYM_RBRACE)
807+
if opener {
808+
depth = depth + 1
809+
} else if closer {
810+
if depth > 0 { depth = depth - 1 }
811+
} else if at_symbol(toks: read toks, i: read k, code: read SYM_LANGLE) {
812+
angle = angle + 1
813+
} else if at_symbol(toks: read toks, i: read k, code: read SYM_RANGLE) {
814+
if angle > 0 { angle = angle - 1 }
815+
} else if depth == 0 && angle == 0 && at_symbol(toks: read toks, i: read k, code: read SYM_COMMA) {
816+
if param_seg_missing_effect(toks: read toks, segStart: read segStart, segEnd: read k, sums: read sums) { hit = true }
817+
segStart = k + 1
818+
}
819+
k = k + 1
820+
}
821+
if param_seg_missing_effect(toks: read toks, segStart: read segStart, segEnd: read close, sums: read sums) { hit = true }
822+
return hit
823+
}
824+
635825
// ---------------------------------------------------------------------------
636826
// RS0024 UNKNOWN_TYPE.
637827
// ---------------------------------------------------------------------------
@@ -959,8 +1149,11 @@ fn main() -> Unit {
9591149
let mut invalidSelf = false
9601150
let mut retainsBad = false
9611151
let mut unknownType = false
1152+
let mut missingEffect = false
9621153
// RS0024: forward-reference-tolerant known-type set for the whole file.
9631154
let declared = collect_declared_types(toks: read toks)
1155+
// RS0008: user sum names whose every variant is payloadless.
1156+
let payloadlessSums = collect_payloadless_sums(toks: read toks)
9641157
// RS0033: any decimal integer literal in the file that overflows i64.
9651158
let outOfRangeInt = has_out_of_range_int(toks: read toks)
9661159
let mut i = 0
@@ -1114,6 +1307,7 @@ fn main() -> Unit {
11141307
if fn_has_invalid_self(toks: read toks, start: read i) { invalidSelf = true }
11151308
if fn_retains_bad(toks: read toks, start: read i) { retainsBad = true }
11161309
if fn_types_unknown(toks: read toks, start: read i, declared: read declared) { unknownType = true }
1310+
if fn_missing_param_effect(toks: read toks, start: read i, sums: read payloadlessSums) { missingEffect = true }
11171311
i = r
11181312
}
11191313
} else {
@@ -1152,6 +1346,9 @@ fn main() -> Unit {
11521346
if retainsBad {
11531347
Log.write(message: read "RS0007")
11541348
}
1349+
if missingEffect {
1350+
Log.write(message: read "RS0008")
1351+
}
11551352
if unknownType {
11561353
Log.write(message: read "RS0024")
11571354
}
@@ -1167,7 +1364,7 @@ fn main() -> Unit {
11671364
if headerCount >= 2 {
11681365
Log.write(message: read "RS0006")
11691366
}
1170-
let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime || invalidSelf || outOfRangeInt || retainsBad || unknownType
1367+
let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime || invalidSelf || outOfRangeInt || retainsBad || unknownType || missingEffect
11711368
if anyDiag == false {
11721369
Log.write(message: read "CLEAN")
11731370
}

0 commit comments

Comments
 (0)