Skip to content

Commit f5c60dc

Browse files
olwangclaude
andcommitted
feat(selfhost-check): type-inference slice 1 — conservative expr_type_root + RS0013 (foundation; gate unchanged at 26)
Begin the semantic type tier of the self-hosted checker: a conservative expression type-of pass (`expr_type_root`) and the RS0013 (invalid-try) sub-rules that consume it. `expr_type_root` types ONLY the forms the oracle computes with certainty (literals, value keywords, Some/Ok/Err, sum-variant heads, unqualified same-file calls via fn_return_root, and let-/param-typed idents) and returns "" (unknown) for Binary/Index/Field/qualified & receiver calls/object/array/closure — unknown => no fire => no false positive. It is the foundation the later slices (RS0210/RS0207/RS0208) reuse. RS0013 is emitted (sub-rules A: `?` in a non-Result/Option-returning fn; B: `?` on a confidently concrete non-Result/Option operand) but is deliberately NOT added to CHECKER_TARGET_CODES, so it is filtered from the parity comparison and the gate stays green at 26 codes. Reason (FALLBACK): the oracle's third sub-rule (error-type mismatch) fires on ast-call-missing- effect-nested.rss via qualified stdlib calls (`File.open_write?`/`File.write?` → FileError) whose error type needs the full stdlib signature DB the token- level checker intentionally lacks; typing qualified calls would risk corpus- wide false positives. So RS0013 cannot reach 0-mismatch at the spec's conservatism level yet. Verified: checker_parity_corpus 619/619 ok, 0 mismatches, 0 run-failures at the same 26 codes. RS0013 fires correctly on all 11 sub-rule-A/B files (incl. fixtures try-operator-non-result{,-value}.rss) and on zero clean `?` files. Ledger milestone 2l records the foundation and the precise blocker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 53a9fd3 commit f5c60dc

2 files changed

Lines changed: 225 additions & 1 deletion

File tree

docs/ledgers/rss-selfhost-ledger.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,3 +1196,55 @@ gap is VM value-representation / intrinsic-dispatch cost (the next big lever).
11961196
- **Status:** the frontend-object AST parity ladder (structure → line:col
11971197
line:col:len) is CLOSED. Remaining self-host frontier is the semantic tier
11981198
(type-inference + borrow-checker codes; see the SH-026 step-2 tail).
1199+
1200+
### Milestone 2l — type-inference engine slice 1: conservative `expr_type_root` + RS0013 (FALLBACK: foundation committed, RS0013 left OUT of the gate)
1201+
1202+
- **Goal:** begin the semantic type tier — a CONSERVATIVE expression type-of pass
1203+
(`selfhost/check.rss::expr_type_root`) and use it to land RS0013 (invalid-try),
1204+
the most FP-safe type code. Foundation reused by later slices (RS0210 operator,
1205+
RS0207 argument, RS0208 return).
1206+
- **Built (committed):** `expr_type_root(toks, s, e, bodyOpen, exprPos, popen,
1207+
pclose, declared, allFns)` — types ONLY the forms the oracle computes with
1208+
certainty, else "" (unknown => no fire => no false positive):
1209+
* String / number (`number_literal_root` = Float iff text has `.`, else Int) /
1210+
Char literal · `true|false`→Bool · `null`→JsonLiteral · `Unit`→Unit ·
1211+
`None`→Option · `Some(..)`→Option · `Ok|Err(..)`→Result · a sum-variant head →
1212+
its owning sum (`variant_owner`) · an unqualified call `name(..)` → same-file
1213+
`fn_return_root` or a declared-type constructor · a bare ident → a let-typed
1214+
local (recursively) or a declared-type param (`param_type_root`).
1215+
* Everything else — Binary / Index / Field / qualified & receiver calls
1216+
(`Ns.m(..)` / `x.m(..)`) / object / array / closure — returns "" by design
1217+
(those need the full stdlib signature DB the token-level checker lacks; keeping
1218+
them unknown is the FP-safety mechanism). Helpers `try_operand_root` +
1219+
`fn_invalid_try` reproduce two of the three RS0013 sub-rules token-level.
1220+
- **RS0013 sub-rules (oracle has THREE, not two):**
1221+
* **A — result-returns** (`analyzer/runtime_guarantee.rs::
1222+
check_try_operator_result_returns`): any `?` in a fn whose return base ∉
1223+
{Result, Option}. Token-level, exact. Reproduced. (10 corpus files, incl.
1224+
fixture `try-operator-non-result.rss`.)
1225+
* **B — value-is-result** (`checks/body/try_checks.rs::check_try_value_is_result`):
1226+
a `?` operand of a confidently-known concrete non-Result/Option type.
1227+
Reproduced via `expr_type_root`. (1 file: fixture
1228+
`try-operator-non-result-value.rss` — operand `load(..)``Image`.)
1229+
* **C — error-type mismatch** (`checks/body/try_checks.rs::check_try_error_types`):
1230+
a `?` whose operand `Result<T, E_op>` has `E_op` ≠ the fn's declared error
1231+
type. Reproduced for unqualified operands only.
1232+
- **BLOCKER (why RS0013 is NOT in CHECKER_TARGET_CODES):**
1233+
`tests/fixtures/fail/ast-call-missing-effect-nested.rss` (fn returns
1234+
`Result<Unit, IOError>`) fires sub-rule C on TWO **qualified stdlib** operands —
1235+
`File.open_write(..)?` and `File.write(..)?`, whose error type the oracle knows
1236+
is `FileError` (≠ `IOError`). Reproducing that needs per-method stdlib
1237+
error-type inference on qualified/receiver calls, which the spec deliberately
1238+
keeps UNKNOWN (typing them risks corpus-wide false positives on the many clean
1239+
`File.*?`/`Json.*?` calls whose error type matches). So RS0013 cannot reach
1240+
0-mismatch at the spec's conservatism level — FALLBACK taken.
1241+
- **Outcome:** `expr_type_root` + the RS0013 sub-rule A/B wiring are committed and
1242+
exercised across all 619 corpus files (0 run-failures). RS0013 is EMITTED by the
1243+
checker but stays OUT of `CHECKER_TARGET_CODES` (filtered from parity), so the
1244+
gate is unchanged. It fires correctly on 11 of the 13 oracle-RS0013 files (all
1245+
sub-rule A + B); the 2 sub-rule-C files stay unflagged (the documented blocker),
1246+
and every clean `?` file stays unflagged (0 false positives, verified).
1247+
- **Gate:** `checker_parity_corpus` 619/619 ok, 0 mismatches, 0 run-failures at
1248+
the SAME 26 codes (RS0013 absent). Green.
1249+
- **Next slice:** RS0013 becomes gateable once qualified-call error-type inference
1250+
exists; meanwhile `expr_type_root` is ready for RS0210/RS0207/RS0208.

selfhost/check.rss

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
26052769
fn 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

Comments
 (0)