Skip to content

Commit 77fcd51

Browse files
olwangclaude
andcommitted
test/docs: strengthen self-host parity gates + refresh stale self-host docs
Follow-up review findings on the self-host harness: - Add non-ignored negative-parser and positive-RS0005-checker smoke tests, so a tool degenerating to always-OK / always-CLEAN is caught without the ignored corpus gate (the accept-only / no-duplicate tiny samples could not). - rss_dump_with now errors on a malformed non-empty dump line instead of silently dropping it (filter_map), so a stray/garbled line fails the harness. - Refresh stale counts: plan table and ledger SH-018/SH-023 now read 556/556 (was 544/545/546) and ~46x (was ~5100x); SH-023 note reflects the recover-past- malformed scanner (was "stops at first"). - FORMAT.md: add Char and Unknown to the KIND list; reconcile the "richest line" claim with lexer.rss's current 0:0:0 placeholder positions. Verified: self-host lib tests + corpus gates (lexer/parser/checker 556x, perf) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 55d415c commit 77fcd51

4 files changed

Lines changed: 72 additions & 21 deletions

File tree

crates/rsscript/src/selfhost_parity.rs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,17 @@ fn rss_dump_with(exe: &RegVmExecutable, source: &str) -> Result<Vec<CanonTok>, S
147147
let output = exe
148148
.eval_main_with_args([source.to_string()])
149149
.map_err(|e| format!("rss lexer failed to run: {e:?}"))?;
150-
Ok(output
150+
// Fail on any malformed non-empty dump line rather than silently dropping it
151+
// (a stray debug line or a garbled token would otherwise vanish and let a
152+
// broken lexer pass parity by emitting fewer/no tokens).
153+
output
151154
.stdout
152155
.lines()
153156
.filter(|l| !l.is_empty())
154-
.filter_map(parse_line)
155-
.collect())
157+
.map(|l| {
158+
parse_line(l).ok_or_else(|| format!("rss lexer emitted a malformed dump line: {l:?}"))
159+
})
160+
.collect()
156161
}
157162

158163
/// Convenience: compile + run once (used by the single-file smoke test).
@@ -417,6 +422,24 @@ fn parser_parity_tiny_sample() {
417422
compare_parse(oracle, actual, parse_position_tier()).unwrap_or_else(|msg| panic!("{msg}"));
418423
}
419424

425+
/// Phase-2 NEGATIVE smoke (non-ignored): the rss parser must REJECT malformed
426+
/// source, matching the Rust oracle. The accept-only tiny sample above would
427+
/// still pass if the rss parser degenerated to always printing `OK`; this closes
428+
/// that gap without needing the (ignored) full-corpus gate.
429+
#[test]
430+
fn parser_rejects_malformed_source_smoke() {
431+
let source = "fn main() -> Unit {\n return Unit\n}\n\nfn\n";
432+
let oracle = parse_oracle_error("parser-negative.rss", source);
433+
assert!(
434+
oracle.is_some(),
435+
"oracle Rust parser must reject the malformed sample (else the smoke test proves nothing)"
436+
);
437+
let exe = compile_parser().expect("rss parser should compile");
438+
let actual = run_parser(&exe, source).expect("rss parser should run");
439+
// Recognition tier: both must reject (accept-vs-reject only).
440+
compare_parse(oracle, actual, false).unwrap_or_else(|msg| panic!("{msg}"));
441+
}
442+
420443
/// Phase-2 gate (ignored by default): the rss parser's accept/reject matches the
421444
/// Rust parser over the whole `.rss` corpus.
422445
#[test]
@@ -534,6 +557,27 @@ fn checker_parity_tiny_sample() {
534557
assert_eq!(oracle, actual, "checker parity diverged on tiny sample");
535558
}
536559

560+
/// Phase-3 POSITIVE smoke (non-ignored): the rss checker must REPORT RS0005 for a
561+
/// duplicate declaration, matching the analyzer. The no-duplicate tiny sample
562+
/// above would still pass if the rss checker degenerated to always printing
563+
/// `CLEAN`; this closes that gap without the (ignored) full-corpus gate.
564+
#[test]
565+
fn checker_reports_rs0005_for_duplicate_declaration_smoke() {
566+
let source =
567+
"fn dup() -> Unit {\n return Unit\n}\nfn dup() -> Unit {\n return Unit\n}\n";
568+
let oracle = checker_oracle_codes("checker-duplicate.rss", source);
569+
assert!(
570+
oracle.contains(&"RS0005".to_string()),
571+
"oracle analyzer must report RS0005 for the duplicate declaration; got {oracle:?}"
572+
);
573+
let exe = compile_checker().expect("rss checker should compile");
574+
let actual = run_checker(&exe, source).expect("rss checker should run");
575+
assert_eq!(
576+
oracle, actual,
577+
"checker parity diverged on the duplicate-declaration smoke test"
578+
);
579+
}
580+
537581
/// Phase-3 gate (ignored by default): the rss checker's target-code diagnostics
538582
/// match the analyzer over the whole `.rss` corpus.
539583
#[test]

docs/ledgers/rss-selfhost-ledger.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ gap is VM value-representation / intrinsic-dispatch cost (the next big lever).
501501
emits `(*pos)` on read and as the assignment target for such a param, since
502502
`mut T` already lowers to `&mut T`. Non-Copy `mut` params keep their `&mut Struct`
503503
lowering and stay non-reassignable (only fields/elements are mutable).
504-
- **Tests:** `crate::selfhost_parity::lexer_parity_corpus` (tier 0, 544/544);
504+
- **Tests:** `crate::selfhost_parity::lexer_parity_corpus` (tier 0, 556/556);
505505
`tests/fixtures/pass/mut-scalar-writeback.rss` (Int + Bool write-back).
506506
- **Status:** fixed (scalar Copy `mut` params are reassignable with caller
507507
write-back; non-Copy `mut` params stay non-reassignable).
@@ -770,8 +770,8 @@ gap is VM value-representation / intrinsic-dispatch cost (the next big lever).
770770

771771
- **Tool:** self-hosted checker (`selfhost/check.rss`) run on the reg-VM vs
772772
`crate::analyze_source` filtered to error-severity `RS0005`
773-
(DUPLICATE_DECLARATION), over the whole 546-file corpus.
774-
- **Symptom (positive):** the checker reproduces RS0005 with **546/546** parity
773+
(DUPLICATE_DECLARATION), over the whole 556-file corpus.
774+
- **Symptom (positive):** the checker reproduces RS0005 with **556/556** parity
775775
using ONLY top-level declaration structure — no statement/expression/pattern
776776
body parsing (confirms SH-021: RS0005 is decidable from declaration shape). It
777777
reuses the proven `selfhost/parser.rss` recognizer verbatim; the sole addition
@@ -801,10 +801,11 @@ gap is VM value-representation / intrinsic-dispatch cost (the next big lever).
801801
threaded cursors returned by value (SH-018). `Set<String>` (`Set<String>.new()`,
802802
`Set.contains<String>`, `Set.insert<String>`) worked as the duplicate detector;
803803
`features: local` was NOT needed (no `StringBuilder`/`local` bindings). The
804-
scanner conservatively STOPS on the first malformed/unknown top-level item
805-
(mirroring the recognizer), which can only under-report on syntactically broken
806-
files — safe, since the analyzer emits RS0005 on exactly the 2 well-formed
807-
fixtures and the other 544 files stay CLEAN (zero false positives).
804+
scanner RECOVERS past an unrecognized top-level item (skips one token and keeps
805+
scanning, mirroring the analyzer's recovery) rather than stopping at the first
806+
one, so a later duplicate is not missed. Parity holds: the analyzer emits RS0005
807+
on exactly the 2 well-formed duplicate fixtures and the other 554 files stay
808+
CLEAN (zero false positives).
808809
- **Classification:** docs (records the analyzer's duplicate-symbol namespace
809810
rule and that RS0005 is a declaration-only property).
810811
- **Tests:** `crate::selfhost_parity::checker_parity_tiny_sample` and

docs/self-hosting-stress-test-plan.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,16 @@ runs it on the reg-VM in-process, passing the corpus file's *content* as `argv[0
187187

188188
| Phase | rss tool | Oracle | Corpus result |
189189
|-------|----------|--------|---------------|
190-
| 1 — lexer | `selfhost/lexer.rss` | `crate::lexer::lex` (canonical token dump) | **544/544 tier-0**, 0 run-failures |
191-
| 2 — parser | `selfhost/parser.rss` | `crate::syntax::parse_source_raw` (accept/reject) | **545/545 recognition** |
192-
| 3 — checker | `selfhost/check.rss` | `crate::analyze_source` (code `RS0005`) | **546/546** |
193-
| 4 — perf | lexer on VM vs native | wall-clock over corpus | **~5100× slower** (see SH-022) |
190+
| 1 — lexer | `selfhost/lexer.rss` | `crate::lexer::lex` (canonical token dump) | **556/556 tier-0**, 0 run-failures |
191+
| 2 — parser | `selfhost/parser.rss` | `crate::syntax::parse_source_raw` (accept/reject) | **556/556 recognition** |
192+
| 3 — checker | `selfhost/check.rss` | `crate::analyze_source` (code `RS0005`) | **556/556** |
193+
| 4 — perf | lexer on VM vs native | wall-clock over corpus | **~46× slower** (post-SH-022; was ~5100×) |
194194

195195
Gates (all green): `cargo test -p rsscript --features native-jit --lib` (3 tiny-sample
196-
tests); the corpus gates run with `-- --ignored` (`lexer_parity_corpus`,
197-
`parser_parity_corpus`, `checker_parity_corpus`, `lexer_perf_corpus`).
196+
tests plus a non-ignored negative-parser and positive-`RS0005`-checker smoke test, so
197+
a degenerate always-`OK`/always-`CLEAN` tool is caught without the corpus gate); the
198+
corpus gates run with `-- --ignored` (`lexer_parity_corpus`, `parser_parity_corpus`,
199+
`checker_parity_corpus`, `lexer_perf_corpus`).
198200

199201
### Findings (ledger `SH-016``SH-023`)
200202
- **SH-016** — no character-literal syntax; `'` lexes to `?`, cascading a misleading

selfhost/FORMAT.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,21 @@ tab-separated fields:
1010
- `line`, `col` — 1-indexed position of the token start (matches `Span.line` /
1111
`Span.column`). `len` — byte length of the token (matches `Span.length`).
1212
- `KIND` — one of the exact `TokenKind` variant names:
13-
`Ident Number String InterpolatedString MultilineString Keyword Symbol Eof`.
14-
- `PAYLOAD` — the token's raw text (ident/number/keyword/symbol text, or the raw
15-
content captured between string delimiters). For `Eof` the payload is empty.
13+
`Ident Number String Char InterpolatedString MultilineString Keyword Symbol
14+
Unknown Eof`. `Char` is a character literal `'c'`; `Unknown` is a source
15+
character outside the lexical inventory (payload is that raw char).
16+
- `PAYLOAD` — the token's raw text (ident/number/keyword/symbol/char text, or the
17+
raw content captured between string delimiters). For `Eof` the payload is empty.
1618
- PAYLOAD is escaped so a token is always exactly one line:
1719
`\``\\`, newline → `\n`, tab → `\t`, carriage return → `\r` (backslash first).
1820

1921
## Comparison tiers (env `RSS_SELFHOST_TIER`, default `0`)
2022

21-
The rss lexer always emits the full richest line. The harness parses both dumps
23+
The rss lexer emits all three fields on every line; the harness parses both dumps
2224
into structured tokens and compares only the fields for the active tier, so a
23-
single rss implementation supports graded parity:
25+
single rss implementation supports graded parity. (The current `selfhost/lexer.rss`
26+
has not implemented span tracking yet, so it emits placeholder `0:0:0` positions
27+
and is exercised at tier 0 — see the note below.)
2428

2529
- **tier 0** (default): compare `(KIND, PAYLOAD)` — pure tokenization logic.
2630
- **tier 1**: also compare `(line, col)` — span position arithmetic.

0 commit comments

Comments
 (0)