From 5d39bff0c165c562bde66e21ce7e80da254d0bff Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 12 Oct 2026 04:49:37 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(crux-judge):=20scan=5Fonce=20and=20main?= =?UTF-8?q?=20back=20at=20grade=20B=20=E2=80=94=20CB-200=20616=20again=20(?= =?UTF-8?q?#5173)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two definitions added since v0.70.3 graded B- (TDG 74.5, cyclomatic 18) and took CB-200 from 616 to 618 definitions below min_grade B. - pyjson `scan_once`: the six bare-word constants (null, true, false, NaN, Infinity, -Infinity) move to `scan_literal` + `literal_value`; they are still tried before a number, so `-Infinity` is not read as `-`. - main `main`: argument decoding (`utf8_args`), the `sandbox` command (`sandbox_cmd`) and the collect exit mapping (`collect_cmd`) become helpers under one `run` dispatch. Exit codes are unchanged. The baseline is not touched. Refs: #5173 Agent: aprender-cd Co-Authored-By: Claude Opus 5.5 --- tools/aprender-crux-judge/src/main.rs | 76 ++++++++++++++----------- tools/aprender-crux-judge/src/pyjson.rs | 30 +++++++--- 2 files changed, 66 insertions(+), 40 deletions(-) diff --git a/tools/aprender-crux-judge/src/main.rs b/tools/aprender-crux-judge/src/main.rs index 99747f6872..f1b8e4a587 100644 --- a/tools/aprender-crux-judge/src/main.rs +++ b/tools/aprender-crux-judge/src/main.rs @@ -196,49 +196,59 @@ fn sandbox_report(probed: Result<&sandbox::Sandbox, &str>) -> (String, u8) { } } -fn main() -> ExitCode { - let mut argv = Vec::new(); - for a in std::env::args_os().skip(1) { - match a.into_string() { - Ok(s) => argv.push(s), - Err(a) => { - return ExitCode::from(usage_error(&format!( - "argument is not UTF-8: {}", - a.to_string_lossy() - ))); - } - } - } - match argv.first().map(String::as_str) { - Some("eval" | "extract" | "lint") => return ExitCode::from(oracles::cli(&argv)), - Some("certify" | "check") => return ExitCode::from(certify::cli(&argv)), - _ => {} - } - if argv.first().map(String::as_str) == Some("sandbox") { - if let Some(extra) = argv.get(1) { - return ExitCode::from(usage_error(&format!("unrecognized arguments: {extra}"))); - } - let (line, rc) = sandbox_report(sandbox::sandbox()); - println!("{line}"); - return ExitCode::from(rc); +/// The arguments after the program name, or the usage exit code for the first one that is not UTF-8. +fn utf8_args() -> Result, u8> { + std::env::args_os() + .skip(1) + .map(|a| { + a.into_string().map_err(|a| { + usage_error(&format!("argument is not UTF-8: {}", a.to_string_lossy())) + }) + }) + .collect() +} + +/// `sandbox`: print the probe's one line; it takes no arguments. +fn sandbox_cmd(argv: &[String]) -> u8 { + if let Some(extra) = argv.get(1) { + return usage_error(&format!("unrecognized arguments: {extra}")); } - let args = match parse(&argv) { - Parsed::Run(args) => args, - Parsed::Exit(rc) => return ExitCode::from(rc), - }; - match collect::collect(&args) { - Ok(rc) => ExitCode::from(u8::try_from(rc).unwrap_or(1)), + let (line, rc) = sandbox_report(sandbox::sandbox()); + println!("{line}"); + rc +} + +/// The default command: collect, with a decline exiting 2 and a crash 1. +fn collect_cmd(args: &collect::Args) -> u8 { + match collect::collect(args) { + Ok(rc) => u8::try_from(rc).unwrap_or(1), Err(e) if e.declines() => { eprintln!("decline: {}: {e}", e.kind); - ExitCode::from(2) + 2 } Err(e) => { eprintln!("crash: {}: {e}", e.kind); - ExitCode::from(1) + 1 } } } +fn run(argv: &[String]) -> u8 { + match argv.first().map(String::as_str) { + Some("eval" | "extract" | "lint") => oracles::cli(argv), + Some("certify" | "check") => certify::cli(argv), + Some("sandbox") => sandbox_cmd(argv), + _ => match parse(argv) { + Parsed::Run(args) => collect_cmd(&args), + Parsed::Exit(rc) => rc, + }, + } +} + +fn main() -> ExitCode { + ExitCode::from(utf8_args().map_or_else(|rc| rc, |argv| run(&argv))) +} + #[cfg(test)] mod tests { use super::*; diff --git a/tools/aprender-crux-judge/src/pyjson.rs b/tools/aprender-crux-judge/src/pyjson.rs index 921cdef139..a5dbb8a59f 100644 --- a/tools/aprender-crux-judge/src/pyjson.rs +++ b/tools/aprender-crux-judge/src/pyjson.rs @@ -16,6 +16,18 @@ enum ScanErr { type Scan = Result; +/// The value of one of `Doc::scan_literal`'s bare words. +fn literal_value(lit: &str) -> Val { + match lit { + "true" => Val::Bool(true), + "false" => Val::Bool(false), + "NaN" => Val::Float(f64::NAN), + "Infinity" => Val::Float(f64::INFINITY), + "-Infinity" => Val::Float(f64::NEG_INFINITY), + _ => Val::None, + } +} + /// Where a container scan goes after an item. enum Step { /// At the closing bracket. @@ -56,16 +68,20 @@ impl Doc<'_> { '"' => self.scanstring(idx + 1).map(|(v, e)| (Val::Str(v), e)), '{' => self.parse_object(idx + 1), '[' => self.parse_array(idx + 1), - 'n' if self.starts(idx, "null") => Ok((Val::None, idx + 4)), - 't' if self.starts(idx, "true") => Ok((Val::Bool(true), idx + 4)), - 'f' if self.starts(idx, "false") => Ok((Val::Bool(false), idx + 5)), - 'N' if self.starts(idx, "NaN") => Ok((Val::Float(f64::NAN), idx + 3)), - 'I' if self.starts(idx, "Infinity") => Ok((Val::Float(f64::INFINITY), idx + 8)), - '-' if self.starts(idx, "-Infinity") => Ok((Val::Float(f64::NEG_INFINITY), idx + 9)), - _ => self.match_number(idx), + _ => self + .scan_literal(idx) + .map_or_else(|| self.match_number(idx), Ok), } } + /// The bare-word constants Python's scanner accepts, tried before a number so `-Infinity` is not read as `-`. + fn scan_literal(&self, idx: usize) -> Option<(Val, usize)> { + ["null", "true", "false", "NaN", "Infinity", "-Infinity"] + .into_iter() + .find(|lit| self.starts(idx, lit)) + .map(|lit| (literal_value(lit), idx + lit.len())) + } + fn is_at(&self, i: usize, c: char) -> bool { i < self.s.len() && self.s[i] == c } From 2f449abb786897f029da40e745b91d85d81fe659 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 12 Oct 2026 04:58:59 +0200 Subject: [PATCH 2/2] test(crux-judge): every bare-word literal decodes through scan_literal (#5173) Refs: #5173 Agent: aprender-cd Co-Authored-By: Claude Opus 5.5 --- tools/aprender-crux-judge/src/pyjson.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tools/aprender-crux-judge/src/pyjson.rs b/tools/aprender-crux-judge/src/pyjson.rs index a5dbb8a59f..9c6d0d67a8 100644 --- a/tools/aprender-crux-judge/src/pyjson.rs +++ b/tools/aprender-crux-judge/src/pyjson.rs @@ -553,6 +553,16 @@ mod tests { } } + #[test] + fn bare_word_literals_decode() { + let v = loads("[null, true, false, NaN, Infinity, -Infinity, -1, -1.5]") + .expect("every bare word decodes"); + assert_eq!( + dumps(&v, true).expect("the literals dump"), + "[null, true, false, NaN, Infinity, -Infinity, -1, -1.5]" + ); + } + #[test] fn surrogate_pair_joins() { let v = loads("\"\\ud83d\\ude00x\"").expect("a surrogate pair decodes");