Skip to content

Commit 6dbc59f

Browse files
olwangclaude
andcommitted
feat(selfhost-checker): add RS0007 (unknown/copy retained parameter)
Corpus-green over 619 files: retains(p) flags when p is not a param or is a Copy-scalar param. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8c23ce4 commit 6dbc59f

2 files changed

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

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

selfhost/check.rss

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,119 @@ fn seg_is_share(toks: read List<Tok>, segStart: read Int, segEnd: read Int) -> B
519519
return tk_text(toks: read toks, i: read (segStart + 2)) == "share"
520520
}
521521

522+
// The 17 Copy scalar type names (crate scalar/primitive set).
523+
fn is_scalar_name(name: read String) -> Bool {
524+
return name == "Bool" || name == "Byte" || name == "Char" || name == "Float" || name == "Float32" || name == "Float64" || name == "Int" || name == "Int8" || name == "Int16" || name == "Int32" || name == "Int64" || name == "UInt" || name == "UInt8" || name == "UInt16" || name == "UInt32" || name == "UInt64" || name == "Unit"
525+
}
526+
527+
// Copy classification of a param type region for RS0007. The type begins after
528+
// `name :` and an optional data-effect; then optional prefixes precede the base.
529+
// Copy iff (no fresh/noescape prefix) AND base ∈ scalars AND next token is not `<`.
530+
fn seg_type_is_copy(toks: read List<Tok>, segStart: read Int, segEnd: read Int) -> Bool {
531+
let mut k = segStart + 2
532+
if is_effect_kw(toks: read toks, i: read k) { k = k + 1 }
533+
let mut sawFreshNoescape = false
534+
let mut scanning = true
535+
while scanning {
536+
let t = tk_text(toks: read toks, i: read k)
537+
if t == "fresh" || t == "noescape" {
538+
sawFreshNoescape = true
539+
k = k + 1
540+
} else if t == "owned" || t == "handle" || t == "weak" {
541+
k = k + 1
542+
} else {
543+
scanning = false
544+
}
545+
}
546+
if sawFreshNoescape { return false }
547+
let base = tk_text(toks: read toks, i: read k)
548+
if is_scalar_name(name: read base) == false { return false }
549+
if at_symbol(toks: read toks, i: read (k + 1), code: read SYM_LANGLE) { return false }
550+
return true
551+
}
552+
553+
// Status of a named param for RS0007: -1 = this segment is not param `pname`,
554+
// 2 = matched and Copy, 1 = matched and non-Copy.
555+
fn one_param_status(toks: read List<Tok>, segStart: read Int, segEnd: read Int, pname: read String) -> Int {
556+
if segStart >= segEnd { return -1 }
557+
if is_ident_tok(toks: read toks, i: read segStart) == false { return -1 }
558+
if at_symbol(toks: read toks, i: read (segStart + 1), code: read SYM_COLON) == false { return -1 }
559+
if tk_text(toks: read toks, i: read segStart) != pname { return -1 }
560+
if seg_type_is_copy(toks: read toks, segStart: read segStart, segEnd: read segEnd) { return 2 }
561+
return 1
562+
}
563+
564+
// Walk the param list [open,close] looking for param `pname`. Returns 0 (not a
565+
// param), 1 (param, non-Copy), or 2 (param, Copy).
566+
fn param_retains_status(toks: read List<Tok>, open: read Int, close: read Int, pname: read String) -> Int {
567+
let mut k = open + 1
568+
let mut depth = 0
569+
let mut angle = 0
570+
let mut segStart = open + 1
571+
let mut status = 0
572+
while k < close {
573+
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)
574+
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)
575+
if opener {
576+
depth = depth + 1
577+
} else if closer {
578+
if depth > 0 { depth = depth - 1 }
579+
} else if at_symbol(toks: read toks, i: read k, code: read SYM_LANGLE) {
580+
angle = angle + 1
581+
} else if at_symbol(toks: read toks, i: read k, code: read SYM_RANGLE) {
582+
if angle > 0 { angle = angle - 1 }
583+
} else if depth == 0 && angle == 0 && at_symbol(toks: read toks, i: read k, code: read SYM_COMMA) {
584+
let s = one_param_status(toks: read toks, segStart: read segStart, segEnd: read k, pname: read pname)
585+
if s != -1 { status = s }
586+
segStart = k + 1
587+
}
588+
k = k + 1
589+
}
590+
let s = one_param_status(toks: read toks, segStart: read segStart, segEnd: read close, pname: read pname)
591+
if s != -1 { status = s }
592+
return status
593+
}
594+
595+
// One effects-clause item [segStart,segEnd): a `retains(p)` where p is not a
596+
// param, or is a Copy param → RS0007.
597+
fn retains_item_bad(toks: read List<Tok>, segStart: read Int, segEnd: read Int, popen: read Int, pclose: read Int) -> Bool {
598+
if effect_item_kind(toks: read toks, segStart: read segStart, segEnd: read segEnd) != 2 { return false }
599+
let pname = tk_text(toks: read toks, i: read (segStart + 2))
600+
let status = param_retains_status(toks: read toks, open: read popen, close: read pclose, pname: read pname)
601+
return status == 0 || status == 2
602+
}
603+
604+
// RS0007: an `effects(retains(p))` item whose p is not a param, or is a Copy param.
605+
fn fn_retains_bad(toks: read List<Tok>, start: read Int) -> Bool {
606+
let effOpen = find_effects_open(toks: read toks, start: read start)
607+
if effOpen < 0 { return false }
608+
let effClose = find_matching(toks: read toks, open: read effOpen, openSym: read SYM_LPAREN, closeSym: read SYM_RPAREN)
609+
if effClose < 0 { return false }
610+
let ns = fn_name_start(toks: read toks, i: read start)
611+
let sigEnd = function_signature_end(toks: read toks, start: read (ns - 1))
612+
let popen = find_param_open(toks: read toks, from: read ns, end: read sigEnd)
613+
if popen < 0 { return false }
614+
let pclose = find_matching(toks: read toks, open: read popen, openSym: read SYM_LPAREN, closeSym: read SYM_RPAREN)
615+
if pclose < 0 { return false }
616+
let mut k = effOpen + 1
617+
let mut segStart = effOpen + 1
618+
let mut depth = 0
619+
let mut hit = false
620+
while k < effClose {
621+
if at_symbol(toks: read toks, i: read k, code: read SYM_LPAREN) {
622+
depth = depth + 1
623+
} else if at_symbol(toks: read toks, i: read k, code: read SYM_RPAREN) {
624+
if depth > 0 { depth = depth - 1 }
625+
} else if depth == 0 && at_symbol(toks: read toks, i: read k, code: read SYM_COMMA) {
626+
if retains_item_bad(toks: read toks, segStart: read segStart, segEnd: read k, popen: read popen, pclose: read pclose) { hit = true }
627+
segStart = k + 1
628+
}
629+
k = k + 1
630+
}
631+
if retains_item_bad(toks: read toks, segStart: read segStart, segEnd: read effClose, popen: read popen, pclose: read pclose) { hit = true }
632+
return hit
633+
}
634+
522635
fn main() -> Unit {
523636
let source = Args.get_or_default(index: 0, default: read "")
524637
let toks = tokenize(source: read source)
@@ -541,6 +654,7 @@ fn main() -> Unit {
541654
let mut unknownEffect = false
542655
let mut removedRuntime = false
543656
let mut invalidSelf = false
657+
let mut retainsBad = false
544658
// RS0033: any decimal integer literal in the file that overflows i64.
545659
let outOfRangeInt = has_out_of_range_int(toks: read toks)
546660
let mut i = 0
@@ -691,6 +805,7 @@ fn main() -> Unit {
691805
if effects_name_probe(toks: read toks, start: read i, mode: read 0) { unknownEffect = true }
692806
if effects_name_probe(toks: read toks, start: read i, mode: read 1) { removedRuntime = true }
693807
if fn_has_invalid_self(toks: read toks, start: read i) { invalidSelf = true }
808+
if fn_retains_bad(toks: read toks, start: read i) { retainsBad = true }
694809
i = r
695810
}
696811
} else {
@@ -726,6 +841,9 @@ fn main() -> Unit {
726841
if invalidSelf {
727842
Log.write(message: read "RS0028")
728843
}
844+
if retainsBad {
845+
Log.write(message: read "RS0007")
846+
}
729847
if outOfRangeInt {
730848
Log.write(message: read "RS0033")
731849
}
@@ -738,7 +856,7 @@ fn main() -> Unit {
738856
if headerCount >= 2 {
739857
Log.write(message: read "RS0006")
740858
}
741-
let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime || invalidSelf || outOfRangeInt
859+
let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime || invalidSelf || outOfRangeInt || retainsBad
742860
if anyDiag == false {
743861
Log.write(message: read "CLEAN")
744862
}

0 commit comments

Comments
 (0)