mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
* Add inline values flags and special-method lookup lookup_maybe_method clears AttributeError from descr_get. slot_tp_richcompare treats a miss as NotImplemented. hackcheck rejects object.__setattr__ on types. Heap types with a managed dict and itemsize 0 get INLINE_VALUES; _testinternalcapi.has_inline_values reports the valid bit. super is a base type; subclass __get__ binds the instance. __classcell__/__qualname__ may share a name with a slot. Unset member access uses the type's fully qualified name. Assisted-by: Grok:grok-4.6 * Call custom mro and descr_get for type.__new__ type.mro() computes C3 from the current bases. A metaclass other than type supplies the MRO via mro(). super() compares obj.__class__ to the instance type. slot_tp_new looks up __new__ through descr_get. is_subtype walks the stored MRO without an identity shortcut. __slots__ names are sorted so __class__ assignment matches. Assisted-by: Grok:grok-4.6 * Expose _testinternalcapi recursion and rare events get_recursion_depth feeds test.support.infinite_recursion. SPECIALIZATION_* and SHARED_KEYS_MAX_SIZE match the adaptive counters. Rare-event hooks increment on __class__/__bases__/function/builtins writes. Assisted-by: Grok:grok-4.6 * Add remaining _testinternalcapi code and locale APIs Expose get_co_localskinds, get_code_var_counts, verify_stateless_code, code_returns_only_none, normalize_path, EncodeLocaleEx, DecodeLocaleEx, and run_in_subinterp_with_config. Mark unimplemented compiler IR and specialization monitoring tests as expectedFailure. Assisted-by: Grok:grok-4.6 * Fix lint for _testinternalcapi identifiers Add CPython identifier spellings to the cspell dictionary and apply the rustfmt signature that CI rewrote. Assisted-by: Grok:grok-4.6 * Expand _testinternalcapi with remaining test APIs Add constants, compiler_cleandoc, dict_getitem_knownhash, time helpers, interpreter/frame accessors, configs, static types, a persistent hamt map, and GC/size counters used by the test suite. Assisted-by: Grok:grok-4.6 * Expose compiler IR APIs in _testinternalcapi Add InstructionSequence and compiler_codegen, optimize_cfg, and assemble_code_object. Drop expectedFailure marks on tests that pass. Assisted-by: Grok:grok-4.6 * Match optimize_cfg consts and addop locations Write optimized consts back into the caller's list, mark SETUP_* pseudo-ops as having an argument, and store addop locations in instruction-sequence field order. Drop expectedFailure marks on tests that pass. Assisted-by: Grok:grok-4.6 * Fix hamt API, SETUP_* HAS_ARG, and FOR_ITER events Honor explicit pseudo flags when generating opcode metadata so SETUP_FINALLY/CLEANUP/WITH keep HAS_ARG after regeneration. Complete the _testinternalcapi hamt mapping: identity on set/delete, richcompare, weakrefs, subscript, contains, and length-bearing iterators. Fire RAISE/STOP_ITERATION for FOR_ITER StopIteration, and skip both END_FOR and POP_ITER for the exhausted-branch destination. Assisted-by: Grok:grok-4.6 * Skip FOR_ITER RAISE for sequence iterators List/range/tuple exhaustion matches FOR_ITER_LIST/RANGE/TUPLE and must not emit a monitoring RAISE. That event stopped pdb's monitoring backend on for-over-list with Internal StopIteration. Assisted-by: Grok:grok-4.6 * Fix generator FOR_ITER events and review issues Restore settrace StopIteration for generator for-loops so test_bdb next/return cases pass, while sequence iterators still skip RAISE. Honor review notes: addop oparg bounds, linetable locations, AST instance errors, MRO subclass lock, builtin-dict notify, direct-base layout flags, and leftover _testinternalcapi args. Assisted-by: Grok:grok-4.6
434 lines
11 KiB
Python
434 lines
11 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]
|
|
#[inline]
|
|
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:
|
|
arms = "|".join(
|
|
f"Self::{opcode.rust_name}"
|
|
for opcode in self
|
|
if opcode.properties.oparg or opcode.have_argument
|
|
)
|
|
if arms:
|
|
inner = f"matches!(self, {arms})"
|
|
else:
|
|
inner = "false"
|
|
return f"""
|
|
/// Does this opcode have 'HAS_ARG_FLAG' set.
|
|
#[must_use]
|
|
pub const fn has_arg(self) -> bool {{
|
|
{inner}
|
|
}}
|
|
"""
|
|
|
|
@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]
|
|
#[inline]
|
|
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:
|
|
specialized_to_base = self.specialized_to_base
|
|
|
|
if not specialized_to_base:
|
|
inner = "None"
|
|
else:
|
|
table_type = f"super::{self.info.enum_name}"
|
|
entries = ",\n".join(
|
|
f"Some({table_type}::{specialized_to_base[name]})"
|
|
if name in specialized_to_base
|
|
else "None"
|
|
for name in self.rust_names_by_id
|
|
)
|
|
|
|
inner = f"""
|
|
const DEOPT: [Option<{table_type}>; {self.table_size}] = [
|
|
{entries}
|
|
];
|
|
|
|
DEOPT[self.as_numeric() as usize]
|
|
"""
|
|
|
|
return f"""
|
|
#[must_use]
|
|
#[inline]
|
|
pub const fn deopt(self) -> Option<Self> {{
|
|
{inner}
|
|
}}
|
|
"""
|
|
|
|
@property
|
|
def fn_cache_entries(self) -> str:
|
|
entries_by_base: dict[str, int] = {}
|
|
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:
|
|
entries_by_base[name] = size - 1
|
|
|
|
if not entries_by_base:
|
|
inner = "0"
|
|
else:
|
|
entries = ", ".join(
|
|
str(entries_by_base.get(self.resolve_deoptimized(name), 0))
|
|
for name in self.rust_names_by_id
|
|
)
|
|
|
|
inner = f"""
|
|
const CACHE_ENTRIES: [u8; {self.table_size}] = [{entries}];
|
|
|
|
CACHE_ENTRIES[self.as_numeric() as usize] as usize
|
|
"""
|
|
|
|
return f"""
|
|
#[must_use]
|
|
#[inline]
|
|
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 specialized_to_base(self) -> dict[str, str]:
|
|
"""Maps a specialized opcode's name to its family's base opcode name."""
|
|
res = {}
|
|
for target, specialized in self.info.deopts.items():
|
|
for name in specialized:
|
|
res[name] = target
|
|
|
|
return res
|
|
|
|
@property
|
|
def instrumented_to_base(self) -> dict[str, str]:
|
|
"""Maps an instrumented opcode's name to its base opcode name."""
|
|
return {iname: name for name, iname in self.instrumented_mapping.items()}
|
|
|
|
def resolve_deoptimized(self, name: str) -> str:
|
|
"""
|
|
Mirrors `deoptimize`: resolves a specialized opcode to its family's
|
|
base, an instrumented opcode to its base, or returns the name
|
|
unchanged.
|
|
"""
|
|
if name in self.specialized_to_base:
|
|
return self.specialized_to_base[name]
|
|
|
|
return self.instrumented_to_base.get(name, name)
|
|
|
|
@property
|
|
def table_size(self) -> int:
|
|
return {"u8": 256, "u16": 65536}[self.size]
|
|
|
|
@property
|
|
def rust_names_by_id(self) -> list[str | None]:
|
|
"""The opcode name at each numeric id, `None` where no opcode is assigned."""
|
|
names_by_id = {opcode.id: opcode.rust_name for opcode in self}
|
|
return [names_by_id.get(i) for i in range(self.table_size)]
|
|
|
|
@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()
|