From acc69e6b7bd099696498626385ae2967abf04be8 Mon Sep 17 00:00:00 2001 From: James Fitzgerald Date: Sun, 1 Feb 2026 10:58:47 -0600 Subject: [PATCH 1/6] add --compile-only cli flag and runmode variant --- src/settings.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/settings.rs b/src/settings.rs index 1847e22c2d4..59a9e08ad7c 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -10,6 +10,7 @@ pub enum RunMode { Module(String), InstallPip(InstallPipMode), Repl, + CompileOnly(Vec), } pub enum InstallPipMode { @@ -52,6 +53,7 @@ struct CliArgs { warning_control: Vec, implementation_option: Vec, check_hash_based_pycs: CheckHashPycsMode, + compile_only: bool, #[cfg(feature = "flame-it")] profile_output: Option, @@ -100,7 +102,8 @@ Options (and corresponding environment variables): --help-all: print complete help information and exit RustPython extensions: - +--compile-only file ... : compile files without executing (terminates option list) +--install-pip [ensurepip|get-pip] : install pip using specified method Arguments: file : program read from script file @@ -150,6 +153,7 @@ fn parse_args() -> Result<(CliArgs, RunMode, Vec), lexopt::Error> { Long("check-hash-based-pycs") => { args.check_hash_based_pycs = parser.value()?.parse()? } + Long("compile-only") => args.compile_only = true, // TODO: make these more specific Long("help-env") => help(parser), @@ -179,6 +183,13 @@ fn parse_args() -> Result<(CliArgs, RunMode, Vec), lexopt::Error> { } Value(script_name) => { let script_name = script_name.string()?; + if args.compile_only { + return Ok(( + args, + RunMode::CompileOnly(argv(script_name, parser)?), + vec![], + )); + } let mode = if script_name == "-" { RunMode::Repl } else { @@ -189,7 +200,11 @@ fn parse_args() -> Result<(CliArgs, RunMode, Vec), lexopt::Error> { _ => return Err(arg.unexpected()), } } - Ok((args, RunMode::Repl, vec![])) + if args.compile_only { + Ok((args, RunMode::CompileOnly(vec![]), vec![])) + } else { + Ok((args, RunMode::Repl, vec![])) + } } fn help(parser: lexopt::Parser) -> ! { From 65f8f1e7d8861cc47de897e6c61216f4e1d0c7a9 Mon Sep 17 00:00:00 2001 From: James Fitzgerald Date: Sun, 1 Feb 2026 10:59:02 -0600 Subject: [PATCH 2/6] implement compile-only mode handler --- src/lib.rs | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index b73725a0fe2..aab58d09f56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -238,7 +238,7 @@ fn run_rustpython(vm: &VirtualMachine, run_mode: RunMode) -> PyResult<()> { RunMode::Module(_) => env::current_dir() .ok() .and_then(|p| p.to_str().map(|s| s.to_owned())), - RunMode::Script(_) | RunMode::InstallPip(_) => None, // handled by run_script + RunMode::Script(_) | RunMode::InstallPip(_) | RunMode::CompileOnly(_) => None, // handled by run_script RunMode::Repl => Some(String::new()), }; @@ -287,6 +287,38 @@ fn run_rustpython(vm: &VirtualMachine, run_mode: RunMode) -> PyResult<()> { run_file(vm, scope.clone(), &script_path) } RunMode::Repl => Ok(()), + RunMode::CompileOnly(files) => { + debug!("Compiling {} file(s)", files.len()); + if files.is_empty() { + eprintln!("No files specified for --compile-only"); + return Ok(()); + } + let mut success = true; + for file in files { + match std::fs::read_to_string(&file) { + Ok(source) => { + if let Err(err) = + vm.compile(&source, vm::compiler::Mode::Exec, file.clone()) + { + eprintln!("Error compiling {file}: {err}"); + success = false; + } + } + Err(err) => { + eprintln!("Error reading {file}: {err}"); + success = false; + } + } + } + if !success { + let exit_code = vm.ctx.new_int(1); + return Err(vm.new_exception( + vm.ctx.exceptions.system_exit.to_owned(), + vec![exit_code.into()], + )); + } + Ok(()) + } }; let result = if is_repl || vm.state.config.settings.inspect { shell::run_shell(vm, scope) From ab1d681f1b400e58c48284abec7bd6e53a4bfce2 Mon Sep 17 00:00:00 2001 From: James Fitzgerald Date: Sun, 1 Feb 2026 10:59:06 -0600 Subject: [PATCH 3/6] add unit test for compile-only mode --- src/lib.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index aab58d09f56..c39bba2bf11 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -408,4 +408,41 @@ mod tests { })()); }) } + + #[test] + fn test_compile_only() { + interpreter().enter(|vm| { + let valid = "def foo(x, y):\n return x + y\n"; + assert!( + vm.compile(valid, vm::compiler::Mode::Exec, "".to_owned()) + .is_ok() + ); + + let syntax_error = "def foo(:\n"; + assert!( + vm.compile(syntax_error, vm::compiler::Mode::Exec, "".to_owned()) + .is_err() + ); + + let duplicate_param = "def foo(x, x):\n pass\n"; + assert!( + vm.compile( + duplicate_param, + vm::compiler::Mode::Exec, + "".to_owned() + ) + .is_err() + ); + + let break_outside_loop = "def foo():\n break\n"; + assert!( + vm.compile( + break_outside_loop, + vm::compiler::Mode::Exec, + "".to_owned() + ) + .is_err() + ); + }) + } } From b9d1330f351d89665e08eddd6aec51b14d09153a Mon Sep 17 00:00:00 2001 From: James Fitzgerald Date: Sun, 1 Feb 2026 11:33:59 -0600 Subject: [PATCH 4/6] fix --compile-only option termination --- src/settings.rs | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/settings.rs b/src/settings.rs index 59a9e08ad7c..9be9fa505d9 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -53,7 +53,6 @@ struct CliArgs { warning_control: Vec, implementation_option: Vec, check_hash_based_pycs: CheckHashPycsMode, - compile_only: bool, #[cfg(feature = "flame-it")] profile_output: Option, @@ -153,7 +152,13 @@ fn parse_args() -> Result<(CliArgs, RunMode, Vec), lexopt::Error> { Long("check-hash-based-pycs") => { args.check_hash_based_pycs = parser.value()?.parse()? } - Long("compile-only") => args.compile_only = true, + Long("compile-only") => { + let files: Vec = parser + .raw_args()? + .map(|a| a.string()) + .collect::>()?; + return Ok((args, RunMode::CompileOnly(files), vec![])); + } // TODO: make these more specific Long("help-env") => help(parser), @@ -183,13 +188,6 @@ fn parse_args() -> Result<(CliArgs, RunMode, Vec), lexopt::Error> { } Value(script_name) => { let script_name = script_name.string()?; - if args.compile_only { - return Ok(( - args, - RunMode::CompileOnly(argv(script_name, parser)?), - vec![], - )); - } let mode = if script_name == "-" { RunMode::Repl } else { @@ -200,11 +198,7 @@ fn parse_args() -> Result<(CliArgs, RunMode, Vec), lexopt::Error> { _ => return Err(arg.unexpected()), } } - if args.compile_only { - Ok((args, RunMode::CompileOnly(vec![]), vec![])) - } else { - Ok((args, RunMode::Repl, vec![])) - } + Ok((args, RunMode::Repl, vec![])) } fn help(parser: lexopt::Parser) -> ! { From 2ad616c2090a44cf538f8f1b1d4c61b361fe85d0 Mon Sep 17 00:00:00 2001 From: James Fitzgerald Date: Sun, 1 Feb 2026 11:34:03 -0600 Subject: [PATCH 5/6] exit non-zero when no files specified --- src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index c39bba2bf11..3546a5f929c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -291,7 +291,11 @@ fn run_rustpython(vm: &VirtualMachine, run_mode: RunMode) -> PyResult<()> { debug!("Compiling {} file(s)", files.len()); if files.is_empty() { eprintln!("No files specified for --compile-only"); - return Ok(()); + let exit_code = vm.ctx.new_int(1); + return Err(vm.new_exception( + vm.ctx.exceptions.system_exit.to_owned(), + vec![exit_code.into()], + )); } let mut success = true; for file in files { From dde30cd8f9915f0f6cd3230e5eff139ddb365514 Mon Sep 17 00:00:00 2001 From: James Fitzgerald Date: Sun, 1 Feb 2026 11:39:20 -0600 Subject: [PATCH 6/6] add docstring for compileonly variant --- src/settings.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/settings.rs b/src/settings.rs index 9be9fa505d9..4d7fb1c93b1 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -10,6 +10,7 @@ pub enum RunMode { Module(String), InstallPip(InstallPipMode), Repl, + /// Compile files without executing them. Used for syntax/compile validation. CompileOnly(Vec), }