Match CPython wording for compile() optimize and flags validation (#7765)

CPython's `builtin_compile_impl` (Python/bltinmodule.c) accepts only
optimize ∈ {-1, 0, 1, 2}; anything else raises
`ValueError("compile(): invalid optimize value")`. The previous logic
only validated via `i32::try_into::<u8>()`, which silently accepts every
value in [0, 255], so `compile(..., optimize=3)`, `optimize=99`, etc.
were silently truncated to a u8. The error wording also had the wrong
word order.

Replace the cast-based check with a `match` against the spec range.

Adjacent: the unrecognised-flags message used American spelling
("unrecognized") and missed the colon separator. CPython uses British
"unrecognised" with a colon — match it.

Verified byte-identical with CPython 3.14.4 across 12 boundary values
for optimize and 4 cases for flags. Preserves the existing OverflowError
path for `optimize=1 << 1000` (raised at the ArgPrimitiveIndex<i32>
conversion layer, before this check).
This commit is contained in:
Changjoon authored and GitHub committed 2026-05-04 10:26:16 +09:00
1 parent 323a45ef07
commit 206b59a791
2 files changed
+51 -7

No files matched your search

+5 -7
View File
@@ -268,12 +268,10 @@ mod builtins {
let mode_str = args.mode.as_str();
let optimize: i32 = args.optimize.map_or(-1, |v| v.value);
let optimize: u8 = if optimize == -1 {
vm.state.config.settings.optimize
} else {
optimize
.try_into()
.map_err(|_| vm.new_value_error("compile() optimize value invalid"))?
let optimize: u8 = match optimize {
-1 => vm.state.config.settings.optimize,
0..=2 => optimize as u8,
_ => return Err(vm.new_value_error("compile(): invalid optimize value")),
};
if args
@@ -348,7 +346,7 @@ mod builtins {
let flags: i32 = args.flags.map_or(0, |v| v.value);
if !(flags & !_ast::PY_COMPILE_FLAGS_MASK).is_zero() {
return Err(vm.new_value_error("compile() unrecognized flags"));
return Err(vm.new_value_error("compile(): unrecognised flags"));
}
let allow_incomplete = !(flags & _ast::PY_CF_ALLOW_INCOMPLETE_INPUT).is_zero();
+46
View File
@@ -0,0 +1,46 @@
from testutils import assert_raises
# compile() basic mode acceptance
assert isinstance(
compile("x = 1", "<test>", "exec"), type(compile("", "<test>", "exec"))
)
assert compile("1 + 1", "<test>", "eval") is not None
assert compile("1", "<test>", "single") is not None
# `optimize` accepts -1 (use config default), 0, 1, 2 only.
# Anything else raises ValueError with CPython's exact wording.
for ok in (-1, 0, 1, 2):
compile("x = 1", "<test>", "exec", optimize=ok)
def _check_optimize_error(value):
try:
compile("x = 1", "<test>", "exec", optimize=value)
except ValueError as e:
assert str(e) == "compile(): invalid optimize value", repr(e)
else:
raise AssertionError(f"expected ValueError for optimize={value!r}")
for bad in (3, 4, 99, 255, 256, 1000, -2, -99, -128):
_check_optimize_error(bad)
# Huge `optimize` values raise OverflowError during argument conversion,
# not ValueError. The exact wording differs from CPython here (Rust i32
# vs C int) — checking the type only, matching test_compile.py.
assert_raises(OverflowError, compile, "x = 1", "<test>", "exec", optimize=1 << 1000)
# Unrecognised `flags` bits raise ValueError. CPython uses British spelling
# ("unrecognised") so the message must match exactly.
def _check_flags_error(flags):
try:
compile("x = 1", "<test>", "exec", flags=flags)
except ValueError as e:
assert str(e) == "compile(): unrecognised flags", repr(e)
else:
raise AssertionError(f"expected ValueError for flags={flags!r}")
_check_flags_error(99999)
_check_flags_error(0x10000)