@@ -2602,6 +2602,170 @@ fn file_unsafe_use(toks: read List<Tok>) -> Bool {
26022602 return hit
26032603}
26042604
2605+ // ---------------------------------------------------------------------------
2606+ // Type-inference engine — slice 1: conservative expression type-of + RS0013.
2607+ //
2608+ // `expr_type_root` computes the type ROOT of an expression region, but ONLY for
2609+ // the forms whose type the oracle (crate::hir::infer + checks/shared) computes
2610+ // with certainty; everything else (Binary / Index / Field / qualified & receiver
2611+ // calls / object / array / closure) returns "" (UNKNOWN). Returning unknown =>
2612+ // no diagnostic => no false positive; that conservatism is the safety mechanism
2613+ // and the reason this foundation is reused by the later slices (RS0210 operator,
2614+ // RS0207 argument, RS0208 return). See crate::checks::shared::builtin_value_type_name,
2615+ // crate::hir::number_literal_type_name, and checks/body/try_checks.rs.
2616+ //
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).
2625+ // ---------------------------------------------------------------------------
2626+
2627+ // crate::hir::number_literal_type_name reproduced: a numeric literal is Float iff
2628+ // its text contains a `.` (code point 46), else Int (`1e5` has no `.` => Int).
2629+ fn number_literal_root(text: read String) -> String {
2630+ let cs = String.chars(value: read text)
2631+ let n = List.len(list: read cs)
2632+ let mut i = 0
2633+ let mut isFloat = false
2634+ while i < n {
2635+ if Char.to_code(value: read List.get(list: read cs, index: read i)) == 46 { isFloat = true }
2636+ i = i + 1
2637+ }
2638+ if isFloat { return "Float" }
2639+ return "Int"
2640+ }
2641+
2642+ // Conservative expression type-of: the type ROOT of the region [s0,e), or "" when
2643+ // not provably known. Only the SURE forms are typed:
2644+ // * String / number / Char literal · true|false -> Bool
2645+ // * null -> JsonLiteral · Unit -> Unit · None -> Option
2646+ // * Some(...) -> Option · Ok|Err(...) -> Result
2647+ // * a sum variant head -> its owning sum
2648+ // * an unqualified call name(...) -> same-file fn return root, or a declared
2649+ // type name it constructs
2650+ // * a bare ident -> a let-typed local (recursively) or a declared-type param
2651+ // Anything else (Binary / Index / Field / qualified or receiver call / object /
2652+ // array / closure) -> "".
2653+ fn expr_type_root(toks: read List<Tok >, s0: read Int, e: read Int, bodyOpen: read Int, exprPos: read Int, popen: read Int, pclose: read Int, declared: read Set<String >, allFns: read Set<String >) -> String {
2654+ let mut s = s0
2655+ if is_effect_kw(toks: read toks, i: read s) { s = s + 1 }
2656+ if s >= e { return "" }
2657+ let k0 = tk_kind(toks: read toks, i: read s)
2658+ if (s + 1) == e {
2659+ if k0 == TOK_STRING { return "String" }
2660+ if k0 == TOK_NUMBER { return number_literal_root(text: read tk_text(toks: read toks, i: read s)) }
2661+ if k0 == TOK_CHAR { return "Char" }
2662+ }
2663+ if is_ident_tok(toks: read toks, i: read s) == false { return "" }
2664+ let head = tk_text(toks: read toks, i: read s)
2665+ if (s + 1) == e {
2666+ if head == "true" || head == "false" { return "Bool" }
2667+ if head == "null" { return "JsonLiteral" }
2668+ if head == "Unit" { return "Unit" }
2669+ if head == "None" { return "Option" }
2670+ }
2671+ let after = dotted_end(toks: read toks, i: read s)
2672+ // A constructor / unqualified call spanning the whole region: `head ( ... )`.
2673+ if after == (s + 1) && at_symbol(toks: read toks, i: read (s + 1), code: read SYM_LPAREN) {
2674+ let close = find_matching(toks: read toks, open: read (s + 1), openSym: read SYM_LPAREN, closeSym: read SYM_RPAREN)
2675+ if close != (e - 1) { return "" }
2676+ if head == "Some" { return "Option" }
2677+ if head == "Ok" || head == "Err" { return "Result" }
2678+ let owner = variant_owner(toks: read toks, vname: read head)
2679+ if String.len(value: read owner) > 0 { return owner }
2680+ if Set.contains<String >(set: read allFns, value: read head) {
2681+ return fn_return_root(toks: read toks, fnName: read head)
2682+ }
2683+ if Set.contains<String >(set: read declared, value: read head) { return head }
2684+ return ""
2685+ }
2686+ // A bare, unqualified ident spanning the whole region.
2687+ if after == (s + 1) && (s + 1) == e {
2688+ let owner = variant_owner(toks: read toks, vname: read head)
2689+ if String.len(value: read owner) > 0 { return owner }
2690+ let lr = find_let_rhs_start(toks: read toks, bodyOpen: read bodyOpen, matchPos: read exprPos, name: read head)
2691+ if lr >= 0 {
2692+ let re = next_line_or_block_end(toks: read toks, start: read lr, end: read exprPos)
2693+ return expr_type_root(toks: read toks, s0: read lr, e: read re, bodyOpen: read bodyOpen, exprPos: read lr, popen: read popen, pclose: read pclose, declared: read declared, allFns: read allFns)
2694+ }
2695+ if popen >= 0 && pclose >= 0 {
2696+ return param_type_root(toks: read toks, open: read popen, close: read pclose, name: read head)
2697+ }
2698+ return ""
2699+ }
2700+ return ""
2701+ }
2702+
2703+ // The type root of a `?`-operand ending just before the `?` at `qPos`. Only the
2704+ // two operand shapes whose start is unambiguous token-level are typed: a call
2705+ // `name(...)?` (unqualified only) and a bare ident `x?`; anything else (qualified
2706+ // call, index, field, literal) -> "" (unknown => FP-safe).
2707+ fn try_operand_root(toks: read List<Tok >, qPos: read Int, bodyOpen: read Int, popen: read Int, pclose: read Int, declared: read Set<String >, allFns: read Set<String >) -> String {
2708+ let prev = qPos - 1
2709+ if prev <= bodyOpen { return "" }
2710+ if at_symbol(toks: read toks, i: read prev, code: read SYM_RPAREN) {
2711+ let mut d = 0
2712+ let mut j = prev
2713+ let mut op = -1
2714+ while j > bodyOpen && op < 0 {
2715+ if at_symbol(toks: read toks, i: read j, code: read SYM_RPAREN) {
2716+ d = d + 1
2717+ } else if at_symbol(toks: read toks, i: read j, code: read SYM_LPAREN) {
2718+ d = d - 1
2719+ if d == 0 { op = j }
2720+ }
2721+ j = j - 1
2722+ }
2723+ if op <= bodyOpen { return "" }
2724+ let callee = op - 1
2725+ if is_ident_tok(toks: read toks, i: read callee) == false { return "" }
2726+ if at_symbol(toks: read toks, i: read (callee - 1), code: read SYM_DOT) { return "" }
2727+ return expr_type_root(toks: read toks, s0: read callee, e: read qPos, bodyOpen: read bodyOpen, exprPos: read callee, popen: read popen, pclose: read pclose, declared: read declared, allFns: read allFns)
2728+ }
2729+ if is_ident_tok(toks: read toks, i: read prev) {
2730+ if at_symbol(toks: read toks, i: read (prev - 1), code: read SYM_DOT) { return "" }
2731+ return expr_type_root(toks: read toks, s0: read prev, e: read qPos, bodyOpen: read bodyOpen, exprPos: read prev, popen: read popen, pclose: read pclose, declared: read declared, allFns: read allFns)
2732+ }
2733+ return ""
2734+ }
2735+
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
2738+ // Result/Option (check_try_operator_result_returns). (B) a `?` whose operand has
2739+ // a confidently-known concrete non-Result/Option type (check_try_value_is_result).
2740+ fn fn_invalid_try(toks: read List<Tok >, start: read Int, declared: read Set<String >, allFns: read Set<String >) -> Bool {
2741+ let ns = fn_name_start(toks: read toks, i: read start)
2742+ let sigEnd = function_signature_end(toks: read toks, start: read (ns - 1))
2743+ if at_symbol(toks: read toks, i: read sigEnd, code: read SYM_LBRACE) == false { return false }
2744+ let bodyClose = find_matching(toks: read toks, open: read sigEnd, openSym: read SYM_LBRACE, closeSym: read SYM_RBRACE)
2745+ if bodyClose < 0 { return false }
2746+ let popen = find_param_open(toks: read toks, from: read ns, end: read sigEnd)
2747+ let mut pclose = -1
2748+ if popen >= 0 {
2749+ pclose = find_matching(toks: read toks, open: read popen, openSym: read SYM_LPAREN, closeSym: read SYM_RPAREN)
2750+ }
2751+ let retRoot = return_type_root_at(toks: read toks, fnkw: read (ns - 1))
2752+ let returnsResult = retRoot == "Result" || retRoot == "Option"
2753+ let mut k = sigEnd + 1
2754+ let mut hit = false
2755+ while k < bodyClose {
2756+ if at_symbol(toks: read toks, i: read k, code: read SYM_QUESTION) {
2757+ if returnsResult == false {
2758+ hit = true
2759+ } else {
2760+ 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)
2761+ if String.len(value: read root) > 0 && root != "Result" && root != "Option" { hit = true }
2762+ }
2763+ }
2764+ k = k + 1
2765+ }
2766+ return hit
2767+ }
2768+
26052769fn main() -> Unit {
26062770 let source = Args.get_or_default(index: 0, default: read "")
26072771 let toks = tokenize(source: read source)
@@ -2630,6 +2794,10 @@ fn main() -> Unit {
26302794 let mut pureBad = false
26312795 // RS0029: any `await` in a non-async function.
26322796 let mut badAwait = false
2797+ // RS0013: an invalid `?` (non-Result return, or a concrete non-Result operand).
2798+ // Emitted but filtered from parity (not in CHECKER_TARGET_CODES); see the
2799+ // type-inference engine section above.
2800+ let mut invalidTry = false
26332801 // RS0023: `Fd` in a non-native fn signature or a non-resource type field.
26342802 let mut fdSurface = false
26352803 // RS0021: any non-exhaustive match statement or expression in the file.
@@ -2836,6 +3004,7 @@ fn main() -> Unit {
28363004 if fn_pure_bad(toks: read toks, start: read i, resources: read resources, sumVariants: read sumVariants, pureFns: read pureFns, allFns: read allFns, declared: read declared) { pureBad = true }
28373005 if fn_has_nonexhaustive_match(toks: read toks, start: read i, declared: read declared, allFns: read allFns) { nonExhaustive = true }
28383006 if fn_has_bad_await(toks: read toks, start: read i) { badAwait = true }
3007+ if fn_invalid_try(toks: read toks, start: read i, declared: read declared, allFns: read allFns) { invalidTry = true }
28393008 if fn_has_fd_surface(toks: read toks, start: read i) { fdSurface = true }
28403009 // RS0027: an unknown protocol in this fn's generic bounds.
28413010 if generics_unknown_protocol(toks: read toks, nameEnd: read dotted_end(toks: read toks, i: read ns), protocols: read protocols) { unknownProtocol = true }
@@ -2917,6 +3086,9 @@ fn main() -> Unit {
29173086 if badAwait {
29183087 Log.write(message: read "RS0029")
29193088 }
3089+ if invalidTry {
3090+ Log.write(message: read "RS0013")
3091+ }
29203092 if asyncNotConsumed {
29213093 Log.write(message: read "RS0022")
29223094 }
@@ -2960,7 +3132,7 @@ fn main() -> Unit {
29603132 if featureViol {
29613133 Log.write(message: read "RS0101")
29623134 }
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
3135+ 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
29643136 if anyDiag == false {
29653137 Log.write(message: read "CLEAN")
29663138 }
0 commit comments