Skip to content

Commit 3cc988d

Browse files
olwangclaude
andcommitted
selfhost Phase 3 scaffold: checker recognition harness + stub (RS0005)
Oracle = crate::analyze_source filtered to target codes (start: RS0005 DUPLICATE_DECLARATION). rss checker prints found codes / CLEAN. Stub proves the loop; corpus baseline shows exactly 2 files flag RS0005 (duplicate-declarations, duplicate-fields); 544 clean. Real duplicate-declaration checker replaces the stub next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c34363d commit 3cc988d

2 files changed

Lines changed: 143 additions & 1 deletion

File tree

crates/rsscript/src/selfhost_parity.rs

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
use std::path::PathBuf;
1313

1414
use crate::lexer::{TokenKind, lex};
15-
use crate::{RegVmExecutable, reg_vm_compile_source};
15+
use crate::{RegVmExecutable, Severity, analyze_source, reg_vm_compile_source};
1616

1717
/// One token in the canonical dump. Positions are `None` when the producer
1818
/// emitted placeholders (the rss lexer does so until spans are implemented).
@@ -451,3 +451,121 @@ fn parser_parity_corpus() {
451451
mismatches.len()
452452
);
453453
}
454+
455+
// ---------------------------------------------------------------------------
456+
// Phase 3 — checker parity (semantic diagnostics).
457+
//
458+
// The rss checker (`selfhost/check.rss`) reproduces a chosen subset of analyzer
459+
// diagnostics and prints the codes it finds (one per line, or `CLEAN`). Oracle:
460+
// the real analyzer `crate::analyze_source`, filtered to the same target codes.
461+
// We start with RS0005 (DUPLICATE_DECLARATION — duplicate top-level item names
462+
// and duplicate struct/sum fields), decidable from declaration structure alone
463+
// (no expression/statement parsing needed; see SH-021).
464+
// ---------------------------------------------------------------------------
465+
466+
/// Diagnostic codes the rss checker is expected to reproduce.
467+
const CHECKER_TARGET_CODES: &[&str] = &["RS0005"];
468+
469+
fn is_target_code(code: &str) -> bool {
470+
CHECKER_TARGET_CODES.contains(&code)
471+
}
472+
473+
/// Oracle: the set of target diagnostic codes the real analyzer reports.
474+
fn checker_oracle_codes(file: &str, source: &str) -> Vec<String> {
475+
let mut codes: Vec<String> = analyze_source(file, source)
476+
.into_iter()
477+
.filter(|d| d.severity == Severity::Error && is_target_code(&d.code))
478+
.map(|d| d.code)
479+
.collect();
480+
codes.sort();
481+
codes.dedup();
482+
codes
483+
}
484+
485+
fn compile_checker() -> Result<RegVmExecutable, String> {
486+
let path = selfhost_dir().join("check.rss");
487+
let src = std::fs::read_to_string(&path)
488+
.map_err(|e| format!("cannot read {}: {e}", path.display()))?;
489+
reg_vm_compile_source("selfhost/check.rss", &src)
490+
.map_err(|e| format!("rss checker failed to compile: {e:?}"))
491+
}
492+
493+
/// Run the rss checker; parse the target codes it reports (`CLEAN` => none).
494+
fn run_checker(exe: &RegVmExecutable, source: &str) -> Result<Vec<String>, String> {
495+
let output = exe
496+
.eval_main_with_args([source.to_string()])
497+
.map_err(|e| format!("rss checker failed to run: {e:?}"))?;
498+
let mut codes: Vec<String> = output
499+
.stdout
500+
.lines()
501+
.map(|l| l.trim())
502+
.filter(|l| !l.is_empty() && *l != "CLEAN" && is_target_code(l))
503+
.map(|l| l.to_string())
504+
.collect();
505+
codes.sort();
506+
codes.dedup();
507+
Ok(codes)
508+
}
509+
510+
/// Phase-3 proof: the rss checker agrees with the analyzer on a tiny sample
511+
/// (no duplicates → both report no target codes).
512+
#[test]
513+
fn checker_parity_tiny_sample() {
514+
let sample_path = selfhost_dir().join("samples/tiny.rss");
515+
let source = std::fs::read_to_string(&sample_path)
516+
.unwrap_or_else(|e| panic!("cannot read {}: {e}", sample_path.display()));
517+
let oracle = checker_oracle_codes("samples/tiny.rss", &source);
518+
let exe = compile_checker().expect("rss checker should compile");
519+
let actual = run_checker(&exe, &source).expect("rss checker should run");
520+
assert_eq!(oracle, actual, "checker parity diverged on tiny sample");
521+
}
522+
523+
/// Phase-3 gate (ignored by default): the rss checker's target-code diagnostics
524+
/// match the analyzer over the whole `.rss` corpus.
525+
#[test]
526+
#[ignore]
527+
fn checker_parity_corpus() {
528+
let root = workspace_root();
529+
let files = collect_rss_files(&root);
530+
let exe = compile_checker().expect("rss checker should compile");
531+
let mut run_failures: Vec<String> = Vec::new();
532+
let mut mismatches: Vec<String> = Vec::new();
533+
let mut ok = 0usize;
534+
for file in &files {
535+
let rel = file.strip_prefix(&root).unwrap_or(file).display().to_string();
536+
let Ok(source) = std::fs::read_to_string(file) else {
537+
run_failures.push(format!("{rel}: unreadable"));
538+
continue;
539+
};
540+
let oracle = checker_oracle_codes(&rel, &source);
541+
match run_checker(&exe, &source) {
542+
Err(e) => run_failures.push(format!("{rel}: {e}")),
543+
Ok(actual) => {
544+
if actual == oracle {
545+
ok += 1;
546+
} else {
547+
mismatches.push(format!("{rel}: oracle={oracle:?} rss={actual:?}"));
548+
}
549+
}
550+
}
551+
}
552+
let total = files.len();
553+
eprintln!(
554+
"\n=== checker_parity_corpus (codes {CHECKER_TARGET_CODES:?}) ===\n files: {total}\n \
555+
ok: {ok}\n run-failures: {}\n code-mismatches: {}\n",
556+
run_failures.len(),
557+
mismatches.len()
558+
);
559+
for line in run_failures.iter().take(20) {
560+
eprintln!("[run-fail] {line}");
561+
}
562+
for line in mismatches.iter().take(20) {
563+
eprintln!("[mismatch] {line}");
564+
}
565+
assert!(
566+
run_failures.is_empty() && mismatches.is_empty(),
567+
"checker parity failed: {} run-failures, {} mismatches (of {total})",
568+
run_failures.len(),
569+
mismatches.len()
570+
);
571+
}

selfhost/check.rss

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Self-hosted rss checker — Phase 3 (semantic diagnostic parity).
2+
//
3+
// Reads rss source as argv[0] and prints the analyzer diagnostic codes it finds,
4+
// one per line, or `CLEAN` if none. Oracle: crate::analyze_source filtered to the
5+
// target codes (see selfhost_parity.rs). Target: RS0005 (DUPLICATE_DECLARATION —
6+
// duplicate top-level item names + duplicate struct/sum fields), decidable from
7+
// declaration structure alone (no body parsing; see ledger SH-021).
8+
//
9+
// This is the Phase-3 STUB (reports CLEAN) — it proves the harness loop and the
10+
// reject-set baseline. The real duplicate-declaration checker replaces it.
11+
//
12+
// rss constraints (SH-016/017/018): no char literals, single-line expressions,
13+
// cursors threaded positionally.
14+
15+
fn main() -> Unit {
16+
let source = Args.get_or_default(index: 0, default: read "")
17+
let n = String.len(value: read source)
18+
if n < 0 {
19+
Log.write(message: read "RS0005")
20+
} else {
21+
Log.write(message: read "CLEAN")
22+
}
23+
return Unit
24+
}

0 commit comments

Comments
 (0)