Skip to content

Commit 67e5a34

Browse files
olwangclaude
andcommitted
feat(selfhost): checker step-2 milestone 2d — RS0004/RS0012 via parse_effects port
Ports parse_effects into check.rss and adds two effects-clause diagnostics: - RS0004 UNKNOWN_EFFECT: `fresh` or an unrecognized effect name. - RS0012 REMOVED_RUNTIME_EFFECT: io/allocates/may_panic/may_fail/async/suspends. parse_effects is per-item: a malformed item (`effects(a,,b)`, `effects(retains())`, `effects(custom(x))`) recovers to RS0015 and is skipped, while valid items in the same clause are still checked. effect_item_kind mirrors the exact validity rule (bare single-token name, or retains(ident) spanning the whole segment). Also fixes a latent bug shared by the RS0002/3/11 scanners: the signature scan must begin at the `fn` keyword (ns-1), not the attribute-led decl start — otherwise function_signature_end stops at the later `fn` and misses the whole signature of `#deprecated(...) fn …`. CHECKER_TARGET_CODES now 10 codes; checker_parity_corpus byte-exact over 619 files, 0 mismatches, 0 run-failures. RS0007 stays deferred (semantic type_ref_is_copy sub-condition). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6d18e0a commit 67e5a34

3 files changed

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

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

docs/ledgers/rss-selfhost-ledger.md

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -983,16 +983,27 @@ gap is VM value-representation / intrinsic-dispatch cost (the next big lever).
983983
- **Diagnostics (step 2, milestone 2c — DONE, 2026-07-02):** added **RS0010**
984984
(REMOVED_PROFILE_DECLARATION — any `profile:` decl) and **RS0011**
985985
(REMOVED_SHARE_EFFECT — a parameter written `name: share …`, no data effect,
986-
type name `share`). Both purely structural. `CHECKER_TARGET_CODES` = **8 codes**;
987-
`checker_parity_corpus` byte-exact **619 files, 0 mismatches, 0 run-failures**.
988-
MEASURED-BUT-DEFERRED (footprint known, 19 corpus files): the effects-clause
989-
family RS0004/RS0007/RS0012 needs a `parse_effects` port — malformed clauses
990-
(`effects(a,,b)`, `effects(retains())`, `effects(custom(x))`) must recover to
991-
RS0015, NOT the effect-name checks, and RS0007 has a semantic `type_ref_is_copy`
992-
sub-condition; RS0008 (missing-effect) needs a Copy/sum-type table; RS0009
993-
(invalid-pure) needs body analysis; RS0033 (int-range) needs literal-value
994-
parsing; RS0028 (invalid-self) needs the protocol-method + first-param rules.
995-
Next genuinely semantic tier: RS0021 exhaustiveness / RS0024 unknown-type (need a
986+
type name `share`). Both purely structural.
987+
- **Diagnostics (step 2, milestone 2d — DONE, 2026-07-02):** ported `parse_effects`
988+
and added **RS0004** (UNKNOWN_EFFECT — `fresh`/unrecognized effect name) and
989+
**RS0012** (REMOVED_RUNTIME_EFFECT — io/allocates/may_panic/may_fail/async/
990+
suspends). KEY: parse_effects is PER-ITEM — a malformed item (`,,` empty slot,
991+
`retains()`, `custom(x)`) recovers to RS0015 and is SKIPPED, while valid items in
992+
the SAME clause still get checked (`effects(no_panic,, native)` → both names
993+
checked, empty slot → RS0015 only). `effect_item_kind` mirrors the exact
994+
validity: bare single-token Name, or `retains(ident)` (close+1==end, start+3==
995+
close, inner ident); everything else malformed. Also fixed a latent bug: the
996+
signature scan must start at the `fn` keyword (`ns-1`), not the attribute-led decl
997+
start — otherwise `function_signature_end` stops at the later `fn` (a top-level-
998+
item boundary), which had hidden the effects clause of `#deprecated(...) fn …`
999+
(and would have mis-scanned RS0002/3/11 there too). `CHECKER_TARGET_CODES` =
1000+
**10 codes**; `checker_parity_corpus` byte-exact **619 files, 0 mismatches, 0
1001+
run-failures**. STILL DEFERRED from this family: **RS0007** (retains-param) has a
1002+
semantic `type_ref_is_copy` sub-condition (a retained Copy param → RS0007) that
1003+
isn't token-decidable. Other deferred: RS0008 (missing-effect, needs Copy/sum
1004+
table), RS0009 (invalid-pure, needs body analysis), RS0033 (int-range, needs
1005+
literal-value parsing), RS0028 (invalid-self, protocol/first-param rules). Next
1006+
genuinely semantic tier: RS0021 exhaustiveness / RS0024 unknown-type (need a
9961007
`Map<String,Def>` symbol-table pass).
9971008
- **Lexer spans (step 3):** added a `len` field to the shared `Tok`
9981009
(= consumed source span `j-i`, matching the Rust lexer's `index-start`) and made

selfhost/check.rss

Lines changed: 122 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,10 @@ fn find_param_open(toks: read List<Tok>, from: read Int, end: read Int) -> Int {
204204
// function.return_ty.is_none()). True iff no top-level `->` follows the param list.
205205
fn fn_missing_return(toks: read List<Tok>, start: read Int) -> Bool {
206206
let ns = fn_name_start(toks: read toks, i: read start)
207-
let sigEnd = function_signature_end(toks: read toks, start: read start)
207+
// Signature scan must begin at the `fn` keyword (ns - 1), not the decl start:
208+
// when the decl is attribute-led (`#deprecated(...) fn …`), starting at `#`
209+
// makes function_signature_end stop at the later `fn` (a top-level-item start).
210+
let sigEnd = function_signature_end(toks: read toks, start: read (ns - 1))
208211
let open = find_param_open(toks: read toks, from: read ns, end: read sigEnd)
209212
if open < 0 { return false }
210213
let close = find_matching(toks: read toks, open: read open, openSym: read SYM_LPAREN, closeSym: read SYM_RPAREN)
@@ -234,7 +237,10 @@ fn seg_is_untyped(toks: read List<Tok>, segStart: read Int, segEnd: read Int) ->
234237
// ()[]{}<>) and tests each segment with seg_is_untyped.
235238
fn fn_has_untyped_param(toks: read List<Tok>, start: read Int) -> Bool {
236239
let ns = fn_name_start(toks: read toks, i: read start)
237-
let sigEnd = function_signature_end(toks: read toks, start: read start)
240+
// Signature scan must begin at the `fn` keyword (ns - 1), not the decl start:
241+
// when the decl is attribute-led (`#deprecated(...) fn …`), starting at `#`
242+
// makes function_signature_end stop at the later `fn` (a top-level-item start).
243+
let sigEnd = function_signature_end(toks: read toks, start: read (ns - 1))
238244
let open = find_param_open(toks: read toks, from: read ns, end: read sigEnd)
239245
if open < 0 { return false }
240246
let close = find_matching(toks: read toks, open: read open, openSym: read SYM_LPAREN, closeSym: read SYM_RPAREN)
@@ -265,10 +271,110 @@ fn fn_has_untyped_param(toks: read List<Tok>, start: read Int) -> Bool {
265271
return untyped
266272
}
267273

274+
// Valid v0.6 effect names for an `effects(...)` clause (check_function_effects).
275+
fn is_valid_effect_name(name: read String) -> Bool {
276+
return name == "no_panic" || name == "noalloc" || name == "no_block" || name == "pure" || name == "unsafe" || name == "native" || name == "parallel"
277+
}
278+
279+
// Removed runtime effects → RS0012 (removed_runtime_effect_replacement set).
280+
fn is_removed_runtime_effect(name: read String) -> Bool {
281+
return name == "io" || name == "allocates" || name == "may_panic" || name == "may_fail" || name == "async" || name == "suspends"
282+
}
283+
284+
// Index of the `(` of a top-level `effects(...)` clause in a function signature,
285+
// or -1. The clause sits after the param list, before the body.
286+
fn find_effects_open(toks: read List<Tok>, start: read Int) -> Int {
287+
let ns = fn_name_start(toks: read toks, i: read start)
288+
// Signature scan must begin at the `fn` keyword (ns - 1), not the decl start:
289+
// when the decl is attribute-led (`#deprecated(...) fn …`), starting at `#`
290+
// makes function_signature_end stop at the later `fn` (a top-level-item start).
291+
let sigEnd = function_signature_end(toks: read toks, start: read (ns - 1))
292+
let open = find_param_open(toks: read toks, from: read ns, end: read sigEnd)
293+
if open < 0 { return -1 }
294+
let pclose = find_matching(toks: read toks, open: read open, openSym: read SYM_LPAREN, closeSym: read SYM_RPAREN)
295+
if pclose < 0 { return -1 }
296+
let mut k = pclose + 1
297+
let mut depth = 0
298+
let mut r = -1
299+
while k < sigEnd && r < 0 {
300+
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) {
301+
depth = depth + 1
302+
} 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) {
303+
if depth > 0 { depth = depth - 1 }
304+
} else if depth == 0 && tk_text(toks: read toks, i: read k) == "effects" && at_symbol(toks: read toks, i: read (k + 1), code: read SYM_LPAREN) {
305+
r = k + 1
306+
}
307+
k = k + 1
308+
}
309+
return r
310+
}
311+
312+
// Classify one effects-clause item [segStart,segEnd) exactly like parse_effects:
313+
// 1 = a valid bare `Name` (single-token effect name)
314+
// 2 = a valid `retains(ident)` item
315+
// 0 = malformed (empty, non-ident, `name(args)`, bad retains) → skipped, no check
316+
fn effect_item_kind(toks: read List<Tok>, segStart: read Int, segEnd: read Int) -> Int {
317+
if segStart >= segEnd { return 0 }
318+
if is_ident_tok(toks: read toks, i: read segStart) == false { return 0 }
319+
let name = tk_text(toks: read toks, i: read segStart)
320+
if name == "retains" {
321+
if at_symbol(toks: read toks, i: read (segStart + 1), code: read SYM_LPAREN) {
322+
let close = find_matching(toks: read toks, open: read (segStart + 1), openSym: read SYM_LPAREN, closeSym: read SYM_RPAREN)
323+
if close >= 0 && (close + 1) == segEnd && (segStart + 3) == close && is_ident_tok(toks: read toks, i: read (segStart + 2)) {
324+
return 2
325+
}
326+
}
327+
return 0
328+
}
329+
if (segStart + 1) == segEnd { return 1 }
330+
return 0
331+
}
332+
333+
// Walk a function's `effects(...)` items (only the VALID ones — malformed items
334+
// recover to RS0015 and are skipped). `mode`: 0 = RS0004 (unknown effect: `fresh`
335+
// or an unrecognized name), 1 = RS0012 (removed runtime effect).
336+
fn effects_name_probe(toks: read List<Tok>, start: read Int, mode: read Int) -> Bool {
337+
let effOpen = find_effects_open(toks: read toks, start: read start)
338+
if effOpen < 0 { return false }
339+
let effClose = find_matching(toks: read toks, open: read effOpen, openSym: read SYM_LPAREN, closeSym: read SYM_RPAREN)
340+
if effClose < 0 { return false }
341+
let mut k = effOpen + 1
342+
let mut segStart = effOpen + 1
343+
let mut depth = 0
344+
let mut hit = false
345+
while k < effClose {
346+
if at_symbol(toks: read toks, i: read k, code: read SYM_LPAREN) {
347+
depth = depth + 1
348+
} else if at_symbol(toks: read toks, i: read k, code: read SYM_RPAREN) {
349+
if depth > 0 { depth = depth - 1 }
350+
} else if depth == 0 && at_symbol(toks: read toks, i: read k, code: read SYM_COMMA) {
351+
if effect_name_hits(toks: read toks, segStart: read segStart, segEnd: read k, mode: read mode) { hit = true }
352+
segStart = k + 1
353+
}
354+
k = k + 1
355+
}
356+
if effect_name_hits(toks: read toks, segStart: read segStart, segEnd: read effClose, mode: read mode) { hit = true }
357+
return hit
358+
}
359+
360+
// Does a single VALID bare-name item trigger `mode`? (retains/malformed items
361+
// never trigger RS0004/RS0012.)
362+
fn effect_name_hits(toks: read List<Tok>, segStart: read Int, segEnd: read Int, mode: read Int) -> Bool {
363+
if effect_item_kind(toks: read toks, segStart: read segStart, segEnd: read segEnd) != 1 { return false }
364+
let name = tk_text(toks: read toks, i: read segStart)
365+
if mode == 1 { return is_removed_runtime_effect(name: read name) }
366+
if name == "fresh" { return true }
367+
if is_removed_runtime_effect(name: read name) { return false }
368+
return is_valid_effect_name(name: read name) == false
369+
}
370+
268371
// RS0011: a parameter written `name: share …` (no data effect, type name `share`).
269372
fn fn_has_share_param(toks: read List<Tok>, start: read Int) -> Bool {
270373
let ns = fn_name_start(toks: read toks, i: read start)
271-
let sigEnd = function_signature_end(toks: read toks, start: read start)
374+
// Signature scan must begin at the `fn` keyword (ns - 1), not the decl start:
375+
// when the decl is attribute-led (`#deprecated(...) fn …`), starting at `#`
376+
// makes function_signature_end stop at the later `fn` (a top-level-item start).
377+
let sigEnd = function_signature_end(toks: read toks, start: read (ns - 1))
272378
let open = find_param_open(toks: read toks, from: read ns, end: read sigEnd)
273379
if open < 0 { return false }
274380
let close = find_matching(toks: read toks, open: read open, openSym: read SYM_LPAREN, closeSym: read SYM_RPAREN)
@@ -322,10 +428,12 @@ fn main() -> Unit {
322428
// RS0002 (any fn missing an explicit return type) / RS0003 (any untyped param).
323429
let mut missingReturn = false
324430
let mut untypedParam = false
325-
// Signature-family diagnostics: RS0010 (removed profile), RS0011 (removed
326-
// share data effect on a parameter).
431+
// Signature-family diagnostics: RS0004 (unknown effect), RS0010 (removed
432+
// profile), RS0011 (removed share data effect), RS0012 (removed runtime effect).
327433
let mut hasProfile = false
328434
let mut shareParam = false
435+
let mut unknownEffect = false
436+
let mut removedRuntime = false
329437
let mut i = 0
330438
let mut running = true
331439
while running {
@@ -471,6 +579,8 @@ fn main() -> Unit {
471579
if fn_missing_return(toks: read toks, start: read i) { missingReturn = true }
472580
if fn_has_untyped_param(toks: read toks, start: read i) { untypedParam = true }
473581
if fn_has_share_param(toks: read toks, start: read i) { shareParam = true }
582+
if effects_name_probe(toks: read toks, start: read i, mode: read 0) { unknownEffect = true }
583+
if effects_name_probe(toks: read toks, start: read i, mode: read 1) { removedRuntime = true }
474584
i = r
475585
}
476586
} else {
@@ -488,6 +598,9 @@ fn main() -> Unit {
488598
if untypedParam {
489599
Log.write(message: read "RS0003")
490600
}
601+
if unknownEffect {
602+
Log.write(message: read "RS0004")
603+
}
491604
if found {
492605
Log.write(message: read "RS0005")
493606
}
@@ -497,6 +610,9 @@ fn main() -> Unit {
497610
if shareParam {
498611
Log.write(message: read "RS0011")
499612
}
613+
if removedRuntime {
614+
Log.write(message: read "RS0012")
615+
}
500616
if hasUnknownFeat {
501617
Log.write(message: read "RS0016")
502618
}
@@ -506,7 +622,7 @@ fn main() -> Unit {
506622
if headerCount >= 2 {
507623
Log.write(message: read "RS0006")
508624
}
509-
let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam
625+
let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime
510626
if anyDiag == false {
511627
Log.write(message: read "CLEAN")
512628
}

0 commit comments

Comments
 (0)