@@ -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