@@ -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+
24782605fn 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