mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
* Align compiler and VM with final CPython 3.15 bytecode Reconstruct the compiler-only baseline from validated487ae47 on the current main and frozen mapping prerequisitec78d811. Preserve the full source delta while dropping obsolete prerequisite ancestry; detailed old-to-new provenance is retained in the recovery artifact. Assisted-by: OpenAI Codex: model identifier unavailable * Fix final script validation, exception cleanup and private cache contracts Replay reviewed compiler CI repairs2191a9d,1f1fe04,a2dacafon the frozen current prerequisite. Intermediate compile/diagnostic corrections are combined where they address the same failure. Assisted-by: OpenAI Codex: model identifier unavailable * Align canonical compiler fixtures and generator frame state Replay reviewed compiler CI repairs476bb74on the frozen current prerequisite. Intermediate compile/diagnostic corrections are combined where they address the same failure. Assisted-by: OpenAI Codex: model identifier unavailable * Preserve code-name owners and final warning and constant-fold contracts Replay reviewed compiler CI repairs6482adfon the frozen current prerequisite. Intermediate compile/diagnostic corrections are combined where they address the same failure. Assisted-by: OpenAI Codex: model identifier unavailable * Skip unavailable monitoring C API tests before tool registration Replay reviewed compiler CI repairs246c309on the frozen current prerequisite. Intermediate compile/diagnostic corrections are combined where they address the same failure. Assisted-by: OpenAI Codex: model identifier unavailable * Assert exact final constant-list iterable folding Replay reviewed compiler CI repairs7ecedcfon the frozen current prerequisite. Intermediate compile/diagnostic corrections are combined where they address the same failure. Assisted-by: OpenAI Codex: model identifier unavailable * Align local monitoring events and exception lifecycle state with final 3.15 Replay reviewed compiler CI repairsec206b1,191fe58on the frozen current prerequisite. Intermediate compile/diagnostic corrections are combined where they address the same failure. Assisted-by: OpenAI Codex: model identifier unavailable * Align 3.15 code objects and codegen with CPython - Store co_consts, co_names, co_localsplusnames, co_localspluskinds, co_linetable, co_exceptiontable and co_name as objects on PyCode; build co_code and the varnames/cellvars/freevars tuples lazily in _co_cached, with co_code deoptimized through deopt_code. - Move the compiler constant cache (_PyCode_ConstantKey, const_cache_insert) to builtins/code/const_cache.rs. - Write every code field with w_object in marshal and pass the read field objects to MarshalBag::make_code through CodeFields. - Emit RETURN_GENERATOR/POP_TOP in enter_scope and place SETUP_CLEANUP after them; drop the generator part of insert_prefix_instructions. - Add PyCode::first_traceable (_co_firsttraceable) for _PyFrame_IsIncomplete and sys.monitoring. - Follow the CPython DISABLE branches in sys.monitoring, the check_matched signature in warnings, the 3.15 % format error messages for str and bytes, code_returns_only_none, and the FOR_ITER stack analysis. - Compute IMPORT_NAME flags with in_exception_handler. - Replace the linetable/exceptiontable byte tests in codegen with dis(show_positions=True) insta snapshots in _opcode. - Remove the opcode metadata verification tooling, byte-exact scripts, private cache tests and redundant snippets. - Restore test_code.py and test_monitoring.py to v3.15.0 plus RUSTPYTHON markers. Assisted-by: Claude Code:claude-opus-5-5 * Remove generator snippet checks covered by Lib/test Drop extra_tests/snippets/generator_iterator_prefix.py. Its loop, delegation, async, f_back and never-awaited checks are covered by test_generators, test_coroutines, test_asyncgen, test_yield_from and test_monitoring. Move the remaining checks into existing files: genexpr prefix tracing and monitoring into stdlib_sys.py, and the caller frame seen from the eager iterator into stdlib_sys_getframe.py. Assisted-by: Claude Code:claude-opus-5-5 * Emit the generator prefix for top-level await modules Module code compiled with PyCF_ALLOW_TOP_LEVEL_AWAIT and marked ste_coroutine now starts with RETURN_GENERATOR; POP_TOP. The prefix and RESUME emission move into emit_scope_prefix, shared by enter_scope and the module entry points, as in codegen_enter_scope. Fixes test_builtin.test_compile_top_level_await. Assisted-by: Claude Code:claude-opus-5-5 * Allow the c_long cast in _opcode.stack_effect c_long is i32 on Windows, where clippy reports the cast to i32 as unnecessary. Assisted-by: Claude Code:claude-opus-5-5 * Update malachite crates to 0.14.0 pymath requires malachite-bigint ^0, so the example projects, which have no lock file, resolve it to 0.14.0 while the workspace pinned 0.13.0, and fail to build. Assisted-by: Claude Code:claude-opus-5-5 --------- Co-authored-by: dot <dot@youknowone.org>
363 lines
15 KiB
Rust
363 lines
15 KiB
Rust
use core::ops::ControlFlow;
|
|
use rustpython_compiler_core::bytecode::{
|
|
CodeObject, CommonConstant, ConstantData, Constants, Instruction, OpArg, OpArgState,
|
|
};
|
|
use rustpython_jit::{CompiledCode, JitType};
|
|
use rustpython_wtf8::{Wtf8, Wtf8Buf};
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct Function {
|
|
code: Box<CodeObject>,
|
|
annotations: HashMap<Wtf8Buf, StackValue>,
|
|
}
|
|
|
|
impl Function {
|
|
pub(crate) fn compile(self) -> CompiledCode {
|
|
let mut arg_types = Vec::new();
|
|
for arg in self.code.arg_names().args {
|
|
let arg_type = match self.annotations.get(AsRef::<Wtf8>::as_ref(arg.as_str())) {
|
|
Some(StackValue::String(annotation)) => match annotation.as_str() {
|
|
"int" => JitType::Int,
|
|
"float" => JitType::Float,
|
|
"bool" => JitType::Bool,
|
|
_ => panic!("Unrecognised jit type"),
|
|
},
|
|
_ => panic!("Argument have annotation"),
|
|
};
|
|
arg_types.push(arg_type);
|
|
}
|
|
|
|
let ret_type = match self.annotations.get(AsRef::<Wtf8>::as_ref("return")) {
|
|
Some(StackValue::String(annotation)) => match annotation.as_str() {
|
|
"int" => Some(JitType::Int),
|
|
"float" => Some(JitType::Float),
|
|
"bool" => Some(JitType::Bool),
|
|
_ => panic!("Unrecognised jit type"),
|
|
},
|
|
_ => None,
|
|
};
|
|
|
|
rustpython_jit::compile(&self.code, &arg_types, ret_type).expect("Compile failure")
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Debug, Clone)]
|
|
enum StackValue {
|
|
String(String),
|
|
None,
|
|
Map(HashMap<Wtf8Buf, Self>),
|
|
Code(Box<CodeObject>),
|
|
Function(Function),
|
|
Slice(Box<[Self; 3]>),
|
|
Frozenset(Vec<Self>),
|
|
}
|
|
|
|
impl From<ConstantData> for StackValue {
|
|
fn from(value: ConstantData) -> Self {
|
|
match value {
|
|
ConstantData::Str { value } => {
|
|
Self::String(value.into_string().expect("surrogate in test code"))
|
|
}
|
|
ConstantData::None => Self::None,
|
|
ConstantData::Code { code } => Self::Code(code),
|
|
ConstantData::Slice { elements } => {
|
|
let [start, stop, step] = *elements;
|
|
Self::Slice(Box::new([start.into(), stop.into(), step.into()]))
|
|
}
|
|
ConstantData::Frozenset { elements } => {
|
|
Self::Frozenset(elements.into_iter().map(Into::into).collect())
|
|
}
|
|
c => unimplemented!("constant {:?} isn't yet supported in py_function!", c),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Extract annotations from an annotate function's bytecode.
|
|
/// The annotate function uses BUILD_MAP with key-value pairs loaded before it.
|
|
/// Keys are parameter names (from LOAD_CONST), values are type names (from LOAD_NAME/LOAD_GLOBAL).
|
|
fn extract_annotations_from_annotate_code(code: &CodeObject) -> HashMap<Wtf8Buf, StackValue> {
|
|
let mut annotations = HashMap::new();
|
|
let mut stack: Vec<(bool, usize)> = Vec::new(); // (is_const, index)
|
|
let mut op_arg_state = OpArgState::default();
|
|
|
|
for &word in code.instructions.iter() {
|
|
let (instruction, arg) = op_arg_state.get(word);
|
|
|
|
match instruction {
|
|
Instruction::LoadConst { consti } => {
|
|
stack.push((true, consti.get(arg).as_usize()));
|
|
}
|
|
Instruction::LoadName { namei } => {
|
|
stack.push((false, namei.get(arg) as usize));
|
|
}
|
|
Instruction::LoadGlobal { namei } => {
|
|
stack.push((false, (namei.get(arg) >> 1) as usize));
|
|
}
|
|
Instruction::BuildMap { count } => {
|
|
let count = count.get(arg) as usize;
|
|
// Stack has key-value pairs in order: k1, v1, k2, v2, ...
|
|
// So we need count * 2 items from the stack
|
|
let start = stack.len().saturating_sub(count * 2);
|
|
let pairs: Vec<_> = stack.drain(start..).collect();
|
|
|
|
for chunk in pairs.chunks(2) {
|
|
if chunk.len() == 2 {
|
|
let (key_is_const, key_idx) = chunk[0];
|
|
let (val_is_const, val_idx) = chunk[1];
|
|
|
|
// Key should be a const string (parameter name)
|
|
if key_is_const
|
|
&& let ConstantData::Str { value } =
|
|
&code.constants[(key_idx as u32).into()]
|
|
{
|
|
let param_name = value;
|
|
// Value can be a name (type ref) or a const string (forward ref)
|
|
let type_name = if val_is_const {
|
|
match code.constants.get(val_idx) {
|
|
Some(ConstantData::Str { value }) => {
|
|
value.as_str().map_or_else(
|
|
|_| value.to_string_lossy().into_owned(),
|
|
|s| s.to_owned(),
|
|
)
|
|
}
|
|
Some(other) => panic!(
|
|
"Unsupported annotation const for '{param_name:?}' at idx {val_idx}: {other:?}"
|
|
),
|
|
None => panic!(
|
|
"Annotation const idx out of bounds for '{:?}': {} (len={})",
|
|
param_name,
|
|
val_idx,
|
|
code.constants.len()
|
|
),
|
|
}
|
|
} else {
|
|
match code.names.get(val_idx) {
|
|
Some(name) => name.clone(),
|
|
None => panic!(
|
|
"Annotation name idx out of bounds for '{:?}': {} (len={})",
|
|
param_name,
|
|
val_idx,
|
|
code.names.len()
|
|
),
|
|
}
|
|
};
|
|
annotations.insert(param_name.clone(), StackValue::String(type_name));
|
|
}
|
|
}
|
|
}
|
|
// Return after processing BUILD_MAP - we got our annotations
|
|
return annotations;
|
|
}
|
|
Instruction::Resume { .. }
|
|
| Instruction::LoadFast { .. }
|
|
| Instruction::CompareOp { .. }
|
|
| Instruction::ExtendedArg
|
|
| Instruction::Cache
|
|
| Instruction::NotTaken => {
|
|
// Ignore these instructions for annotation extraction
|
|
}
|
|
Instruction::ReturnValue => {
|
|
// End of function - return what we have
|
|
return annotations;
|
|
}
|
|
_ => {
|
|
// For other instructions, clear the stack tracking as we don't understand the effect
|
|
stack.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
annotations
|
|
}
|
|
|
|
pub(crate) struct StackMachine {
|
|
stack: Vec<StackValue>,
|
|
locals: HashMap<String, StackValue>,
|
|
}
|
|
|
|
impl StackMachine {
|
|
pub(crate) fn new() -> Self {
|
|
Self {
|
|
stack: Vec::new(),
|
|
locals: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn run(&mut self, code: CodeObject) {
|
|
let mut op_arg_state = OpArgState::default();
|
|
let _ = code.instructions.iter().try_for_each(|&word| {
|
|
let (instruction, arg) = op_arg_state.get(word);
|
|
self.process_instruction(instruction, arg, &code.constants, &code.names)
|
|
});
|
|
}
|
|
|
|
fn process_instruction(
|
|
&mut self,
|
|
instruction: Instruction,
|
|
arg: OpArg,
|
|
constants: &Constants<ConstantData>,
|
|
names: &[String],
|
|
) -> ControlFlow<()> {
|
|
match instruction {
|
|
Instruction::Resume { .. } | Instruction::Cache | Instruction::NotTaken => {
|
|
// No-op for JIT tests
|
|
}
|
|
Instruction::LoadConst { consti } => {
|
|
self.stack.push(constants[consti.get(arg)].clone().into())
|
|
}
|
|
Instruction::LoadCommonConstant { idx } => {
|
|
self.stack.push(match idx.get(arg) {
|
|
CommonConstant::None => StackValue::None,
|
|
CommonConstant::EmptyStr => StackValue::String(String::new()),
|
|
constant => unimplemented!(
|
|
"common constant {constant:?} isn't yet supported in py_function!"
|
|
),
|
|
});
|
|
}
|
|
Instruction::LoadName { namei } => self
|
|
.stack
|
|
.push(StackValue::String(names[namei.get(arg) as usize].clone())),
|
|
Instruction::StoreName { namei } => {
|
|
let idx = namei.get(arg);
|
|
self.locals
|
|
.insert(names[idx as usize].clone(), self.stack.pop().unwrap());
|
|
}
|
|
Instruction::StoreAttr { .. } => {
|
|
// Do nothing except throw away the stack values
|
|
self.stack.pop().unwrap();
|
|
self.stack.pop().unwrap();
|
|
}
|
|
Instruction::BuildMap { count } => {
|
|
let mut map = HashMap::new();
|
|
for _ in 0..count.get(arg) {
|
|
let value = self.stack.pop().unwrap();
|
|
let name = if let Some(StackValue::String(name)) = self.stack.pop() {
|
|
Wtf8Buf::from(name)
|
|
} else {
|
|
unimplemented!("no string keys isn't yet supported in py_function!")
|
|
};
|
|
map.insert(name, value);
|
|
}
|
|
self.stack.push(StackValue::Map(map));
|
|
}
|
|
Instruction::MakeFunction => {
|
|
let code = if let Some(StackValue::Code(code)) = self.stack.pop() {
|
|
code
|
|
} else {
|
|
panic!("Expected function code")
|
|
};
|
|
// Other attributes will be set by SET_FUNCTION_ATTRIBUTE
|
|
self.stack.push(StackValue::Function(Function {
|
|
code,
|
|
annotations: HashMap::new(), // empty annotations, will be set later if needed
|
|
}));
|
|
}
|
|
Instruction::SetFunctionAttribute { flag } => {
|
|
// Stack: [..., attr_value, func] -> [..., func]
|
|
let func = if let Some(StackValue::Function(func)) = self.stack.pop() {
|
|
func
|
|
} else {
|
|
panic!("Expected function on stack for SET_FUNCTION_ATTRIBUTE")
|
|
};
|
|
let attr_value = self.stack.pop().expect("Expected attribute value on stack");
|
|
|
|
let flag_value = flag.get(arg);
|
|
|
|
match flag_value {
|
|
rustpython_compiler_core::bytecode::MakeFunctionFlag::Annotate => {
|
|
// Handle ANNOTATE flag (PEP 649 style - Python 3.14+)
|
|
if let StackValue::Function(annotate_func) = attr_value {
|
|
let annotate_code = &annotate_func.code;
|
|
let annotations = extract_annotations_from_annotate_code(annotate_code);
|
|
|
|
let updated_func = Function {
|
|
code: func.code,
|
|
annotations,
|
|
};
|
|
self.stack.push(StackValue::Function(updated_func));
|
|
} else {
|
|
panic!("Expected annotate function for ANNOTATE flag");
|
|
}
|
|
}
|
|
rustpython_compiler_core::bytecode::MakeFunctionFlag::Annotations => {
|
|
// Handle old ANNOTATIONS flag (Python 3.12 style)
|
|
if let StackValue::Map(annotations) = attr_value {
|
|
let updated_func = Function {
|
|
code: func.code,
|
|
annotations,
|
|
};
|
|
self.stack.push(StackValue::Function(updated_func));
|
|
} else {
|
|
panic!("Expected annotations to be a map");
|
|
}
|
|
}
|
|
_ => {
|
|
// For other attributes, just push the function back unchanged
|
|
self.stack.push(StackValue::Function(func));
|
|
}
|
|
}
|
|
}
|
|
Instruction::ReturnValue => return ControlFlow::Break(()),
|
|
Instruction::ExtendedArg => {}
|
|
_ => unimplemented!(
|
|
"instruction {:?} isn't yet supported in py_function!",
|
|
instruction
|
|
),
|
|
}
|
|
ControlFlow::Continue(())
|
|
}
|
|
|
|
pub(crate) fn get_function(&self, name: &str) -> Function {
|
|
if let Some(StackValue::Function(function)) = self.locals.get(name) {
|
|
function.clone()
|
|
} else {
|
|
panic!("There was no function named {name}")
|
|
}
|
|
}
|
|
}
|
|
|
|
macro_rules! jit_function {
|
|
($func_name:ident => $($t:tt)*) => {
|
|
{
|
|
let code = rustpython_derive::py_compile!(
|
|
crate_name = "rustpython_compiler_core",
|
|
source = $($t)*
|
|
);
|
|
let code = code.decode(rustpython_compiler_core::bytecode::BasicBag);
|
|
let mut machine = $crate::common::StackMachine::new();
|
|
machine.run(code);
|
|
machine.get_function(stringify!($func_name)).compile()
|
|
}
|
|
};
|
|
($func_name:ident($($arg_name:ident:$arg_type:ty),*) -> $ret_type:ty => $($t:tt)*) => {
|
|
{
|
|
let jit_code = jit_function!($func_name => $($t)*);
|
|
|
|
move |$($arg_name:$arg_type),*| -> Result<$ret_type, rustpython_jit::JitArgumentError> {
|
|
jit_code
|
|
.invoke(&[$($arg_name.into()),*])
|
|
.map(|ret| match ret {
|
|
Some(ret) => ret.try_into().expect("jit function returned unexpected type"),
|
|
None => panic!("jit function unexpectedly returned None")
|
|
})
|
|
}
|
|
}
|
|
};
|
|
($func_name:ident($($arg_name:ident:$arg_type:ty),*) => $($t:tt)*) => {
|
|
{
|
|
let jit_code = jit_function!($func_name => $($t)*);
|
|
|
|
move |$($arg_name:$arg_type),*| -> Result<(), rustpython_jit::JitArgumentError> {
|
|
jit_code
|
|
.invoke(&[$($arg_name.into()),*])
|
|
.map(|ret| match ret {
|
|
Some(ret) => panic!("jit function unexpectedly returned a value {:?}", ret),
|
|
None => ()
|
|
})
|
|
}
|
|
}
|
|
};
|
|
}
|