mirror of
https://github.com/RustPython/RustPython.git
synced 2026-06-02 19:39:49 +09:00
* Share marshal ref table between code object and its internals
read_marshal_bytes, _str, _str_vec, _name_tuple, and _const_tuple now
take a shared ref table and resolve TYPE_REF / register FLAG_REF
entries. deserialize_code is split into a public wrapper and an inner
function that receives the ref table; deserialize_value_depth opens a
fresh inner ref space when it hits Type::Code, mirroring CPython's
behaviour of putting the code object itself at ref slot 0. Nested code
objects inside const tuples reuse the surrounding code's ref space via
the new read_const_value helper.
* Align PYC magic number, FORMAT_VERSION, and header check with CPython 3.14
PYC_MAGIC_NUMBER changes from 2994 to 3627, matching CPython 3.14's
pyc_magic_number_token (0x0a0d0e2b). marshal FORMAT_VERSION drops from
5 to 4 (the encoder/marshal.version value; the decoder already accepts
both). check_pyc_magic_number_bytes now compares all four magic bytes
instead of the first two.
* Accept CPython-tagged .pyc as read-only bytecode source
SourceFileLoader.get_code now also looks for .pyc files using
_RP_FALLBACK_CACHE_TAGS (currently ('cpython-314',)) in addition to
sys.implementation.cache_tag. The matched .pyc is only used for
reading; recompilation still writes to the RustPython-tagged path, so
CPython's .pyc is never overwritten. Source-stat / hash / timestamp
validation logic is unchanged.
* Apply rustfmt to marshal helpers
* Marshal PySlice from format version 4 instead of 5
CPython's marshal supports TYPE_SLICE from format version 4 onwards
and that is the default version. Rejecting slice dumps below version
5 made marshal.dumps(slice(...)) fail with the default version and
broke test.test_marshal.SliceTestCase.test_slice.
* Revert "Accept CPython-tagged .pyc as read-only bytecode source"
Lib/importlib/_bootstrap_external.py is CPython's own code copied
verbatim; local patches here defeat compatibility tracking. The
cpython-XX cache_tag fallback needs to live on the RustPython side
(Rust code or sys.implementation.cache_tag policy), not as edits to
the imported standard library.
This reverts commit 1fc426d0fb5fcdb50d35cad13bbb43e8f6ce1c7f.
* Set marshal FORMAT_VERSION to 5 to match CPython 3.14.5
Py_MARSHAL_VERSION is 5 in CPython 3.14.5 (Include/marshal.h:16) and
TYPE_SLICE serialization rejects version < 5 (Python/marshal.c:720).
Restore the same threshold and constant so marshal.version and the
slice-marshal gate match CPython.
* Thread marshal recursion depth through nested code objects
Code objects embedded in const-tuples reset the depth budget on each
recursion, so a hostile or pathological marshal stream of code-in-tuple-
in-code can blow the stack despite MAX_MARSHAL_STACK_DEPTH. Pass the
current depth through deserialize_code_inner and read_marshal_const_tuple
and decrement at each code-object/tuple boundary.
Also route dict keys through deserialize_value_after_header so TYPE_CODE
keys decode instead of failing with BadType.
* align compiler to CPython
* Align codegen with CPython compile.c
Rename CFG helpers and accessors to the names used in CPython's
compile.c (basicblock_next_instr, basicblock_last_instr,
basicblock_append_instructions, bb_has_fallthrough, is_jump,
make_cfg_traversal_stack, mark_warm/mark_cold, etc.). Drop the unused
boolop-folding gate, mark_cpython_cfg_label_block helper, and
ComprehensionLoopControl::iter_range field.
Track an is_coroutine flag on SymbolTable, set in async def, await, and
async comprehensions, and propagate it through non-generator
comprehensions per symtable_handle_comprehension().
Mark SetupCleanup/SetupFinally/SetupWith as has_arg pseudo-ops, mark
ForIter as a terminator, and add has_arg/has_const on AnyInstruction.
Fix Instruction::stack_effect_jump to delegate to the opcode's
stack_effect_jump rather than stack_effect.
* Align codegen IR with CPython CFG structures
* Match CPython CFG annotation offset arithmetic
* Propagate CPython CFG label translation errors
* Align CPython exception target labeling flow
* Propagate CPython CFG traversal stack allocation errors
* Match CPython optimize_load_fast allocation flow
* Propagate CPython basicblock allocation errors
* Propagate CPython redundant NOP cleanup errors
* Propagate CPython unused const cleanup errors
* Propagate CPython const folding errors
* Propagate CPython swaptimize allocation errors
* Match CPython list-to-tuple fold allocation
* Skip const folding on CPython allocation failures
* Skip subscript folding on CPython allocation failures
* Propagate CPython assembler allocation errors
* Propagate CPython localsplus allocation errors
* Propagate CPython localsplus setup allocation errors
* Propagate CPython jump label map allocation errors
* Propagate CPython instruction sequence allocation errors
* Propagate CPython instruction label allocation errors
* Guard CPython codegen block allocation
* Guard CPython label shadow allocation
* Propagate CPython label shadow allocation errors
* Align CPython c-array allocation updates
* Align CPython ref stack growth
* Match CPython CFG builder debug check
* Avoid Rust-only CFG append clone
* Reuse CPython cleared block slots
* Use CPython block append in copy_basicblock
* Match CPython cfg builder creation order
* Clear label map after CPython apply pass
* Match CPython cfg builder allocation check
* Propagate CPython c-array size errors
* Drop Rust-only label uniqueness check
* Drop Rust-only label shadow debug checks
* Propagate CFG block index overflow
* Propagate CFG label oparg overflow
* Model CPython basicblock instruction storage
* Drop Rust-only recorded CFG precheck
* Model CPython instruction sequence storage
* Propagate instruction sequence offset overflow
* Model CPython instruction sequence labels
* Match CPython jump offset arithmetic
* Match CPython exception table arithmetic
* Match CPython label index arithmetic
* Match CPython instruction offset casts
* Match CPython jump offset indexing
* Match CPython oparg locals casts
* Match CPython localsplus offset arithmetic
* Match CPython cell prefix indexing
* Match CPython C array growth arithmetic
* Match CPython label map allocation arithmetic
* Match CPython label map size tracking
* Use CPython label map size in sequence passes
* Assert CPython label map clearing invariants
* Match CPython label oparg assignment
* Match CPython compiler direct arithmetic
* Match CPython load fast local casts
* Match CPython load fast depth assert
* Match CPython resume depth flagging
* Match CPython stack depth arithmetic
* Drop Rust-only stack overflow error
* Return CPython stack depth directly
* Match CPython C array growth errors
* Match CPython instruction insert asserts
* Match CPython unreachable pseudo jump
* Match CPython CFG size guard
* Match CPython superinstruction assert
* Match CPython redundant jump assert
* Match CPython stackdepth errors
* Match CPython jump offset flow
* Match CPython assembler buffer defaults
* Match CPython bytecode emit growth
* Match CPython assembler entry growth
* Match CPython assembler growth overflow check
* Match CPython remove_unreachable structure
* Match CPython static swap flow
* Inline CPython code unit preprocessing
* Match CPython C array growth checks
* Match CPython label map size guard
* Match CPython load-fast flow
* Simplify CPython CFG condition flow
* Align exception fallthrough propagation
* Match CPython pseudo target table
* Match CPython annotations CFG assert
* Match CPython inverted op assert
* Match CPython many-locals guard
* Reject deopt opcodes in CFG stack effects
* Match CPython invalid stack effect error
* Test CPython deopt stack effect guard
* Match CPython load-fast extended-arg assert
* Match CPython instruction allocation asserts
* Match CPython basicblock last-instr asserts
* Match CPython opcode range asserts
* Assert CPython fallthrough line propagation invariant
* Assert CPython CFG target offset sign
* Assert CPython exception fallthrough invariant
* Assert CPython exception stack bounds
* Assert CPython traversal stack allocation
* Match CPython label-map allocation in shadow
* Mirror CPython label-map sentinel fill
* Match CPython CFG builder allocation asserts
* Match CPython exception stack structure
* Match CPython ref stack structure
* Match CPython CFG traversal stack structure
* Mirror CPython CFG traversal stack pointer
* Use CPython fixed exception handler stack
* Mirror CPython ref stack capacity field
* Match CPython swap optimizer scratch stack
* Align static swap helpers with CPython blocks
* Align swaptimize signature with CPython
* Match CPython redundant pair pass result
* Match CPython inline pass result
* Match CPython redundant NOP pass results
* Fix bytecode metadata after upstream rebase
* Match CPython opcode stack metadata
* Add CPython identifiers to cspell dictionary
Add CNOTAB, LNOTAB, ialloc, ioffset, iused, nblocks, ncellsused,
ncellvars, nextop, noffsets, nvars, swaptimize, untargeted to
.cspell.dict/cpython.txt for the new CFG/assembler code in
crates/codegen/src/ir.rs.
* Fix CI failures from bytecode-parity work
- clippy: drop redundant `test_` prefix on three test functions and
remove an unnecessary `u32` cast in basicblock_clear_reuses_cpython_spare_slots_in_offset_order
- insta: regenerate nested_double_async_with snapshot to match the new
CFG output that drops unreferenced labels after the redundant-NOP pass
- regrtest: drop `@expectedFailure` markers from test_func_args,
test_meth_args (test_compile), test_disassemble_with,
test_disassemble_try_finally (test_dis), and test_except_star
(test_monitoring) which now pass
* Resync generated opcode metadata
Empty conf.toml since WithExceptStart and Setup{Cleanup,Finally,With}
stack effects already match CPython, so the TODO override entries are
stale and only cause CI hook diffs.
Regenerate opcode_metadata.rs and drop the matching SetupCleanup/
SetupFinally/SetupWith assertions on PseudoOpcode::has_arg(); their
`HAS_ARG` flag comes from pseudo definitions in bytecodes.c that the
upstream analyzer does not propagate through PseudoInstruction.properties,
so the generated has_arg() excludes them. has_target() still covers
these block-push pseudos via is_block_push().
* Drop is_block_push has_arg invariant
The CPython invariant `assert(OPCODE_HAS_ARG(op) || !IS_BLOCK_PUSH(op))`
relies on SETUP_{FINALLY,CLEANUP,WITH} carrying `HAS_ARG_FLAG` in
CPython's metadata. The autogen tool reads pseudo-opcode properties from
target instructions and does not propagate the pseudo's own
HAS_ARG flag, so PseudoOpcode::has_arg() omits these three opcodes.
Drop the debug_assert that fired inside py_freeze proc-macro expansion.
* Auto-generate has_eval_break and route AnyInstruction has_arg/has_const via macro
Add fn_has_eval_break to generate_rs_opcode_metadata.py using CPython's
Properties.eval_breaker, removing the hand-written matches! body for
Opcode::has_eval_break and PseudoOpcode::has_eval_break.
Forward has_arg/has_const from Instruction and PseudoInstruction to
their opcode, so AnyInstruction can use either_real_pseudo! like the
other has_* accessors instead of an open-coded match.
371 lines
9.0 KiB
Python
371 lines
9.0 KiB
Python
#!/usr/bin/env python
|
|
from __future__ import annotations
|
|
|
|
import collections
|
|
import dataclasses
|
|
import io
|
|
import os
|
|
import pathlib
|
|
import subprocess
|
|
import sys
|
|
import typing
|
|
|
|
import tomllib
|
|
from cpython import Analysis, get_analysis, get_stack_effect
|
|
from opcodes import OpcodeInfo
|
|
from utils import DEFAULT_INPUT, ROOT, get_conf, to_pascal_case
|
|
|
|
OUT_FILE = ROOT / "crates/compiler-core/src/bytecode/opcode_metadata.rs"
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
class OpcodeGen:
|
|
info: OpcodeDef
|
|
|
|
@property
|
|
def fn_as_info_size(self) -> str:
|
|
return f"""
|
|
/// Returns [`Self`] as [`{self.size}`].
|
|
#[must_use]
|
|
pub const fn as_{self.size}(self) -> {self.size} {{
|
|
self.as_numeric()
|
|
}}
|
|
"""
|
|
|
|
@property
|
|
def fn_try_from_numeric(self) -> str:
|
|
return f"""
|
|
pub const fn try_from_{self.size}(
|
|
value: {self.size},
|
|
) -> Result<Self, MarshalError> {{
|
|
Self::try_from_numeric(value)
|
|
}}
|
|
"""
|
|
|
|
@property
|
|
def fn_has_arg(self) -> str:
|
|
return self.gen_fn_has_attr("has_arg", "oparg", "HAS_ARG_FLAG")
|
|
|
|
@property
|
|
def fn_has_const(self) -> str:
|
|
return self.gen_fn_has_attr("has_const", "uses_co_consts", "HAS_CONST_FLAG")
|
|
|
|
@property
|
|
def fn_has_name(self) -> str:
|
|
return self.gen_fn_has_attr("has_name", "uses_co_names", "HAS_NAME_FLAG")
|
|
|
|
@property
|
|
def fn_has_jump(self) -> str:
|
|
return self.gen_fn_has_attr("has_jump", "jumps", "HAS_JUMP_FLAG")
|
|
|
|
@property
|
|
def fn_has_free(self) -> str:
|
|
return self.gen_fn_has_attr("has_free", "has_free", "HAS_FREE_FLAG")
|
|
|
|
@property
|
|
def fn_has_local(self) -> str:
|
|
return self.gen_fn_has_attr("has_local", "uses_locals", "HAS_LOCAL_FLAG")
|
|
|
|
@property
|
|
def fn_has_eval_break(self) -> str:
|
|
return self.gen_fn_has_attr(
|
|
"has_eval_break", "eval_breaker", "HAS_EVAL_BREAK_FLAG"
|
|
)
|
|
|
|
@property
|
|
def fn_is_instrumented(self) -> str:
|
|
arms = "|".join(
|
|
f"Self::{opcode.rust_name}" for opcode in self if opcode.is_instrumented
|
|
)
|
|
|
|
arms = arms.strip()
|
|
if arms:
|
|
inner = f"matches!(self, {arms})"
|
|
else:
|
|
inner = "false"
|
|
|
|
return f"""
|
|
#[must_use]
|
|
pub const fn is_instrumented(self) -> bool {{
|
|
{inner}
|
|
}}
|
|
"""
|
|
|
|
@property
|
|
def fn_to_base(self) -> str:
|
|
arms = ",\n".join(
|
|
f"Self::{iname} => Self::{name}"
|
|
for name, iname in self.instrumented_mapping.items()
|
|
)
|
|
|
|
arms = arms.strip()
|
|
if not arms:
|
|
inner = "None"
|
|
else:
|
|
inner = f"""
|
|
Some(match self {{
|
|
{arms},
|
|
_ => return None,
|
|
|
|
}})
|
|
"""
|
|
|
|
return f"""
|
|
#[must_use]
|
|
pub const fn to_base(self) -> Option<Self> {{
|
|
{inner}
|
|
}}
|
|
"""
|
|
|
|
@property
|
|
def fn_to_instrumented(self) -> str:
|
|
arms = ",\n".join(
|
|
f"Self::{name} => Self::{iname}"
|
|
for name, iname in self.instrumented_mapping.items()
|
|
)
|
|
|
|
arms = arms.strip()
|
|
if not arms:
|
|
inner = "None"
|
|
else:
|
|
inner = f"""
|
|
Some(match self {{
|
|
{arms},
|
|
_ => return None,
|
|
|
|
}})
|
|
"""
|
|
|
|
return f"""
|
|
#[must_use]
|
|
pub const fn to_instrumented(self) -> Option<Self> {{
|
|
{inner}
|
|
}}
|
|
"""
|
|
|
|
@property
|
|
def fn_deopt(self) -> str:
|
|
arms = ""
|
|
for target, specialized in self.info.deopts.items():
|
|
ops = "|".join(f"Self::{op}" for op in specialized)
|
|
arms += f"{ops} => Self::{target},\n"
|
|
|
|
arms = arms.strip()
|
|
|
|
if not arms:
|
|
inner = "None"
|
|
else:
|
|
inner = f"""
|
|
Some(match self {{
|
|
{arms}
|
|
_ => return None,
|
|
}})
|
|
"""
|
|
|
|
return f"""
|
|
#[must_use]
|
|
pub const fn deopt(self) -> Option<Self> {{
|
|
{inner}
|
|
}}
|
|
"""
|
|
|
|
@property
|
|
def fn_cache_entries(self) -> str:
|
|
arms = ""
|
|
for opcode in self:
|
|
name = opcode.rust_name
|
|
if opcode.is_instrumented:
|
|
continue
|
|
if getattr(opcode, "family", None) and (opcode.family.name != name):
|
|
continue
|
|
|
|
try:
|
|
size = opcode.cache_entry
|
|
except AttributeError:
|
|
continue
|
|
|
|
if size > 1:
|
|
arms += f"Self::{name} => {size - 1},\n"
|
|
|
|
arms = arms.strip()
|
|
if not arms:
|
|
inner = "0"
|
|
else:
|
|
inner = f"""
|
|
match self.deoptimize() {{
|
|
{arms}
|
|
_ => 0,
|
|
}}
|
|
"""
|
|
|
|
return f"""
|
|
#[must_use]
|
|
pub const fn cache_entries(self) -> usize {{
|
|
{inner}
|
|
}}
|
|
"""
|
|
|
|
@property
|
|
def fn_stack_effect_info(self) -> str:
|
|
oparg_used = False
|
|
arms = ""
|
|
for opcode in self:
|
|
name = opcode.rust_name
|
|
|
|
popped = opcode.stack_effect_popped
|
|
pushed = opcode.stack_effect_pushed
|
|
|
|
pushed_comment = ""
|
|
popped_comment = ""
|
|
|
|
if popped != opcode.cpy_popped:
|
|
popped_comment = f"// TODO: Differs from CPython `{opcode.cpy_popped}`"
|
|
|
|
if pushed != opcode.cpy_pushed:
|
|
pushed_comment = f"// TODO: Differs from CPython `{opcode.cpy_pushed}`"
|
|
|
|
oparg_used = oparg_used or any("oparg" in expr for expr in (pushed, popped))
|
|
|
|
arms += f"""
|
|
Self::{name} => (
|
|
{pushed}, {pushed_comment}
|
|
{popped}, {popped_comment}
|
|
),
|
|
""".strip()
|
|
|
|
arms = arms.strip()
|
|
|
|
oparg_arg = "_oparg"
|
|
oparg_cast = ""
|
|
if oparg_used:
|
|
oparg_arg = "oparg"
|
|
oparg_cast = f"""
|
|
// Reason for converting {oparg_arg} to i32 is because of expressions like `1 + (oparg -1)`
|
|
// that causes underflow errors.
|
|
let oparg = i32::try_from({oparg_arg}).expect("{oparg_arg} does not fit in an `i32`");
|
|
"""
|
|
|
|
return f"""
|
|
#[must_use]
|
|
pub fn stack_effect_info(&self, {oparg_arg}: u32) -> StackEffect {{
|
|
{oparg_cast}
|
|
|
|
let (pushed, popped) = match self {{
|
|
{arms}
|
|
}};
|
|
|
|
debug_assert!(u32::try_from(pushed).is_ok());
|
|
debug_assert!(u32::try_from(popped).is_ok());
|
|
|
|
StackEffect::new(pushed as u32, popped as u32)
|
|
}}
|
|
"""
|
|
|
|
def gen(self) -> str:
|
|
methods = "\n\n".join(
|
|
getattr(self, attr).strip()
|
|
for attr in sorted(dir(self))
|
|
if attr.startswith("fn_")
|
|
)
|
|
|
|
impls = "\n\n".join(
|
|
getattr(self, attr).strip()
|
|
for attr in sorted(dir(self))
|
|
if attr.startswith("impl_")
|
|
)
|
|
|
|
return f"""
|
|
impl super::{self.info.enum_name} {{
|
|
{methods}
|
|
}}
|
|
|
|
{impls}
|
|
"""
|
|
|
|
def gen_fn_has_attr(self, fn_name: str, properties_attr: str, doc_flag: str) -> str:
|
|
arms = "|".join(
|
|
f"Self::{opcode.rust_name}"
|
|
for opcode in self
|
|
if getattr(opcode.properties, properties_attr)
|
|
)
|
|
|
|
if arms:
|
|
inner = f"matches!(self, {arms})"
|
|
else:
|
|
inner = "false"
|
|
|
|
return f"""
|
|
/// Does this opcode have '{doc_flag}' set.
|
|
#[must_use]
|
|
pub const fn {fn_name}(self) -> bool {{
|
|
{inner}
|
|
}}
|
|
"""
|
|
|
|
@property
|
|
def instrumented_mapping(self) -> dict[str, str]:
|
|
names, inames = set(), set()
|
|
for opcode in self:
|
|
name = opcode.rust_name
|
|
if opcode.is_instrumented:
|
|
inames.add(name)
|
|
else:
|
|
names.add(name)
|
|
|
|
res = {}
|
|
for iname in sorted(inames):
|
|
name = iname.removeprefix("Instrumented")
|
|
if name not in names:
|
|
continue
|
|
|
|
res[name] = iname
|
|
|
|
return res
|
|
|
|
@property
|
|
def size(self) -> str:
|
|
return self.info.size
|
|
|
|
def __iter__(self):
|
|
yield from self.info.opcodes
|
|
|
|
|
|
def rustfmt(code: str) -> str:
|
|
return subprocess.check_output(["rustfmt", "--emit=stdout"], input=code, text=True)
|
|
|
|
|
|
def main():
|
|
override_conf = get_conf()
|
|
inp = DEFAULT_INPUT.read_text()
|
|
opcode_infos = OpcodeInfo.iter_infos(inp, override_conf)
|
|
|
|
outfile = io.StringIO()
|
|
|
|
for info in opcode_infos:
|
|
gen = OpcodeGen(info).gen()
|
|
outfile.write(gen)
|
|
|
|
generated = outfile.getvalue()
|
|
|
|
script_path = pathlib.Path(__file__).resolve().relative_to(ROOT).as_posix()
|
|
|
|
output = rustfmt(
|
|
f"""
|
|
// This file is generated by {script_path}
|
|
// Do not edit!
|
|
|
|
use crate::{{
|
|
bytecode::instruction::StackEffect,
|
|
marshal::MarshalError,
|
|
}};
|
|
|
|
{generated}
|
|
"""
|
|
)
|
|
|
|
OUT_FILE.write_text(output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|