Skip to content

Commit 9f2a69c

Browse files
olwangclaude
andcommitted
feat(selfhost): engine slice 2 — stdlib error-type map completes RS0013 (sub-rule C) → 27 gated codes
Add RS0013 (invalid-try) sub-rule C to the self-hosted rss checker: inside a fn returning Result<T, E>, a `?` whose operand Result error type is known and != E now fires, matching the analyzer (checks/body/try_checks.rs::check_try_error_types). New infra in selfhost/check.rss: - stdlib_error_type(ns, method): namespace->error-type map read from the .rssi interfaces. Filesystem (File/Directory/Env/Path)->FileError, JSON-shaped codecs (Json/Toml/Yaml)->JsonError. Per-method exceptions that break module uniformity are excluded (yield "" => FP-safe): File.bytes_stream->ChannelError and Path.{from_string,resolve_relative,safe_relative}->String. - return_error_type_at / result_error_root: parse the error (2nd) type arg of a Result<T, E> return. - fn_error_type_by_name: declared error type of an unqualified same-file fn. - try_operand_error_type: the ?-operand's Result error type (qualified stdlib call via the map, or unqualified same-file call via its fn error type; else unknown). - fn_invalid_try extended with sub-rule C. Measure-first: added RS0013 with only A+B, the corpus reported exactly the two sub-rule-C files (ast-call-missing-effect-nested.rss, try-operator-error-type- mismatch.rss). Built the map to cover exactly those; a first full run surfaced one FP (File.bytes_stream in a ChannelError fn) — fixed by excluding it. RS0013 added to CHECKER_TARGET_CODES (now 27). checker_parity_corpus: 619/619 ok, 0 mismatches, 0 run-failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f5c60dc commit 9f2a69c

3 files changed

Lines changed: 202 additions & 11 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"];
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"];
507507

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

docs/ledgers/rss-selfhost-ledger.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,3 +1248,47 @@ gap is VM value-representation / intrinsic-dispatch cost (the next big lever).
12481248
the SAME 26 codes (RS0013 absent). Green.
12491249
- **Next slice:** RS0013 becomes gateable once qualified-call error-type inference
12501250
exists; meanwhile `expr_type_root` is ready for RS0210/RS0207/RS0208.
1251+
1252+
### Milestone 2m — type-inference engine slice 2: stdlib namespace→error-type map + RS0013 sub-rule C → RS0013 GATED (27 codes)
1253+
1254+
- **Goal:** complete RS0013 by adding sub-rule C (error-type mismatch) — the
1255+
blocker from slice 2l — and add RS0013 to `CHECKER_TARGET_CODES`.
1256+
- **Measure-first:** added RS0013 to the target with only sub-rules A+B and ran the
1257+
corpus once. Exactly TWO `[mismatch]` files (the real sub-rule-C set) — no guess:
1258+
* `fail/ast-call-missing-effect-nested.rss`: fn returns `Result<Unit, IOError>`;
1259+
`File.open_write(..)?` / `File.write(..)?``FileError``IOError`.
1260+
* `fail/try-operator-error-type-mismatch.rss`: fn returns `Result<_, AppError>`;
1261+
`load_config()?` (same-file fn) → `ConfigError``AppError`.
1262+
- **Built (committed):**
1263+
* `stdlib_error_type(ns, method)` — the namespace→error-type map read from the
1264+
`.rssi` interfaces. Filesystem (`File`/`Directory`/`Env`/`Path`) → `FileError`;
1265+
JSON-shaped codecs (`Json`/`Toml`/`Yaml`) → `JsonError`. Per-method exceptions
1266+
keyed and EXCLUDED (yield "") because they break module uniformity:
1267+
`File.bytes_stream``ChannelError` (async streaming), and
1268+
`Path.{from_string,resolve_relative,safe_relative}``String` error. Every
1269+
other namespace → "" (unknown ⇒ no fire ⇒ FP-safe).
1270+
* `return_error_type_at` / `result_error_root` — parse the second (error) type
1271+
arg of a `Result<T, E>` return (mirrors `return_type_root_at`).
1272+
* `fn_error_type_by_name` — the declared error type of an unqualified same-file
1273+
`fn` (mirrors `fn_return_root`).
1274+
* `try_operand_error_type` — the `?`-operand's Result error type: a qualified
1275+
`Ns.method(..)?` via the map, or an unqualified `name(..)?` via that fn's
1276+
error type; anything else (bare ident, index, field, `Ok`/`Err`/`Some`) → "".
1277+
* `fn_invalid_try` extended: inside a fn whose return root is exactly `Result`
1278+
with a known 2-arg error type E, a `?` whose operand error type is known and
1279+
≠ E fires RS0013 (sub-rule C).
1280+
- **FP discipline:** the first full run at the mapped families surfaced ONE false
1281+
positive — `examples/scripts/async/common_io.rss`: `File.bytes_stream(..)?` in a
1282+
`Result<_, ChannelError>` fn. `File` is NOT uniform (bytes_stream → ChannelError),
1283+
so the blanket File→FileError mis-fired. Fixed by excluding `File.bytes_stream`
1284+
(and the String-error `Path` methods). Re-verified whole-corpus uniformity of the
1285+
mapped families before re-running.
1286+
- **Gate:** `checker_parity_corpus` **619/619 ok, 0 mismatches, 0 run-failures** at
1287+
**27 codes** (RS0013 added). Green. The two sub-rule-C fixtures now match the
1288+
oracle exactly; every clean `File.*?`/`Json.*?`/etc. operand stays unflagged.
1289+
- **CHECKER_TARGET_CODES (27):** RS0002, RS0003, RS0004, RS0005, RS0006, RS0007,
1290+
RS0008, RS0009, RS0010, RS0011, RS0012, RS0016, RS0017, RS0021, RS0024, RS0028,
1291+
RS0033, RS0029, RS0023, RS0035, RS0027, RS0014, RS0018, RS0019, RS0022, RS0101,
1292+
RS0013.
1293+
- **Next slice:** `expr_type_root` + the error-type map are ready for RS0210
1294+
(operator), RS0207 (argument), RS0208 (return).

selfhost/check.rss

Lines changed: 157 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2614,14 +2614,15 @@ fn file_unsafe_use(toks: read List<Tok>) -> Bool {
26142614
// RS0207 argument, RS0208 return). See crate::checks::shared::builtin_value_type_name,
26152615
// crate::hir::number_literal_type_name, and checks/body/try_checks.rs.
26162616
//
2617-
// NOTE: RS0013 is emitted below but is intentionally NOT in CHECKER_TARGET_CODES
2618-
// (so it is filtered from the parity comparison and the gate stays green). Two of
2619-
// its three oracle sub-rules are reproduced faithfully — (A) `?` in a function
2620-
// whose return type is not Result/Option, and (B) a `?` whose operand has a
2621-
// confidently-known concrete non-Result/Option type. The third sub-rule
2622-
// (error-type mismatch) fires on qualified stdlib calls whose error type needs
2623-
// the full stdlib signature DB, which is deliberately kept UNKNOWN here; see the
2624-
// ledger milestone for the precise blocker (ast-call-missing-effect-nested.rss).
2617+
// RS0013 is in CHECKER_TARGET_CODES: all three oracle sub-rules are reproduced —
2618+
// (A) `?` in a function whose return type is not Result/Option, (B) a `?` whose
2619+
// operand has a confidently-known concrete non-Result/Option type, and (C) inside
2620+
// a fn returning `Result<T, E>`, a `?` whose operand Result error type is known
2621+
// and differs from E. Sub-rule C reads the operand error type from a small stdlib
2622+
// namespace->error-type map (filesystem->FileError, json/toml/yaml->JsonError) or,
2623+
// for an unqualified same-file call, from that fn's declared error type; anything
2624+
// else stays UNKNOWN (no fire) — the FP-safety mechanism. See stdlib_error_type,
2625+
// return_error_type_at, try_operand_error_type, and checks/body/try_checks.rs.
26252626
// ---------------------------------------------------------------------------
26262627

26272628
// crate::hir::number_literal_type_name reproduced: a numeric literal is Float iff
@@ -2733,10 +2734,151 @@ fn try_operand_root(toks: read List<Tok>, qPos: read Int, bodyOpen: read Int, po
27332734
return ""
27342735
}
27352736

2736-
// RS0013 INVALID_TRY_OPERATOR (sub-rules A + B): true iff the fn at `start` uses
2737-
// `?` in a way that is invalid. (A) any `?` in a fn whose return root is not
2737+
// Stdlib namespace -> Result error type. Built from the `.rssi` interfaces: each
2738+
// `pub native fn Ns.method(...) -> Result<T, ErrType>` declares a per-module error
2739+
// type, uniform across the module for these two families. Filesystem (File /
2740+
// Directory / Env / Path) -> FileError; the JSON-shaped codecs (Json / Toml /
2741+
// Yaml) -> JsonError. Two families have per-method exceptions that must be keyed
2742+
// by `Ns.method`: `File.bytes_stream` returns ChannelError (async streaming), and
2743+
// `Path.{from_string,resolve_relative,safe_relative}` return a `String` error —
2744+
// all excluded (yield "") rather than mis-asserting an error type. A namespace not
2745+
// in the map also yields "" (unknown => no diagnostic => FP-safe). See
2746+
// stdlib/{fs,env,path,json,toml,yaml}/*.rssi, packages/async/interface/file.rssi,
2747+
// and crate::checks::body::try_checks::result_error_type_name.
2748+
fn stdlib_error_type(ns: read String, method: read String) -> String {
2749+
if ns == "File" {
2750+
if method == "bytes_stream" { return "" }
2751+
return "FileError"
2752+
}
2753+
if ns == "Directory" || ns == "Env" { return "FileError" }
2754+
if ns == "Path" {
2755+
if method == "from_string" || method == "resolve_relative" || method == "safe_relative" { return "" }
2756+
return "FileError"
2757+
}
2758+
if ns == "Json" || ns == "Toml" || ns == "Yaml" { return "JsonError" }
2759+
return ""
2760+
}
2761+
2762+
// Second (error) type-arg root of a `Result<T, E>` return type for the fn whose
2763+
// keyword-anchor sits at `fnkw`, or "" when the return is not a 2-arg Result.
2764+
// Mirrors return_type_root_at but yields E instead of the head root.
2765+
fn return_error_type_at(toks: read List<Tok>, fnkw: read Int) -> String {
2766+
let sigEnd = function_signature_end(toks: read toks, start: read fnkw)
2767+
let open = find_param_open(toks: read toks, from: read fnkw, end: read sigEnd)
2768+
if open < 0 { return "" }
2769+
let close = find_matching(toks: read toks, open: read open, openSym: read SYM_LPAREN, closeSym: read SYM_RPAREN)
2770+
if close < 0 { return "" }
2771+
if at_symbol(toks: read toks, i: read (close + 1), code: read SYM_ARROW) == false { return "" }
2772+
let rs = close + 2
2773+
let mut re = rs
2774+
let mut scanning = true
2775+
while scanning {
2776+
let stop = at_ident(toks: read toks, i: read re, wid: read WORD_EFFECTS) || at_symbol(toks: read toks, i: read re, code: read SYM_LBRACE) || at_symbol(toks: read toks, i: read re, code: read SYM_EQ)
2777+
if re < sigEnd && stop == false {
2778+
re = re + 1
2779+
} else {
2780+
scanning = false
2781+
}
2782+
}
2783+
return result_error_root(toks: read toks, s: read rs, e: read re)
2784+
}
2785+
2786+
// Error root of a `Result< T , E >` type region [s,e): the root of its second
2787+
// top-level type argument, or "" when the region is not a 2-arg Result.
2788+
fn result_error_root(toks: read List<Tok>, s: read Int, e: read Int) -> String {
2789+
let p = skip_type_prefixes(toks: read toks, s: read s, e: read e)
2790+
if p >= e { return "" }
2791+
if is_ident_tok(toks: read toks, i: read p) == false { return "" }
2792+
if tk_text(toks: read toks, i: read p) != "Result" { return "" }
2793+
let lt = p + 1
2794+
if at_symbol(toks: read toks, i: read lt, code: read SYM_LANGLE) == false { return "" }
2795+
let mut k = lt + 1
2796+
let mut angle = 0
2797+
let mut depth = 0
2798+
let mut commaPos = -1
2799+
let mut rangle = -1
2800+
while k < e && rangle < 0 {
2801+
if at_symbol(toks: read toks, i: read k, code: read SYM_LANGLE) {
2802+
angle = angle + 1
2803+
} else if at_symbol(toks: read toks, i: read k, code: read SYM_RANGLE) {
2804+
if angle == 0 { rangle = k } else { angle = angle - 1 }
2805+
} else if at_symbol(toks: read toks, i: read k, code: read SYM_LPAREN) || at_symbol(toks: read toks, i: read k, code: read SYM_LBRACKET) {
2806+
depth = depth + 1
2807+
} 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_RBRACKET) {
2808+
if depth > 0 { depth = depth - 1 }
2809+
} else if angle == 0 && depth == 0 && at_symbol(toks: read toks, i: read k, code: read SYM_COMMA) {
2810+
if commaPos < 0 { commaPos = k }
2811+
}
2812+
k = k + 1
2813+
}
2814+
if commaPos < 0 || rangle < 0 { return "" }
2815+
return type_region_root(toks: read toks, s: read (commaPos + 1), e: read rangle)
2816+
}
2817+
2818+
// Error root of the `Result<T, E>` return of the first unqualified same-file
2819+
// `fn fnName(...)`, or "". Mirrors fn_return_root but yields E.
2820+
fn fn_error_type_by_name(toks: read List<Tok>, fnName: read String) -> String {
2821+
let n = List.len(list: read toks)
2822+
let mut i = 0
2823+
let mut result = ""
2824+
let mut done = false
2825+
while i < n && done == false {
2826+
if at_ident(toks: read toks, i: read i, wid: read WORD_FN) {
2827+
let nameIdx = i + 1
2828+
let named = is_ident_tok(toks: read toks, i: read nameIdx) && tk_text(toks: read toks, i: read nameIdx) == fnName
2829+
if named && at_symbol(toks: read toks, i: read (nameIdx + 1), code: read SYM_DOT) == false {
2830+
result = return_error_type_at(toks: read toks, fnkw: read i)
2831+
done = true
2832+
}
2833+
}
2834+
i = i + 1
2835+
}
2836+
return result
2837+
}
2838+
2839+
// Result error type of the `?`-operand ending just before the `?` at `qPos`, or
2840+
// "" when it is not confidently known. Only two operand shapes yield an error
2841+
// type: a qualified stdlib call `Ns.method(...)?` (via the namespace map) and an
2842+
// unqualified same-file call `name(...)?` (via that fn's declared error type).
2843+
// Anything else (bare ident, index, field, Ok/Err/Some literal) -> "" (FP-safe).
2844+
fn try_operand_error_type(toks: read List<Tok>, qPos: read Int, bodyOpen: read Int, allFns: read Set<String>) -> String {
2845+
let prev = qPos - 1
2846+
if prev <= bodyOpen { return "" }
2847+
if at_symbol(toks: read toks, i: read prev, code: read SYM_RPAREN) == false { return "" }
2848+
let mut d = 0
2849+
let mut j = prev
2850+
let mut op = -1
2851+
while j > bodyOpen && op < 0 {
2852+
if at_symbol(toks: read toks, i: read j, code: read SYM_RPAREN) {
2853+
d = d + 1
2854+
} else if at_symbol(toks: read toks, i: read j, code: read SYM_LPAREN) {
2855+
d = d - 1
2856+
if d == 0 { op = j }
2857+
}
2858+
j = j - 1
2859+
}
2860+
if op <= bodyOpen { return "" }
2861+
let callee = op - 1
2862+
if is_ident_tok(toks: read toks, i: read callee) == false { return "" }
2863+
let method = tk_text(toks: read toks, i: read callee)
2864+
if at_symbol(toks: read toks, i: read (callee - 1), code: read SYM_DOT) {
2865+
let nsPos = callee - 2
2866+
if is_ident_tok(toks: read toks, i: read nsPos) == false { return "" }
2867+
if at_symbol(toks: read toks, i: read (nsPos - 1), code: read SYM_DOT) { return "" }
2868+
return stdlib_error_type(ns: read tk_text(toks: read toks, i: read nsPos), method: read method)
2869+
}
2870+
if Set.contains<String>(set: read allFns, value: read method) {
2871+
return fn_error_type_by_name(toks: read toks, fnName: read method)
2872+
}
2873+
return ""
2874+
}
2875+
2876+
// RS0013 INVALID_TRY_OPERATOR (sub-rules A + B + C): true iff the fn at `start`
2877+
// uses `?` in a way that is invalid. (A) any `?` in a fn whose return root is not
27382878
// Result/Option (check_try_operator_result_returns). (B) a `?` whose operand has
27392879
// a confidently-known concrete non-Result/Option type (check_try_value_is_result).
2880+
// (C) inside a fn returning `Result<T, E>`, a `?` whose operand's Result error
2881+
// type is confidently known and differs from E (check_try_error_types).
27402882
fn fn_invalid_try(toks: read List<Tok>, start: read Int, declared: read Set<String>, allFns: read Set<String>) -> Bool {
27412883
let ns = fn_name_start(toks: read toks, i: read start)
27422884
let sigEnd = function_signature_end(toks: read toks, start: read (ns - 1))
@@ -2750,6 +2892,7 @@ fn fn_invalid_try(toks: read List<Tok>, start: read Int, declared: read Set<Stri
27502892
}
27512893
let retRoot = return_type_root_at(toks: read toks, fnkw: read (ns - 1))
27522894
let returnsResult = retRoot == "Result" || retRoot == "Option"
2895+
let fnErr = return_error_type_at(toks: read toks, fnkw: read (ns - 1))
27532896
let mut k = sigEnd + 1
27542897
let mut hit = false
27552898
while k < bodyClose {
@@ -2759,6 +2902,10 @@ fn fn_invalid_try(toks: read List<Tok>, start: read Int, declared: read Set<Stri
27592902
} else {
27602903
let root = try_operand_root(toks: read toks, qPos: read k, bodyOpen: read sigEnd, popen: read popen, pclose: read pclose, declared: read declared, allFns: read allFns)
27612904
if String.len(value: read root) > 0 && root != "Result" && root != "Option" { hit = true }
2905+
if retRoot == "Result" && String.len(value: read fnErr) > 0 {
2906+
let opErr = try_operand_error_type(toks: read toks, qPos: read k, bodyOpen: read sigEnd, allFns: read allFns)
2907+
if String.len(value: read opErr) > 0 && opErr != fnErr { hit = true }
2908+
}
27622909
}
27632910
}
27642911
k = k + 1

0 commit comments

Comments
 (0)