Skip to content

Commit 5c09911

Browse files
olwangclaude
andcommitted
feat(selfhost): RS0201 UNNAMED_ARGUMENT — call-naming parity (28 codes green)
Add RS0201 to the self-hosted checker (selfhost/check.rss): a resolved call site that passes an argument without a `name:` label where the callee requires named arguments. Named args are required for public same-file functions and for core qualified calls to a curated function-namespace allowlist {String, Image, Log}; receiver-call shorthand, private same-file helpers, enum-variant and type constructors, and unresolved/imported namespaces are all skipped (no false positive). Empty malformed arg slots (`f(, x)`, `f(x,, y)`) count as unnamed; a lone trailing comma does not. Reusable call-signature infra built for this (and the later RS0202 cluster): * collect_call_fn_sigs — same-file fn name sets: `pubFns` (public unqualified) and `dottedFns` (dotted method names), for call resolution. * call_requires_named / is_core_named_namespace — call-kind classification matching the parser's is_qualified_namespace_receiver (uppercase dotted head = qualified) plus measure-first namespace curation. * args_have_unnamed / seg_is_unnamed — top-level arg splitter that respects (), [], <>, {} nesting and closure param pipes `|a, b|`, with the malformed empty-slot and trailing-comma rules. Measure-first over the 619-file corpus: 0 code-mismatches, 0 run-failures (checker_parity_corpus green). CHECKER_TARGET_CODES: 27 -> 28 codes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9f2a69c commit 5c09911

2 files changed

Lines changed: 189 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","RS0008","RS0009","RS0010","RS0011","RS0012","RS0016","RS0017","RS0021","RS0024","RS0028","RS0033","RS0029","RS0023","RS0035","RS0027","RS0014","RS0018","RS0019","RS0022","RS0101","RS0013"];
506+
const CHECKER_TARGET_CODES: &[&str] = &["RS0002","RS0003","RS0004","RS0005","RS0006","RS0007","RS0008","RS0009","RS0010","RS0011","RS0012","RS0016","RS0017","RS0021","RS0024","RS0028","RS0033","RS0029","RS0023","RS0035","RS0027","RS0014","RS0018","RS0019","RS0022","RS0101","RS0013","RS0201"];
507507

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

selfhost/check.rss

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ const SYM_SEMI: Int = 59
2929
// '?' — the try-operator symbol code (single-char symbol, val = code point).
3030
const SYM_QUESTION: Int = 63
3131

32+
// '|' — the closure-parameter delimiter (single-char symbol). Used by the RS0201
33+
// arg-splitter to skip commas inside a closure param list `|a, b|`.
34+
const SYM_PIPE: Int = 124
35+
3236
fn tk_text(toks: read List<Tok>, i: read Int) -> String {
3337
if i < 0 { return "" }
3438
if i >= List.len(list: read toks) { return "" }
@@ -2913,6 +2917,179 @@ fn fn_invalid_try(toks: read List<Tok>, start: read Int, declared: read Set<Stri
29132917
return hit
29142918
}
29152919

2920+
// ---------------------------------------------------------------------------
2921+
// RS0201 UNNAMED_ARGUMENT (check_argument_naming, checks/calls.rs).
2922+
//
2923+
// A call argument passed WITHOUT a `name:` label where the callee requires named
2924+
// arguments. Named args are REQUIRED for public same-file functions, and for
2925+
// core/native/stdlib qualified calls (`Ns.method(...)`); they are NOT required
2926+
// for receiver-call shorthand (`value.method(...)`), private same-file helpers,
2927+
// enum-variant constructors (`Ok`/`Some`/user variants), or type constructors
2928+
// (which also permit field shorthand). The parser (syntax/parser/expr.rs
2929+
// is_qualified_namespace_receiver) treats an UPPERCASE dotted head as a qualified
2930+
// namespace call and a lowercase head as a receiver call — replicated here by the
2931+
// first-character-uppercase test. FILE-LEVEL: emit once if ANY resolved call site
2932+
// carries an unnamed argument. Conservative on anything not confidently a
2933+
// named-required call (skip => no false positive).
2934+
// ---------------------------------------------------------------------------
2935+
2936+
// Index of the `<` that matches the `>` at `closePos`, scanning backwards, or -1.
2937+
fn back_match_angle(toks: read List<Tok>, closePos: read Int) -> Int {
2938+
let mut depth = 0
2939+
let mut k = closePos
2940+
let mut res = -1
2941+
while k >= 0 && res < 0 {
2942+
if at_symbol(toks: read toks, i: read k, code: read SYM_RANGLE) {
2943+
depth = depth + 1
2944+
} else if at_symbol(toks: read toks, i: read k, code: read SYM_LANGLE) {
2945+
depth = depth - 1
2946+
if depth == 0 { res = k }
2947+
}
2948+
k = k - 1
2949+
}
2950+
return res
2951+
}
2952+
2953+
// Collect same-file fn name sets: `pubFns` = unqualified fn names declared `pub`;
2954+
// `dottedFns` = full dotted names of same-file dotted fns (e.g. `Cache.put`), used
2955+
// to treat a qualified call to a same-file method as positional-allowed.
2956+
fn collect_call_fn_sigs(toks: read List<Tok>, pubFns: mut Set<String>, dottedFns: mut Set<String>) -> Unit {
2957+
let n = List.len(list: read toks)
2958+
let mut i = 0
2959+
while i < n {
2960+
if at_ident(toks: read toks, i: read i, wid: read WORD_FN) {
2961+
let nameIdx = i + 1
2962+
if is_ident_tok(toks: read toks, i: read nameIdx) {
2963+
let full = dotted_name_text(toks: read toks, i: read nameIdx)
2964+
let isDotted = at_symbol(toks: read toks, i: read (nameIdx + 1), code: read SYM_DOT)
2965+
let mut isPub = false
2966+
let mut b = i - 1
2967+
let mut scanning = true
2968+
while scanning {
2969+
if at_ident(toks: read toks, i: read b, wid: read WORD_PUB) {
2970+
isPub = true
2971+
b = b - 1
2972+
} else if at_ident(toks: read toks, i: read b, wid: read WORD_ASYNC) || at_ident(toks: read toks, i: read b, wid: read WORD_NATIVE) {
2973+
b = b - 1
2974+
} else {
2975+
scanning = false
2976+
}
2977+
}
2978+
if isDotted {
2979+
Set.insert<String>(set: mut dottedFns, value: read full)
2980+
} else if isPub {
2981+
Set.insert<String>(set: mut pubFns, value: read full)
2982+
}
2983+
}
2984+
}
2985+
i = i + 1
2986+
}
2987+
return Unit
2988+
}
2989+
2990+
// Core function-namespaces whose qualified calls (`Ns.method(...)`) require named
2991+
// arguments. Curated (measure-first) to the namespaces the corpus oracle actually
2992+
// flags: broadening to every stdlib/builtin type would risk false positives on
2993+
// stdlib sum-variant construction (`JsonValue.String(...)`) and on unknown/imported
2994+
// namespaces (`ChatMessage.system(...)`, which the oracle leaves as UNKNOWN_CALLEE
2995+
// with no naming diagnostic). See checks/calls.rs check_argument_naming.
2996+
fn is_core_named_namespace(head: read String) -> Bool {
2997+
return head == "String" || head == "Image" || head == "Log"
2998+
}
2999+
3000+
// True iff the call whose arg-list `(` is at `op` requires named arguments: a
3001+
// public same-file unqualified fn, or a core qualified call to a curated
3002+
// function-namespace. Bails (false) on receiver calls, private same-file helpers,
3003+
// variant/type constructors, and any callee shape it cannot resolve confidently.
3004+
fn call_requires_named(toks: read List<Tok>, op: read Int, pubFns: read Set<String>, dottedFns: read Set<String>) -> Bool {
3005+
let mut nameLast = op - 1
3006+
if at_symbol(toks: read toks, i: read nameLast, code: read SYM_RANGLE) {
3007+
let openAngle = back_match_angle(toks: read toks, closePos: read nameLast)
3008+
if openAngle < 0 { return false }
3009+
nameLast = openAngle - 1
3010+
}
3011+
if is_ident_tok(toks: read toks, i: read nameLast) == false { return false }
3012+
if at_symbol(toks: read toks, i: read (nameLast - 1), code: read SYM_DOT) {
3013+
if is_ident_tok(toks: read toks, i: read (nameLast - 2)) == false { return false }
3014+
let dotHead = nameLast - 2
3015+
if at_symbol(toks: read toks, i: read (dotHead - 1), code: read SYM_DOT) { return false }
3016+
let head = tk_text(toks: read toks, i: read dotHead)
3017+
let full = String.concat(left: read String.concat(left: read head, right: read "."), right: read tk_text(toks: read toks, i: read nameLast))
3018+
if Set.contains<String>(set: read dottedFns, value: read full) { return false }
3019+
return is_core_named_namespace(head: read head)
3020+
}
3021+
let name = tk_text(toks: read toks, i: read nameLast)
3022+
return Set.contains<String>(set: read pubFns, value: read name)
3023+
}
3024+
3025+
// True iff a call-arg segment [s,e) is an unnamed argument: empty (a malformed
3026+
// slot from a leading/double comma), or a value not prefixed by a `name:` label.
3027+
fn seg_is_unnamed(toks: read List<Tok>, s: read Int, e: read Int) -> Bool {
3028+
if s >= e { return true }
3029+
if is_ident_tok(toks: read toks, i: read s) && at_symbol(toks: read toks, i: read (s + 1), code: read SYM_COLON) {
3030+
return false
3031+
}
3032+
return true
3033+
}
3034+
3035+
// True iff the arg list [op+1, close) has any unnamed argument. A single trailing
3036+
// empty segment (trailing comma) does NOT count; empty leading/middle segments do.
3037+
fn args_have_unnamed(toks: read List<Tok>, op: read Int, close: read Int) -> Bool {
3038+
let mut k = op + 1
3039+
let mut depth = 0
3040+
let mut angle = 0
3041+
let mut inPipe = false
3042+
let mut segStart = op + 1
3043+
let mut hit = false
3044+
while k < close {
3045+
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)
3046+
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)
3047+
if opener {
3048+
depth = depth + 1
3049+
} else if closer {
3050+
if depth > 0 { depth = depth - 1 }
3051+
} else if at_symbol(toks: read toks, i: read k, code: read SYM_LANGLE) {
3052+
angle = angle + 1
3053+
} else if at_symbol(toks: read toks, i: read k, code: read SYM_RANGLE) {
3054+
if angle > 0 { angle = angle - 1 }
3055+
} else if depth == 0 && angle == 0 && at_symbol(toks: read toks, i: read k, code: read SYM_PIPE) {
3056+
inPipe = inPipe == false
3057+
} else if depth == 0 && angle == 0 && inPipe == false && at_symbol(toks: read toks, i: read k, code: read SYM_COMMA) {
3058+
if seg_is_unnamed(toks: read toks, s: read segStart, e: read k) { hit = true }
3059+
segStart = k + 1
3060+
}
3061+
k = k + 1
3062+
}
3063+
if segStart < close {
3064+
if seg_is_unnamed(toks: read toks, s: read segStart, e: read close) { hit = true }
3065+
}
3066+
return hit
3067+
}
3068+
3069+
// RS0201 over a fn body [sigEnd+1, bodyClose): any resolved named-required call
3070+
// site with an unnamed argument.
3071+
fn fn_has_unnamed_arg(toks: read List<Tok>, start: read Int, pubFns: read Set<String>, dottedFns: read Set<String>) -> Bool {
3072+
let ns = fn_name_start(toks: read toks, i: read start)
3073+
let sigEnd = function_signature_end(toks: read toks, start: read (ns - 1))
3074+
if at_symbol(toks: read toks, i: read sigEnd, code: read SYM_LBRACE) == false { return false }
3075+
let bodyClose = find_matching(toks: read toks, open: read sigEnd, openSym: read SYM_LBRACE, closeSym: read SYM_RBRACE)
3076+
if bodyClose < 0 { return false }
3077+
let mut op = sigEnd + 1
3078+
let mut hit = false
3079+
while op < bodyClose {
3080+
if at_symbol(toks: read toks, i: read op, code: read SYM_LPAREN) {
3081+
if call_requires_named(toks: read toks, op: read op, pubFns: read pubFns, dottedFns: read dottedFns) {
3082+
let close = find_matching(toks: read toks, open: read op, openSym: read SYM_LPAREN, closeSym: read SYM_RPAREN)
3083+
if close >= 0 && close <= bodyClose {
3084+
if args_have_unnamed(toks: read toks, op: read op, close: read close) { hit = true }
3085+
}
3086+
}
3087+
}
3088+
op = op + 1
3089+
}
3090+
return hit
3091+
}
3092+
29163093
fn main() -> Unit {
29173094
let source = Args.get_or_default(index: 0, default: read "")
29183095
let toks = tokenize(source: read source)
@@ -2971,6 +3148,12 @@ fn main() -> Unit {
29713148
let mut allFns = Set<String>.new()
29723149
let mut noBlockFns = Set<String>.new()
29733150
let mut noPanicFns = Set<String>.new()
3151+
// RS0201: public unqualified fn names + dotted (method) fn names, for the
3152+
// named-argument requirement.
3153+
let mut pubFns = Set<String>.new()
3154+
let mut dottedFns = Set<String>.new()
3155+
collect_call_fn_sigs(toks: read toks, pubFns: mut pubFns, dottedFns: mut dottedFns)
3156+
let mut unnamedArg = false
29743157
// Signature table (cross-function): same-file async fn names, for RS0022.
29753158
let mut asyncFns = Set<String>.new()
29763159
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)
@@ -3152,6 +3335,7 @@ fn main() -> Unit {
31523335
if fn_has_nonexhaustive_match(toks: read toks, start: read i, declared: read declared, allFns: read allFns) { nonExhaustive = true }
31533336
if fn_has_bad_await(toks: read toks, start: read i) { badAwait = true }
31543337
if fn_invalid_try(toks: read toks, start: read i, declared: read declared, allFns: read allFns) { invalidTry = true }
3338+
if fn_has_unnamed_arg(toks: read toks, start: read i, pubFns: read pubFns, dottedFns: read dottedFns) { unnamedArg = true }
31553339
if fn_has_fd_surface(toks: read toks, start: read i) { fdSurface = true }
31563340
// RS0027: an unknown protocol in this fn's generic bounds.
31573341
if generics_unknown_protocol(toks: read toks, nameEnd: read dotted_end(toks: read toks, i: read ns), protocols: read protocols) { unknownProtocol = true }
@@ -3236,6 +3420,9 @@ fn main() -> Unit {
32363420
if invalidTry {
32373421
Log.write(message: read "RS0013")
32383422
}
3423+
if unnamedArg {
3424+
Log.write(message: read "RS0201")
3425+
}
32393426
if asyncNotConsumed {
32403427
Log.write(message: read "RS0022")
32413428
}
@@ -3279,7 +3466,7 @@ fn main() -> Unit {
32793466
if featureViol {
32803467
Log.write(message: read "RS0101")
32813468
}
3282-
let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime || invalidSelf || outOfRangeInt || retainsBad || unknownType || missingEffect || pureBad || nonExhaustive || badAwait || invalidTry || fdSurface || lowerConflict || unknownProtocol || noallocAlloc || noBlockCall || noPanicCall || asyncNotConsumed || featureViol
3469+
let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime || invalidSelf || outOfRangeInt || retainsBad || unknownType || missingEffect || pureBad || nonExhaustive || badAwait || invalidTry || fdSurface || lowerConflict || unknownProtocol || noallocAlloc || noBlockCall || noPanicCall || asyncNotConsumed || featureViol || unnamedArg
32833470
if anyDiag == false {
32843471
Log.write(message: read "CLEAN")
32853472
}

0 commit comments

Comments
 (0)