Skip to content

Commit 7fd0ce1

Browse files
olwangclaude
andcommitted
feat(selfhost-checker): RS0101 feature-gating — corpus-green 619/619
Add RS0101 (FEATURE_VIOLATION) to the self-hosted checker, reproducing all three oracle sources over the whole corpus byte-exact: - checks/features.rs feature_uses — construct -> required feature, gated against the file's declared `features:` header (order-independent pre-pass): local : `local` let/closure (WORD_LOCAL), `manage` expr, `take` data-effect (distinguished from `.take(` method and a `take:` binding), `ResourcePool<T>` type/callee unsafe : `effects(unsafe)` (WORD_UNSAFE — a header token self-gates) async : `async` modifier (`async fn`/`async let`/`async for`, next kw fn/let/for), `spawn`/`await`/`task_group`/`select`; the `async` EFFECT name in `effects(...)` is a removed runtime effect (RS0012), not an async construct, so it is excluded - analyzer/signatures.rs check_native_effect — a `native fn` missing `effects(native)` fires RS0101 regardless of the declared features - checks/body/semantics.rs check_match_pattern_effects — `match take` without `features: local` (subsumed by the `take` keyword probe) Native is tracked in the fn walk (missing-effect source fires even when the feature is declared); local/async/unsafe are whole-file token probes. Reserved feature keywords only appear in a `features:` header when declared, so a header token always self-gates. Reuses effects_name_probe (new mode 2 = native effect) and fn_is_native. CHECKER_TARGET_CODES = 26 codes; checker_parity_corpus byte-exact 619/619, 0 mismatches, 0 run-failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 64563f3 commit 7fd0ce1

2 files changed

Lines changed: 150 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"];
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"];
507507

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

selfhost/check.rss

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,9 @@ fn effect_name_hits(toks: read List<Tok>, segStart: read Int, segEnd: read Int,
370370
if effect_item_kind(toks: read toks, segStart: read segStart, segEnd: read segEnd) != 1 { return false }
371371
let name = tk_text(toks: read toks, i: read segStart)
372372
if mode == 1 { return is_removed_runtime_effect(name: read name) }
373+
// mode 2: RS0101 — the `native` effect (a `native fn` boundary, or any fn with
374+
// `effects(native)`). Used to gate the `native` file feature.
375+
if mode == 2 { return name == "native" }
373376
if name == "fresh" { return true }
374377
if is_removed_runtime_effect(name: read name) { return false }
375378
return is_valid_effect_name(name: read name) == false
@@ -2475,6 +2478,130 @@ fn fn_noalloc_alloc(toks: read List<Tok>, start: read Int, declared: read Set<St
24752478
return hit
24762479
}
24772480

2481+
// ---------------------------------------------------------------------------
2482+
// RS0101 FEATURE_VIOLATION (feature-gating).
2483+
//
2484+
// A feature-requiring construct is used but the file header does not declare the
2485+
// required `features:`. FILE-LEVEL: emit "RS0101" once if ANY unfeatured use
2486+
// exists (the checker compares a code SET, so only presence matters). Three
2487+
// oracle sources are reproduced:
2488+
// * checks/features.rs feature_uses — construct -> required feature:
2489+
// local : `local` let/closure, `manage` expr, `take` data-effect,
2490+
// `ResourcePool<T>` type/callee
2491+
// native : a fn with `effects(native)`
2492+
// unsafe : a fn with `effects(unsafe)`
2493+
// async : `async fn`/`async let`/`async for`, `spawn`, `await`,
2494+
// `task_group`, `select`
2495+
// * analyzer/signatures.rs check_native_effect — a `native fn` that does NOT
2496+
// surface `effects(native)` is RS0101 REGARDLESS of the declared features.
2497+
// * checks/body/semantics.rs check_match_pattern_effects — `match take`
2498+
// without `features: local` (subsumed by the `take` keyword probe).
2499+
//
2500+
// A reserved feature keyword (`local`/`async`/`unsafe`) only appears in a
2501+
// `features:` header when that feature is DECLARED, so a header token always
2502+
// self-gates; the only other occurrences are real uses. `native` is the sole
2503+
// exception (the missing-effect source fires even when declared), so it is
2504+
// tracked separately in the fn walk.
2505+
// ---------------------------------------------------------------------------
2506+
2507+
// The `local`/`native`/`unsafe`/`async` features this file declares, collected
2508+
// from every top-level `features:` header (order-independent pre-pass).
2509+
fn collect_declared_features(toks: read List<Tok>) -> Set<String> {
2510+
let mut declared = Set<String>.new()
2511+
let n = List.len(list: read toks)
2512+
let mut i = 0
2513+
while i < n {
2514+
if at_ident(toks: read toks, i: read i, wid: read WORD_FEATURES) && at_symbol(toks: read toks, i: read (i + 1), code: read SYM_COLON) {
2515+
let lineEnd = declaration_line_end(toks: read toks, start: read i)
2516+
let mut j = i + 2
2517+
while j < lineEnd {
2518+
if is_ident_tok(toks: read toks, i: read j) {
2519+
let fname = tk_text(toks: read toks, i: read j)
2520+
if is_known_feature(name: read fname) {
2521+
Set.insert<String>(set: mut declared, value: read fname)
2522+
}
2523+
}
2524+
j = j + 1
2525+
}
2526+
i = lineEnd
2527+
} else {
2528+
i = i + 1
2529+
}
2530+
}
2531+
return declared
2532+
}
2533+
2534+
// True if the ident/keyword token at i is `word` and is not a method name (not
2535+
// preceded by `.`).
2536+
fn kw_not_dotted(toks: read List<Tok>, i: read Int, word: read String) -> Bool {
2537+
if is_kw_text(toks: read toks, i: read i, word: read word) == false { return false }
2538+
return at_symbol(toks: read toks, i: read (i - 1), code: read SYM_DOT) == false
2539+
}
2540+
2541+
// A `local`-feature construct anywhere in the file: a `local` let/closure, a
2542+
// `manage` expr, a `take` data-effect, or a `ResourcePool<T>` type/callee. The
2543+
// `take` effect is distinguished from a `.take(` method (preceded by `.`) and a
2544+
// binding/field named `take` (followed by `:`).
2545+
fn file_local_use(toks: read List<Tok>) -> Bool {
2546+
let n = List.len(list: read toks)
2547+
let mut i = 0
2548+
let mut hit = false
2549+
while i < n {
2550+
if at_ident(toks: read toks, i: read i, wid: read WORD_LOCAL) {
2551+
hit = true
2552+
} else if kw_not_dotted(toks: read toks, i: read i, word: read "manage") {
2553+
hit = true
2554+
} else if kw_not_dotted(toks: read toks, i: read i, word: read "take") && at_symbol(toks: read toks, i: read (i + 1), code: read SYM_COLON) == false {
2555+
hit = true
2556+
} else if is_ident_tok(toks: read toks, i: read i) && tk_text(toks: read toks, i: read i) == "ResourcePool" {
2557+
hit = true
2558+
}
2559+
i = i + 1
2560+
}
2561+
return hit
2562+
}
2563+
2564+
// An `async`-feature construct anywhere: `async` (fn/let/for) modifier keyword,
2565+
// or a `spawn`/`await`/`task_group`/`select` prefix/statement keyword.
2566+
fn file_async_use(toks: read List<Tok>) -> Bool {
2567+
let n = List.len(list: read toks)
2568+
let mut i = 0
2569+
let mut hit = false
2570+
while i < n {
2571+
// The `async` MODIFIER (`async fn`/`async let`/`async for`) is a feature
2572+
// use; the `async` EFFECT NAME in `effects(..., async, ...)` is a removed
2573+
// runtime effect (RS0012), NOT an async construct, so gate on the next kw.
2574+
if at_ident(toks: read toks, i: read i, wid: read WORD_ASYNC) && (at_ident(toks: read toks, i: read (i + 1), wid: read WORD_FN) || at_ident(toks: read toks, i: read (i + 1), wid: read WORD_FOR) || is_kw_text(toks: read toks, i: read (i + 1), word: read "let")) {
2575+
hit = true
2576+
} else if kw_not_dotted(toks: read toks, i: read i, word: read "spawn") {
2577+
hit = true
2578+
} else if kw_not_dotted(toks: read toks, i: read i, word: read "await") {
2579+
hit = true
2580+
} else if kw_not_dotted(toks: read toks, i: read i, word: read "task_group") {
2581+
hit = true
2582+
} else if kw_not_dotted(toks: read toks, i: read i, word: read "select") {
2583+
hit = true
2584+
}
2585+
i = i + 1
2586+
}
2587+
return hit
2588+
}
2589+
2590+
// An `unsafe`-feature construct anywhere: an `effects(unsafe)` clause. `unsafe`
2591+
// is only ever an effect name or a `features: unsafe` header token (the latter
2592+
// implies the feature is declared, so it self-gates), so a bare WORD_UNSAFE
2593+
// token is a faithful proxy.
2594+
fn file_unsafe_use(toks: read List<Tok>) -> Bool {
2595+
let n = List.len(list: read toks)
2596+
let mut i = 0
2597+
let mut hit = false
2598+
while i < n {
2599+
if at_ident(toks: read toks, i: read i, wid: read WORD_UNSAFE) { hit = true }
2600+
i = i + 1
2601+
}
2602+
return hit
2603+
}
2604+
24782605
fn main() -> Unit {
24792606
let source = Args.get_or_default(index: 0, default: read "")
24802607
let toks = tokenize(source: read source)
@@ -2536,6 +2663,15 @@ fn main() -> Unit {
25362663
let asyncNotConsumed = has_unconsumed_async_call(toks: read toks, asyncFns: read asyncFns)
25372664
// RS0033: any decimal integer literal in the file that overflows i64.
25382665
let outOfRangeInt = has_out_of_range_int(toks: read toks)
2666+
// RS0101: feature-gating. Declared features (pre-pass, order-independent) and
2667+
// whole-file construct probes for local/async/unsafe; native is tracked in the
2668+
// fn walk below (the missing-effect source fires even when native is declared).
2669+
let declaredFeatures = collect_declared_features(toks: read toks)
2670+
let localViol = file_local_use(toks: read toks) && Set.contains<String>(set: read declaredFeatures, value: read "local") == false
2671+
let asyncViol = file_async_use(toks: read toks) && Set.contains<String>(set: read declaredFeatures, value: read "async") == false
2672+
let unsafeViol = file_unsafe_use(toks: read toks) && Set.contains<String>(set: read declaredFeatures, value: read "unsafe") == false
2673+
let mut nativeEffect = false
2674+
let mut nativeMissing = false
25392675
let mut i = 0
25402676
let mut running = true
25412677
while running {
@@ -2707,6 +2843,11 @@ fn main() -> Unit {
27072843
if fn_noalloc_alloc(toks: read toks, start: read i, declared: read declared) { noallocAlloc = true }
27082844
if fn_guarantee_call_bad(toks: read toks, start: read i, effName: read "no_block", allFns: read allFns, guaranteeFns: read noBlockFns) { noBlockCall = true }
27092845
if fn_guarantee_call_bad(toks: read toks, start: read i, effName: read "no_panic", allFns: read allFns, guaranteeFns: read noPanicFns) { noPanicCall = true }
2846+
// RS0101: a fn with `effects(native)` (gated by the `native`
2847+
// feature), and a `native fn` missing that effect (ungated).
2848+
let hasNativeEff = effects_name_probe(toks: read toks, start: read i, mode: read 2)
2849+
if hasNativeEff { nativeEffect = true }
2850+
if fn_is_native(toks: read toks, start: read i) && hasNativeEff == false { nativeMissing = true }
27102851
// RS0035: an invalid `#lower_name` pin or a lowered-name collision.
27112852
if fn_has_lower_name(toks: read toks, start: read i) {
27122853
let pin = fn_lower_name_pin(toks: read toks, start: read i)
@@ -2812,7 +2953,14 @@ fn main() -> Unit {
28122953
if headerCount >= 2 {
28132954
Log.write(message: read "RS0006")
28142955
}
2815-
let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime || invalidSelf || outOfRangeInt || retainsBad || unknownType || missingEffect || pureBad || nonExhaustive || badAwait || fdSurface || lowerConflict || unknownProtocol || noallocAlloc || noBlockCall || noPanicCall || asyncNotConsumed
2956+
// RS0101: any unfeatured feature-requiring construct. Native fires if a fn has
2957+
// `effects(native)` without the feature, OR a `native fn` omits that effect.
2958+
let nativeViol = nativeMissing || (nativeEffect && Set.contains<String>(set: read declaredFeatures, value: read "native") == false)
2959+
let featureViol = localViol || asyncViol || unsafeViol || nativeViol
2960+
if featureViol {
2961+
Log.write(message: read "RS0101")
2962+
}
2963+
let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime || invalidSelf || outOfRangeInt || retainsBad || unknownType || missingEffect || pureBad || nonExhaustive || badAwait || fdSurface || lowerConflict || unknownProtocol || noallocAlloc || noBlockCall || noPanicCall || asyncNotConsumed || featureViol
28162964
if anyDiag == false {
28172965
Log.write(message: read "CLEAN")
28182966
}

0 commit comments

Comments
 (0)