Skip to content

Commit 8f8c721

Browse files
olwangclaude
andcommitted
feat(selfhost): AST producer — feature section order + diagnostics + body-less fns → 521/585
Three fixes, restructuring the top-level driver into sections matching ast_oracle_dump: - Features are now emitted in a FIRST pre-pass (emit_all_features) regardless of where the `features:` header sits in source — the oracle dumps by struct-field order, not source order (e.g. a header after all fns). Item pass skips headers. - Feature diagnostics post-pass (emit_feature_diags): unknown-feature markers then duplicate-feature markers, emitted AFTER all items (section 5). Depth-tracked so a `features:` FIELD inside a struct body is never mistaken for a header. - has-body now derives from sigEnd: a function has a body iff function_signature_end lands on `{`. Previously find_block_open scanned the whole stream and a body-less top-level decl (`fn f() -> Unit` with no braces) wrongly borrowed a LATER fn's block. No-body decls return sigEnd (correct for multi-line signatures too). Corpus reach: 459 -> 521 / 585 byte-exact (+62, ~89%), still 0 run-failures. Floor -> 521. New samples features_dup, features_late, nobody_fn; 30 samples green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2211cbb commit 8f8c721

5 files changed

Lines changed: 135 additions & 17 deletions

File tree

crates/rsscript/src/selfhost_parity.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1522,7 +1522,7 @@ const AST_SAMPLE_MIN: usize = 6;
15221522
/// Floor for `ast_parity_corpus` — the number of corpus files whose rss AST dump
15231523
/// already matches the oracle byte-for-byte. Ratchets up as the producer's
15241524
/// coverage grows; a drop signals a regression. (Full parity = files.len().)
1525-
const AST_CORPUS_PARITY_FLOOR: usize = 459;
1525+
const AST_CORPUS_PARITY_FLOOR: usize = 521;
15261526

15271527
/// Step-2 measurement gate (ignored by default): how many corpus files the rss
15281528
/// producer reproduces byte-for-byte. Not full parity yet — this ratchets a floor

selfhost/astdump.rss

Lines changed: 106 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1480,12 +1480,14 @@ fn emit_function(toks: read List<Tok>, start: read Int, d: read Int) -> Int {
14801480
retStart = j + 2
14811481
}
14821482
}
1483-
// body
1484-
let bodyOpen = find_block_open(toks: read toks, start: read j, limit: read List.len(list: read toks))
1485-
let hasBody = bodyOpen >= 0 && (isNative == false || same_stmt_line(toks: read toks, a: read start, b: read bodyOpen))
14861483
// Signature region [pclose+1, sigEnd): return type, effects(...), and the
1487-
// default-impl marker `= _`.
1484+
// default-impl marker `= _`. function_signature_end stops at the body `{` or
1485+
// the next top-level item — so a body exists iff sigEnd lands on a `{`. (A raw
1486+
// find_block_open would wrongly borrow a LATER fn's `{` for a body-less decl.)
14881487
let sigEnd = function_signature_end(toks: read toks, start: read (pclose + 1))
1488+
let mut bodyOpen = -1
1489+
if at_symbol(toks: read toks, i: read sigEnd, code: read SYM_LBRACE) { bodyOpen = sigEnd }
1490+
let hasBody = bodyOpen >= 0 && (isNative == false || same_stmt_line(toks: read toks, a: read start, b: read bodyOpen))
14891491
let effPos = find_effects_kw(toks: read toks, start: read (pclose + 1), end: read sigEnd)
14901492
let defaultImpl = has_default_impl(toks: read toks, start: read (pclose + 1), end: read sigEnd)
14911493
// A `fresh` anywhere in the return-type region (e.g. `-> Result<fresh String, E>`)
@@ -1541,8 +1543,9 @@ fn emit_function(toks: read List<Tok>, start: read Int, d: read Int) -> Int {
15411543
return bclose + 1
15421544
}
15431545
emit(depth: read (d + 2), s: read "block")
1544-
// native no-body: consume to end of declaration line.
1545-
return declaration_line_end(toks: read toks, start: read start)
1546+
// No body: the declaration ends exactly at sigEnd (next top-level item / EOF),
1547+
// which is correct even for a signature spanning multiple lines.
1548+
return sigEnd
15461549
}
15471550

15481551
fn same_stmt_line(toks: read List<Tok>, a: read Int, b: read Int) -> Bool {
@@ -2355,6 +2358,10 @@ fn main() -> Unit {
23552358
let toks = tokenize(source: read source)
23562359
let ntok = List.len(list: read toks)
23572360
emit(depth: read 0, s: read "program")
2361+
// Section 1: features are dumped FIRST regardless of source position (the
2362+
// oracle emits by struct-field order, not source order), so a pre-pass emits
2363+
// every `feature <name>` before any item; the item pass then skips headers.
2364+
emit_all_features(toks: read toks, ntok: read ntok)
23582365
let mut i = 0
23592366
let mut running = true
23602367
while running {
@@ -2367,16 +2374,8 @@ fn main() -> Unit {
23672374
} else {
23682375
let cls = classify_top(toks: read toks, i: read i)
23692376
if cls.isFeatures {
2370-
// features line: emit `feature <name>` per known feature word.
2371-
let fend = declaration_line_end(toks: read toks, start: read (i + 2))
2372-
let mut k = i + 2
2373-
while k < fend {
2374-
if is_feature_word(toks: read toks, i: read k) {
2375-
emit(depth: read 1, s: read String.concat(left: read "feature ", right: read tk_text(toks: read toks, i: read k)))
2376-
}
2377-
k = k + 1
2378-
}
2379-
i = fend
2377+
// Already emitted in the pre-pass; skip the header line.
2378+
i = declaration_line_end(toks: read toks, start: read (i + 2))
23802379
} else if cls.isProfile {
23812380
i = declaration_line_end(toks: read toks, start: read i)
23822381
} else if starts_type_decl(toks: read toks, i: read i, isPub: read cls.isPub) {
@@ -2402,6 +2401,97 @@ fn main() -> Unit {
24022401
}
24032402
}
24042403
}
2404+
// Section 5: feature diagnostics, emitted AFTER all items (unknown-feature
2405+
// then duplicate-feature), matching ast_oracle_dump's ordering.
2406+
emit_feature_diags(toks: read toks, ntok: read ntok)
2407+
return Unit
2408+
}
2409+
2410+
// True if token i begins a top-level `features:` header (depth checked by caller).
2411+
fn is_features_header(toks: read List<Tok>, i: read Int) -> Bool {
2412+
return is_kw_text(toks: read toks, i: read i, word: read "features") && at_symbol(toks: read toks, i: read (i + 1), code: read SYM_COLON)
2413+
}
2414+
2415+
// Section 1 pre-pass: emit `feature <name>` for every known feature across all
2416+
// top-level `features:` headers, in source order.
2417+
fn emit_all_features(toks: read List<Tok>, ntok: read Int) -> Unit {
2418+
let mut depth = 0
2419+
let mut i = 0
2420+
while i < ntok {
2421+
if at_symbol(toks: read toks, i: read i, code: read SYM_LPAREN) || at_symbol(toks: read toks, i: read i, code: read SYM_LBRACE) || at_symbol(toks: read toks, i: read i, code: read SYM_LBRACKET) {
2422+
depth = depth + 1
2423+
} else if at_symbol(toks: read toks, i: read i, code: read SYM_RPAREN) || at_symbol(toks: read toks, i: read i, code: read SYM_RBRACE) || at_symbol(toks: read toks, i: read i, code: read SYM_RBRACKET) {
2424+
if depth > 0 { depth = depth - 1 }
2425+
} else if depth == 0 && is_features_header(toks: read toks, i: read i) {
2426+
let fend = declaration_line_end(toks: read toks, start: read (i + 2))
2427+
let mut k = i + 2
2428+
while k < fend {
2429+
if is_feature_word(toks: read toks, i: read k) {
2430+
emit(depth: read 1, s: read String.concat(left: read "feature ", right: read tk_text(toks: read toks, i: read k)))
2431+
}
2432+
k = k + 1
2433+
}
2434+
}
2435+
i = i + 1
2436+
}
2437+
return Unit
2438+
}
2439+
2440+
// True if a known-feature token with the same text as token k appears earlier in
2441+
// the same header [hStart, k) (per-header duplicate, mirroring parse_features).
2442+
fn feat_dup_before(toks: read List<Tok>, hStart: read Int, k: read Int) -> Bool {
2443+
let target = tk_text(toks: read toks, i: read k)
2444+
let mut m = hStart
2445+
while m < k {
2446+
if is_feature_word(toks: read toks, i: read m) && tk_text(toks: read toks, i: read m) == target {
2447+
return true
2448+
}
2449+
m = m + 1
2450+
}
2451+
return false
2452+
}
2453+
2454+
// Section 5 post-pass: unknown-feature markers (all headers) then duplicate-feature
2455+
// markers (all headers), matching ast_oracle_dump.
2456+
fn emit_feature_diags(toks: read List<Tok>, ntok: read Int) -> Unit {
2457+
let mut depth = 0
2458+
let mut i = 0
2459+
while i < ntok {
2460+
if at_symbol(toks: read toks, i: read i, code: read SYM_LPAREN) || at_symbol(toks: read toks, i: read i, code: read SYM_LBRACE) || at_symbol(toks: read toks, i: read i, code: read SYM_LBRACKET) {
2461+
depth = depth + 1
2462+
} else if at_symbol(toks: read toks, i: read i, code: read SYM_RPAREN) || at_symbol(toks: read toks, i: read i, code: read SYM_RBRACE) || at_symbol(toks: read toks, i: read i, code: read SYM_RBRACKET) {
2463+
if depth > 0 { depth = depth - 1 }
2464+
} else if depth == 0 && is_features_header(toks: read toks, i: read i) {
2465+
let fend = declaration_line_end(toks: read toks, start: read (i + 2))
2466+
let mut k = i + 2
2467+
while k < fend {
2468+
if at_symbol(toks: read toks, i: read k, code: read SYM_COMMA) == false && tk_kind(toks: read toks, i: read k) != TOK_EOF && is_feature_word(toks: read toks, i: read k) == false {
2469+
emit(depth: read 1, s: read String.concat(left: read "unknown-feature ", right: read escape(s: read tk_text(toks: read toks, i: read k))))
2470+
}
2471+
k = k + 1
2472+
}
2473+
}
2474+
i = i + 1
2475+
}
2476+
depth = 0
2477+
i = 0
2478+
while i < ntok {
2479+
if at_symbol(toks: read toks, i: read i, code: read SYM_LPAREN) || at_symbol(toks: read toks, i: read i, code: read SYM_LBRACE) || at_symbol(toks: read toks, i: read i, code: read SYM_LBRACKET) {
2480+
depth = depth + 1
2481+
} else if at_symbol(toks: read toks, i: read i, code: read SYM_RPAREN) || at_symbol(toks: read toks, i: read i, code: read SYM_RBRACE) || at_symbol(toks: read toks, i: read i, code: read SYM_RBRACKET) {
2482+
if depth > 0 { depth = depth - 1 }
2483+
} else if depth == 0 && is_features_header(toks: read toks, i: read i) {
2484+
let fend = declaration_line_end(toks: read toks, start: read (i + 2))
2485+
let mut k = i + 2
2486+
while k < fend {
2487+
if is_feature_word(toks: read toks, i: read k) && feat_dup_before(toks: read toks, hStart: read (i + 2), k: read k) {
2488+
emit(depth: read 1, s: read String.concat(left: read "duplicate-feature ", right: read tk_text(toks: read toks, i: read k)))
2489+
}
2490+
k = k + 1
2491+
}
2492+
}
2493+
i = i + 1
2494+
}
24052495
return Unit
24062496
}
24072497

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
features: local, local
2+
3+
fn main() -> Unit {
4+
return Unit
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
fn helper(x: read Int) -> Int {
2+
return x
3+
}
4+
5+
features: local

selfhost/samples/ast/nobody_fn.rss

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// expect: RS0305
2+
features: local
3+
4+
struct Workspace {
5+
id: Int
6+
}
7+
8+
struct Config {
9+
workspace: Workspace
10+
}
11+
12+
fn use_config(config: read Config, workspace: read Workspace) -> Unit
13+
fn make_config() -> fresh Config
14+
15+
fn run() -> Unit {
16+
local config = make_config()
17+
use_config(config: read (manage config), workspace: read config.workspace)
18+
}

0 commit comments

Comments
 (0)