mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
Align compiler and VM with pinned CPython 3.15 ABI
Couple opcode/cache/stack metadata with iterator pairs, generator preambles, ordinary import packing, common constants, compiler lowering and VM/JIT consumers. Pin official source/tool hashes and retain strict raw code and marshal comparisons. Use private development magic 0xDA16 and cache tag devabi2 until serialization and Python integration are coherent. Final clean corpus matches recursive code fields in 111/111 requests; full marshal/header exactness remains 0/111. Eight existing disassembly tests remain blocked by unchanged Lib metadata. Monitoring export and frozendict/update_lib integration remain explicit follow-on gates. Validation: compiler-core 23, codegen 807, CAPI 121 (4 existing ignored), focused certified-controller pytest, release/frozen witnesses, workspace and CAPI all-target Clippy. Independent source/artifact review covers manifest ec1013591ae97b4c519f4a2c78a8200664f3479f0c5452991b5fea4d1be669ea. Assisted-by: OpenAI Codex:model identifier unavailable
This commit is contained in:
1 parent
a10744303f
commit
a974ec938d
38 files changed
+7077
-923
No files matched your search
+880
-393
File diff suppressed because it is too large.
Load diff
+350
-120
@@ -455,7 +455,11 @@ impl InstructionInfo {
|
||||
|
||||
/// flowgraph.c loads_const
|
||||
const fn loads_const(&self) -> bool {
|
||||
self.instr.has_const() || matches!(self.instr.real_opcode(), Some(Opcode::LoadSmallInt))
|
||||
self.instr.has_const()
|
||||
|| matches!(
|
||||
self.instr.real_opcode(),
|
||||
Some(Opcode::LoadSmallInt | Opcode::LoadCommonConstant)
|
||||
)
|
||||
}
|
||||
|
||||
/// flowgraph.c STORES_TO
|
||||
@@ -478,6 +482,26 @@ impl InstructionInfo {
|
||||
false
|
||||
}
|
||||
|
||||
/// flowgraph.c maybe_instr_make_load_common_const
|
||||
fn maybe_instr_make_load_common_const(&mut self, constant: &ConstantData) -> bool {
|
||||
use oparg::CommonConstant;
|
||||
let idx = match constant {
|
||||
ConstantData::None => CommonConstant::None,
|
||||
ConstantData::Boolean { value: true } => CommonConstant::True,
|
||||
ConstantData::Boolean { value: false } => CommonConstant::False,
|
||||
ConstantData::Str { value } if value.is_empty() => CommonConstant::EmptyStr,
|
||||
ConstantData::Integer { value } if value.to_i32() == Some(-1) => {
|
||||
CommonConstant::MinusOne
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
self.instr_set_op1(
|
||||
Opcode::LoadCommonConstant.into(),
|
||||
OpArg::new(u32::from(idx)),
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
/// flowgraph.c make_super_instruction
|
||||
fn make_super_instruction(inst1: &mut Self, inst2: &mut Self, super_op: AnyInstruction) {
|
||||
let line1 = inst1.instruction_lineno();
|
||||
@@ -1084,7 +1108,7 @@ fn resolve_unconditional_jumps(instr_sequence: &mut InstructionSequence) {
|
||||
/// assemble.c resolve_jump_offsets
|
||||
fn resolve_jump_offsets(instr_sequence: &mut InstructionSequence) {
|
||||
// The offset (in code units) of END_SEND from SEND in the yield-from sequence.
|
||||
const END_SEND_OFFSET: i32 = 5;
|
||||
const END_SEND_OFFSET: i32 = 6;
|
||||
for i in 0..instr_sequence.instr_used {
|
||||
let instr = &mut instr_sequence.instrs[i];
|
||||
let opcode = instr.info.instr.expect_real();
|
||||
@@ -2227,7 +2251,7 @@ impl Blocks {
|
||||
AnyInstruction::Real(Instruction::CallIntrinsic1 { func }) => {
|
||||
match func.get(inst.arg) {
|
||||
IntrinsicFunction1::ListToTuple => {
|
||||
if matches!(nextop, Some(Instruction::GetIter)) {
|
||||
if matches!(nextop, Some(Instruction::GetIter { .. })) {
|
||||
self[block_idx].instructions[i].set_to_nop();
|
||||
} else {
|
||||
fold_constant_intrinsic_list_to_tuple(
|
||||
@@ -2410,6 +2434,7 @@ impl Blocks {
|
||||
Instruction::FormatSimple
|
||||
| Instruction::GetAnext
|
||||
| Instruction::GetLen
|
||||
| Instruction::GetIter { .. }
|
||||
| Instruction::GetYieldFromIter
|
||||
| Instruction::ImportFrom { .. }
|
||||
| Instruction::MatchKeys
|
||||
@@ -2420,10 +2445,8 @@ impl Blocks {
|
||||
let effect = instr.stack_effect_info(arg_u32);
|
||||
let net_pushed = effect.pushed() as isize - effect.popped() as isize;
|
||||
debug_assert!(net_pushed >= 0);
|
||||
// CPython optimize_load_fast() shadows the outer
|
||||
// instruction index in this produced-value loop.
|
||||
for produced in 0..net_pushed {
|
||||
push_ref(&mut refs, produced, NOT_LOCAL)?;
|
||||
for _ in 0..net_pushed {
|
||||
push_ref(&mut refs, i as isize, NOT_LOCAL)?;
|
||||
}
|
||||
}
|
||||
AnyInstruction::Real(
|
||||
@@ -2447,10 +2470,19 @@ impl Blocks {
|
||||
Instruction::EndSend | Instruction::SetFunctionAttribute { .. },
|
||||
) => {
|
||||
let effect = instr.stack_effect_info(arg_u32);
|
||||
debug_assert_eq!(effect.popped(), 2);
|
||||
debug_assert_eq!(
|
||||
effect.popped(),
|
||||
if matches!(instr.real(), Some(Instruction::EndSend)) {
|
||||
3
|
||||
} else {
|
||||
2
|
||||
}
|
||||
);
|
||||
debug_assert_eq!(effect.pushed(), 1);
|
||||
let tos = ref_stack_pop(&mut refs);
|
||||
let _ = ref_stack_pop(&mut refs);
|
||||
for _ in 1..effect.popped() {
|
||||
let _ = ref_stack_pop(&mut refs);
|
||||
}
|
||||
push_ref(&mut refs, tos.instr, tos.local)?;
|
||||
}
|
||||
AnyInstruction::Real(Instruction::CheckExcMatch) => {
|
||||
@@ -2634,8 +2666,14 @@ impl Blocks {
|
||||
|
||||
let opcode = instr_info.instr.real_opcode();
|
||||
let is_redundant_pair = matches!(opcode, Some(Opcode::PopTop))
|
||||
&& (matches!(prev_opcode, Some(Opcode::LoadConst | Opcode::LoadSmallInt))
|
||||
|| (prev_oparg == 1 && matches!(prev_opcode, Some(Opcode::Copy))));
|
||||
&& (matches!(
|
||||
prev_opcode,
|
||||
Some(
|
||||
Opcode::LoadConst
|
||||
| Opcode::LoadSmallInt
|
||||
| Opcode::LoadCommonConstant
|
||||
)
|
||||
) || (prev_oparg == 1 && matches!(prev_opcode, Some(Opcode::Copy))));
|
||||
|
||||
if is_redundant_pair {
|
||||
let (prev_block, prev_instr_idx) =
|
||||
@@ -3452,6 +3490,8 @@ impl Blocks {
|
||||
debug_assert!(dest <= instr_count);
|
||||
let num_removed = instr_count - dest;
|
||||
self[block_idx].instruction_used = dest;
|
||||
// Later instructions reuse these slots and must not inherit a removed handler.
|
||||
self[block_idx].instructions[dest..instr_count].fill(InstructionInfo::empty());
|
||||
num_removed
|
||||
}
|
||||
|
||||
@@ -4009,6 +4049,35 @@ impl CodeInfo {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn insert_genexpr_prefix(
|
||||
&mut self,
|
||||
mut iterator: InstructionInfo,
|
||||
) -> crate::InternalResult<()> {
|
||||
let mut resume = InstructionInfo::empty();
|
||||
resume.instr_set_op1(
|
||||
Opcode::Resume.into(),
|
||||
OpArg::new(oparg::ResumeLocation::AtGenExprStart.into()),
|
||||
);
|
||||
resume.lineno_override = Some(NO_LOCATION_OVERRIDE);
|
||||
let mut load = iterator;
|
||||
load.instr_set_op1(Opcode::LoadFast.into(), OpArg::new(0));
|
||||
iterator.arg = OpArg::new(0);
|
||||
let mut return_generator = InstructionInfo::empty();
|
||||
return_generator.instr_set_op0(Instruction::ReturnGenerator.into());
|
||||
return_generator.location.line = self.metadata.firstlineno;
|
||||
return_generator.end_location.line = self.metadata.firstlineno;
|
||||
return_generator.lineno_override = Some(LINE_ONLY_LOCATION_OVERRIDE);
|
||||
let mut pop = return_generator;
|
||||
pop.instr_set_op0(Instruction::PopTop.into());
|
||||
for (i, info) in [resume, load, iterator, return_generator, pop]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
instruction_sequence_insert_instruction(&mut self.instr_sequence, i, info)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn push_unmapped_instr_sequence_label(&mut self) -> crate::InternalResult<()> {
|
||||
instruction_sequence_label_map_push_unmapped_label(
|
||||
&mut self.instr_sequence_label_map,
|
||||
@@ -4344,7 +4413,12 @@ fn insert_prefix_instructions(
|
||||
let firstlineno = metadata.firstlineno;
|
||||
debug_assert!(firstlineno.get() > 0);
|
||||
|
||||
if is_generator(flags) {
|
||||
if is_generator(flags)
|
||||
&& !entry
|
||||
.used_instructions()
|
||||
.iter()
|
||||
.any(|info| matches!(info.instr.real(), Some(Instruction::ReturnGenerator)))
|
||||
{
|
||||
let location = SourceLocation {
|
||||
line: firstlineno,
|
||||
character_offset: OneIndexed::MIN,
|
||||
@@ -4517,6 +4591,13 @@ fn load_const_truthiness(
|
||||
Some(constant.truthiness())
|
||||
}
|
||||
Instruction::LoadSmallInt { i } => Some(i.get(arg) != 0),
|
||||
Instruction::LoadCommonConstant { idx } => {
|
||||
use oparg::CommonConstant;
|
||||
Some(!matches!(
|
||||
idx.get(arg),
|
||||
CommonConstant::None | CommonConstant::False | CommonConstant::EmptyStr
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -4534,7 +4615,9 @@ fn instr_make_load_const(
|
||||
instr: &mut InstructionInfo,
|
||||
constant: ConstantData,
|
||||
) -> crate::InternalResult<()> {
|
||||
if instr.maybe_instr_make_load_smallint(&constant) {
|
||||
if instr.maybe_instr_make_load_smallint(&constant)
|
||||
|| instr.maybe_instr_make_load_common_const(&constant)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -4627,6 +4710,21 @@ fn fold_const_binop(
|
||||
/// flowgraph.c get_const_value
|
||||
fn get_const_value(metadata: &CodeUnitMetadata, info: &InstructionInfo) -> Option<ConstantData> {
|
||||
match info.instr.real_opcode() {
|
||||
Some(Opcode::LoadCommonConstant) => {
|
||||
use oparg::CommonConstant;
|
||||
match CommonConstant::try_from(u32::from(info.arg)).ok()? {
|
||||
CommonConstant::None => Some(ConstantData::None),
|
||||
CommonConstant::EmptyStr => Some(ConstantData::Str {
|
||||
value: Wtf8Buf::new(),
|
||||
}),
|
||||
CommonConstant::True => Some(ConstantData::Boolean { value: true }),
|
||||
CommonConstant::False => Some(ConstantData::Boolean { value: false }),
|
||||
CommonConstant::MinusOne => Some(ConstantData::Integer {
|
||||
value: BigInt::from(-1),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Some(Opcode::LoadSmallInt) => {
|
||||
let v = u32::from(info.arg) as i32;
|
||||
Some(ConstantData::Integer {
|
||||
@@ -5449,7 +5547,7 @@ fn optimize_lists_and_sets(
|
||||
|
||||
let contains_or_iter = matches!(
|
||||
nextop,
|
||||
Some(Instruction::GetIter | Instruction::ContainsOp { .. })
|
||||
Some(Instruction::GetIter { .. } | Instruction::ContainsOp { .. })
|
||||
);
|
||||
let seq_size = u32::from(block.instructions[i].arg) as usize;
|
||||
if seq_size > STACK_USE_GUIDELINE || (seq_size < MIN_CONST_SEQUENCE_SIZE && !contains_or_iter) {
|
||||
@@ -5539,84 +5637,88 @@ fn basicblock_optimize_load_const(
|
||||
metadata: &mut CodeUnitMetadata,
|
||||
block: &mut Block,
|
||||
) -> crate::InternalResult<()> {
|
||||
let mut i = 0;
|
||||
let mut effective_opcode = Instruction::Nop.into();
|
||||
let mut effective_oparg = OpArg::new(0);
|
||||
while i < block.instruction_used {
|
||||
if matches!(
|
||||
block.instructions[i].instr.real(),
|
||||
Some(Instruction::LoadConst { .. })
|
||||
) && let Some(constant) = get_const_value(metadata, &block.instructions[i])
|
||||
{
|
||||
block.instructions[i].maybe_instr_make_load_smallint(&constant);
|
||||
}
|
||||
|
||||
let curr = block.instructions[i];
|
||||
let curr_arg = curr.arg;
|
||||
|
||||
let is_copy_of_load_const = matches!(
|
||||
(effective_opcode, curr.instr.real()),
|
||||
(AnyInstruction::Real(Instruction::LoadConst { .. }), Some(Instruction::Copy { i }))
|
||||
if i.get(curr_arg) == 1
|
||||
);
|
||||
if !is_copy_of_load_const {
|
||||
effective_opcode = curr.instr;
|
||||
effective_oparg = curr_arg;
|
||||
}
|
||||
debug_assert!(!effective_opcode.is_assembler());
|
||||
let Some(const_instr @ (Instruction::LoadConst { .. } | Instruction::LoadSmallInt { .. })) =
|
||||
effective_opcode.real()
|
||||
else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
let const_arg = effective_oparg;
|
||||
|
||||
if i + 1 >= block.instruction_used {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let next = block.instructions[i + 1];
|
||||
let next_arg = next.arg;
|
||||
|
||||
if let Some(is_true) = load_const_truthiness(const_instr, const_arg, metadata) {
|
||||
let const_jump = match (next.instr.real_opcode(), next.instr.pseudo_opcode()) {
|
||||
(_, Some(PseudoOpcode::JumpIfTrue)) => Some((true, false)),
|
||||
(_, Some(PseudoOpcode::JumpIfFalse)) => Some((false, false)),
|
||||
(Some(Opcode::PopJumpIfTrue), _) => Some((true, true)),
|
||||
(Some(Opcode::PopJumpIfFalse), _) => Some((false, true)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some((jump_if_true, pops_condition)) = const_jump {
|
||||
if pops_condition {
|
||||
block.instructions[i].set_to_nop();
|
||||
}
|
||||
if is_true == jump_if_true {
|
||||
block.instructions[i + 1].instr = PseudoOpcode::Jump.into();
|
||||
} else {
|
||||
block.instructions[i + 1].set_to_nop();
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// The remaining combinations require both instructions to be real.
|
||||
let Some(next_instr) = next.instr.real() else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Instruction::LoadConst { consti } = const_instr {
|
||||
let constant = &metadata.consts[consti.get(const_arg).as_usize()];
|
||||
if matches!(constant, ConstantData::None)
|
||||
&& let Instruction::IsOp { invert } = next_instr
|
||||
for i in 0..block.instruction_used {
|
||||
'instruction: {
|
||||
if matches!(
|
||||
block.instructions[i].instr.real(),
|
||||
Some(Instruction::LoadConst { .. })
|
||||
) && let Some(constant) = get_const_value(metadata, &block.instructions[i])
|
||||
{
|
||||
block.instructions[i].maybe_instr_make_load_smallint(&constant);
|
||||
}
|
||||
|
||||
let curr = block.instructions[i];
|
||||
let curr_arg = curr.arg;
|
||||
|
||||
let is_copy_of_load_const = matches!(
|
||||
(effective_opcode, curr.instr.real()),
|
||||
(AnyInstruction::Real(Instruction::LoadConst { .. }), Some(Instruction::Copy { i }))
|
||||
if i.get(curr_arg) == 1
|
||||
);
|
||||
if !is_copy_of_load_const {
|
||||
effective_opcode = curr.instr;
|
||||
effective_oparg = curr_arg;
|
||||
}
|
||||
debug_assert!(!effective_opcode.is_assembler());
|
||||
let Some(
|
||||
const_instr @ (Instruction::LoadConst { .. }
|
||||
| Instruction::LoadSmallInt { .. }
|
||||
| Instruction::LoadCommonConstant { .. }),
|
||||
) = effective_opcode.real()
|
||||
else {
|
||||
break 'instruction;
|
||||
};
|
||||
let const_arg = effective_oparg;
|
||||
|
||||
if i + 1 >= block.instruction_used {
|
||||
break 'instruction;
|
||||
}
|
||||
|
||||
let next = block.instructions[i + 1];
|
||||
let next_arg = next.arg;
|
||||
|
||||
if let Some(is_true) = load_const_truthiness(const_instr, const_arg, metadata) {
|
||||
let const_jump = match (next.instr.real_opcode(), next.instr.pseudo_opcode()) {
|
||||
(_, Some(PseudoOpcode::JumpIfTrue)) => Some((true, false)),
|
||||
(_, Some(PseudoOpcode::JumpIfFalse)) => Some((false, false)),
|
||||
(Some(Opcode::PopJumpIfTrue), _) => Some((true, true)),
|
||||
(Some(Opcode::PopJumpIfFalse), _) => Some((false, true)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some((jump_if_true, pops_condition)) = const_jump {
|
||||
if pops_condition {
|
||||
block.instructions[i].set_to_nop();
|
||||
}
|
||||
if is_true == jump_if_true {
|
||||
block.instructions[i + 1].instr = PseudoOpcode::Jump.into();
|
||||
} else {
|
||||
block.instructions[i + 1].set_to_nop();
|
||||
}
|
||||
break 'instruction;
|
||||
}
|
||||
}
|
||||
|
||||
// The remaining combinations require both instructions to be real.
|
||||
let Some(next_instr) = next.instr.real() else {
|
||||
break 'instruction;
|
||||
};
|
||||
|
||||
let is_none = match const_instr {
|
||||
Instruction::LoadConst { consti } => matches!(
|
||||
&metadata.consts[consti.get(const_arg).as_usize()],
|
||||
ConstantData::None
|
||||
),
|
||||
Instruction::LoadCommonConstant { idx } => {
|
||||
idx.get(const_arg) == oparg::CommonConstant::None
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if is_none && let Instruction::IsOp { invert } = next_instr {
|
||||
let mut jump_idx = i + 2;
|
||||
if jump_idx >= block.instruction_used {
|
||||
i += 1;
|
||||
continue;
|
||||
break 'instruction;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
@@ -5626,14 +5728,12 @@ fn basicblock_optimize_load_const(
|
||||
block.instructions[jump_idx].set_to_nop();
|
||||
jump_idx += 1;
|
||||
if jump_idx >= block.instruction_used {
|
||||
i += 1;
|
||||
continue;
|
||||
break 'instruction;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(jump_instr) = block.instructions[jump_idx].instr.real() else {
|
||||
i += 1;
|
||||
continue;
|
||||
break 'instruction;
|
||||
};
|
||||
|
||||
let mut invert = matches!(
|
||||
@@ -5646,8 +5746,7 @@ fn basicblock_optimize_load_const(
|
||||
}
|
||||
Instruction::PopJumpIfTrue { .. } => {}
|
||||
_ => {
|
||||
i += 1;
|
||||
continue;
|
||||
break 'instruction;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5659,27 +5758,32 @@ fn basicblock_optimize_load_const(
|
||||
Opcode::PopJumpIfNone
|
||||
}
|
||||
.into();
|
||||
i += 1;
|
||||
continue;
|
||||
break 'instruction;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
const_instr,
|
||||
Instruction::LoadConst { .. }
|
||||
| Instruction::LoadSmallInt { .. }
|
||||
| Instruction::LoadCommonConstant { .. }
|
||||
) && matches!(next_instr, Instruction::ToBool)
|
||||
&& let Some(value) = load_const_truthiness(const_instr, const_arg, metadata)
|
||||
{
|
||||
let const_idx = add_const(metadata, ConstantData::Boolean { value })?;
|
||||
block.instructions[i].set_to_nop();
|
||||
|
||||
block.instructions[i + 1]
|
||||
.instr_set_op1(Opcode::LoadConst.into(), OpArg::new(const_idx as u32));
|
||||
break 'instruction;
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
const_instr,
|
||||
Instruction::LoadConst { .. } | Instruction::LoadSmallInt { .. }
|
||||
) && matches!(next_instr, Instruction::ToBool)
|
||||
&& let Some(value) = load_const_truthiness(const_instr, const_arg, metadata)
|
||||
block.instructions[i].instr.real_opcode(),
|
||||
Some(Opcode::LoadConst)
|
||||
) && let Some(constant) = get_const_value(metadata, &block.instructions[i])
|
||||
{
|
||||
let const_idx = add_const(metadata, ConstantData::Boolean { value })?;
|
||||
block.instructions[i].set_to_nop();
|
||||
|
||||
block.instructions[i + 1]
|
||||
.instr_set_op1(Opcode::LoadConst.into(), OpArg::new(const_idx as u32));
|
||||
i += 1;
|
||||
continue;
|
||||
block.instructions[i].maybe_instr_make_load_common_const(&constant);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -6968,7 +7072,9 @@ pub(crate) fn label_exception_targets(blocks: &mut Blocks) -> crate::InternalRes
|
||||
} else if let Some(Instruction::Resume { context: _ }) = instr.real() {
|
||||
blocks[bi].instructions[i].except_handler = handler;
|
||||
let resume_arg = u32::from(arg);
|
||||
if resume_arg != u32::from(oparg::ResumeLocation::AtFuncStart) {
|
||||
if resume_arg != u32::from(oparg::ResumeLocation::AtFuncStart)
|
||||
&& resume_arg != u32::from(oparg::ResumeLocation::AtGenExprStart)
|
||||
{
|
||||
debug_assert!(last_yield_except_depth >= 0);
|
||||
if last_yield_except_depth == 1 {
|
||||
blocks[bi].instructions[i].arg =
|
||||
@@ -6976,6 +7082,8 @@ pub(crate) fn label_exception_targets(blocks: &mut Blocks) -> crate::InternalRes
|
||||
}
|
||||
last_yield_except_depth = -1;
|
||||
}
|
||||
} else if matches!(instr.real(), Some(Instruction::ReturnGenerator)) {
|
||||
blocks[bi].instructions[i].except_handler = None;
|
||||
} else {
|
||||
blocks[bi].instructions[i].except_handler = handler;
|
||||
}
|
||||
@@ -7726,7 +7834,7 @@ mod tests {
|
||||
|
||||
// CPython `basicblock_optimize_load_const()` keeps the previous
|
||||
// LOAD_CONST as the effective opcode for a following `COPY 1`, so the
|
||||
// COPY is NOPed and TO_BOOL becomes LOAD_CONST True.
|
||||
// COPY is NOPed and TO_BOOL becomes LOAD_COMMON_CONSTANT True.
|
||||
assert!(matches!(
|
||||
code.blocks[0].instructions[0].instr.real(),
|
||||
Some(Instruction::LoadConst { .. })
|
||||
@@ -7738,11 +7846,11 @@ mod tests {
|
||||
let load_bool = &code.blocks[0].instructions[2];
|
||||
assert!(matches!(
|
||||
load_bool.instr.real(),
|
||||
Some(Instruction::LoadConst { .. })
|
||||
Some(Instruction::LoadCommonConstant { .. })
|
||||
));
|
||||
assert_eq!(
|
||||
code.metadata.consts[u32::from(load_bool.arg) as usize],
|
||||
ConstantData::Boolean { value: true }
|
||||
u32::from(load_bool.arg),
|
||||
u32::from(oparg::CommonConstant::True)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7793,7 +7901,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimize_load_fast_records_no_input_opcode_ref_at_cpython_produced_index() {
|
||||
fn optimize_load_fast_records_no_input_opcode_ref_at_cpython_instruction_index() {
|
||||
let mut block = Block::default();
|
||||
test_block_push(&mut block, test_instr(Opcode::LoadFast.into(), 10));
|
||||
test_block_push(&mut block, test_instr(Instruction::GetLen, 10));
|
||||
@@ -7807,13 +7915,11 @@ mod tests {
|
||||
.optimize_load_fast()
|
||||
.expect("optimize_load_fast succeeds");
|
||||
|
||||
// CPython `optimize_load_fast()` shadows the outer instruction index in
|
||||
// the produced-value loop for GET_LEN, so the produced ref is recorded
|
||||
// with index 0 here. The original LOAD_FAST is therefore not considered
|
||||
// the consumed producer.
|
||||
// CPython records GET_LEN's result at instruction index 1. The
|
||||
// SWAP/POP_TOP consumes the original local reference, so it can borrow.
|
||||
assert!(matches!(
|
||||
code.blocks[0].instructions[0].instr.real(),
|
||||
Some(Instruction::LoadFast { .. })
|
||||
Some(Instruction::LoadFastBorrow { .. })
|
||||
));
|
||||
}
|
||||
|
||||
@@ -8079,4 +8185,128 @@ mod tests {
|
||||
|
||||
assert_eq!(blocks[0].instructions[0].target, BlockIdx::new(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpython315_common_constants_lower_and_round_trip_without_numeric_aliasing() {
|
||||
use oparg::CommonConstant;
|
||||
for (constant, common) in [
|
||||
(ConstantData::None, CommonConstant::None),
|
||||
(
|
||||
ConstantData::Str {
|
||||
value: Wtf8Buf::new(),
|
||||
},
|
||||
CommonConstant::EmptyStr,
|
||||
),
|
||||
(ConstantData::Boolean { value: true }, CommonConstant::True),
|
||||
(
|
||||
ConstantData::Boolean { value: false },
|
||||
CommonConstant::False,
|
||||
),
|
||||
(int_const(-1), CommonConstant::MinusOne),
|
||||
] {
|
||||
let mut metadata = test_code_info(Block::default()).metadata;
|
||||
metadata.consts.insert_full(constant.clone());
|
||||
let mut block = Block::default();
|
||||
test_block_push(&mut block, test_instr(Opcode::LoadConst.into(), 1));
|
||||
basicblock_optimize_load_const(&mut metadata, &mut block).unwrap();
|
||||
let info = block.instructions[0];
|
||||
assert_eq!(info.instr.real_opcode(), Some(Opcode::LoadCommonConstant));
|
||||
assert_eq!(u32::from(info.arg), u32::from(common));
|
||||
assert_eq!(get_const_value(&metadata, &info), Some(constant.clone()));
|
||||
assert_eq!(
|
||||
load_const_truthiness(info.instr.expect_real(), info.arg, &metadata),
|
||||
Some(constant.truthiness())
|
||||
);
|
||||
}
|
||||
for constant in [
|
||||
int_const(0),
|
||||
int_const(1),
|
||||
ConstantData::Float { value: -1.0 },
|
||||
] {
|
||||
assert!(
|
||||
!test_instr(Opcode::LoadConst.into(), 1)
|
||||
.maybe_instr_make_load_common_const(&constant)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpython315_get_iter_tracks_both_stack_slots_for_borrow_analysis() {
|
||||
let mut block = Block::default();
|
||||
for (op, arg) in [
|
||||
(Opcode::LoadFast, 0),
|
||||
(Opcode::GetIter, 0),
|
||||
(Opcode::Swap, 2),
|
||||
(Opcode::PopTop, 0),
|
||||
(Opcode::PopTop, 0),
|
||||
] {
|
||||
let mut info = test_instr(op.into(), 1);
|
||||
info.arg = OpArg::new(arg);
|
||||
test_block_push(&mut block, info);
|
||||
}
|
||||
let mut code = test_code_info(block);
|
||||
code.blocks.optimize_load_fast().unwrap();
|
||||
assert_eq!(
|
||||
code.blocks[0].instructions[0].instr.real_opcode(),
|
||||
Some(Opcode::LoadFastBorrow)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpython315_end_send_preserves_result_borrow_and_consumes_iterator_pair() {
|
||||
let mut block = Block::default();
|
||||
for (op, arg) in [
|
||||
(
|
||||
Opcode::LoadCommonConstant,
|
||||
u32::from(oparg::CommonConstant::None),
|
||||
),
|
||||
(Opcode::PushNull, 0),
|
||||
(Opcode::LoadFast, 0),
|
||||
(Opcode::EndSend, 0),
|
||||
(Opcode::PopTop, 0),
|
||||
] {
|
||||
let mut info = test_instr(op.into(), 1);
|
||||
info.arg = OpArg::new(arg);
|
||||
test_block_push(&mut block, info);
|
||||
}
|
||||
let mut code = test_code_info(block);
|
||||
assert_eq!(code.blocks.calculate_stackdepth().unwrap(), 3);
|
||||
code.blocks.optimize_load_fast().unwrap();
|
||||
assert_eq!(
|
||||
code.blocks[0].instructions[2].instr.real_opcode(),
|
||||
Some(Opcode::LoadFastBorrow)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpython315_redundant_nop_compaction_clears_vacated_metadata() {
|
||||
let handler = ExceptHandlerInfo {
|
||||
handler_block: BlockIdx::new(2),
|
||||
preserve_lasti: true,
|
||||
};
|
||||
let mut block = Block::default();
|
||||
for _ in 0..2 {
|
||||
let mut nop = test_instr(Instruction::Nop, 10);
|
||||
nop.lineno_override = Some(NO_LOCATION_OVERRIDE);
|
||||
nop.target = BlockIdx::new(1);
|
||||
nop.except_handler = Some(handler);
|
||||
test_block_push(&mut block, nop);
|
||||
}
|
||||
let mut jump = test_instr(Opcode::PopJumpIfTrue.into(), 10);
|
||||
jump.target = BlockIdx::new(1);
|
||||
jump.except_handler = Some(handler);
|
||||
test_block_push(&mut block, jump);
|
||||
let mut blocks = Blocks::from([block, Block::default(), Block::default()]);
|
||||
assert_eq!(blocks.basicblock_remove_redundant_nops(BlockIdx::new(0)), 2);
|
||||
assert_eq!(blocks[0].instructions[0].target, jump.target);
|
||||
assert_eq!(blocks[0].instructions[0].except_handler, Some(handler));
|
||||
for slot in &blocks[0].instructions[1..3] {
|
||||
assert_eq!(slot.target, BlockIdx::NULL);
|
||||
assert_eq!(slot.except_handler, None);
|
||||
}
|
||||
blocks.normalize_jumps_in_block(BlockIdx::new(0)).unwrap();
|
||||
let not_taken = &blocks[0].instructions[1];
|
||||
assert_eq!(not_taken.instr.real_opcode(), Some(Opcode::NotTaken));
|
||||
assert_eq!(not_taken.except_handler, None);
|
||||
}
|
||||
}
|
||||
@@ -734,6 +734,26 @@ impl Deref for CodeUnits {
|
||||
}
|
||||
|
||||
impl CodeUnits {
|
||||
/// Apply final 3.15 GET_ITER mode fixups before code publication.
|
||||
/// Python/specialize.c:fixup_getiter normalizes every nonzero mode.
|
||||
pub fn normalize_get_iter(&mut self, flags: CodeFlags) {
|
||||
let units = self.units.get_mut();
|
||||
let mut index = 0;
|
||||
while index < units.len() {
|
||||
let unit = &mut units[index];
|
||||
if matches!(unit.op, Instruction::GetIter { .. }) && unit.arg != 0.into() {
|
||||
let mode = if flags.intersects(CodeFlags::COROUTINE | CodeFlags::ITERABLE_COROUTINE)
|
||||
{
|
||||
2
|
||||
} else {
|
||||
3
|
||||
};
|
||||
unit.arg = mode.into();
|
||||
}
|
||||
index += 1 + unit.op.cache_entries();
|
||||
}
|
||||
}
|
||||
|
||||
/// Disable adaptive specialization by setting all counters to unreachable.
|
||||
/// Used for CPython-compiled bytecode where specialization may not be safe.
|
||||
pub fn disable_specialization(&self) {
|
||||
|
||||
@@ -333,261 +333,261 @@ define_opcodes!(
|
||||
FormatWithSpec = 13,
|
||||
GetAiter = 14,
|
||||
GetAnext = 15,
|
||||
GetIter = 16,
|
||||
GetIter { mode: Arg<u32> } = 70,
|
||||
Reserved = 17,
|
||||
GetLen = 18,
|
||||
GetYieldFromIter = 19,
|
||||
InterpreterExit = 20,
|
||||
LoadBuildClass = 21,
|
||||
LoadLocals = 22,
|
||||
MakeFunction = 23,
|
||||
MatchKeys = 24,
|
||||
MatchMapping = 25,
|
||||
MatchSequence = 26,
|
||||
Nop = 27,
|
||||
NotTaken = 28,
|
||||
PopExcept = 29,
|
||||
PopIter = 30,
|
||||
PopTop = 31,
|
||||
PushExcInfo = 32,
|
||||
PushNull = 33,
|
||||
ReturnGenerator = 34,
|
||||
ReturnValue = 35,
|
||||
SetupAnnotations = 36,
|
||||
StoreSlice = 37,
|
||||
StoreSubscr = 38,
|
||||
ToBool = 39,
|
||||
UnaryInvert = 40,
|
||||
UnaryNegative = 41,
|
||||
UnaryNot = 42,
|
||||
WithExceptStart = 43,
|
||||
GetLen = 16,
|
||||
GetYieldFromIter = 219,
|
||||
InterpreterExit = 18,
|
||||
LoadBuildClass = 19,
|
||||
LoadLocals = 20,
|
||||
MakeFunction = 21,
|
||||
MatchKeys = 22,
|
||||
MatchMapping = 23,
|
||||
MatchSequence = 24,
|
||||
Nop = 25,
|
||||
NotTaken = 26,
|
||||
PopExcept = 27,
|
||||
PopIter = 28,
|
||||
PopTop = 29,
|
||||
PushExcInfo = 30,
|
||||
PushNull = 31,
|
||||
ReturnGenerator = 32,
|
||||
ReturnValue = 33,
|
||||
SetupAnnotations = 34,
|
||||
StoreSlice = 35,
|
||||
StoreSubscr = 36,
|
||||
ToBool = 37,
|
||||
UnaryInvert = 38,
|
||||
UnaryNegative = 39,
|
||||
UnaryNot = 40,
|
||||
WithExceptStart = 41,
|
||||
BinaryOp {
|
||||
op: Arg<oparg::BinaryOperator>,
|
||||
} = 44,
|
||||
} = 42,
|
||||
BuildInterpolation {
|
||||
format: Arg<u32>,
|
||||
} = 45,
|
||||
} = 43,
|
||||
BuildList {
|
||||
count: Arg<u32>,
|
||||
} = 46,
|
||||
} = 44,
|
||||
BuildMap {
|
||||
count: Arg<u32>,
|
||||
} = 47,
|
||||
} = 45,
|
||||
BuildSet {
|
||||
count: Arg<u32>,
|
||||
} = 48,
|
||||
} = 46,
|
||||
BuildSlice {
|
||||
argc: Arg<oparg::BuildSliceArgCount>,
|
||||
} = 49,
|
||||
} = 47,
|
||||
BuildString {
|
||||
count: Arg<u32>,
|
||||
} = 50,
|
||||
} = 48,
|
||||
BuildTuple {
|
||||
count: Arg<u32>,
|
||||
} = 51,
|
||||
} = 49,
|
||||
Call {
|
||||
argc: Arg<u32>,
|
||||
} = 52,
|
||||
} = 50,
|
||||
CallIntrinsic1 {
|
||||
func: Arg<oparg::IntrinsicFunction1>,
|
||||
} = 53,
|
||||
} = 51,
|
||||
CallIntrinsic2 {
|
||||
func: Arg<oparg::IntrinsicFunction2>,
|
||||
} = 54,
|
||||
} = 52,
|
||||
CallKw {
|
||||
argc: Arg<u32>,
|
||||
} = 55,
|
||||
} = 53,
|
||||
CompareOp {
|
||||
opname: Arg<oparg::ComparisonOperator>,
|
||||
} = 56,
|
||||
} = 54,
|
||||
ContainsOp {
|
||||
invert: Arg<oparg::Invert>,
|
||||
} = 57,
|
||||
} = 55,
|
||||
ConvertValue {
|
||||
oparg: Arg<oparg::ConvertValueOparg>,
|
||||
} = 58,
|
||||
} = 56,
|
||||
Copy {
|
||||
i: Arg<u32>,
|
||||
} = 59,
|
||||
} = 57,
|
||||
CopyFreeVars {
|
||||
n: Arg<u32>,
|
||||
} = 60,
|
||||
} = 58,
|
||||
DeleteAttr {
|
||||
namei: Arg<oparg::NameIdx>,
|
||||
} = 61,
|
||||
} = 59,
|
||||
DeleteDeref {
|
||||
i: Arg<oparg::VarNum>,
|
||||
} = 62,
|
||||
} = 60,
|
||||
DeleteFast {
|
||||
var_num: Arg<oparg::VarNum>,
|
||||
} = 63,
|
||||
} = 61,
|
||||
DeleteGlobal {
|
||||
namei: Arg<oparg::NameIdx>,
|
||||
} = 64,
|
||||
} = 62,
|
||||
DeleteName {
|
||||
namei: Arg<oparg::NameIdx>,
|
||||
} = 65,
|
||||
} = 63,
|
||||
DictMerge {
|
||||
i: Arg<u32>,
|
||||
} = 66,
|
||||
} = 64,
|
||||
DictUpdate {
|
||||
i: Arg<u32>,
|
||||
} = 67,
|
||||
EndAsyncFor = 68,
|
||||
ExtendedArg = 69,
|
||||
} = 65,
|
||||
EndAsyncFor = 66,
|
||||
ExtendedArg = 67,
|
||||
ForIter {
|
||||
delta: Arg<oparg::Label>,
|
||||
} = 70,
|
||||
} = 68,
|
||||
GetAwaitable {
|
||||
r#where: Arg<u32>,
|
||||
} = 71,
|
||||
} = 69,
|
||||
ImportFrom {
|
||||
namei: Arg<oparg::NameIdx>,
|
||||
} = 72,
|
||||
} = 71,
|
||||
ImportName {
|
||||
namei: Arg<oparg::NameIdx>,
|
||||
} = 73,
|
||||
} = 72,
|
||||
IsOp {
|
||||
invert: Arg<oparg::Invert>,
|
||||
} = 74,
|
||||
} = 73,
|
||||
JumpBackward {
|
||||
delta: Arg<oparg::Label>,
|
||||
} = 75,
|
||||
} = 74,
|
||||
JumpBackwardNoInterrupt {
|
||||
delta: Arg<oparg::Label>,
|
||||
} = 76,
|
||||
} = 75,
|
||||
JumpForward {
|
||||
delta: Arg<oparg::Label>,
|
||||
} = 77,
|
||||
} = 76,
|
||||
ListAppend {
|
||||
i: Arg<u32>,
|
||||
} = 78,
|
||||
} = 77,
|
||||
ListExtend {
|
||||
i: Arg<u32>,
|
||||
} = 79,
|
||||
} = 78,
|
||||
LoadAttr {
|
||||
namei: Arg<oparg::LoadAttr>,
|
||||
} = 80,
|
||||
} = 79,
|
||||
LoadCommonConstant {
|
||||
idx: Arg<oparg::CommonConstant>,
|
||||
} = 81,
|
||||
} = 80,
|
||||
LoadConst {
|
||||
consti: Arg<oparg::ConstIdx>,
|
||||
} = 82,
|
||||
} = 81,
|
||||
LoadDeref {
|
||||
i: Arg<oparg::VarNum>,
|
||||
} = 83,
|
||||
} = 82,
|
||||
LoadFast {
|
||||
var_num: Arg<oparg::VarNum>,
|
||||
} = 84,
|
||||
} = 83,
|
||||
LoadFastAndClear {
|
||||
var_num: Arg<oparg::VarNum>,
|
||||
} = 85,
|
||||
} = 84,
|
||||
LoadFastBorrow {
|
||||
var_num: Arg<oparg::VarNum>,
|
||||
} = 86,
|
||||
} = 85,
|
||||
LoadFastBorrowLoadFastBorrow {
|
||||
var_nums: Arg<oparg::VarNums>,
|
||||
} = 87,
|
||||
} = 86,
|
||||
LoadFastCheck {
|
||||
var_num: Arg<oparg::VarNum>,
|
||||
} = 88,
|
||||
} = 87,
|
||||
LoadFastLoadFast {
|
||||
var_nums: Arg<oparg::VarNums>,
|
||||
} = 89,
|
||||
} = 88,
|
||||
LoadFromDictOrDeref {
|
||||
i: Arg<oparg::VarNum>,
|
||||
} = 90,
|
||||
} = 89,
|
||||
LoadFromDictOrGlobals {
|
||||
i: Arg<oparg::NameIdx>,
|
||||
} = 91,
|
||||
} = 90,
|
||||
LoadGlobal {
|
||||
namei: Arg<oparg::NameIdx>,
|
||||
} = 92,
|
||||
} = 91,
|
||||
LoadName {
|
||||
namei: Arg<oparg::NameIdx>,
|
||||
} = 93,
|
||||
} = 92,
|
||||
LoadSmallInt {
|
||||
i: Arg<u32>,
|
||||
} = 94,
|
||||
} = 93,
|
||||
LoadSpecial {
|
||||
method: Arg<oparg::SpecialMethod>,
|
||||
} = 95,
|
||||
} = 94,
|
||||
LoadSuperAttr {
|
||||
namei: Arg<oparg::LoadSuperAttr>,
|
||||
} = 96,
|
||||
} = 95,
|
||||
MakeCell {
|
||||
i: Arg<oparg::VarNum>,
|
||||
} = 97,
|
||||
} = 96,
|
||||
MapAdd {
|
||||
i: Arg<u32>,
|
||||
} = 98,
|
||||
} = 97,
|
||||
MatchClass {
|
||||
count: Arg<u32>,
|
||||
} = 99,
|
||||
} = 98,
|
||||
PopJumpIfFalse {
|
||||
delta: Arg<oparg::Label>,
|
||||
} = 100,
|
||||
} = 99,
|
||||
PopJumpIfNone {
|
||||
delta: Arg<oparg::Label>,
|
||||
} = 101,
|
||||
} = 100,
|
||||
PopJumpIfNotNone {
|
||||
delta: Arg<oparg::Label>,
|
||||
} = 102,
|
||||
} = 101,
|
||||
PopJumpIfTrue {
|
||||
delta: Arg<oparg::Label>,
|
||||
} = 103,
|
||||
} = 102,
|
||||
RaiseVarargs {
|
||||
argc: Arg<oparg::RaiseKind>,
|
||||
} = 104,
|
||||
} = 103,
|
||||
Reraise {
|
||||
depth: Arg<u32>,
|
||||
} = 105,
|
||||
} = 104,
|
||||
Send {
|
||||
delta: Arg<oparg::Label>,
|
||||
} = 106,
|
||||
} = 105,
|
||||
SetAdd {
|
||||
i: Arg<u32>,
|
||||
} = 107,
|
||||
} = 106,
|
||||
SetFunctionAttribute {
|
||||
flag: Arg<oparg::MakeFunctionFlag>,
|
||||
} = 108,
|
||||
} = 107,
|
||||
SetUpdate {
|
||||
i: Arg<u32>,
|
||||
} = 109,
|
||||
} = 108,
|
||||
StoreAttr {
|
||||
namei: Arg<oparg::NameIdx>,
|
||||
} = 110,
|
||||
} = 109,
|
||||
StoreDeref {
|
||||
i: Arg<oparg::VarNum>,
|
||||
} = 111,
|
||||
} = 110,
|
||||
StoreFast {
|
||||
var_num: Arg<oparg::VarNum>,
|
||||
} = 112,
|
||||
} = 111,
|
||||
StoreFastLoadFast {
|
||||
var_nums: Arg<oparg::VarNums>,
|
||||
} = 113,
|
||||
} = 112,
|
||||
StoreFastStoreFast {
|
||||
var_nums: Arg<oparg::VarNums>,
|
||||
} = 114,
|
||||
} = 113,
|
||||
StoreGlobal {
|
||||
namei: Arg<oparg::NameIdx>,
|
||||
} = 115,
|
||||
} = 114,
|
||||
StoreName {
|
||||
namei: Arg<oparg::NameIdx>,
|
||||
} = 116,
|
||||
} = 115,
|
||||
Swap {
|
||||
i: Arg<u32>,
|
||||
} = 117,
|
||||
} = 116,
|
||||
UnpackEx {
|
||||
counts: Arg<oparg::UnpackExArgs>,
|
||||
} = 118,
|
||||
} = 117,
|
||||
UnpackSequence {
|
||||
count: Arg<u32>,
|
||||
} = 119,
|
||||
} = 118,
|
||||
YieldValue {
|
||||
arg: Arg<u32>,
|
||||
} = 120,
|
||||
} = 119,
|
||||
Resume {
|
||||
context: Arg<oparg::ResumeContext>,
|
||||
} = 128,
|
||||
@@ -603,99 +603,99 @@ define_opcodes!(
|
||||
BinaryOpSubscrListSlice = 138,
|
||||
BinaryOpSubscrStrInt = 139,
|
||||
BinaryOpSubscrTupleInt = 140,
|
||||
BinaryOpSubtractFloat = 141,
|
||||
BinaryOpSubtractInt = 142,
|
||||
CallAllocAndEnterInit = 143,
|
||||
CallBoundMethodExactArgs = 144,
|
||||
CallBoundMethodGeneral = 145,
|
||||
CallBuiltinClass = 146,
|
||||
CallBuiltinFast = 147,
|
||||
CallBuiltinFastWithKeywords = 148,
|
||||
CallBuiltinO = 149,
|
||||
CallIsinstance = 150,
|
||||
CallKwBoundMethod = 151,
|
||||
CallKwNonPy = 152,
|
||||
CallKwPy = 153,
|
||||
CallLen = 154,
|
||||
CallListAppend = 155,
|
||||
CallMethodDescriptorFast = 156,
|
||||
CallMethodDescriptorFastWithKeywords = 157,
|
||||
CallMethodDescriptorNoargs = 158,
|
||||
CallMethodDescriptorO = 159,
|
||||
CallNonPyGeneral = 160,
|
||||
CallPyExactArgs = 161,
|
||||
CallPyGeneral = 162,
|
||||
CallStr1 = 163,
|
||||
CallTuple1 = 164,
|
||||
CallType1 = 165,
|
||||
CompareOpFloat = 166,
|
||||
CompareOpInt = 167,
|
||||
CompareOpStr = 168,
|
||||
ContainsOpDict = 169,
|
||||
ContainsOpSet = 170,
|
||||
ForIterGen = 171,
|
||||
ForIterList = 172,
|
||||
ForIterRange = 173,
|
||||
ForIterTuple = 174,
|
||||
JumpBackwardJit = 175,
|
||||
JumpBackwardNoJit = 176,
|
||||
LoadAttrClass = 177,
|
||||
LoadAttrClassWithMetaclassCheck = 178,
|
||||
LoadAttrGetattributeOverridden = 179,
|
||||
LoadAttrInstanceValue = 180,
|
||||
LoadAttrMethodLazyDict = 181,
|
||||
LoadAttrMethodNoDict = 182,
|
||||
LoadAttrMethodWithValues = 183,
|
||||
LoadAttrModule = 184,
|
||||
LoadAttrNondescriptorNoDict = 185,
|
||||
LoadAttrNondescriptorWithValues = 186,
|
||||
LoadAttrProperty = 187,
|
||||
LoadAttrSlot = 188,
|
||||
LoadAttrWithHint = 189,
|
||||
LoadConstImmortal = 190,
|
||||
LoadConstMortal = 191,
|
||||
LoadGlobalBuiltin = 192,
|
||||
LoadGlobalModule = 193,
|
||||
LoadSuperAttrAttr = 194,
|
||||
LoadSuperAttrMethod = 195,
|
||||
ResumeCheck = 196,
|
||||
SendGen = 197,
|
||||
StoreAttrInstanceValue = 198,
|
||||
StoreAttrSlot = 199,
|
||||
StoreAttrWithHint = 200,
|
||||
StoreSubscrDict = 201,
|
||||
StoreSubscrListInt = 202,
|
||||
ToBoolAlwaysTrue = 203,
|
||||
ToBoolBool = 204,
|
||||
ToBoolInt = 205,
|
||||
ToBoolList = 206,
|
||||
ToBoolNone = 207,
|
||||
ToBoolStr = 208,
|
||||
UnpackSequenceList = 209,
|
||||
UnpackSequenceTuple = 210,
|
||||
UnpackSequenceTwoTuple = 211,
|
||||
InstrumentedEndFor = 234,
|
||||
InstrumentedPopIter = 235,
|
||||
InstrumentedEndSend = 236,
|
||||
InstrumentedForIter = 237,
|
||||
InstrumentedInstruction = 238,
|
||||
InstrumentedJumpForward = 239,
|
||||
InstrumentedNotTaken = 240,
|
||||
InstrumentedPopJumpIfTrue = 241,
|
||||
InstrumentedPopJumpIfFalse = 242,
|
||||
InstrumentedPopJumpIfNone = 243,
|
||||
InstrumentedPopJumpIfNotNone = 244,
|
||||
InstrumentedResume = 245,
|
||||
InstrumentedReturnValue = 246,
|
||||
InstrumentedYieldValue = 247,
|
||||
InstrumentedEndAsyncFor = 248,
|
||||
InstrumentedLoadSuperAttr = 249,
|
||||
InstrumentedCall = 250,
|
||||
InstrumentedCallKw = 251,
|
||||
InstrumentedCallFunctionEx = 252,
|
||||
InstrumentedJumpBackward = 253,
|
||||
InstrumentedLine = 254,
|
||||
EnterExecutor = 255,
|
||||
BinaryOpSubtractFloat = 142,
|
||||
BinaryOpSubtractInt = 143,
|
||||
CallAllocAndEnterInit = 144,
|
||||
CallBoundMethodExactArgs = 145,
|
||||
CallBoundMethodGeneral = 146,
|
||||
CallBuiltinClass = 147,
|
||||
CallBuiltinFast = 148,
|
||||
CallBuiltinFastWithKeywords = 149,
|
||||
CallBuiltinO = 150,
|
||||
CallIsinstance = 153,
|
||||
CallKwBoundMethod = 154,
|
||||
CallKwNonPy = 155,
|
||||
CallKwPy = 156,
|
||||
CallLen = 157,
|
||||
CallListAppend = 158,
|
||||
CallMethodDescriptorFast = 159,
|
||||
CallMethodDescriptorFastWithKeywords = 160,
|
||||
CallMethodDescriptorNoargs = 161,
|
||||
CallMethodDescriptorO = 162,
|
||||
CallNonPyGeneral = 163,
|
||||
CallPyExactArgs = 164,
|
||||
CallPyGeneral = 165,
|
||||
CallStr1 = 166,
|
||||
CallTuple1 = 167,
|
||||
CallType1 = 168,
|
||||
CompareOpFloat = 169,
|
||||
CompareOpInt = 170,
|
||||
CompareOpStr = 171,
|
||||
ContainsOpDict = 172,
|
||||
ContainsOpSet = 173,
|
||||
ForIterGen = 174,
|
||||
ForIterList = 175,
|
||||
ForIterRange = 176,
|
||||
ForIterTuple = 177,
|
||||
JumpBackwardJit = 181,
|
||||
JumpBackwardNoJit = 182,
|
||||
LoadAttrClass = 183,
|
||||
LoadAttrClassWithMetaclassCheck = 184,
|
||||
LoadAttrGetattributeOverridden = 185,
|
||||
LoadAttrInstanceValue = 186,
|
||||
LoadAttrMethodLazyDict = 187,
|
||||
LoadAttrMethodNoDict = 188,
|
||||
LoadAttrMethodWithValues = 189,
|
||||
LoadAttrModule = 190,
|
||||
LoadAttrNondescriptorNoDict = 191,
|
||||
LoadAttrNondescriptorWithValues = 192,
|
||||
LoadAttrProperty = 193,
|
||||
LoadAttrSlot = 194,
|
||||
LoadAttrWithHint = 195,
|
||||
LoadConstImmortal = 220,
|
||||
LoadConstMortal = 221,
|
||||
LoadGlobalBuiltin = 196,
|
||||
LoadGlobalModule = 197,
|
||||
LoadSuperAttrAttr = 198,
|
||||
LoadSuperAttrMethod = 199,
|
||||
ResumeCheck = 200,
|
||||
SendGen = 203,
|
||||
StoreAttrInstanceValue = 205,
|
||||
StoreAttrSlot = 206,
|
||||
StoreAttrWithHint = 207,
|
||||
StoreSubscrDict = 208,
|
||||
StoreSubscrListInt = 209,
|
||||
ToBoolAlwaysTrue = 210,
|
||||
ToBoolBool = 211,
|
||||
ToBoolInt = 212,
|
||||
ToBoolList = 213,
|
||||
ToBoolNone = 214,
|
||||
ToBoolStr = 215,
|
||||
UnpackSequenceList = 216,
|
||||
UnpackSequenceTuple = 217,
|
||||
UnpackSequenceTwoTuple = 218,
|
||||
InstrumentedEndFor = 233,
|
||||
InstrumentedPopIter = 234,
|
||||
InstrumentedEndSend = 235,
|
||||
InstrumentedForIter = 236,
|
||||
InstrumentedInstruction = 237,
|
||||
InstrumentedJumpForward = 238,
|
||||
InstrumentedNotTaken = 239,
|
||||
InstrumentedPopJumpIfTrue = 240,
|
||||
InstrumentedPopJumpIfFalse = 241,
|
||||
InstrumentedPopJumpIfNone = 242,
|
||||
InstrumentedPopJumpIfNotNone = 243,
|
||||
InstrumentedResume = 244,
|
||||
InstrumentedReturnValue = 245,
|
||||
InstrumentedYieldValue = 246,
|
||||
InstrumentedEndAsyncFor = 247,
|
||||
InstrumentedLoadSuperAttr = 248,
|
||||
InstrumentedCall = 249,
|
||||
InstrumentedCallKw = 250,
|
||||
InstrumentedCallFunctionEx = 251,
|
||||
InstrumentedJumpBackward = 252,
|
||||
InstrumentedLine = 253,
|
||||
EnterExecutor = 254,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1342,6 +1342,7 @@ const _: () = assert!(core::mem::size_of::<PseudoInstruction>() == 2);
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloc::vec;
|
||||
|
||||
#[test]
|
||||
fn eval_break_flags_match_cpython_jump_metadata() {
|
||||
@@ -1436,10 +1437,71 @@ mod tests {
|
||||
assert!(!AnyInstruction::from(PseudoOpcode::Jump).has_const());
|
||||
}
|
||||
|
||||
include!("target_stack_cases.rs");
|
||||
|
||||
#[test]
|
||||
fn get_iter_modes_normalize_before_publication() {
|
||||
use crate::bytecode::{CodeFlags, CodeUnit, CodeUnits};
|
||||
for flags in [
|
||||
CodeFlags::empty(),
|
||||
CodeFlags::COROUTINE,
|
||||
CodeFlags::ITERABLE_COROUTINE,
|
||||
] {
|
||||
for input in 0..=3u8 {
|
||||
let mut units = CodeUnits::from(vec![
|
||||
CodeUnit::new(Opcode::GetIter.as_instruction(), input.into()),
|
||||
CodeUnit::new(Opcode::Cache.as_instruction(), 0.into()),
|
||||
CodeUnit::new(Opcode::ReturnValue.as_instruction(), 0.into()),
|
||||
]);
|
||||
units.normalize_get_iter(flags);
|
||||
let expected = if input == 0 {
|
||||
0
|
||||
} else if flags.is_empty() {
|
||||
3
|
||||
} else {
|
||||
2
|
||||
};
|
||||
assert_eq!(u8::from(units[0].arg), expected);
|
||||
assert_eq!(units[1].op.as_opcode(), Opcode::Cache);
|
||||
assert_eq!(units[2].op.as_opcode(), Opcode::ReturnValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_load_const_exports_as_public_opcode() {
|
||||
use crate::bytecode::{CodeUnit, CodeUnits};
|
||||
for opcode in [Opcode::LoadConstImmortal, Opcode::LoadConstMortal] {
|
||||
let units = CodeUnits::from(vec![CodeUnit::new(opcode.as_instruction(), 7.into())]);
|
||||
assert_eq!(units.original_bytes(), vec![Opcode::LoadConst.as_u8(), 7]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supported_stack_contract_matches_pinned_c_metadata() {
|
||||
let mut checked = 0;
|
||||
for &(opcode, arg, popped, pushed) in TARGET_STACK_CASES {
|
||||
let Ok(op) = AnyOpcode::try_from(opcode) else {
|
||||
continue;
|
||||
};
|
||||
let actual = match op {
|
||||
AnyOpcode::Real(op) => op.stack_effect_info(arg),
|
||||
AnyOpcode::Pseudo(op) => op.stack_effect_info(arg),
|
||||
};
|
||||
assert_eq!(
|
||||
(actual.popped(), actual.pushed()),
|
||||
(popped, pushed),
|
||||
"opcode {opcode}, argument {arg}"
|
||||
);
|
||||
checked += 1;
|
||||
}
|
||||
assert_eq!(checked, 3290, "supported C stack contract coverage changed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_effects_match_cpython_opcode_metadata() {
|
||||
assert_eq!(Opcode::ForIter.stack_effect_info(0).popped(), 1);
|
||||
assert_eq!(Opcode::ForIter.stack_effect_info(0).pushed(), 2);
|
||||
assert_eq!(Opcode::ForIter.stack_effect_info(0).popped(), 2);
|
||||
assert_eq!(Opcode::ForIter.stack_effect_info(0).pushed(), 3);
|
||||
assert_eq!(Opcode::ForIter.stack_effect(0), 1);
|
||||
assert_eq!(Opcode::ForIter.stack_effect_jump(0), 1);
|
||||
|
||||
@@ -1473,10 +1535,8 @@ mod tests {
|
||||
assert!(AnyInstruction::from(PseudoOpcode::Jump).is_no_fallthrough());
|
||||
}
|
||||
|
||||
/// Snapshot of the chained `match` implementations that `Opcode::deopt`
|
||||
/// and `Opcode::cache_entries` used before they were rewritten as table
|
||||
/// lookups. Exists only to pin the observable behavior of the table
|
||||
/// lookups against the logic they replaced.
|
||||
/// Independent match reference for the pinned final 3.15 cache/deopt contract.
|
||||
/// Keep exhaustive numeric iteration below to check generated lookup tables.
|
||||
mod reference {
|
||||
use super::Opcode;
|
||||
|
||||
@@ -1576,6 +1636,7 @@ mod tests {
|
||||
|
||||
pub(super) const fn cache_entries(op: Opcode) -> usize {
|
||||
match deoptimize(op) {
|
||||
Opcode::Resume | Opcode::GetIter | Opcode::CallFunctionEx => 1,
|
||||
Opcode::StoreSubscr => 1,
|
||||
Opcode::ToBool => 3,
|
||||
Opcode::BinaryOp => 5,
|
||||
|
||||
@@ -756,6 +756,16 @@ oparg_enum!(
|
||||
BuiltinList = (5, "list"),
|
||||
/// Built-in `set` type
|
||||
BuiltinSet = (6, "set"),
|
||||
/// The None singleton.
|
||||
None = (7, "None"),
|
||||
/// The empty string.
|
||||
EmptyStr = (8, "empty string"),
|
||||
/// The true singleton.
|
||||
True = (9, "True"),
|
||||
/// The false singleton.
|
||||
False = (10, "False"),
|
||||
/// Integer minus one.
|
||||
MinusOne = (11, "-1"),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -906,11 +916,11 @@ newtype_oparg!(
|
||||
);
|
||||
|
||||
impl ResumeContext {
|
||||
/// [CPython `RESUME_OPARG_LOCATION_MASK`](https://github.com/python/cpython/blob/v3.14.3/Include/internal/pycore_opcode_utils.h#L84)
|
||||
pub const LOCATION_MASK: u32 = 0x3;
|
||||
/// [CPython `RESUME_OPARG_LOCATION_MASK`](https://github.com/python/cpython/blob/v3.15.0/Include/internal/pycore_opcode_utils.h)
|
||||
pub const LOCATION_MASK: u32 = 0x7;
|
||||
|
||||
/// [CPython `RESUME_OPARG_DEPTH1_MASK`](https://github.com/python/cpython/blob/v3.14.3/Include/internal/pycore_opcode_utils.h#L85)
|
||||
pub const DEPTH1_MASK: u32 = 0x4;
|
||||
/// [CPython `RESUME_OPARG_DEPTH1_MASK`](https://github.com/python/cpython/blob/v3.15.0/Include/internal/pycore_opcode_utils.h)
|
||||
pub const DEPTH1_MASK: u32 = 0x8;
|
||||
|
||||
#[must_use]
|
||||
pub const fn new(location: ResumeLocation, is_exception_depth1: bool) -> Self {
|
||||
@@ -926,8 +936,8 @@ impl ResumeContext {
|
||||
/// Resume location is determined by [`Self::LOCATION_MASK`].
|
||||
#[must_use]
|
||||
pub fn location(&self) -> ResumeLocation {
|
||||
// SAFETY: The mask should return a value that is in range.
|
||||
unsafe { ResumeLocation::try_from(self.as_u32() & Self::LOCATION_MASK).unwrap_unchecked() }
|
||||
ResumeLocation::try_from(self.as_u32() & Self::LOCATION_MASK)
|
||||
.expect("invalid RESUME location")
|
||||
}
|
||||
|
||||
/// True if the bit at [`Self::DEPTH1_MASK`] is on.
|
||||
@@ -947,6 +957,8 @@ pub enum ResumeLocation {
|
||||
AfterYieldFrom,
|
||||
/// After an `await` expression.
|
||||
AfterAwait,
|
||||
/// Eager iterator acquisition before generator-expression creation.
|
||||
AtGenExprStart,
|
||||
}
|
||||
|
||||
impl From<ResumeLocation> for ResumeContext {
|
||||
@@ -964,6 +976,7 @@ impl TryFrom<u32> for ResumeLocation {
|
||||
1 => Self::AfterYield,
|
||||
2 => Self::AfterYieldFrom,
|
||||
3 => Self::AfterAwait,
|
||||
4 => Self::AtGenExprStart,
|
||||
_ => return Err(Self::Error::InvalidBytecode),
|
||||
})
|
||||
}
|
||||
@@ -977,6 +990,7 @@ impl ResumeLocation {
|
||||
Self::AfterYield => 1,
|
||||
Self::AfterYieldFrom => 2,
|
||||
Self::AfterAwait => 3,
|
||||
Self::AtGenExprStart => 4,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+49
-46
@@ -15,15 +15,15 @@ impl super::Opcode {
|
||||
#[inline]
|
||||
pub const fn cache_entries(self) -> usize {
|
||||
const CACHE_ENTRIES: [u8; 256] = [
|
||||
0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 3, 1, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 3, 3,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 4, 4, 1, 1, 0, 1, 4, 4, 4, 1, 1,
|
||||
3, 3, 3, 3, 3, 3, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 3, 3, 0, 1, 0, 0,
|
||||
0, 0, 0, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 1, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 3, 1, 1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0, 5, 5, 3,
|
||||
3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 0, 0, 0, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 4, 4, 1, 1, 1, 0, 0,
|
||||
1, 0, 4, 4, 4, 1, 1, 3, 3, 3, 3, 3, 3, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 3, 3, 1, 1, 0, 0, 0,
|
||||
];
|
||||
|
||||
CACHE_ENTRIES[self.as_numeric() as usize] as usize
|
||||
@@ -174,6 +174,7 @@ impl super::Opcode {
|
||||
Some(super::Opcode::BinaryOp),
|
||||
Some(super::Opcode::BinaryOp),
|
||||
Some(super::Opcode::BinaryOp),
|
||||
None,
|
||||
Some(super::Opcode::BinaryOp),
|
||||
Some(super::Opcode::BinaryOp),
|
||||
Some(super::Opcode::Call),
|
||||
@@ -183,6 +184,8 @@ impl super::Opcode {
|
||||
Some(super::Opcode::Call),
|
||||
Some(super::Opcode::Call),
|
||||
Some(super::Opcode::Call),
|
||||
None,
|
||||
None,
|
||||
Some(super::Opcode::Call),
|
||||
Some(super::Opcode::CallKw),
|
||||
Some(super::Opcode::CallKw),
|
||||
@@ -208,6 +211,9 @@ impl super::Opcode {
|
||||
Some(super::Opcode::ForIter),
|
||||
Some(super::Opcode::ForIter),
|
||||
Some(super::Opcode::ForIter),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(super::Opcode::JumpBackward),
|
||||
Some(super::Opcode::JumpBackward),
|
||||
Some(super::Opcode::LoadAttr),
|
||||
@@ -223,14 +229,15 @@ impl super::Opcode {
|
||||
Some(super::Opcode::LoadAttr),
|
||||
Some(super::Opcode::LoadAttr),
|
||||
Some(super::Opcode::LoadAttr),
|
||||
Some(super::Opcode::LoadConst),
|
||||
Some(super::Opcode::LoadConst),
|
||||
Some(super::Opcode::LoadGlobal),
|
||||
Some(super::Opcode::LoadGlobal),
|
||||
Some(super::Opcode::LoadSuperAttr),
|
||||
Some(super::Opcode::LoadSuperAttr),
|
||||
Some(super::Opcode::Resume),
|
||||
None,
|
||||
None,
|
||||
Some(super::Opcode::Send),
|
||||
None,
|
||||
Some(super::Opcode::StoreAttr),
|
||||
Some(super::Opcode::StoreAttr),
|
||||
Some(super::Opcode::StoreAttr),
|
||||
@@ -246,15 +253,8 @@ impl super::Opcode {
|
||||
Some(super::Opcode::UnpackSequence),
|
||||
Some(super::Opcode::UnpackSequence),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(super::Opcode::LoadConst),
|
||||
Some(super::Opcode::LoadConst),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -327,6 +327,7 @@ impl super::Opcode {
|
||||
| Self::ExtendedArg
|
||||
| Self::ForIter
|
||||
| Self::GetAwaitable
|
||||
| Self::GetIter
|
||||
| Self::ImportFrom
|
||||
| Self::ImportName
|
||||
| Self::IsOp
|
||||
@@ -384,7 +385,6 @@ impl super::Opcode {
|
||||
| Self::CallBuiltinFast
|
||||
| Self::CallBuiltinFastWithKeywords
|
||||
| Self::CallBuiltinO
|
||||
| Self::CallIsinstance
|
||||
| Self::CallKwBoundMethod
|
||||
| Self::CallKwNonPy
|
||||
| Self::CallKwPy
|
||||
@@ -423,8 +423,6 @@ impl super::Opcode {
|
||||
| Self::LoadAttrProperty
|
||||
| Self::LoadAttrSlot
|
||||
| Self::LoadAttrWithHint
|
||||
| Self::LoadConstImmortal
|
||||
| Self::LoadConstMortal
|
||||
| Self::LoadGlobalBuiltin
|
||||
| Self::LoadGlobalModule
|
||||
| Self::LoadSuperAttrAttr
|
||||
@@ -434,6 +432,8 @@ impl super::Opcode {
|
||||
| Self::UnpackSequenceList
|
||||
| Self::UnpackSequenceTuple
|
||||
| Self::UnpackSequenceTwoTuple
|
||||
| Self::LoadConstImmortal
|
||||
| Self::LoadConstMortal
|
||||
| Self::InstrumentedForIter
|
||||
| Self::InstrumentedJumpForward
|
||||
| Self::InstrumentedPopJumpIfTrue
|
||||
@@ -609,23 +609,21 @@ impl super::Opcode {
|
||||
Self::Cache => (0, 0),
|
||||
Self::BinarySlice => (1, 3),
|
||||
Self::BuildTemplate => (1, 2),
|
||||
Self::BinaryOpInplaceAddUnicode => (0, 2),
|
||||
Self::BinaryOpInplaceAddUnicode => (1, 2),
|
||||
Self::CallFunctionEx => (1, 4),
|
||||
Self::CheckEgMatch => (2, 2),
|
||||
Self::CheckExcMatch => (2, 2),
|
||||
Self::CleanupThrow => (2, 3),
|
||||
Self::CleanupThrow => (3, 4),
|
||||
Self::DeleteSubscr => (0, 2),
|
||||
Self::EndFor => (0, 1),
|
||||
Self::EndSend => (1, 2),
|
||||
Self::EndSend => (1, 3),
|
||||
Self::ExitInitCheck => (0, 1),
|
||||
Self::FormatSimple => (1, 1),
|
||||
Self::FormatWithSpec => (1, 2),
|
||||
Self::GetAiter => (1, 1),
|
||||
Self::GetAnext => (2, 1),
|
||||
Self::GetIter => (1, 1),
|
||||
Self::Reserved => (0, 0),
|
||||
Self::GetLen => (2, 1),
|
||||
Self::GetYieldFromIter => (1, 1),
|
||||
Self::Reserved => (0, 0),
|
||||
Self::InterpreterExit => (0, 1),
|
||||
Self::LoadBuildClass => (1, 0),
|
||||
Self::LoadLocals => (1, 0),
|
||||
@@ -636,7 +634,7 @@ impl super::Opcode {
|
||||
Self::Nop => (0, 0),
|
||||
Self::NotTaken => (0, 0),
|
||||
Self::PopExcept => (0, 1),
|
||||
Self::PopIter => (0, 1),
|
||||
Self::PopIter => (0, 2),
|
||||
Self::PopTop => (0, 1),
|
||||
Self::PushExcInfo => (2, 1),
|
||||
Self::PushNull => (1, 0),
|
||||
@@ -676,8 +674,9 @@ impl super::Opcode {
|
||||
Self::DictUpdate => (1 + (oparg - 1), 2 + (oparg - 1)),
|
||||
Self::EndAsyncFor => (0, 2),
|
||||
Self::ExtendedArg => (0, 0),
|
||||
Self::ForIter => (2, 1),
|
||||
Self::ForIter => (3, 2),
|
||||
Self::GetAwaitable => (1, 1),
|
||||
Self::GetIter => (2, 1),
|
||||
Self::ImportFrom => (2, 1),
|
||||
Self::ImportName => (1, 2),
|
||||
Self::IsOp => (1, 2),
|
||||
@@ -712,7 +711,7 @@ impl super::Opcode {
|
||||
Self::PopJumpIfTrue => (0, 1),
|
||||
Self::RaiseVarargs => (0, oparg),
|
||||
Self::Reraise => (oparg, 1 + oparg),
|
||||
Self::Send => (2, 2),
|
||||
Self::Send => (3, 3),
|
||||
Self::SetAdd => (1 + (oparg - 1), 2 + (oparg - 1)),
|
||||
Self::SetFunctionAttribute => (1, 2),
|
||||
Self::SetUpdate => (1 + (oparg - 1), 2 + (oparg - 1)),
|
||||
@@ -749,12 +748,12 @@ impl super::Opcode {
|
||||
Self::CallBuiltinFast => (1, 2 + oparg),
|
||||
Self::CallBuiltinFastWithKeywords => (1, 2 + oparg),
|
||||
Self::CallBuiltinO => (1, 2 + oparg),
|
||||
Self::CallIsinstance => (1, 2 + oparg),
|
||||
Self::CallIsinstance => (1, 4),
|
||||
Self::CallKwBoundMethod => (0, 3 + oparg),
|
||||
Self::CallKwNonPy => (1, 3 + oparg),
|
||||
Self::CallKwPy => (0, 3 + oparg),
|
||||
Self::CallLen => (1, 3),
|
||||
Self::CallListAppend => (0, 3),
|
||||
Self::CallListAppend => (1, 3),
|
||||
Self::CallMethodDescriptorFast => (1, 2 + oparg),
|
||||
Self::CallMethodDescriptorFastWithKeywords => (1, 2 + oparg),
|
||||
Self::CallMethodDescriptorNoargs => (1, 2 + oparg),
|
||||
@@ -770,15 +769,15 @@ impl super::Opcode {
|
||||
Self::CompareOpStr => (1, 2),
|
||||
Self::ContainsOpDict => (1, 2),
|
||||
Self::ContainsOpSet => (1, 2),
|
||||
Self::ForIterGen => (1, 1),
|
||||
Self::ForIterList => (2, 1),
|
||||
Self::ForIterRange => (2, 1),
|
||||
Self::ForIterTuple => (2, 1),
|
||||
Self::ForIterGen => (2, 2),
|
||||
Self::ForIterList => (3, 2),
|
||||
Self::ForIterRange => (3, 2),
|
||||
Self::ForIterTuple => (3, 2),
|
||||
Self::JumpBackwardJit => (0, 0),
|
||||
Self::JumpBackwardNoJit => (0, 0),
|
||||
Self::LoadAttrClass => (1 + (oparg & 1), 1),
|
||||
Self::LoadAttrClassWithMetaclassCheck => (1 + (oparg & 1), 1),
|
||||
Self::LoadAttrGetattributeOverridden => (1, 1),
|
||||
Self::LoadAttrGetattributeOverridden => (0, 1),
|
||||
Self::LoadAttrInstanceValue => (1 + (oparg & 1), 1),
|
||||
Self::LoadAttrMethodLazyDict => (2, 1),
|
||||
Self::LoadAttrMethodNoDict => (2, 1),
|
||||
@@ -789,14 +788,12 @@ impl super::Opcode {
|
||||
Self::LoadAttrProperty => (0, 1),
|
||||
Self::LoadAttrSlot => (1 + (oparg & 1), 1),
|
||||
Self::LoadAttrWithHint => (1 + (oparg & 1), 1),
|
||||
Self::LoadConstImmortal => (1, 0),
|
||||
Self::LoadConstMortal => (1, 0),
|
||||
Self::LoadGlobalBuiltin => (1 + (oparg & 1), 0),
|
||||
Self::LoadGlobalModule => (1 + (oparg & 1), 0),
|
||||
Self::LoadSuperAttrAttr => (1, 3),
|
||||
Self::LoadSuperAttrMethod => (2, 3),
|
||||
Self::ResumeCheck => (0, 0),
|
||||
Self::SendGen => (1, 2),
|
||||
Self::SendGen => (2, 3),
|
||||
Self::StoreAttrInstanceValue => (0, 2),
|
||||
Self::StoreAttrSlot => (0, 2),
|
||||
Self::StoreAttrWithHint => (0, 2),
|
||||
@@ -811,10 +808,16 @@ impl super::Opcode {
|
||||
Self::UnpackSequenceList => (oparg, 1),
|
||||
Self::UnpackSequenceTuple => (oparg, 1),
|
||||
Self::UnpackSequenceTwoTuple => (2, 1),
|
||||
Self::InstrumentedEndFor => (1, 2),
|
||||
Self::InstrumentedPopIter => (0, 1),
|
||||
Self::InstrumentedEndSend => (1, 2),
|
||||
Self::InstrumentedForIter => (2, 1),
|
||||
Self::GetYieldFromIter => (
|
||||
1, // TODO: Differs from CPython `None`
|
||||
1, // TODO: Differs from CPython `None`
|
||||
),
|
||||
Self::LoadConstImmortal => (1, 0),
|
||||
Self::LoadConstMortal => (1, 0),
|
||||
Self::InstrumentedEndFor => (2, 3),
|
||||
Self::InstrumentedPopIter => (0, 2),
|
||||
Self::InstrumentedEndSend => (1, 3),
|
||||
Self::InstrumentedForIter => (3, 2),
|
||||
Self::InstrumentedInstruction => (0, 0),
|
||||
Self::InstrumentedJumpForward => (0, 0),
|
||||
Self::InstrumentedNotTaken => (0, 0),
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -663,6 +663,21 @@ impl<'a, 'b> FunctionCompiler<'a, 'b> {
|
||||
self.stack.push(val);
|
||||
Ok(())
|
||||
}
|
||||
Instruction::LoadCommonConstant { idx } => {
|
||||
let val = match idx.get(arg) {
|
||||
oparg::CommonConstant::None => JitValue::None,
|
||||
oparg::CommonConstant::True | oparg::CommonConstant::False => {
|
||||
let value = i64::from(idx.get(arg) == oparg::CommonConstant::True);
|
||||
JitValue::Bool(self.builder.ins().iconst(types::I8, value))
|
||||
}
|
||||
oparg::CommonConstant::MinusOne => {
|
||||
JitValue::Int(self.builder.ins().iconst(types::I64, -1))
|
||||
}
|
||||
_ => return Err(JitCompileError::NotSupported),
|
||||
};
|
||||
self.stack.push(val);
|
||||
Ok(())
|
||||
}
|
||||
Instruction::LoadSmallInt { i } => {
|
||||
let small_int = i.get(arg) as i64;
|
||||
let val = self.builder.ins().iconst(types::I64, small_int);
|
||||
@@ -1508,3 +1523,46 @@ impl<'a, 'b> FunctionCompiler<'a, 'b> {
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bytecode::{BasicBag, ConstantData};
|
||||
|
||||
#[test]
|
||||
fn common_constant_return_types_and_unsupported_domain() {
|
||||
let frozen = rustpython_derive::py_compile!(
|
||||
crate_name = "rustpython_compiler_core",
|
||||
source = "def nothing():\n return None\ndef truth():\n return True\ndef falsehood():\n return False\ndef minus_one():\n return -1\ndef empty_string():\n return ''\n"
|
||||
);
|
||||
let module = frozen.decode(BasicBag);
|
||||
let mut checked = 0;
|
||||
for constant in module.constants.iter() {
|
||||
let ConstantData::Code { code } = constant else {
|
||||
continue;
|
||||
};
|
||||
let (common, return_type) = match code.obj_name.as_str() {
|
||||
"nothing" => (oparg::CommonConstant::None, Some(JitType::None)),
|
||||
"truth" => (oparg::CommonConstant::True, Some(JitType::Bool)),
|
||||
"falsehood" => (oparg::CommonConstant::False, Some(JitType::Bool)),
|
||||
"minus_one" => (oparg::CommonConstant::MinusOne, Some(JitType::Int)),
|
||||
"empty_string" => (oparg::CommonConstant::EmptyStr, None),
|
||||
name => panic!("unexpected fixture code: {name}"),
|
||||
};
|
||||
let mut state = OpArgState::default();
|
||||
assert!(code.instructions.iter().copied().any(|unit| {
|
||||
let (instruction, arg) = state.get(unit);
|
||||
matches!(instruction, Instruction::LoadCommonConstant { idx } if idx.get(arg) == common)
|
||||
}));
|
||||
let result = crate::compile(code, &[], None);
|
||||
if let Some(expected) = return_type {
|
||||
let compiled = result.expect("supported common constant must compile");
|
||||
assert_eq!(compiled.sig.ret, Some(expected));
|
||||
} else {
|
||||
assert!(matches!(result, Err(JitCompileError::NotSupported)));
|
||||
}
|
||||
checked += 1;
|
||||
}
|
||||
assert_eq!(checked, 5);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use core::ops::ControlFlow;
|
||||
use rustpython_compiler_core::bytecode::{
|
||||
CodeObject, ConstantData, Constants, Instruction, OpArg, OpArgState,
|
||||
CodeObject, CommonConstant, ConstantData, Constants, Instruction, OpArg, OpArgState,
|
||||
};
|
||||
use rustpython_jit::{CompiledCode, JitType};
|
||||
use rustpython_wtf8::{Wtf8, Wtf8Buf};
|
||||
@@ -207,6 +207,15 @@ impl StackMachine {
|
||||
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())),
|
||||
|
||||
@@ -511,7 +511,8 @@ impl MemberLayout for CodeFlags {
|
||||
}
|
||||
|
||||
impl PyCode {
|
||||
pub fn new(code: CodeObject) -> Self {
|
||||
pub fn new(mut code: CodeObject) -> Self {
|
||||
code.instructions.normalize_get_iter(code.flags);
|
||||
let sp = code.source_path as *const PyStrInterned as *mut PyStrInterned;
|
||||
let nlocals = i32::try_from(code.varnames.len()).unwrap_or(i32::MAX);
|
||||
let localsplus_names = {
|
||||
|
||||
@@ -174,7 +174,7 @@ impl Destructor for PyCoroutine {
|
||||
if zelf.inner.closed() || zelf.inner.running() {
|
||||
return Ok(());
|
||||
}
|
||||
if zelf.inner.frame_opt().is_none_or(|f| f.lasti() == 0) {
|
||||
if !zelf.inner.started() {
|
||||
crate::warn::warn_unawaited_coroutine(zelf.as_object(), &zelf.inner.qualname(), vm);
|
||||
zelf.inner.closed.store(true);
|
||||
return Ok(());
|
||||
|
||||
@@ -242,7 +242,14 @@ pub(crate) mod stack_analysis {
|
||||
}
|
||||
}
|
||||
}
|
||||
Instruction::GetIter | Instruction::GetAiter => {
|
||||
Instruction::GetIter { .. } => {
|
||||
next_stack = push_value(pop_value(next_stack), Kind::Iterator as i64);
|
||||
next_stack = push_value(next_stack, Kind::Iterator as i64);
|
||||
if next_i < stacks.len() {
|
||||
stacks[next_i] = next_stack;
|
||||
}
|
||||
}
|
||||
Instruction::GetAiter => {
|
||||
next_stack = push_value(pop_value(next_stack), Kind::Iterator as i64);
|
||||
if next_i < stacks.len() {
|
||||
stacks[next_i] = next_stack;
|
||||
@@ -254,19 +261,13 @@ pub(crate) mod stack_analysis {
|
||||
if next_i < stacks.len() {
|
||||
stacks[next_i] = body_stack;
|
||||
}
|
||||
// Exhaustion path: relative forward from after_caches
|
||||
let mut j = next_i + oparg as usize;
|
||||
if j < instructions.len() {
|
||||
let target_op =
|
||||
instructions[j].op.to_base().unwrap_or(instructions[j].op);
|
||||
if matches!(target_op, Instruction::EndFor) {
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
// CPython models the target END_FOR with the yielded-value slot.
|
||||
let j = next_i + oparg as usize;
|
||||
if j < stacks.len() && stacks[j] == UNINITIALIZED {
|
||||
stacks[j] = next_stack;
|
||||
stacks[j] = body_stack;
|
||||
}
|
||||
}
|
||||
|
||||
Instruction::EndAsyncFor => {
|
||||
next_stack = pop_value(pop_value(next_stack));
|
||||
if next_i < stacks.len() {
|
||||
@@ -630,6 +631,11 @@ impl Py<FrameObject> {
|
||||
let mut cur = crate::vm::thread::get_current_frame();
|
||||
while !cur.is_null() {
|
||||
if core::ptr::eq(cur, prev) {
|
||||
// SAFETY: cur was found on this thread's live active chain.
|
||||
let cur = unsafe { crate::frame::first_complete_iframe(cur) };
|
||||
if cur.is_null() {
|
||||
return None;
|
||||
}
|
||||
let iframe_ref = unsafe { &*cur };
|
||||
let fo = iframe_ref.materialize(vm);
|
||||
return Some(fo.to_owned());
|
||||
@@ -652,6 +658,11 @@ impl Py<FrameObject> {
|
||||
// return and free the stack-allocated iframe at any time.
|
||||
vm.state.stop_the_world.stop_the_world(&vm.state);
|
||||
scopeguard::defer! { vm.state.stop_the_world.start_the_world(&vm.state); }
|
||||
// SAFETY: the world is stopped, preserving the live chain.
|
||||
let prev = unsafe { crate::frame::first_complete_iframe(prev) };
|
||||
if prev.is_null() {
|
||||
return None;
|
||||
}
|
||||
let prev_ref = unsafe { &*prev };
|
||||
// Fast path: already materialized.
|
||||
if let Some(fo) = prev_ref.frame_obj() {
|
||||
@@ -659,7 +670,7 @@ impl Py<FrameObject> {
|
||||
}
|
||||
// Slow path: copy the whole chain, linked through retained_back.
|
||||
// SAFETY: the world is stopped, so the owning thread is parked.
|
||||
let fo = unsafe { prev_ref.materialize_detached_chain(vm) };
|
||||
let fo = unsafe { prev_ref.materialize_complete_detached_chain(vm) };
|
||||
return Some(fo);
|
||||
}
|
||||
|
||||
|
||||
@@ -635,6 +635,7 @@ impl Py<PyFunction> {
|
||||
);
|
||||
self.fill_locals_from_args(&frame, func_args, vm)?;
|
||||
if is_gen || is_coro || is_async_gen {
|
||||
vm.with_generator_frame(frame.clone(), |f| f.prepare_generator(vm))?;
|
||||
return Ok(self.make_generator_or_coro(frame, vm));
|
||||
}
|
||||
// Tracing active: use heap frame with full trace support.
|
||||
@@ -842,7 +843,7 @@ impl Py<PyFunction> {
|
||||
&self,
|
||||
args: impl ExactSizeIterator<Item = PyObjectRef>,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyObjectRef {
|
||||
) -> PyResult {
|
||||
let code: PyRef<PyCode> = (*self.code).to_owned();
|
||||
|
||||
debug_assert_eq!(args.len(), code.arg_count as usize);
|
||||
@@ -884,7 +885,8 @@ impl Py<PyFunction> {
|
||||
}
|
||||
}
|
||||
|
||||
self.make_generator_or_coro(frame, vm)
|
||||
vm.with_generator_frame(frame.clone(), |f| f.prepare_generator(vm))?;
|
||||
Ok(self.make_generator_or_coro(frame, vm))
|
||||
}
|
||||
|
||||
pub(crate) fn invoke_prepared_exact_args(
|
||||
@@ -944,7 +946,7 @@ impl Py<PyFunction> {
|
||||
// specialization classification, but calling one produces a
|
||||
// generator/coroutine object instead of running the frame.
|
||||
if self.is_generator_like() {
|
||||
return Ok(self.make_generator_exact_args(args.into_iter(), vm));
|
||||
return self.make_generator_exact_args(args.into_iter(), vm);
|
||||
}
|
||||
self.invoke_prepared_exact_args(args.into_iter(), vm)
|
||||
}
|
||||
@@ -966,7 +968,7 @@ impl Py<PyFunction> {
|
||||
// specialization classification, but calling one produces a
|
||||
// generator/coroutine object instead of running the frame.
|
||||
if self.is_generator_like() {
|
||||
return Ok(self.make_generator_exact_args(taken, vm));
|
||||
return self.make_generator_exact_args(taken, vm);
|
||||
}
|
||||
self.invoke_prepared_exact_args(taken, vm)
|
||||
}
|
||||
@@ -1777,7 +1779,7 @@ pub(crate) fn vectorcall_function(
|
||||
// is called in, and a fresh `MAKE_FUNCTION` each time keeps those out
|
||||
// of the call-site specialization that would otherwise catch it.
|
||||
args.truncate(nargs);
|
||||
return Ok(zelf.make_generator_exact_args(args.into_iter(), vm));
|
||||
return zelf.make_generator_exact_args(args.into_iter(), vm);
|
||||
}
|
||||
|
||||
if !has_kwargs && base_simple && nargs == code.arg_count as usize {
|
||||
|
||||
+20
-17
@@ -40,6 +40,8 @@ pub struct Coro {
|
||||
code: PyRef<PyCode>,
|
||||
pub closed: AtomicCell<bool>, // TODO: https://github.com/RustPython/RustPython/pull/3183#discussion_r720560652
|
||||
running: AtomicCell<bool>,
|
||||
/// Whether execution has resumed beyond the eager creation preamble.
|
||||
started: AtomicCell<bool>,
|
||||
// _weakreflist
|
||||
name: PyMutex<PyStrRef>,
|
||||
qualname: PyMutex<PyStrRef>,
|
||||
@@ -159,16 +161,16 @@ pub(crate) fn flat_resume_enter(
|
||||
if coro.closed.load() {
|
||||
return Ok(FlatEnter::Exhausted);
|
||||
}
|
||||
let value = if coro.frame_opt().is_some_and(|f| f.lasti() > 0) {
|
||||
Some(value)
|
||||
} else if !vm.is_none(&value) {
|
||||
if !coro.started() && !vm.is_none(&value) {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"can't send non-None value to a just-started {}",
|
||||
gen_name(&jen, vm),
|
||||
)));
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
let value = coro
|
||||
.frame_opt()
|
||||
.is_some_and(|f| f.lasti() > 0)
|
||||
.then_some(value);
|
||||
// SAFETY: exclusive access guaranteed by the claim
|
||||
let gen_exc = unsafe { coro.exception.swap(None) };
|
||||
let frame_ref = coro.frame();
|
||||
@@ -184,6 +186,7 @@ pub(crate) fn flat_resume_enter(
|
||||
return Err(exc);
|
||||
}
|
||||
};
|
||||
coro.started.store(true);
|
||||
// The claim outlives this scope; `flat_resume_exit` drops it.
|
||||
core::mem::forget(_claim);
|
||||
Ok(FlatEnter::Entered {
|
||||
@@ -234,6 +237,7 @@ impl Coro {
|
||||
code,
|
||||
closed: AtomicCell::new(false),
|
||||
running: AtomicCell::new(false),
|
||||
started: AtomicCell::new(false),
|
||||
exception: PyAtomicRef::from(None),
|
||||
name: PyMutex::new(name),
|
||||
qualname: PyMutex::new(qualname),
|
||||
@@ -307,6 +311,7 @@ impl Coro {
|
||||
|
||||
let frame = self.frame();
|
||||
vm.resume_gen_frame(&frame, gen_exc, |f| {
|
||||
self.started.store(true);
|
||||
let result = func(f);
|
||||
// SAFETY: exclusive access guaranteed by the claim
|
||||
let _old = unsafe { (*exception_ptr).swap(vm.current_exception()) };
|
||||
@@ -401,16 +406,16 @@ impl Coro {
|
||||
if self.closed.load() {
|
||||
return Self::send_when_closed(jen, vm);
|
||||
}
|
||||
let value = if self.frame_opt().is_some_and(|f| f.lasti() > 0) {
|
||||
Some(value)
|
||||
} else if !vm.is_none(&value) {
|
||||
if !self.started() && !vm.is_none(&value) {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"can't send non-None value to a just-started {}",
|
||||
gen_name(jen, vm),
|
||||
)));
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
let value = self
|
||||
.frame_opt()
|
||||
.is_some_and(|f| f.lasti() > 0)
|
||||
.then_some(value);
|
||||
let result = self.run_claimed(&claim, vm, |f| f.resume(value, vm));
|
||||
self.maybe_close(&result, &claim);
|
||||
drop(claim);
|
||||
@@ -462,7 +467,7 @@ impl Coro {
|
||||
return Ok(vm.ctx.none());
|
||||
}
|
||||
// FRAME_CREATED: mark finished and clear the iframe.
|
||||
if self.frame_opt().is_none_or(|f| f.lasti() == 0) {
|
||||
if !self.started() {
|
||||
self.closed.store(true);
|
||||
self.clear_except_code();
|
||||
return Ok(vm.ctx.none());
|
||||
@@ -493,13 +498,11 @@ impl Coro {
|
||||
/// Whether the frame has run at all, i.e. is stopped at a `yield` rather
|
||||
/// than at its start.
|
||||
pub(crate) fn started(&self) -> bool {
|
||||
self.frame_opt().is_some_and(|f| f.lasti() > 0)
|
||||
self.started.load()
|
||||
}
|
||||
|
||||
pub fn suspended(&self) -> bool {
|
||||
!self.closed.load()
|
||||
&& !self.running.load()
|
||||
&& self.frame_opt().is_some_and(|f| f.lasti() > 0)
|
||||
!self.closed.load() && !self.running.load() && self.started()
|
||||
}
|
||||
|
||||
pub fn running(&self) -> bool {
|
||||
|
||||
+260
-100
@@ -20,6 +20,7 @@ use crate::{
|
||||
},
|
||||
bytecode::{
|
||||
self, ADAPTIVE_COOLDOWN_VALUE, Instruction, LoadAttr, LoadSuperAttr, Opcode, SpecialMethod,
|
||||
oparg::{ResumeContext, ResumeLocation},
|
||||
},
|
||||
convert::{ToPyObject, ToPyResult},
|
||||
coroutine::Coro,
|
||||
@@ -105,6 +106,39 @@ pub fn current_thread_frame_materialize(vm: &VirtualMachine) -> Option<FrameObje
|
||||
Some(iframe.materialize(vm).to_owned())
|
||||
}
|
||||
|
||||
/// Find the first complete frame on a live raw execution chain.
|
||||
///
|
||||
/// # Safety
|
||||
/// Every non-null pointer traversed must belong to a live active frame chain
|
||||
/// on the current thread, or the caller must hold the owning thread stopped.
|
||||
/// The returned pointer is valid only while that chain remains alive.
|
||||
pub(crate) unsafe fn first_complete_iframe(
|
||||
mut current: *const InterpreterFrame,
|
||||
) -> *const InterpreterFrame {
|
||||
while !current.is_null() {
|
||||
// SAFETY: the caller guarantees the chain remains alive.
|
||||
let frame = unsafe { &*current };
|
||||
if !frame.is_incomplete_for_trace() {
|
||||
break;
|
||||
}
|
||||
current = frame.previous();
|
||||
}
|
||||
current
|
||||
}
|
||||
|
||||
/// Resolve the active frame for a legacy trace/profile callback, skipping
|
||||
/// incomplete generator-creation frames before materializing a frame object.
|
||||
/// Raw execution-chain users deliberately keep seeing the actual active frame.
|
||||
pub(crate) fn current_complete_frame_for_trace(vm: &VirtualMachine) -> Option<FrameObjectRef> {
|
||||
// SAFETY: synchronous inspection of the current thread's active chain.
|
||||
let current = unsafe { first_complete_iframe(crate::vm::thread::get_current_frame()) };
|
||||
if current.is_null() {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: the selected frame is alive on this thread's execution chain.
|
||||
Some(unsafe { (*current).materialize(vm).to_owned() })
|
||||
}
|
||||
|
||||
/// Read the globals dict from the topmost frame on this thread's chain.
|
||||
/// Returns `None` if the chain is empty.
|
||||
#[must_use]
|
||||
@@ -137,12 +171,13 @@ pub fn current_builtins() -> Option<PyObjectRef> {
|
||||
Some(unsafe { (*ptr).builtins().to_owned() })
|
||||
}
|
||||
|
||||
/// The frame `offset` positions below the current thread's top frame (offset 0
|
||||
/// is the top), or `None` if the stack is not that deep.
|
||||
/// The complete frame `offset` positions below the current thread's first
|
||||
/// complete frame, or `None` if the stack is not that deep.
|
||||
/// Materializes the FrameObject on demand for stack-allocated frames.
|
||||
#[must_use]
|
||||
pub fn frame_at_offset(offset: usize, vm: &VirtualMachine) -> Option<FrameObjectRef> {
|
||||
let mut cur = crate::vm::thread::get_current_frame();
|
||||
// SAFETY: synchronous inspection of this thread's active chain.
|
||||
let mut cur = unsafe { first_complete_iframe(crate::vm::thread::get_current_frame()) };
|
||||
let mut remaining = offset;
|
||||
while !cur.is_null() {
|
||||
if remaining == 0 {
|
||||
@@ -150,7 +185,8 @@ pub fn frame_at_offset(offset: usize, vm: &VirtualMachine) -> Option<FrameObject
|
||||
return Some(iframe.materialize(vm).to_owned());
|
||||
}
|
||||
remaining -= 1;
|
||||
cur = unsafe { (*cur).previous.load(Relaxed) as *const InterpreterFrame };
|
||||
// SAFETY: `cur` and its predecessors remain alive on this chain.
|
||||
cur = unsafe { first_complete_iframe((*cur).previous()) };
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -1155,6 +1191,40 @@ impl InterpreterFrame {
|
||||
Some((base, total_bytes))
|
||||
}
|
||||
|
||||
/// CPython's incomplete-frame boundary for legacy tracing. Generator
|
||||
/// frames are complete even before their first resume. This VM temporarily
|
||||
/// changes their owner to Thread while executing, so also recognize the
|
||||
/// owning generator's back-reference during that interval.
|
||||
pub(crate) fn is_incomplete_for_trace(&self) -> bool {
|
||||
if FrameOwner::from_i8(self.owner.load(atomic::Ordering::Acquire)) == FrameOwner::Generator
|
||||
|| self.generator.load().is_some()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let instructions = &self.code().instructions;
|
||||
let lasti = self.lasti.load(Relaxed) as usize;
|
||||
let mut index = 0;
|
||||
while index < instructions.len() {
|
||||
let unit = instructions.read_unit(index);
|
||||
if matches!(
|
||||
unit.op,
|
||||
Instruction::Resume { .. }
|
||||
| Instruction::ResumeCheck
|
||||
| Instruction::InstrumentedResume
|
||||
) && u32::from(u8::from(unit.arg)) & ResumeContext::LOCATION_MASK
|
||||
!= ResumeLocation::AtGenExprStart.as_u32()
|
||||
{
|
||||
// lasti is one past the in-flight instruction. At RESUME
|
||||
// itself, lasti == index + 1 and the frame is complete.
|
||||
return lasti <= index;
|
||||
}
|
||||
// Prefixes before the first traceable RESUME are uninstrumented.
|
||||
// Skip inline cache payloads rather than decoding them as opcodes.
|
||||
index += 1 + unit.op.cache_entries();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Get the last instruction index.
|
||||
#[inline(always)]
|
||||
pub fn get_lasti(&self) -> u32 {
|
||||
@@ -1228,10 +1298,43 @@ impl InterpreterFrame {
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
pub(crate) unsafe fn materialize_detached_chain(&self, vm: &VirtualMachine) -> FrameObjectRef {
|
||||
// SAFETY: forwarded unchanged from this method's contract.
|
||||
unsafe { self.materialize_detached_chain_impl::<false>(vm) }
|
||||
}
|
||||
|
||||
/// Copy the complete ancestors used by Python-visible `f_back` traversal.
|
||||
/// The raw snapshot path above deliberately preserves every active node.
|
||||
///
|
||||
/// # Safety
|
||||
/// The caller must hold the world stopped and select a complete top frame.
|
||||
#[cfg(feature = "threading")]
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
pub(crate) unsafe fn materialize_complete_detached_chain(
|
||||
&self,
|
||||
vm: &VirtualMachine,
|
||||
) -> FrameObjectRef {
|
||||
// SAFETY: forwarded unchanged from this method's contract.
|
||||
unsafe { self.materialize_detached_chain_impl::<true>(vm) }
|
||||
}
|
||||
|
||||
#[cfg(feature = "threading")]
|
||||
#[cold]
|
||||
unsafe fn materialize_detached_chain_impl<const SKIP_INCOMPLETE: bool>(
|
||||
&self,
|
||||
vm: &VirtualMachine,
|
||||
) -> FrameObjectRef {
|
||||
let top = unsafe { self.materialize_detached(vm) };
|
||||
let mut child = top.clone();
|
||||
let mut cur = self.previous();
|
||||
while !cur.is_null() {
|
||||
loop {
|
||||
if SKIP_INCOMPLETE {
|
||||
// SAFETY: the caller holds the owning thread stopped.
|
||||
cur = unsafe { first_complete_iframe(cur) };
|
||||
}
|
||||
if cur.is_null() {
|
||||
break;
|
||||
}
|
||||
let caller = unsafe { &*cur };
|
||||
let caller_fo = unsafe { caller.materialize_detached(vm) };
|
||||
{
|
||||
@@ -1756,6 +1859,7 @@ pub enum ExecutionResult {
|
||||
/// CACHE <- the parked frame's lasti is here + 1
|
||||
/// YIELD_VALUE 1
|
||||
/// resumed_at: RESUME
|
||||
/// CACHE
|
||||
/// JUMP_BACKWARD_NO_INTERRUPT -> send_idx
|
||||
/// exit: END_SEND
|
||||
/// ```
|
||||
@@ -2529,6 +2633,18 @@ impl Py<FrameObject> {
|
||||
self.with_exec(vm, |mut exec| exec.run(vm))
|
||||
}
|
||||
|
||||
/// Execute the eager generator preamble and suspend immediately after
|
||||
/// RETURN_GENERATOR. The caller links this frame with `vm.with_generator_frame`.
|
||||
/// The first resume supplies the value consumed by the following POP_TOP.
|
||||
pub(crate) fn prepare_generator(&self, vm: &VirtualMachine) -> PyResult<()> {
|
||||
self.with_exec(vm, |mut exec| match exec.run_until::<true>(vm)? {
|
||||
None => Ok(()),
|
||||
Some(_) => {
|
||||
Err(vm.new_runtime_error("generator preamble did not reach RETURN_GENERATOR"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Resume a suspended generator or coroutine body, pushing `value` as the
|
||||
/// result of the `yield` it stopped at.
|
||||
///
|
||||
@@ -2788,10 +2904,10 @@ pub(crate) fn gen_collapse_allowed(vm: &VirtualMachine) -> bool {
|
||||
/// delegating to, if the trampoline can resume it in this frame's place.
|
||||
///
|
||||
/// Recognizes the shape documented on [`GenCont`] — `lasti` at the `RESUME`
|
||||
/// that follows the delegating `YIELD_VALUE`, with the delegate on top of the
|
||||
/// stack — and requires the `SEND` to have already specialized to `SendGen`,
|
||||
/// so that a `Send` still collecting specialization feedback keeps running
|
||||
/// normally. The frame is left untouched; `park_at_send` commits to it.
|
||||
/// that follows the delegating `YIELD_VALUE`, with the delegate below its
|
||||
/// index-or-NULL stack slot — and requires the `SEND` to have already
|
||||
/// specialized to `SendGen`, so that a `Send` still collecting feedback keeps
|
||||
/// running normally. The frame is left untouched; `park_at_send` commits to it.
|
||||
pub(crate) fn yield_from_delegate(
|
||||
iframe: &InterpreterFrame,
|
||||
vm: &VirtualMachine,
|
||||
@@ -2824,10 +2940,11 @@ pub(crate) fn yield_from_delegate(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let delegate = match iframe.localsplus.stack_last() {
|
||||
Some(Some(top)) => top.as_object(),
|
||||
_ => return None,
|
||||
};
|
||||
let stack = iframe.localsplus.stack_as_slice();
|
||||
let delegate = stack
|
||||
.get(stack.len().checked_sub(2)?)?
|
||||
.as_ref()?
|
||||
.as_object();
|
||||
// The same guards `SendGen` applies before resuming a generator itself.
|
||||
if delegate.downcast_ref_if_exact::<PyGenerator>(vm).is_none()
|
||||
&& delegate.downcast_ref_if_exact::<PyCoroutine>(vm).is_none()
|
||||
@@ -3072,7 +3189,8 @@ pub(crate) fn release_datastack_frame(frame: &Py<FrameObject>, vm: &VirtualMachi
|
||||
{
|
||||
let mut guard = frame.iframe().cold().retained_back.lock();
|
||||
if guard.is_none() {
|
||||
let prev = frame.previous_iframe();
|
||||
// SAFETY: this frame is returning into its still-active caller.
|
||||
let prev = unsafe { first_complete_iframe(frame.previous_iframe()) };
|
||||
*guard = unsafe { owned_chain_frame(prev) };
|
||||
}
|
||||
}
|
||||
@@ -3440,7 +3558,11 @@ impl ExecutingFrame<'_> {
|
||||
/// (COPY_FREE_VARS / RETURN_GENERATOR that precede it are not a 'call',
|
||||
/// and later RESUMEs in a SEND loop are not a new call either).
|
||||
fn trace_call_from_resume(&mut self, vm: &VirtualMachine, resume_type: u32) -> PyResult<()> {
|
||||
if self.call_traced {
|
||||
// RESUME_AT_GEN_EXPR_START runs before the frame is complete. The
|
||||
// first traceable instruction is the ordinary RESUME after creation.
|
||||
if resume_type & ResumeContext::LOCATION_MASK == ResumeLocation::AtGenExprStart.as_u32()
|
||||
|| self.call_traced
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
self.call_traced = true;
|
||||
@@ -3626,7 +3748,22 @@ impl ExecutingFrame<'_> {
|
||||
/// Matches `_PyEval_MonitorRaise` → `PY_MONITORING_EVENT_RAISE` →
|
||||
/// `sys_trace_exception_func` in legacy_tracing.c.
|
||||
fn fire_exception_trace(&self, exc: &Py<PyBaseException>, vm: &VirtualMachine) -> PyResult<()> {
|
||||
if vm.use_tracing.get() && self.trace_is_set(vm) {
|
||||
if !vm.use_tracing.get() {
|
||||
return Ok(());
|
||||
}
|
||||
let trace_is_set = if self.iframe().is_incomplete_for_trace() {
|
||||
// Legacy tracing resolves PyEval_GetFrame to the first complete
|
||||
// caller while sys.monitoring continues to use this code object.
|
||||
current_complete_frame_for_trace(vm).is_some_and(|frame| {
|
||||
frame
|
||||
.iframe()
|
||||
.cold_opt()
|
||||
.is_some_and(|cold| cold.trace.lock().is_some())
|
||||
})
|
||||
} else {
|
||||
self.trace_is_set(vm)
|
||||
};
|
||||
if trace_is_set {
|
||||
let exc_type: PyObjectRef = exc.class().to_owned().into();
|
||||
let exc_value: PyObjectRef = exc.to_owned().into();
|
||||
let exc_tb: PyObjectRef = exc
|
||||
@@ -3639,6 +3776,14 @@ impl ExecutingFrame<'_> {
|
||||
}
|
||||
|
||||
fn run(&mut self, vm: &VirtualMachine) -> PyResult<ExecutionResult> {
|
||||
self.run_until::<false>(vm)
|
||||
.map(|result| result.expect("ordinary execution cannot stop at a generator preamble"))
|
||||
}
|
||||
|
||||
fn run_until<const PREPARE_GENERATOR: bool>(
|
||||
&mut self,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<Option<ExecutionResult>> {
|
||||
flame_guard!(format!(
|
||||
"FrameObject::run({obj_name})",
|
||||
obj_name = self.code.obj_name
|
||||
@@ -3681,7 +3826,7 @@ impl ExecutingFrame<'_> {
|
||||
// principle toggle it, because we refresh `tracing` below right
|
||||
// after that call returns (that cold path is only taken when
|
||||
// tracing was already on, so it costs nothing on the hot path).
|
||||
let mut tracing = vm.use_tracing.get();
|
||||
let mut tracing = !PREPARE_GENERATOR && vm.use_tracing.get();
|
||||
|
||||
// Fire 'line' trace event when line number changes.
|
||||
// Only fire if this frame has a per-frame trace function set
|
||||
@@ -3726,7 +3871,7 @@ impl ExecutingFrame<'_> {
|
||||
idx = lasti_cell.load(Relaxed) as usize;
|
||||
continue;
|
||||
}
|
||||
Ok(Some(value)) => break Ok(value),
|
||||
Ok(Some(value)) => break Ok(Some(value)),
|
||||
Err(e) => break Err(e),
|
||||
}
|
||||
}
|
||||
@@ -3734,7 +3879,7 @@ impl ExecutingFrame<'_> {
|
||||
// The trace callback may have toggled tracing (e.g. via
|
||||
// sys.settrace(None)); refresh before the opcode-trace check
|
||||
// below reuses this flag.
|
||||
tracing = vm.use_tracing.get();
|
||||
tracing = !PREPARE_GENERATOR && vm.use_tracing.get();
|
||||
// Trace callback may have changed lasti via set_f_lineno.
|
||||
// Re-read and restart the loop from the new position.
|
||||
if lasti_cell.load(Relaxed) != (idx as u32 + 1) {
|
||||
@@ -3811,9 +3956,10 @@ impl ExecutingFrame<'_> {
|
||||
frame: &mut ExecutingFrame<'_>,
|
||||
exception: PyBaseExceptionRef,
|
||||
idx: usize,
|
||||
complete_frame: bool,
|
||||
vm: &VirtualMachine,
|
||||
) -> FrameResult {
|
||||
if let Some((loc, _end_loc)) = frame.code.locations.get(idx) {
|
||||
if complete_frame && let Some((loc, _end_loc)) = frame.code.locations.get(idx) {
|
||||
let next = exception.traceback();
|
||||
let new_traceback = PyTraceback::new(
|
||||
next,
|
||||
@@ -3832,10 +3978,10 @@ impl ExecutingFrame<'_> {
|
||||
},
|
||||
)
|
||||
}
|
||||
match handle_signal_exception(self, exception, idx, vm) {
|
||||
match handle_signal_exception(self, exception, idx, !PREPARE_GENERATOR, vm) {
|
||||
Ok(None) => {}
|
||||
Ok(Some(value)) => {
|
||||
break Ok(value);
|
||||
break Ok(Some(value));
|
||||
}
|
||||
Err(exception) => {
|
||||
break Err(exception);
|
||||
@@ -3859,6 +4005,13 @@ impl ExecutingFrame<'_> {
|
||||
crate::opcode_histogram::record(prev_op, op_byte);
|
||||
prev_op = op_byte;
|
||||
}
|
||||
if PREPARE_GENERATOR && matches!(op, Instruction::ReturnGenerator) {
|
||||
// CPython copies the eager frame at RETURN_GENERATOR with its
|
||||
// instruction pointer advanced to POP_TOP. Iterator pairs from
|
||||
// generator expressions must survive the execution borrow too.
|
||||
self.localsplus.promote_stack();
|
||||
break Ok(None);
|
||||
}
|
||||
let result = self.execute_instruction(op, arg, &mut do_extend_arg, vm);
|
||||
// Skip inline cache entries if instruction fell through (no jump).
|
||||
// `cache_entries()` is a table lookup, so it is done here rather
|
||||
@@ -3874,7 +4027,7 @@ impl ExecutingFrame<'_> {
|
||||
match result {
|
||||
Ok(None) => {}
|
||||
Ok(Some(value)) => {
|
||||
break Ok(value);
|
||||
break Ok(Some(value));
|
||||
}
|
||||
// Instruction raised an exception
|
||||
Err(exception) => {
|
||||
@@ -3885,6 +4038,7 @@ impl ExecutingFrame<'_> {
|
||||
idx: usize,
|
||||
is_reraise: bool,
|
||||
is_new_raise: bool,
|
||||
complete_frame: bool,
|
||||
vm: &VirtualMachine,
|
||||
) -> FrameResult {
|
||||
// 1. Extract traceback from exception's '__traceback__' attr.
|
||||
@@ -3903,7 +4057,9 @@ impl ExecutingFrame<'_> {
|
||||
// checking for duplicates. Each time an exception passes through
|
||||
// a frame (e.g., in a loop with repeated raise statements),
|
||||
// a new traceback entry is added.
|
||||
if let Some((loc, _end_loc)) = frame.code.locations.get(idx) {
|
||||
if complete_frame
|
||||
&& let Some((loc, _end_loc)) = frame.code.locations.get(idx)
|
||||
{
|
||||
let next = exception.traceback();
|
||||
|
||||
let new_traceback = PyTraceback::new(
|
||||
@@ -4001,11 +4157,19 @@ impl ExecutingFrame<'_> {
|
||||
self.fire_exception_trace(&exception, vm)?;
|
||||
}
|
||||
|
||||
match handle_exception(self, exception, idx, is_reraise, is_new_raise, vm) {
|
||||
match handle_exception(
|
||||
self,
|
||||
exception,
|
||||
idx,
|
||||
is_reraise,
|
||||
is_new_raise,
|
||||
!PREPARE_GENERATOR,
|
||||
vm,
|
||||
) {
|
||||
// The handler unwound to a new position, so the next
|
||||
// index is whatever it left in the frame.
|
||||
Ok(None) => next_idx = lasti_cell.load(Relaxed),
|
||||
Ok(Some(result)) => break Ok(result),
|
||||
Ok(Some(result)) => break Ok(Some(result)),
|
||||
Err(exception) => {
|
||||
// Fire PY_UNWIND: exception escapes this frame
|
||||
let exception = if vm.state.monitoring_events.load()
|
||||
@@ -4040,16 +4204,18 @@ impl ExecutingFrame<'_> {
|
||||
// In RustPython, we check:
|
||||
// 1. lasti points to RESUME (after YIELD_VALUE)
|
||||
// 2. The previous instruction was YIELD_VALUE with arg >= 1
|
||||
// 3. Stack top is the delegate (receiver)
|
||||
// 3. The delegate (receiver) is below its index-or-NULL slot
|
||||
//
|
||||
// First check if stack is empty - if so, we can't be in yield-from
|
||||
if self.localsplus.stack_is_empty() {
|
||||
if self.localsplus.stack_len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let lasti = self.lasti() as usize;
|
||||
if let Some(unit) = self.code.instructions.get(lasti) {
|
||||
match &unit.op {
|
||||
Instruction::Send { .. } => return Some(self.top_value()),
|
||||
Instruction::Send { .. } | Instruction::SendGen => {
|
||||
return Some(self.nth_value(1));
|
||||
}
|
||||
Instruction::Resume { .. } | Instruction::InstrumentedResume => {
|
||||
// Check if previous instruction was YIELD_VALUE with arg >= 1
|
||||
// This indicates yield-from/await context
|
||||
@@ -4063,8 +4229,8 @@ impl ExecutingFrame<'_> {
|
||||
// YIELD_VALUE arg: 0 = direct yield, >= 1 = yield-from/await
|
||||
// OpArgByte.0 is the raw byte value
|
||||
if u8::from(prev_unit.arg) >= 1 {
|
||||
// In yield-from/await context, delegate is on top of stack
|
||||
return Some(self.top_value());
|
||||
// The iterator pair remains after YIELD_VALUE.
|
||||
return Some(self.nth_value(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4920,36 +5086,40 @@ impl ExecutingFrame<'_> {
|
||||
self.push_value(iter);
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::GetIter => {
|
||||
let iterated_obj = self.pop_value();
|
||||
let iter_obj = PyIter::try_from_object(vm, iterated_obj)?;
|
||||
self.push_value(iter_obj.into());
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::GetYieldFromIter => {
|
||||
// GET_YIELD_FROM_ITER: prepare iterator for yield from
|
||||
// If iterable is a coroutine, ensure we're in a coroutine context
|
||||
// If iterable is a generator, use it directly
|
||||
// Otherwise, call iter() on it
|
||||
Instruction::GetIter { mode } => {
|
||||
// Include/internal/pycore_opcode_utils.h GET_ITER_YIELD_FROM_*.
|
||||
const GET_ITER_ORDINARY: u32 = 0;
|
||||
const GET_ITER_YIELD_FROM: u32 = 1;
|
||||
const GET_ITER_YIELD_FROM_NO_CHECK: u32 = 2;
|
||||
const GET_ITER_YIELD_FROM_CORO_CHECK: u32 = 3;
|
||||
|
||||
let iterable = self.pop_value();
|
||||
let iter = if iterable.class().is(vm.ctx.types.coroutine_type) {
|
||||
// Coroutine requires CO_COROUTINE or CO_ITERABLE_COROUTINE flag
|
||||
if !self.code.flags.intersects(
|
||||
bytecode::CodeFlags::COROUTINE | bytecode::CodeFlags::ITERABLE_COROUTINE,
|
||||
) {
|
||||
let mode = mode.get(arg);
|
||||
let iter = if mode != GET_ITER_ORDINARY
|
||||
&& iterable.class().is(vm.ctx.types.coroutine_type)
|
||||
{
|
||||
// Code construction resolves the compiler-only mode 1
|
||||
// using CO_COROUTINE / CO_ITERABLE_COROUTINE.
|
||||
let reject_coroutine = match mode {
|
||||
GET_ITER_YIELD_FROM_CORO_CHECK => true,
|
||||
GET_ITER_YIELD_FROM_NO_CHECK => false,
|
||||
GET_ITER_YIELD_FROM => !self.code.flags.intersects(
|
||||
bytecode::CodeFlags::COROUTINE
|
||||
| bytecode::CodeFlags::ITERABLE_COROUTINE,
|
||||
),
|
||||
_ => false,
|
||||
};
|
||||
if reject_coroutine {
|
||||
return Err(vm.new_type_error(
|
||||
"cannot 'yield from' a coroutine object in a non-coroutine generator",
|
||||
));
|
||||
}
|
||||
iterable
|
||||
} else if iterable.class().is(vm.ctx.types.generator_type) {
|
||||
// Generator can be used directly
|
||||
iterable
|
||||
} else {
|
||||
// Otherwise, get iterator
|
||||
PyIter::try_from_object(vm, iterable)?.into()
|
||||
};
|
||||
self.push_value(iter);
|
||||
self.push_null();
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::GetLen => {
|
||||
@@ -4965,7 +5135,7 @@ impl ExecutingFrame<'_> {
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::ImportName { namei: idx } => {
|
||||
self.import(vm, Some(self.code.names[idx.get(arg) as usize]))?;
|
||||
self.import(vm, Some(self.code.names[(idx.get(arg) >> 2) as usize]))?;
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::IsOp { invert } => {
|
||||
@@ -5144,6 +5314,11 @@ impl ExecutingFrame<'_> {
|
||||
.expect("builtin_any not initialized"),
|
||||
CommonConstant::BuiltinList => vm.ctx.types.list_type.to_owned().into(),
|
||||
CommonConstant::BuiltinSet => vm.ctx.types.set_type.to_owned().into(),
|
||||
CommonConstant::None => vm.ctx.none(),
|
||||
CommonConstant::EmptyStr => vm.ctx.empty_str.to_owned().into(),
|
||||
CommonConstant::True => vm.ctx.new_bool(true).into(),
|
||||
CommonConstant::False => vm.ctx.new_bool(false).into(),
|
||||
CommonConstant::MinusOne => vm.ctx.new_int(-1).into(),
|
||||
};
|
||||
self.push_value(value);
|
||||
Ok(None)
|
||||
@@ -5620,8 +5795,8 @@ impl ExecutingFrame<'_> {
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::PopTop => {
|
||||
// Pop value from stack and ignore.
|
||||
self.pop_stackref();
|
||||
// Loop unwinding also uses POP_TOP for index-or-NULL slots.
|
||||
self.pop_stackref_opt();
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::EndFor => {
|
||||
@@ -5630,7 +5805,8 @@ impl ExecutingFrame<'_> {
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::PopIter => {
|
||||
// Pop the iterator from stack (end of for loop)
|
||||
// Discard the iterator's index-or-NULL slot and the iterator.
|
||||
self.pop_stackref_opt();
|
||||
self.pop_stackref();
|
||||
Ok(None)
|
||||
}
|
||||
@@ -5935,10 +6111,10 @@ impl ExecutingFrame<'_> {
|
||||
Ok(Some(ExecutionResult::Yield(self.pop_value())))
|
||||
}
|
||||
Instruction::Send { .. } => {
|
||||
// (receiver, v -- receiver, retval)
|
||||
// (receiver, null_or_index, v -- receiver, null_or_index, retval)
|
||||
self.adaptive(|s, ii, cb| s.specialize_send(vm, ii, cb));
|
||||
let exit_label = bytecode::Label::from_u32(self.lasti() + 1 + u32::from(arg));
|
||||
let receiver = self.nth_value(1);
|
||||
let receiver = self.nth_value(2);
|
||||
let can_fast_send = !self.specialization_eval_frame_active(vm)
|
||||
&& (receiver.downcast_ref_if_exact::<PyGenerator>(vm).is_some()
|
||||
|| receiver.downcast_ref_if_exact::<PyCoroutine>(vm).is_some())
|
||||
@@ -5946,7 +6122,7 @@ impl ExecutingFrame<'_> {
|
||||
.builtin_coro(receiver)
|
||||
.is_some_and(|coro| !coro.running() && !coro.closed());
|
||||
let val = self.pop_value();
|
||||
let receiver = self.top_value();
|
||||
let receiver = self.nth_value(1);
|
||||
let ret = if can_fast_send {
|
||||
let coro = self.builtin_coro(receiver).unwrap();
|
||||
if vm.is_none(&val) {
|
||||
@@ -5976,8 +6152,8 @@ impl ExecutingFrame<'_> {
|
||||
}
|
||||
Instruction::SendGen => {
|
||||
let exit_label = bytecode::Label::from_u32(self.lasti() + 1 + u32::from(arg));
|
||||
// Stack: [receiver, val] — peek receiver before popping
|
||||
let receiver = self.nth_value(1);
|
||||
// Stack: [receiver, null_or_index, val] — peek before popping
|
||||
let receiver = self.nth_value(2);
|
||||
let mut started = false;
|
||||
let can_fast_send = !self.specialization_eval_frame_active(vm)
|
||||
&& (receiver.downcast_ref_if_exact::<PyGenerator>(vm).is_some()
|
||||
@@ -5989,7 +6165,7 @@ impl ExecutingFrame<'_> {
|
||||
let val = self.pop_value();
|
||||
|
||||
if can_fast_send {
|
||||
let receiver = self.top_value();
|
||||
let receiver = self.nth_value(1);
|
||||
// Hand an already-suspended generator to the trampoline,
|
||||
// which runs its frame in this same eval loop and re-yields
|
||||
// for this frame — no Rust frame and no instruction per
|
||||
@@ -6024,7 +6200,7 @@ impl ExecutingFrame<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let receiver = self.top_value();
|
||||
let receiver = self.nth_value(1);
|
||||
match self._send(receiver, val, vm)? {
|
||||
PyIterReturn::Return(value) => {
|
||||
self.push_value(value);
|
||||
@@ -6043,9 +6219,9 @@ impl ExecutingFrame<'_> {
|
||||
}
|
||||
}
|
||||
Instruction::EndSend => {
|
||||
// Stack: (receiver, value) -> (value)
|
||||
// Pops receiver, leaves value
|
||||
// (receiver, null_or_index, value) -> (value)
|
||||
let value = self.pop_value();
|
||||
self.pop_stackref_opt(); // discard index-or-NULL
|
||||
self.pop_stackref(); // discard receiver
|
||||
self.push_value(value);
|
||||
Ok(None)
|
||||
@@ -6062,39 +6238,24 @@ impl ExecutingFrame<'_> {
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::CleanupThrow => {
|
||||
// CLEANUP_THROW: (sub_iter, last_sent_val, exc) -> (None, value) OR re-raise
|
||||
// If StopIteration: pop all 3, extract value, push (None, value)
|
||||
// Otherwise: pop all 3, return Err(exc) for unwind_blocks to handle
|
||||
//
|
||||
// Unlike CPython where exception_unwind pops the triple as part of
|
||||
// stack cleanup to handler depth, RustPython pops here explicitly
|
||||
// and lets unwind_blocks find outer handlers.
|
||||
// Compiler sets handler_depth = base + 2 (before exc is pushed).
|
||||
|
||||
// First peek at exc_value (top of stack) without popping
|
||||
// (sub_iter, null_in, last_sent_val, exc) -> (None, null_out, value)
|
||||
// Leave nonmatching exceptions and their stack for unwinding.
|
||||
let exc = self.top_value();
|
||||
|
||||
// Check if it's a StopIteration
|
||||
if let Some(exc_ref) = exc.downcast_ref::<PyBaseException>()
|
||||
&& exc_ref.fast_isinstance(vm.ctx.exceptions.stop_iteration)
|
||||
{
|
||||
// Extract value from StopIteration
|
||||
let value = exc_ref.get_arg(0).unwrap_or_else(|| vm.ctx.none());
|
||||
// Now pop all three
|
||||
self.pop_stackref(); // exc
|
||||
self.pop_stackref(); // last_sent_val
|
||||
let index_or_null = self.pop_stackref_opt();
|
||||
self.pop_stackref(); // sub_iter
|
||||
self.push_value(vm.ctx.none());
|
||||
self.push_stackref_opt(index_or_null);
|
||||
self.push_value(value);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Re-raise other exceptions: pop all three and return Err(exc)
|
||||
let exc = self.pop_value(); // exc
|
||||
self.pop_stackref(); // last_sent_val
|
||||
self.pop_stackref(); // sub_iter
|
||||
|
||||
let exc = exc
|
||||
.to_owned()
|
||||
.downcast::<PyBaseException>()
|
||||
.map_err(|_| vm.new_type_error("exception expected"))?;
|
||||
Err(exc)
|
||||
@@ -7952,7 +8113,7 @@ impl ExecutingFrame<'_> {
|
||||
}
|
||||
Instruction::ForIterRange => {
|
||||
let target = bytecode::Label::from_u32(self.lasti() + 1 + u32::from(arg));
|
||||
let iter = self.top_value();
|
||||
let iter = self.nth_value(1);
|
||||
if let Some(range_iter) = iter.downcast_ref_if_exact::<PyRangeIterator>(vm) {
|
||||
if let Some(value) = range_iter.fast_next() {
|
||||
self.push_value(vm.ctx.new_int(value).into());
|
||||
@@ -7967,7 +8128,7 @@ impl ExecutingFrame<'_> {
|
||||
}
|
||||
Instruction::ForIterList => {
|
||||
let target = bytecode::Label::from_u32(self.lasti() + 1 + u32::from(arg));
|
||||
let iter = self.top_value();
|
||||
let iter = self.nth_value(1);
|
||||
if let Some(list_iter) = iter.downcast_ref_if_exact::<PyListIterator>(vm) {
|
||||
if let Some(value) = list_iter.fast_next() {
|
||||
self.push_value(value);
|
||||
@@ -7982,7 +8143,7 @@ impl ExecutingFrame<'_> {
|
||||
}
|
||||
Instruction::ForIterTuple => {
|
||||
let target = bytecode::Label::from_u32(self.lasti() + 1 + u32::from(arg));
|
||||
let iter = self.top_value();
|
||||
let iter = self.nth_value(1);
|
||||
if let Some(tuple_iter) = iter.downcast_ref_if_exact::<PyTupleIterator>(vm) {
|
||||
if let Some(value) = tuple_iter.fast_next() {
|
||||
self.push_value(value);
|
||||
@@ -7997,7 +8158,7 @@ impl ExecutingFrame<'_> {
|
||||
}
|
||||
Instruction::ForIterGen => {
|
||||
let target = bytecode::Label::from_u32(self.lasti() + 1 + u32::from(arg));
|
||||
let iter = self.top_value();
|
||||
let iter = self.nth_value(1);
|
||||
if self.specialization_eval_frame_active(vm) {
|
||||
self.execute_for_iter(vm, target)?;
|
||||
return Ok(None);
|
||||
@@ -8289,10 +8450,10 @@ impl ExecutingFrame<'_> {
|
||||
Ok(None)
|
||||
}
|
||||
Instruction::InstrumentedEndFor => {
|
||||
// Stack: [value, receiver(iter), ...]
|
||||
// Stack: [..., receiver(iter), null_or_index, value]
|
||||
// PyGen_Check: only fire STOP_ITERATION for generators
|
||||
let is_gen = self
|
||||
.nth_value(1)
|
||||
.nth_value(2)
|
||||
.downcast_ref::<crate::builtins::PyGenerator>()
|
||||
.is_some();
|
||||
let value = self.pop_value();
|
||||
@@ -8304,6 +8465,7 @@ impl ExecutingFrame<'_> {
|
||||
}
|
||||
Instruction::InstrumentedEndSend => {
|
||||
let value = self.pop_value();
|
||||
self.pop_stackref_opt();
|
||||
let receiver = self.pop_value();
|
||||
// PyGen_Check || PyCoro_CheckExact
|
||||
let is_gen_or_coro = receiver
|
||||
@@ -8392,6 +8554,7 @@ impl ExecutingFrame<'_> {
|
||||
}
|
||||
Instruction::InstrumentedPopIter => {
|
||||
// BRANCH_RIGHT is fired by InstrumentedForIter, not here.
|
||||
self.pop_stackref_opt();
|
||||
self.pop_stackref();
|
||||
Ok(None)
|
||||
}
|
||||
@@ -9459,7 +9622,7 @@ impl ExecutingFrame<'_> {
|
||||
value: Option<PyObjectRef>,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
let iter = self.top_value();
|
||||
let iter = self.nth_value(1);
|
||||
if iter.downcast_ref_if_exact::<PyListIterator>(vm).is_some()
|
||||
|| iter.downcast_ref_if_exact::<PyRangeIterator>(vm).is_some()
|
||||
|| iter.downcast_ref_if_exact::<PyTupleIterator>(vm).is_some()
|
||||
@@ -9483,14 +9646,14 @@ impl ExecutingFrame<'_> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Advance the iterator on top of stack.
|
||||
/// Advance the iterator below its index-or-NULL stack slot.
|
||||
/// Returns `true` if iteration continued (item pushed), `false` if exhausted (jumped).
|
||||
fn execute_for_iter(
|
||||
&mut self,
|
||||
vm: &VirtualMachine,
|
||||
target: bytecode::Label,
|
||||
) -> Result<bool, PyBaseExceptionRef> {
|
||||
let top = self.top_value();
|
||||
let top = self.nth_value(1);
|
||||
|
||||
// FOR_ITER_RANGE: bypass generic iterator protocol for range iterators
|
||||
if let Some(range_iter) = top.downcast_ref_if_exact::<PyRangeIterator>(vm) {
|
||||
@@ -9521,10 +9684,7 @@ impl ExecutingFrame<'_> {
|
||||
self.jump(self.for_iter_jump_target(target));
|
||||
Ok(false)
|
||||
}
|
||||
Err(next_error) => {
|
||||
self.pop_stackref();
|
||||
Err(next_error)
|
||||
}
|
||||
Err(next_error) => Err(next_error),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11426,8 +11586,8 @@ impl ExecutingFrame<'_> {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Stack: [receiver, val] — receiver is at position 1
|
||||
let receiver = self.nth_value(1);
|
||||
// Stack: [receiver, null_or_index, val] — receiver is at position 2
|
||||
let receiver = self.nth_value(2);
|
||||
let is_exact_gen_or_coro = receiver.downcast_ref_if_exact::<PyGenerator>(vm).is_some()
|
||||
|| receiver.downcast_ref_if_exact::<PyCoroutine>(vm).is_some();
|
||||
if is_exact_gen_or_coro && !self.specialization_eval_frame_active(vm) {
|
||||
@@ -11693,7 +11853,7 @@ impl ExecutingFrame<'_> {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let iter = self.top_value();
|
||||
let iter = self.nth_value(1);
|
||||
|
||||
let new_op = if iter.downcast_ref_if_exact::<PyRangeIterator>(vm).is_some() {
|
||||
Some(Instruction::ForIterRange)
|
||||
|
||||
@@ -262,7 +262,7 @@ impl VirtualMachine {
|
||||
let is_profile_event = event.is_profile_event();
|
||||
let is_opcode_event = event.is_opcode_event();
|
||||
|
||||
let Some(frame_ref) = crate::frame::current_thread_frame_materialize(self) else {
|
||||
let Some(frame_ref) = crate::frame::current_complete_frame_for_trace(self) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
|
||||
@@ -668,7 +668,7 @@ pub mod sys {
|
||||
const NAME: &str = "rustpython";
|
||||
|
||||
// Keep implementation-specific bytecode caches separate.
|
||||
let cache_tag = format!("{NAME}-{}{}", version::MAJOR, version::MINOR);
|
||||
let cache_tag = version::PYC_CACHE_TAG;
|
||||
let ctx = &vm.ctx;
|
||||
py_namespace!(vm, {
|
||||
"name" => ctx.new_str(NAME),
|
||||
|
||||
@@ -404,8 +404,10 @@ pub(crate) fn instrument_code(code: &Py<PyCode>, events: u32) {
|
||||
|
||||
// Find _co_firsttraceable: index of first RESUME instruction
|
||||
let mut first_traceable = None;
|
||||
for_each_instruction(code, |i, op, _| {
|
||||
for_each_instruction(code, |i, op, oparg| {
|
||||
if first_traceable.is_none()
|
||||
&& u32::from(oparg) & crate::bytecode::oparg::ResumeContext::LOCATION_MASK
|
||||
!= crate::bytecode::oparg::ResumeLocation::AtGenExprStart.as_u32()
|
||||
&& matches!(
|
||||
op,
|
||||
Instruction::Resume { .. }
|
||||
@@ -421,7 +423,7 @@ pub(crate) fn instrument_code(code: &Py<PyCode>, events: u32) {
|
||||
// Phase 4: Place regular INSTRUMENTED_* opcodes whose event is active.
|
||||
// Walk by cache_entries so specialized CACHE payloads are not decoded as opcodes.
|
||||
{
|
||||
let mut i = 0;
|
||||
let mut i = first_traceable;
|
||||
while i < len {
|
||||
let op = code.code.instructions.read_op(i);
|
||||
let oparg = code.code.instructions.read_arg(i).as_u8();
|
||||
|
||||
@@ -68,9 +68,10 @@ pub const RUSTPYTHON_VERSION: &str = const {
|
||||
}
|
||||
};
|
||||
|
||||
// Must be aligned to Lib/importlib/_bootstrap_external.py
|
||||
// Matches CPython 3.14 (Include/internal/pycore_magic_number.h).
|
||||
pub const PYC_MAGIC_NUMBER: u16 = 3627;
|
||||
// Private, non-distributable reconstruction ABI. This is not CPython's magic.
|
||||
// Change token and tag together for every incompatible development format.
|
||||
pub const PYC_MAGIC_NUMBER: u16 = 0xDA16;
|
||||
pub const PYC_CACHE_TAG: &str = "rustpython-315-devabi2";
|
||||
|
||||
// CPython format: magic_number | ('\r' << 16) | ('\n' << 24)
|
||||
// This protects against text-mode file reads
|
||||
|
||||
+40
-9
@@ -2642,6 +2642,24 @@ impl VirtualMachine {
|
||||
&self,
|
||||
frame: FrameObjectRef,
|
||||
f: F,
|
||||
) -> PyResult<R> {
|
||||
self.with_frame_impl::<true, R, F>(frame, f)
|
||||
}
|
||||
|
||||
/// Generator construction runs an incomplete frame whose caller link must
|
||||
/// not escape. RETURN_GENERATOR copies the frame with previous=NULL in CPython.
|
||||
pub(crate) fn with_generator_frame<R, F: FnOnce(FrameObjectRef) -> PyResult<R>>(
|
||||
&self,
|
||||
frame: FrameObjectRef,
|
||||
f: F,
|
||||
) -> PyResult<R> {
|
||||
self.with_frame_impl::<false, R, F>(frame, f)
|
||||
}
|
||||
|
||||
fn with_frame_impl<const RETAIN_BACK: bool, R, F: FnOnce(FrameObjectRef) -> PyResult<R>>(
|
||||
&self,
|
||||
frame: FrameObjectRef,
|
||||
f: F,
|
||||
) -> PyResult<R> {
|
||||
self.check_recursive_call("")?;
|
||||
|
||||
@@ -2688,7 +2706,7 @@ impl VirtualMachine {
|
||||
|
||||
// Capture f_back before clearing previous so code holding a
|
||||
// reference to this FrameObject can walk the chain after return.
|
||||
if !old_chain.is_null() {
|
||||
if RETAIN_BACK && !old_chain.is_null() {
|
||||
let strong = frame.as_object().strong_count();
|
||||
// Only set retained_back if someone else holds a reference (escaped)
|
||||
// AND the caller already has a FrameObject. Materializing the caller
|
||||
@@ -2699,8 +2717,11 @@ impl VirtualMachine {
|
||||
if strong > 1 {
|
||||
let mut guard = frame.iframe().cold().retained_back.lock();
|
||||
if guard.is_none() {
|
||||
let prev_iframe = unsafe { &*old_chain };
|
||||
if let Some(fo) = prev_iframe.frame_obj() {
|
||||
// SAFETY: old_chain remains live until this synchronous entry exits.
|
||||
let visible = unsafe { crate::frame::first_complete_iframe(old_chain) };
|
||||
if let Some(prev_iframe) = unsafe { visible.as_ref() }
|
||||
&& let Some(fo) = prev_iframe.frame_obj()
|
||||
{
|
||||
*guard = Some(fo.to_owned());
|
||||
}
|
||||
}
|
||||
@@ -2826,8 +2847,9 @@ impl VirtualMachine {
|
||||
// The slots above are the last write this thread makes into
|
||||
// the frame object, so it is now readable from anywhere.
|
||||
fo.iframe().detach();
|
||||
if !old_chain.is_null() {
|
||||
let prev_iframe = unsafe { &*old_chain };
|
||||
// SAFETY: old_chain belongs to this still-active thread chain.
|
||||
let visible = unsafe { crate::frame::first_complete_iframe(old_chain) };
|
||||
if let Some(prev_iframe) = unsafe { visible.as_ref() } {
|
||||
let back_fo = prev_iframe.materialize_chain(self);
|
||||
*fo.iframe().cold().retained_back.lock() = Some(back_fo);
|
||||
}
|
||||
@@ -3013,10 +3035,19 @@ impl VirtualMachine {
|
||||
if result.is_err()
|
||||
&& self.use_tracing.get()
|
||||
&& (!self.is_none(&self.profile_func.borrow())
|
||||
|| frame
|
||||
.iframe()
|
||||
.cold_opt()
|
||||
.is_some_and(|c| c.trace.lock().is_some()))
|
||||
|| if frame.iframe().is_incomplete_for_trace() {
|
||||
crate::frame::current_complete_frame_for_trace(self).is_some_and(|visible| {
|
||||
visible
|
||||
.iframe()
|
||||
.cold_opt()
|
||||
.is_some_and(|c| c.trace.lock().is_some())
|
||||
})
|
||||
} else {
|
||||
frame
|
||||
.iframe()
|
||||
.cold_opt()
|
||||
.is_some_and(|c| c.trace.lock().is_some())
|
||||
})
|
||||
{
|
||||
let ret_result = self.trace_event_what(
|
||||
TraceEvent::Return,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// Fresh private devabi2 fixture: compile("pass\n", "byte_exact/embedded_cache.py", "exec").
|
||||
const PRIVATE_PYC: &[u8] = &[
|
||||
22, 218, 13, 10, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 227, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0, 115, 8, 0, 0, 0, 128, 0, 0, 0, 80, 7, 33, 0, 40, 1, 0, 0, 0, 78, 40, 0,
|
||||
0, 0, 0, 40, 0, 0, 0, 0, 115, 0, 0, 0, 0, 122, 28, 98, 121, 116, 101, 95, 101, 120, 97, 99,
|
||||
116, 47, 101, 109, 98, 101, 100, 100, 101, 100, 95, 99, 97, 99, 104, 101, 46, 112, 121, 122, 8,
|
||||
60, 109, 111, 100, 117, 108, 101, 62, 122, 8, 60, 109, 111, 100, 117, 108, 101, 62, 1, 0, 0, 0,
|
||||
115, 8, 0, 0, 0, 241, 3, 1, 1, 1, 217, 0, 4, 115, 0, 0, 0, 0,
|
||||
];
|
||||
@@ -0,0 +1,38 @@
|
||||
//! Direct embedding API format boundary. Python behavior is tested in snippets.
|
||||
|
||||
#![allow(clippy::tests_outside_test_module)]
|
||||
|
||||
use rustpython_vm::{AsObject, Interpreter, builtins::PyCode};
|
||||
|
||||
include!("fixtures/private_cache_bytes.rs");
|
||||
|
||||
#[test]
|
||||
fn foreign_formats_are_rejected_before_bootstrap_lookup() {
|
||||
Interpreter::without_stdlib(Default::default()).enter(|vm| {
|
||||
for magic in [3627u16, 3666, 0xDA15] {
|
||||
let mut bytes = PRIVATE_PYC.to_vec();
|
||||
bytes[..2].copy_from_slice(&magic.to_le_bytes());
|
||||
let error = PyCode::from_pyc(&bytes, None, None, None, vm).unwrap_err();
|
||||
assert!(error.fast_isinstance(vm.ctx.exceptions.value_error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "importlib")]
|
||||
#[test]
|
||||
fn private_format_loads_and_runs_with_external_bootstrap() {
|
||||
Interpreter::without_stdlib(Default::default()).enter(|vm| {
|
||||
assert_eq!(
|
||||
&PRIVATE_PYC[..4],
|
||||
&rustpython_vm::version::PYC_MAGIC_NUMBER_BYTES
|
||||
);
|
||||
let code = PyCode::from_pyc(PRIVATE_PYC, None, None, None, vm).unwrap();
|
||||
assert_eq!(code.arg_count, 0);
|
||||
let scope = vm.new_scope_with_main().unwrap();
|
||||
vm.run_pyc_bytes(PRIVATE_PYC, scope)
|
||||
.unwrap_or_else(|error| {
|
||||
vm.print_exception(&error);
|
||||
panic!("private embedded pyc execution failed");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Ordinary warmed exports retain public opcodes and unchanged raw fields."""
|
||||
|
||||
import marshal
|
||||
import sys
|
||||
import types
|
||||
|
||||
if sys.version_info < (3, 15):
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
source = """
|
||||
def direct(x):
|
||||
return x + 1000
|
||||
def looping(xs):
|
||||
total = 0
|
||||
for x in xs:
|
||||
total += direct(x)
|
||||
return total
|
||||
def delegate(xs):
|
||||
yield from xs
|
||||
"""
|
||||
root = compile(source, "byte_exact/warmed_export.py", "exec")
|
||||
namespace = {}
|
||||
exec(root, namespace)
|
||||
codes = [value for value in root.co_consts if isinstance(value, types.CodeType)]
|
||||
before = [(code.co_code, code.co_linetable, code.co_exceptiontable) for code in codes]
|
||||
for _ in range(100):
|
||||
assert namespace["looping"]([1, 2, 3]) == 3006
|
||||
assert list(namespace["delegate"]([1, 2])) == [1, 2]
|
||||
after = [(code.co_code, code.co_linetable, code.co_exceptiontable) for code in codes]
|
||||
assert before == after
|
||||
|
||||
# Generated cache widths in final 3.15 pycore_opcode_metadata.h. Operand bytes
|
||||
# are not opcode positions and must not be rejected by a byte-membership test.
|
||||
cache_widths = {
|
||||
128: 1,
|
||||
37: 3,
|
||||
36: 1,
|
||||
105: 1,
|
||||
68: 1,
|
||||
118: 1,
|
||||
109: 4,
|
||||
91: 4,
|
||||
95: 1,
|
||||
79: 9,
|
||||
54: 1,
|
||||
55: 1,
|
||||
74: 1,
|
||||
102: 1,
|
||||
99: 1,
|
||||
100: 1,
|
||||
101: 1,
|
||||
70: 1,
|
||||
50: 3,
|
||||
53: 3,
|
||||
4: 1,
|
||||
42: 5,
|
||||
}
|
||||
for code in codes:
|
||||
exported = code.co_code
|
||||
index = 0
|
||||
while index < len(exported):
|
||||
opcode = exported[index]
|
||||
assert opcode not in (219, 220, 221)
|
||||
index += 2 * (1 + cache_widths.get(opcode, 0))
|
||||
assert index == len(exported)
|
||||
loaded = marshal.loads(marshal.dumps(code, 6))
|
||||
assert loaded.co_code == exported
|
||||
assert loaded.co_linetable == code.co_linetable
|
||||
assert loaded.co_exceptiontable == code.co_exceptiontable
|
||||
@@ -0,0 +1,501 @@
|
||||
"""CPython 3.15 iterator-pair, generator-creation, and SEND regression checks."""
|
||||
|
||||
|
||||
def test_iterator_pairs_survive_specialized_loops_and_unwinding():
|
||||
class Iterator:
|
||||
def __init__(self):
|
||||
self.index = 0
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
if self.index == 4:
|
||||
raise StopIteration
|
||||
value = self.index
|
||||
self.index += 1
|
||||
return value
|
||||
|
||||
def values():
|
||||
yield from range(4)
|
||||
|
||||
def consume(items):
|
||||
total = 0
|
||||
for item in items:
|
||||
total += item
|
||||
return total
|
||||
|
||||
def leave(items):
|
||||
for item in items:
|
||||
for inner in range(3):
|
||||
if inner == 1:
|
||||
return item + inner
|
||||
return -1
|
||||
|
||||
for warmup in range(200):
|
||||
assert consume(range(4)) == 6
|
||||
assert consume([0, 1, 2, 3]) == 6
|
||||
assert consume((0, 1, 2, 3)) == 6
|
||||
assert consume(values()) == 6
|
||||
assert consume(Iterator()) == 6
|
||||
assert leave(range(4)) == 1
|
||||
assert [x * y for x in range(3) for y in range(2)] == [0, 0, 0, 1, 0, 2]
|
||||
for x in range(5):
|
||||
if x == 2:
|
||||
break
|
||||
assert x == 2
|
||||
|
||||
|
||||
def test_eager_generator_prefix_preserves_laziness_and_created_state():
|
||||
events = []
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self):
|
||||
events.append("iter")
|
||||
return iter((4, 5))
|
||||
|
||||
def body(value):
|
||||
events.append(value)
|
||||
return value
|
||||
|
||||
g = (body(value) for value in Iterable())
|
||||
assert events == ["iter"]
|
||||
assert not g.gi_suspended
|
||||
try:
|
||||
g.send(1)
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("created generator accepted non-None")
|
||||
assert events == ["iter"]
|
||||
assert next(g) == 4
|
||||
assert events == ["iter", 4]
|
||||
assert g.gi_suspended
|
||||
assert list(g) == [5]
|
||||
|
||||
class BadIterable:
|
||||
def __iter__(self):
|
||||
raise ValueError("eager")
|
||||
|
||||
try:
|
||||
(value for value in BadIterable())
|
||||
except ValueError as error:
|
||||
assert error.args == ("eager",)
|
||||
else:
|
||||
raise AssertionError("iterator acquisition was deferred")
|
||||
|
||||
def outer(value):
|
||||
def generator():
|
||||
events.append("body")
|
||||
yield value
|
||||
|
||||
return generator()
|
||||
|
||||
g = outer(17)
|
||||
assert "body" not in events
|
||||
g.close()
|
||||
assert "body" not in events
|
||||
assert list(outer(19)) == [19]
|
||||
|
||||
|
||||
def test_send_pairs_support_delegation_and_throw_cleanup():
|
||||
def leaf():
|
||||
sent = yield 1
|
||||
return sent
|
||||
|
||||
def chain(depth):
|
||||
if depth:
|
||||
return (yield from chain(depth - 1))
|
||||
return (yield from leaf())
|
||||
|
||||
for warmup in range(200):
|
||||
g = chain(12)
|
||||
assert next(g) == 1
|
||||
assert g.gi_yieldfrom is not None
|
||||
try:
|
||||
g.send(17)
|
||||
except StopIteration as error:
|
||||
assert error.value == 17
|
||||
else:
|
||||
raise AssertionError("delegation did not finish")
|
||||
|
||||
class Delegate:
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
return 3
|
||||
|
||||
def send(self, value):
|
||||
return value
|
||||
|
||||
def throw(self, error):
|
||||
raise StopIteration(23)
|
||||
|
||||
def custom():
|
||||
result = yield from Delegate()
|
||||
yield result
|
||||
|
||||
g = custom()
|
||||
assert next(g) == 3
|
||||
assert g.send(9) == 9
|
||||
assert g.throw(ValueError) == 23
|
||||
assert list(g) == []
|
||||
|
||||
class RaisingDelegate(Delegate):
|
||||
def throw(self, error):
|
||||
raise RuntimeError("delegate")
|
||||
|
||||
def raises():
|
||||
try:
|
||||
yield from RaisingDelegate()
|
||||
except RuntimeError:
|
||||
yield "caught"
|
||||
|
||||
g = raises()
|
||||
assert next(g) == 3
|
||||
assert g.throw(RuntimeError) == "caught"
|
||||
assert list(g) == []
|
||||
|
||||
events = []
|
||||
|
||||
def close_leaf():
|
||||
try:
|
||||
yield 1
|
||||
finally:
|
||||
events.append("closed")
|
||||
|
||||
def close_outer():
|
||||
yield from close_leaf()
|
||||
|
||||
g = close_outer()
|
||||
assert next(g) == 1
|
||||
g.close()
|
||||
assert events == ["closed"]
|
||||
|
||||
|
||||
def test_await_and_async_generator_expression_keep_iterator_shapes():
|
||||
events = []
|
||||
|
||||
class AsyncIterator:
|
||||
def __init__(self):
|
||||
self.index = 0
|
||||
|
||||
def __aiter__(self):
|
||||
events.append("aiter")
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
if self.index == 3:
|
||||
raise StopAsyncIteration
|
||||
value = self.index
|
||||
self.index += 1
|
||||
return value
|
||||
|
||||
g = (value * 2 async for value in AsyncIterator())
|
||||
assert events == ["aiter"]
|
||||
assert not g.ag_running
|
||||
|
||||
async def consume():
|
||||
result = []
|
||||
async for value in g:
|
||||
result.append(value)
|
||||
return result
|
||||
|
||||
coro = consume()
|
||||
try:
|
||||
coro.send(None)
|
||||
except StopIteration as error:
|
||||
assert error.value == [0, 2, 4]
|
||||
else:
|
||||
raise AssertionError("unexpected coroutine suspension")
|
||||
assert events == ["aiter"]
|
||||
|
||||
class Awaitable:
|
||||
def __await__(self):
|
||||
sent = yield "pause"
|
||||
return sent
|
||||
|
||||
async def awaits():
|
||||
return await Awaitable()
|
||||
|
||||
for warmup in range(200):
|
||||
coro = awaits()
|
||||
assert coro.send(None) == "pause"
|
||||
assert coro.cr_await is not None
|
||||
try:
|
||||
coro.send(31)
|
||||
except StopIteration as error:
|
||||
assert error.value == 31
|
||||
else:
|
||||
raise AssertionError("await did not finish")
|
||||
|
||||
|
||||
test_iterator_pairs_survive_specialized_loops_and_unwinding()
|
||||
test_eager_generator_prefix_preserves_laziness_and_created_state()
|
||||
test_send_pairs_support_delegation_and_throw_cleanup()
|
||||
test_await_and_async_generator_expression_keep_iterator_shapes()
|
||||
|
||||
|
||||
def test_eager_prefix_is_untraceable_but_reports_monitoring_errors():
|
||||
import sys
|
||||
|
||||
trace = []
|
||||
|
||||
class Good:
|
||||
def __iter__(self):
|
||||
return iter([1])
|
||||
|
||||
class Bad:
|
||||
def __iter__(self):
|
||||
raise ValueError("eager iterator")
|
||||
|
||||
def create(iterable):
|
||||
return (value for value in iterable)
|
||||
|
||||
def tracer(frame, event, arg):
|
||||
if frame.f_code.co_name in ("create", "<genexpr>", "__iter__"):
|
||||
trace.append((frame.f_code.co_name, event))
|
||||
return tracer
|
||||
|
||||
sys.settrace(tracer)
|
||||
try:
|
||||
generator = create(Good())
|
||||
generator.close()
|
||||
try:
|
||||
create(Bad())
|
||||
except ValueError as error:
|
||||
names = []
|
||||
traceback = error.__traceback__
|
||||
while traceback is not None:
|
||||
names.append(traceback.tb_frame.f_code.co_name)
|
||||
traceback = traceback.tb_next
|
||||
else:
|
||||
raise AssertionError("expected eager iterator error")
|
||||
finally:
|
||||
sys.settrace(None)
|
||||
assert not any(name == "<genexpr>" for name, event in trace)
|
||||
assert trace == [
|
||||
("create", "call"),
|
||||
("create", "line"),
|
||||
("__iter__", "call"),
|
||||
("__iter__", "line"),
|
||||
("__iter__", "return"),
|
||||
("create", "return"),
|
||||
("create", "call"),
|
||||
("create", "line"),
|
||||
("__iter__", "call"),
|
||||
("__iter__", "line"),
|
||||
("__iter__", "exception"),
|
||||
("__iter__", "return"),
|
||||
("create", "exception"),
|
||||
("create", "return"),
|
||||
("create", "exception"),
|
||||
("create", "return"),
|
||||
]
|
||||
assert ("__iter__", "call") in trace
|
||||
assert ("__iter__", "exception") in trace
|
||||
assert names == [
|
||||
"test_eager_prefix_is_untraceable_but_reports_monitoring_errors",
|
||||
"create",
|
||||
"__iter__",
|
||||
]
|
||||
|
||||
monitoring = sys.monitoring
|
||||
records = []
|
||||
event_names = (
|
||||
"PY_START",
|
||||
"PY_RESUME",
|
||||
"PY_UNWIND",
|
||||
"RAISE",
|
||||
"LINE",
|
||||
"INSTRUCTION",
|
||||
)
|
||||
monitoring.use_tool_id(2, "iterator-contract-315")
|
||||
try:
|
||||
for name in event_names:
|
||||
event = getattr(monitoring.events, name)
|
||||
|
||||
def callback(code, *args, name=name):
|
||||
if code.co_name == "<genexpr>":
|
||||
records.append(name)
|
||||
|
||||
monitoring.register_callback(2, event, callback)
|
||||
monitoring.set_events(
|
||||
2, sum(getattr(monitoring.events, name) for name in event_names)
|
||||
)
|
||||
generator = create(Good())
|
||||
generator.close()
|
||||
assert records == []
|
||||
try:
|
||||
create(Bad())
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("expected monitored eager iterator error")
|
||||
finally:
|
||||
monitoring.set_events(2, 0)
|
||||
monitoring.free_tool_id(2)
|
||||
assert records == ["RAISE", "PY_UNWIND"]
|
||||
|
||||
|
||||
test_eager_prefix_is_untraceable_but_reports_monitoring_errors()
|
||||
|
||||
|
||||
def test_created_generator_frames_do_not_retain_materialized_caller():
|
||||
import sys
|
||||
|
||||
def generator():
|
||||
yield 1
|
||||
|
||||
async def coroutine():
|
||||
return 1
|
||||
|
||||
async def async_generator():
|
||||
yield 1
|
||||
|
||||
def create():
|
||||
caller = sys._getframe()
|
||||
return (
|
||||
(value for value in [1]),
|
||||
generator(),
|
||||
coroutine(),
|
||||
async_generator(),
|
||||
caller,
|
||||
)
|
||||
|
||||
genexpr, gen, coro, agen, caller = create()
|
||||
assert caller.f_code.co_name == "create"
|
||||
assert genexpr.gi_frame.f_back is None
|
||||
assert gen.gi_frame.f_back is None
|
||||
assert coro.cr_frame.f_back is None
|
||||
assert agen.ag_frame.f_back is None
|
||||
genexpr.close()
|
||||
gen.close()
|
||||
coro.close()
|
||||
close = agen.aclose()
|
||||
try:
|
||||
close.send(None)
|
||||
except StopIteration:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("unstarted async generator close suspended")
|
||||
|
||||
|
||||
test_created_generator_frames_do_not_retain_materialized_caller()
|
||||
|
||||
|
||||
def test_instrumented_iteration_and_send_keep_both_iterator_slots():
|
||||
import sys
|
||||
|
||||
def iterate(values):
|
||||
result = []
|
||||
for value in values:
|
||||
result.append(value)
|
||||
return result
|
||||
|
||||
def leaf():
|
||||
yield 1
|
||||
return 9
|
||||
|
||||
def delegate():
|
||||
return (yield from leaf())
|
||||
|
||||
monitoring = sys.monitoring
|
||||
records = []
|
||||
event_names = ("BRANCH_LEFT", "BRANCH_RIGHT", "STOP_ITERATION", "PY_YIELD")
|
||||
monitoring.use_tool_id(2, "instrumented-iterator-contract-315")
|
||||
try:
|
||||
for name in event_names:
|
||||
event = getattr(monitoring.events, name)
|
||||
|
||||
def callback(code, *args, name=name):
|
||||
if code.co_name in ("iterate", "delegate"):
|
||||
records.append((code.co_name, name))
|
||||
|
||||
monitoring.register_callback(2, event, callback)
|
||||
monitoring.set_events(
|
||||
2, sum(getattr(monitoring.events, name) for name in event_names)
|
||||
)
|
||||
assert iterate(range(3)) == [0, 1, 2]
|
||||
assert iterate([0, 1, 2]) == [0, 1, 2]
|
||||
assert iterate((0, 1, 2)) == [0, 1, 2]
|
||||
assert iterate(value for value in range(3)) == [0, 1, 2]
|
||||
generator = delegate()
|
||||
assert next(generator) == 1
|
||||
try:
|
||||
next(generator)
|
||||
except StopIteration as error:
|
||||
assert error.value == 9
|
||||
else:
|
||||
raise AssertionError("instrumented delegate did not finish")
|
||||
finally:
|
||||
monitoring.set_events(2, 0)
|
||||
monitoring.free_tool_id(2)
|
||||
assert ("iterate", "BRANCH_LEFT") in records
|
||||
assert ("iterate", "BRANCH_RIGHT") in records
|
||||
assert ("delegate", "PY_YIELD") in records
|
||||
assert ("delegate", "STOP_ITERATION") in records
|
||||
|
||||
|
||||
test_instrumented_iteration_and_send_keep_both_iterator_slots()
|
||||
|
||||
|
||||
def test_eager_iterator_frame_inspection_skips_incomplete_generator():
|
||||
import sys
|
||||
|
||||
iterator_frames = []
|
||||
caller_frames = []
|
||||
seen = []
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self):
|
||||
frame = sys._getframe()
|
||||
caller = sys._getframe(1)
|
||||
iterator_frames.append(frame)
|
||||
caller_frames.append(caller)
|
||||
seen.append((caller.f_code.co_name, frame.f_back.f_code.co_name))
|
||||
return iter([7])
|
||||
|
||||
def create():
|
||||
return (value for value in Iterable())
|
||||
|
||||
generator = create()
|
||||
assert seen == [("create", "create")]
|
||||
# Both real frames have returned; retained f_back must still refer to
|
||||
# the complete creator rather than its hidden generator preamble.
|
||||
assert iterator_frames[0].f_back is caller_frames[0]
|
||||
assert iterator_frames[0].f_back.f_code.co_name == "create"
|
||||
assert generator.gi_frame.f_back is None
|
||||
assert list(generator) == [7]
|
||||
|
||||
|
||||
test_eager_iterator_frame_inspection_skips_incomplete_generator()
|
||||
|
||||
|
||||
def test_unstarted_coroutine_warning_survives_creation_preamble():
|
||||
import gc
|
||||
import warnings
|
||||
|
||||
async def pending():
|
||||
return 7
|
||||
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always", RuntimeWarning)
|
||||
coroutine = pending()
|
||||
del coroutine
|
||||
gc.collect()
|
||||
assert len(caught) == 1
|
||||
assert caught[0].category is RuntimeWarning
|
||||
assert str(caught[0].message).endswith("pending' was never awaited")
|
||||
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always", RuntimeWarning)
|
||||
coroutine = pending()
|
||||
coroutine.close()
|
||||
del coroutine
|
||||
gc.collect()
|
||||
assert caught == []
|
||||
|
||||
|
||||
test_unstarted_coroutine_warning_survives_creation_preamble()
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Private development ABI reader/writer boundary; no public compatibility claim."""
|
||||
|
||||
import _frozen_importlib_external as external
|
||||
import importlib
|
||||
import importlib.util
|
||||
import py_compile
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
if sys.implementation.name == "rustpython":
|
||||
assert external.MAGIC_NUMBER == bytes((0x16, 0xDA, 13, 10))
|
||||
assert sys.implementation.cache_tag == "rustpython-315-devabi2"
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
source = root / "cache_subject.py"
|
||||
source.write_text("value = 41\n", encoding="ascii")
|
||||
code = compile("value = 42\n", "byte_exact/cache_subject.py", "exec")
|
||||
stat = source.stat()
|
||||
headers = [
|
||||
external._code_to_timestamp_pyc(code, int(stat.st_mtime), stat.st_size),
|
||||
external._code_to_hash_pyc(
|
||||
code, importlib.util.source_hash(source.read_bytes()), True
|
||||
),
|
||||
external._code_to_hash_pyc(
|
||||
code, importlib.util.source_hash(source.read_bytes()), False
|
||||
),
|
||||
]
|
||||
rejected = [3627, 3666, 0xDA15]
|
||||
current = 0xDA16
|
||||
|
||||
def load_from(path):
|
||||
sys.modules.pop("cache_subject", None)
|
||||
sys.path.insert(0, str(path))
|
||||
importlib.invalidate_caches()
|
||||
try:
|
||||
return importlib.import_module("cache_subject").value
|
||||
finally:
|
||||
sys.path.pop(0)
|
||||
sys.modules.pop("cache_subject", None)
|
||||
|
||||
# Source caches must fall back to source on every foreign magic.
|
||||
cache = Path(importlib.util.cache_from_source(str(source)))
|
||||
cache.parent.mkdir()
|
||||
for payload in headers:
|
||||
for magic in rejected + [current]:
|
||||
candidate = magic.to_bytes(2, "little") + b"\r\n" + payload[4:]
|
||||
cache.write_bytes(candidate)
|
||||
assert load_from(root) == (42 if magic == current else 41)
|
||||
|
||||
# Sourceless files cannot silently fall back or accept foreign formats.
|
||||
source.unlink()
|
||||
sourceless = root / "cache_subject.pyc"
|
||||
for payload in headers:
|
||||
for magic in rejected + [current]:
|
||||
sourceless.write_bytes(
|
||||
magic.to_bytes(2, "little") + b"\r\n" + payload[4:]
|
||||
)
|
||||
if magic == current:
|
||||
assert load_from(root) == 42
|
||||
else:
|
||||
try:
|
||||
load_from(root)
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("foreign sourceless cache accepted")
|
||||
|
||||
# ZIP source fallback and pyc-only rejection use the same token.
|
||||
for header_index, payload in enumerate(headers):
|
||||
for with_source in (False, True):
|
||||
for magic in rejected + [current]:
|
||||
archive = root / f"case_{header_index}_{with_source}_{magic}.zip"
|
||||
with zipfile.ZipFile(archive, "w") as output:
|
||||
output.writestr(
|
||||
"cache_subject.pyc",
|
||||
magic.to_bytes(2, "little") + b"\r\n" + payload[4:],
|
||||
)
|
||||
if with_source:
|
||||
output.writestr("cache_subject.py", "value = 41\n")
|
||||
if magic == current or with_source:
|
||||
value = load_from(archive)
|
||||
if magic != current:
|
||||
assert value == 41
|
||||
elif with_source and header_index == 0:
|
||||
# ZIP timestamp validation may select the source.
|
||||
assert value in (41, 42)
|
||||
else:
|
||||
assert value == 42
|
||||
else:
|
||||
try:
|
||||
load_from(archive)
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("foreign ZIP cache accepted")
|
||||
|
||||
# Explicit py_compile writes under -B; all writers must use the private token.
|
||||
source.write_text("value = 41\n", encoding="ascii")
|
||||
explicit = root / "explicit.pyc"
|
||||
previous = sys.dont_write_bytecode
|
||||
sys.dont_write_bytecode = True
|
||||
try:
|
||||
py_compile.compile(str(source), cfile=str(explicit), doraise=True)
|
||||
assert explicit.read_bytes()[:4] == external.MAGIC_NUMBER
|
||||
finally:
|
||||
sys.dont_write_bytecode = previous
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Compile one ordinary request; emit unmodified marshal before field inspection."""
|
||||
|
||||
import json
|
||||
import marshal
|
||||
import struct
|
||||
import sys
|
||||
import types
|
||||
|
||||
FIELDS = (
|
||||
"co_argcount",
|
||||
"co_posonlyargcount",
|
||||
"co_kwonlyargcount",
|
||||
"co_nlocals",
|
||||
"co_stacksize",
|
||||
"co_flags",
|
||||
"co_code",
|
||||
"co_consts",
|
||||
"co_names",
|
||||
"co_varnames",
|
||||
"co_freevars",
|
||||
"co_cellvars",
|
||||
"co_filename",
|
||||
"co_name",
|
||||
"co_qualname",
|
||||
"co_firstlineno",
|
||||
"co_linetable",
|
||||
"co_exceptiontable",
|
||||
)
|
||||
|
||||
|
||||
def describe(value):
|
||||
kind = type(value)
|
||||
if kind is types.CodeType:
|
||||
return {
|
||||
"type": "code",
|
||||
"fields": {name: describe(getattr(value, name)) for name in FIELDS},
|
||||
}
|
||||
if value is None or value is Ellipsis:
|
||||
return {"type": kind.__name__}
|
||||
if kind in (bool, int, str):
|
||||
return {"type": kind.__name__, "value": value}
|
||||
if kind is bytes:
|
||||
return {"type": "bytes", "value": list(value)}
|
||||
if kind is float:
|
||||
return {"type": "float", "bits": list(struct.pack("<d", value))}
|
||||
if kind is complex:
|
||||
return {
|
||||
"type": "complex",
|
||||
"bits": list(struct.pack("<dd", value.real, value.imag)),
|
||||
}
|
||||
if kind in (tuple, frozenset):
|
||||
# Retain observed iteration order. No diagnostic or wire normalization.
|
||||
return {"type": kind.__name__, "items": [describe(item) for item in value]}
|
||||
raise TypeError("uncaptured constant type: " + kind.__name__)
|
||||
|
||||
|
||||
def capture(request):
|
||||
code = compile(
|
||||
request["source"],
|
||||
request["filename"],
|
||||
request["mode"],
|
||||
flags=request["flags"],
|
||||
dont_inherit=True,
|
||||
optimize=request["optimize"],
|
||||
)
|
||||
raw = list(marshal.dumps(code, 6))
|
||||
# Read exported fields only after serialization: getters may acquire owners.
|
||||
fields = describe(code)
|
||||
import _frozen_importlib_external
|
||||
|
||||
return {
|
||||
"marshal": raw,
|
||||
"code": fields,
|
||||
"magic": list(_frozen_importlib_external.MAGIC_NUMBER),
|
||||
"marshal_version": marshal.version,
|
||||
"version": list(sys.version_info),
|
||||
"runtime": runtime_provenance(),
|
||||
}
|
||||
|
||||
|
||||
def runtime_provenance():
|
||||
modules = {}
|
||||
for name in ("json", "types", "struct", "marshal", "_frozen_importlib_external"):
|
||||
module = sys.modules[name]
|
||||
spec = getattr(module, "__spec__", None)
|
||||
modules[name] = {
|
||||
"file": getattr(module, "__file__", None),
|
||||
"origin": getattr(spec, "origin", None),
|
||||
"cached": getattr(spec, "cached", None),
|
||||
}
|
||||
return {
|
||||
"path": list(sys.path),
|
||||
"pycache_prefix": getattr(sys, "pycache_prefix", None),
|
||||
"has_pycache_prefix": hasattr(sys, "pycache_prefix"),
|
||||
"dont_write_bytecode": sys.dont_write_bytecode,
|
||||
"modules": modules,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(capture(json.load(sys.stdin)), sort_keys=True))
|
||||
@@ -0,0 +1,174 @@
|
||||
"""Strict finite compiler comparison. This does not establish general parity."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def requests(fixtures, selected):
|
||||
names = [fixture["name"] for fixture in fixtures]
|
||||
if len(names) != len(set(names)):
|
||||
raise ValueError("duplicate fixture names")
|
||||
unknown = set(selected) - set(names)
|
||||
if unknown:
|
||||
raise ValueError("unknown fixtures: " + ", ".join(sorted(unknown)))
|
||||
chosen = [f for f in fixtures if not selected or f["name"] in selected]
|
||||
if not chosen:
|
||||
raise ValueError("empty requested corpus")
|
||||
result = []
|
||||
for fixture in chosen:
|
||||
if not fixture["name"].isascii() or not fixture["source"].isascii():
|
||||
raise ValueError("only fixed ASCII fixtures are supported")
|
||||
for optimize in (0, 1, 2):
|
||||
result.append(
|
||||
{
|
||||
**fixture,
|
||||
"filename": "byte_exact/" + fixture["name"] + ".py",
|
||||
"optimize": optimize,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def differences(left, right, path=""):
|
||||
if type(left) is not type(right):
|
||||
return [path]
|
||||
if isinstance(left, dict):
|
||||
if left.keys() != right.keys():
|
||||
return [path]
|
||||
return [
|
||||
p
|
||||
for key in left
|
||||
for p in differences(left[key], right[key], path + "/" + key)
|
||||
]
|
||||
if isinstance(left, list):
|
||||
if len(left) != len(right):
|
||||
return [path]
|
||||
return [
|
||||
p
|
||||
for i, (a, b) in enumerate(zip(left, right))
|
||||
for p in differences(a, b, path + "/" + str(i))
|
||||
]
|
||||
return [] if left == right else [path]
|
||||
|
||||
|
||||
def validate_cache_prefix(path):
|
||||
path = Path(path).resolve()
|
||||
if not path.is_dir() or any(path.iterdir()):
|
||||
raise ValueError("CPython cache prefix must be an existing empty directory")
|
||||
return path
|
||||
|
||||
|
||||
def compare_captures(reference, candidate):
|
||||
return {
|
||||
key: differences(reference[key], candidate[key], key)
|
||||
for key in ("marshal", "magic", "code")
|
||||
}
|
||||
|
||||
|
||||
def capture_environment(lib, cpython_cache_prefix):
|
||||
env = {
|
||||
"PATH": os.environ.get("PATH", ""),
|
||||
"LC_ALL": "C.UTF-8",
|
||||
"TZ": "UTC",
|
||||
"PYTHONHASHSEED": "0",
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
# CPython redirects normal source-cache reads before startup imports.
|
||||
# RustPython currently ignores this setting; its embedded Lib path must
|
||||
# independently be verified cache-free.
|
||||
"PYTHONPYCACHEPREFIX": str(cpython_cache_prefix),
|
||||
}
|
||||
if lib:
|
||||
env["RUSTPYTHONPATH"] = str(Path(lib).resolve())
|
||||
return env
|
||||
|
||||
|
||||
def run_capture(executable, request, lib, cpython_cache_prefix):
|
||||
env = capture_environment(lib, cpython_cache_prefix)
|
||||
proc = subprocess.run(
|
||||
[executable, "-S", "-B", str(ROOT / "byte_exact_capture.py")],
|
||||
input=json.dumps(request),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
timeout=60,
|
||||
)
|
||||
if proc.returncode:
|
||||
raise RuntimeError(f"capture exit {proc.returncode}: {proc.stderr}")
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--reference", required=True)
|
||||
parser.add_argument("--candidate", required=True)
|
||||
parser.add_argument("--candidate-lib", required=True)
|
||||
parser.add_argument("--cpython-cache-prefix", type=Path, required=True)
|
||||
parser.add_argument(
|
||||
"--fixtures", type=Path, default=ROOT / "byte_exact_fixtures.json"
|
||||
)
|
||||
parser.add_argument("--case", action="append", default=[])
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
cache_prefix = validate_cache_prefix(args.cpython_cache_prefix)
|
||||
corpus = requests(json.loads(args.fixtures.read_text()), args.case)
|
||||
except ValueError as error:
|
||||
parser.error(str(error))
|
||||
rows = []
|
||||
report = {
|
||||
"status": "incomplete",
|
||||
"expected": len(corpus),
|
||||
"rows": rows,
|
||||
"reference_sha256": digest(args.reference),
|
||||
"candidate_sha256": digest(args.candidate),
|
||||
"harness_sha256": digest(__file__),
|
||||
"capture_sha256": digest(ROOT / "byte_exact_capture.py"),
|
||||
"fixtures_sha256": digest(args.fixtures),
|
||||
"cpython_pycache_prefix": str(cache_prefix),
|
||||
"candidate_lib": str(Path(args.candidate_lib).resolve()),
|
||||
}
|
||||
|
||||
def save():
|
||||
args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
|
||||
|
||||
save()
|
||||
for request in corpus:
|
||||
row = {"request": request, "status": "incomplete"}
|
||||
rows.append(row)
|
||||
try:
|
||||
reference = run_capture(args.reference, request, None, cache_prefix)
|
||||
candidate = run_capture(
|
||||
args.candidate, request, args.candidate_lib, cache_prefix
|
||||
)
|
||||
validate_cache_prefix(cache_prefix)
|
||||
mismatches = compare_captures(reference, candidate)
|
||||
row.update(
|
||||
reference=reference,
|
||||
candidate=candidate,
|
||||
mismatches=mismatches,
|
||||
status="exact" if not any(mismatches.values()) else "mismatch",
|
||||
)
|
||||
except Exception as error:
|
||||
row.update(status="error", error=str(error))
|
||||
save()
|
||||
raise
|
||||
save()
|
||||
report.update(status="complete", exact=sum(r["status"] == "exact" for r in rows))
|
||||
save()
|
||||
print(json.dumps({"completed": len(rows), "exact": report["exact"]}))
|
||||
return 0 if report["exact"] == len(corpus) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,224 @@
|
||||
[
|
||||
{
|
||||
"name": "empty",
|
||||
"source": "",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "assignment",
|
||||
"source": "x = 1\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "expression",
|
||||
"source": "a + 2",
|
||||
"mode": "eval",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "interactive",
|
||||
"source": "1 + 2\n",
|
||||
"mode": "single",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "function",
|
||||
"source": "def f(a, /, b=2, *, c=3):\n return a + b * c\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "closure",
|
||||
"source": "def outer(x):\n def inner(y):\n return x + y\n return inner\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "branch_loop",
|
||||
"source": "def f(xs):\n total = 0\n for x in xs:\n if x:\n total += x\n return total\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "exception",
|
||||
"source": "def f(x):\n try:\n return 1 / x\n except ZeroDivisionError:\n return None\n finally:\n x = 0\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "comprehension",
|
||||
"source": "xs = [x * x for x in range(4) if x]\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "generator",
|
||||
"source": "def f(xs):\n yield from xs\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "coroutine",
|
||||
"source": "async def f(g):\n return await g()\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
"source": "class C:\n value = 1\n def f(self):\n return self.value\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "constants",
|
||||
"source": "a = (-0.0, 0.0, 2j, b'abc', 'abc', (1, 2))\nb = (1, 2)\nc = x in {'a', 'b'}\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "doc_assert",
|
||||
"source": "\"module doc\"\ndef f(x):\n \"function doc\"\n assert x\n return __debug__\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "future_annotations",
|
||||
"source": "def f(x: T) -> U:\n return x\n",
|
||||
"mode": "exec",
|
||||
"flags": 16777216
|
||||
},
|
||||
{
|
||||
"name": "formatting",
|
||||
"source": "result = f\"{value!r:>10}\"\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "template",
|
||||
"source": "result = t\"value={value}\"\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "matching",
|
||||
"source": "match value:\n case [a, b]:\n result = a + b\n case _:\n result = None\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "yield_plain",
|
||||
"source": "def f():\n yield 1\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "import_module",
|
||||
"source": "import os\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "import_function",
|
||||
"source": "def f():\n import os\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "import_handler",
|
||||
"source": "try:\n import os\nexcept ImportError:\n import sys\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "genexpr_list",
|
||||
"source": "def f(xs):\n return list(x + 1 for x in xs if x)\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "genexpr_set",
|
||||
"source": "def f(xs):\n return set(x + 1 for x in xs if x)\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "genexpr_tuple",
|
||||
"source": "def f(xs):\n return tuple(x + 1 for x in xs if x)\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "async_genexpr",
|
||||
"source": "async def f(xs):\n return list(x async for x in xs)\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "borrow_locals",
|
||||
"source": "def f(a, b):\n return a, b, a\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "exception_alias",
|
||||
"source": "try:\n work()\nexcept Exception as error:\n result = error\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "assembly_siblings",
|
||||
"source": "def left():\n return 1\ndef right():\n return 1\n",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "dis_if_ors",
|
||||
"source": "if True or False or False:\n pass",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "dis_if_ands",
|
||||
"source": "if True and False and False:\n pass",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "dis_if_mixed",
|
||||
"source": "if (True and False) or (False and True):\n pass",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "dis_nested_bool_op",
|
||||
"source": "x = Test() and False or False",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "dis_const_no_op",
|
||||
"source": "x = not True",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "dis_constant_true_if_pass_keeps_line_anchor_nop",
|
||||
"source": "if 1:\n pass",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "dis_nested_double_async_with",
|
||||
"source": "async def test():\n for stop_exc in (StopIteration('spam'), StopAsyncIteration('ham')):\n with self.subTest(type=type(stop_exc)):\n try:\n async with egg():\n raise stop_exc\n except Exception as ex:\n self.assertIs(ex, stop_exc)\n else:\n self.fail(f'{stop_exc} was suppressed')",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"name": "dis_bare_function_annotations_check_attribute_and_subscript_expressions",
|
||||
"source": "def f(one: int):\n int.new_attr: int\n [list][0].new_attr: [int, str]\n my_lst = [1]\n my_lst[one]: int\n return my_lst",
|
||||
"mode": "exec",
|
||||
"flags": 0
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Controller integrity checks; these do not test interpreter behavior."""
|
||||
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from unittest.mock import patch
|
||||
|
||||
import byte_exact_capture
|
||||
from byte_exact_compile import (
|
||||
capture_environment,
|
||||
compare_captures,
|
||||
differences,
|
||||
requests,
|
||||
validate_cache_prefix,
|
||||
)
|
||||
|
||||
|
||||
class CorpusTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.fixture = {
|
||||
"name": "ordinary",
|
||||
"source": "x = 1\n",
|
||||
"mode": "exec",
|
||||
"flags": 0,
|
||||
}
|
||||
|
||||
def test_empty_corpus_is_an_error(self):
|
||||
with self.assertRaisesRegex(ValueError, "empty"):
|
||||
requests([], [])
|
||||
|
||||
def test_unknown_selection_is_an_error(self):
|
||||
with self.assertRaisesRegex(ValueError, "unknown"):
|
||||
requests([self.fixture], ["missing"])
|
||||
|
||||
def test_duplicate_names_are_an_error(self):
|
||||
with self.assertRaisesRegex(ValueError, "duplicate"):
|
||||
requests([self.fixture, self.fixture], [])
|
||||
|
||||
def test_explicit_selection_retains_all_optimizations(self):
|
||||
corpus = requests([self.fixture], ["ordinary"])
|
||||
self.assertEqual([r["optimize"] for r in corpus], [0, 1, 2])
|
||||
self.assertTrue(all(r["filename"] == "byte_exact/ordinary.py" for r in corpus))
|
||||
|
||||
def test_ascii_boundary(self):
|
||||
with self.assertRaisesRegex(ValueError, "ASCII"):
|
||||
requests([{**self.fixture, "source": "x = 'é'"}], [])
|
||||
|
||||
def test_equal_fields(self):
|
||||
self.assertEqual(differences({"a": [1, 2]}, {"a": [1, 2]}), [])
|
||||
|
||||
def test_byte_difference_is_retained(self):
|
||||
self.assertEqual(differences([1, 128], [1, 0], "marshal"), ["marshal/1"])
|
||||
|
||||
def test_nested_difference_has_single_leaf_path(self):
|
||||
self.assertEqual(
|
||||
differences({"code": {"flags": 1}}, {"code": {"flags": 2}}), ["/code/flags"]
|
||||
)
|
||||
|
||||
def test_length_difference_is_not_normalized(self):
|
||||
self.assertEqual(differences([1, 0], [1], "code"), ["code"])
|
||||
|
||||
def test_types_are_not_coerced(self):
|
||||
self.assertEqual(differences(True, 1, "constant"), ["constant"])
|
||||
|
||||
def test_cache_prefix_is_explicit_in_sanitized_environment(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
prefix = validate_cache_prefix(directory)
|
||||
env = capture_environment(None, prefix)
|
||||
self.assertEqual(env["PYTHONPYCACHEPREFIX"], str(prefix))
|
||||
self.assertEqual(env["PYTHONDONTWRITEBYTECODE"], "1")
|
||||
self.assertNotIn("RUSTPYTHONPATH", env)
|
||||
|
||||
def test_populated_cache_prefix_is_rejected(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
(Path(directory) / "existing.pyc").write_bytes(b"cache")
|
||||
with self.assertRaisesRegex(ValueError, "empty"):
|
||||
validate_cache_prefix(directory)
|
||||
|
||||
def test_runtime_provenance_is_not_a_comparison_field(self):
|
||||
reference = {"marshal": [1], "magic": [2], "code": {}, "runtime": "reference"}
|
||||
candidate = {**reference, "runtime": "candidate"}
|
||||
self.assertEqual(
|
||||
compare_captures(reference, candidate),
|
||||
{"marshal": [], "magic": [], "code": []},
|
||||
)
|
||||
|
||||
def test_runtime_provenance_follows_serialization_and_fields(self):
|
||||
events = []
|
||||
request = {**self.fixture, "filename": "ordinary.py", "optimize": 0}
|
||||
with (
|
||||
patch.object(
|
||||
byte_exact_capture.marshal,
|
||||
"dumps",
|
||||
side_effect=lambda *a: events.append("marshal") or b"raw",
|
||||
),
|
||||
patch.object(
|
||||
byte_exact_capture,
|
||||
"describe",
|
||||
side_effect=lambda value: events.append("fields") or {},
|
||||
),
|
||||
patch.object(
|
||||
byte_exact_capture,
|
||||
"runtime_provenance",
|
||||
side_effect=lambda: events.append("runtime") or {},
|
||||
),
|
||||
):
|
||||
byte_exact_capture.capture(request)
|
||||
self.assertEqual(events, ["marshal", "fields", "runtime"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,78 @@
|
||||
# Final 3.15 compiler ABI reconstruction
|
||||
|
||||
This is new work based on public RustPython commit
|
||||
`06007c81fa99e813cbf4583dab326764ba0ef017`. Earlier local checkpoints were lost
|
||||
with their execution environment. Their historical results are not validation
|
||||
of this reconstruction.
|
||||
|
||||
The source target is CPython v3.15.0,
|
||||
`d220409a13b77de7a6c30abe16ea90db01528edc`. `target_source.json` pins the
|
||||
generator dependencies and directly consulted ABI sources. The active metadata
|
||||
generator verifies those hashes before importing the CPython generator modules.
|
||||
The target generator directory must also be free of bytecode caches; the gate
|
||||
rejects existing pycs and disables writes before importing from the verified path.
|
||||
`verify_stack_contract.py` compiles the pinned C metadata independently and
|
||||
checks 3,430 argument-specific stack rows; Rust tests compare every supported
|
||||
opcode against those rows. Unsupported specialized instructions are not claimed
|
||||
implemented merely because their reference rows are recorded.
|
||||
|
||||
## Private cache contract
|
||||
|
||||
The intermediate ABI uses magic `0xDA16` (bytes `16 da 0d 0a`) and cache tag
|
||||
`rustpython-315-devabi2`. Available repository and pinned-source definitions were
|
||||
checked for collisions; this is not a globally reserved token. The lost
|
||||
`0xDA15` development format is deliberately rejected, as are public CPython
|
||||
formats. Change both token and tag whenever the intermediate format changes
|
||||
incompatibly.
|
||||
|
||||
The shared Rust magic feeds importlib, source and sourceless caches, ZIP imports,
|
||||
explicit `py_compile`/`compileall` writers, and direct Rust pyc readers. `-B`
|
||||
does not prohibit cache reads or explicit writes. Validation uses a fresh owned
|
||||
Lib copy, records cache inventories, and checks foreign timestamp/hash caches
|
||||
and ZIP fallback/rejection. The CLI prepends `BUILDTIME_RUSTPYTHONPATH` ahead of
|
||||
`RUSTPYTHONPATH`; a new environment path alone does not isolate cache reads.
|
||||
Strict captures must verify actual module paths and keep their embedded Lib copy
|
||||
separate from cache-writing Rust embedding tests. Headerless marshal and explicit frozen-data input
|
||||
remain caller-version-coupled. Frozen modules must be rebuilt using the same
|
||||
compiler and VM target; private pyc magic does not validate their separate
|
||||
compressed format.
|
||||
|
||||
## Output and integration gates
|
||||
|
||||
The strict harness captures untouched marshal version 6 bytes before inspecting
|
||||
code fields. It compares magic, marshal and recursively captured fields without
|
||||
normalizing byte streams, exception ranges, reference flags, or nested code.
|
||||
An empty corpus is an error. Its finite fixtures are witnesses, not a general
|
||||
compiler-equivalence proof. Behavioral regressions belong in Python snippets;
|
||||
Rust tests additionally check structural compiler and ownership invariants.
|
||||
The reference invocation requires an empty `PYTHONPYCACHEPREFIX` so existing
|
||||
certified-build caches are not read. The harness verifies that directory remains
|
||||
empty and records runtime paths, cache settings and already-imported module
|
||||
origins after serialization and field capture. These diagnostics are provenance;
|
||||
they do not alter the three strict comparison domains.
|
||||
|
||||
Private load-constant specializations deoptimize to public `LOAD_CONST` during
|
||||
export. Active LINE/INSTRUCTION monitoring export remains unresolved: the
|
||||
current getter reads live instructions and cannot reconstruct generic monitoring
|
||||
wrappers without per-code metadata. Ordinary warmed tests do not cover that
|
||||
case; canonical immutable storage requires a separately reviewed next stage.
|
||||
Retained private `GET_YIELD_FROM_ITER` must never be emitted by the
|
||||
compiler. Warmed export tests cover ordinary specialization and marshal reads.
|
||||
|
||||
No Lib files are modified. Consequently the unchanged Python `opcode` and
|
||||
`dis` metadata are a known deferred integration gap. Neither private magic nor
|
||||
passing raw-code fixtures makes this a distributable 3.15 implementation.
|
||||
The future exact-copy unit is `Lib/opcode.py`, `Lib/_opcode_metadata.py`, and
|
||||
`Lib/dis.py`; final metadata also requires builtin `frozendict`, a separate
|
||||
Rust-first dependency absent from this baseline. Incorrect disassembly snapshots
|
||||
must not be accepted to hide that integration gap.
|
||||
Any future Lib update must use the repository's `update_lib` script, after
|
||||
verifying its supported scope and exact source pin. Manual copying or editing
|
||||
these files is not permitted.
|
||||
The public magic transition requires full serialization compatibility,
|
||||
reader/writer/frozen agreement, and the separately planned exact Python update.
|
||||
|
||||
Marshal reference ownership, compiler constant-origin chronology and mortal
|
||||
interning remain separate work. No count offsets, flag masking, equal-text
|
||||
over-interning or weakening of static-reference invariants is permitted.
|
||||
The filename/surrogate family remains outside this stage.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Private legacy instruction. The compiler must never emit this opcode.
|
||||
[GetYieldFromIter.stack_effect]
|
||||
popped = "1"
|
||||
pushed = "1"
|
||||
@@ -1,26 +1,37 @@
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from functools import cache
|
||||
|
||||
from target_source import verify_import_origins, verify_source
|
||||
|
||||
try:
|
||||
CPYTHON_ROOT = pathlib.Path(os.environ["CPYTHON_ROOT"]).expanduser().resolve()
|
||||
except KeyError:
|
||||
raise ValueError("Missing environment variable 'CPYTHON_ROOT'")
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
verify_source(CPYTHON_ROOT)
|
||||
verify_import_origins(CPYTHON_ROOT)
|
||||
|
||||
CPYTHON_TOOLS_LIB = CPYTHON_ROOT / "Tools" / "cases_generator"
|
||||
|
||||
|
||||
if (path := CPYTHON_TOOLS_LIB.as_posix()) not in sys.path:
|
||||
sys.path.append(path)
|
||||
sys.path.insert(0, path)
|
||||
|
||||
|
||||
from analyzer import SKIP_PROPERTIES, Analysis, Family, Properties, analyze_files
|
||||
from stack import get_stack_effect
|
||||
|
||||
verify_import_origins(CPYTHON_ROOT)
|
||||
|
||||
|
||||
@cache
|
||||
def get_analysis() -> Analysis:
|
||||
from generators_common import DEFAULT_INPUT
|
||||
|
||||
verify_import_origins(CPYTHON_ROOT)
|
||||
analysis = analyze_files([DEFAULT_INPUT])
|
||||
# Our speration is done at the enum definition
|
||||
analysis.instructions |= analysis.pseudos
|
||||
|
||||
@@ -38,6 +38,8 @@ class OpcodeInfo:
|
||||
|
||||
res[family_name].append(member_name)
|
||||
|
||||
if "LoadConstImmortal" in names:
|
||||
res["LoadConst"].extend(["LoadConstImmortal", "LoadConstMortal"])
|
||||
return dict(res)
|
||||
|
||||
def __iter__(self):
|
||||
@@ -81,7 +83,11 @@ def iter_opcodes(text: str, override_confs: OverrideConfs) -> Iterable[Opcode]:
|
||||
cpython_name = opcode.cpython_name
|
||||
|
||||
kwargs = {}
|
||||
if instr := analysis.instructions.get(cpython_name):
|
||||
if instr := analysis.instructions.get(
|
||||
"LOAD_CONST"
|
||||
if rust_name in ("LoadConstImmortal", "LoadConstMortal")
|
||||
else cpython_name
|
||||
):
|
||||
kwargs["properties"] = properties_with_pseudo_flags(instr)
|
||||
kwargs["family"] = getattr(instr, "family", None)
|
||||
kwargs["cache_entry"] = getattr(instr, "size", -1)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#define Py_BUILD_CORE
|
||||
#define NEED_OPCODE_METADATA
|
||||
#include "Python.h"
|
||||
#include "pycore_opcode_metadata.h"
|
||||
#include <stdio.h>
|
||||
int main(void) {
|
||||
const int args[] = {0,1,2,3,4,5,7,8,15,16,31,255,256,65535};
|
||||
for (int op=0; op<512; op++) {
|
||||
for (unsigned int i=0; i<sizeof(args)/sizeof(args[0]); i++) {
|
||||
int popped = _PyOpcode_num_popped(op,args[i]);
|
||||
int pushed = _PyOpcode_num_pushed(op,args[i]);
|
||||
if (popped >= 0 && pushed >= 0) {
|
||||
printf("%d %d %d %d\n",op,args[i],popped,pushed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"commit": "d220409a13b77de7a6c30abe16ea90db01528edc",
|
||||
"files": {
|
||||
"Include/internal/pycore_interpframe.h": "87f52ece3a11f8dfde0274c95c7064efc90b859dd73386cca8cdb83609f21821",
|
||||
"Include/internal/pycore_opcode_metadata.h": "ce5fb59eadbe061266d7582d5cdd0bf655bb93d6d08fb660a8edf0a19c198fec",
|
||||
"Include/internal/pycore_opcode_utils.h": "17bb85a7bcabbc29a505949512527f97070daf68f8692f015ac8b6a0f0f0c4e3",
|
||||
"Include/internal/pycore_uop_ids.h": "dc3b00ba46b803ae4c46cb6b5063ee6814a9330f586692ff4bb500a0b888c421",
|
||||
"Include/opcode_ids.h": "ffd77843d3b0b58ba476fdc2eca3afc0436b6c5a97adc0cae7e39c1bd0221778",
|
||||
"Objects/codeobject.c": "b95a59389e3da52283d96b9bfd1a6479ca5d7a6e45c4eeadf5433d88fa84e6d8",
|
||||
"Objects/frameobject.c": "84d0268c5248e5049158b63ab82556e04be6122f8ebc400bf7723a13b9b98c62",
|
||||
"Objects/unicodeobject.c": "dcb23f28d01be5b0aa344e7f4d6f79b58cb958139f67f3747cbfd0867ab68f5a",
|
||||
"Python/assemble.c": "831156f971b07c1ceae22c97a90e9e56363b4a2856896f5383690a30509b94c8",
|
||||
"Python/bytecodes.c": "aa81f2f18d44cf4e04aa75b5e00e5e50b022c560028030bd062d781bd734c9ea",
|
||||
"Python/codegen.c": "536a4f1c79b1e0aa292f9954d9e1ca8b02e6c3eddbd08dd111a0998c41848a5d",
|
||||
"Python/compile.c": "4bff663c758db06b355305406771bc8289af275ca5391daf798aa68d5250f098",
|
||||
"Python/flowgraph.c": "331f430d7e028b7bd2ab58ec44f7ffaf4a7ac4d53985cdf723477d9406b12aac",
|
||||
"Python/instrumentation.c": "1650fd63b57447c4ac6d8cd7a8dbd81e5bf0124fd4ce7615b2c348880757fadf",
|
||||
"Python/marshal.c": "2d2fe69cf2e2f2fcc2693084b91c615ba8c0edba83b003b8bc6408bf1edf77e1",
|
||||
"Python/specialize.c": "372091addcc5d351b6960ecd9e181ef6ea4af7f367f6d0b5aa655c2a2f7334ce",
|
||||
"Python/sysmodule.c": "a7c715a7d5db0ffa601b151235dfe7e928b57bd8150cf02eafcd1d5cafcde25f",
|
||||
"Tools/cases_generator/_typing_backports.py": "4fe25f5705e4f9f047ba330c70264032291c64d24cbd5f2d6da0615978521021",
|
||||
"Tools/cases_generator/analyzer.py": "10c410377629b4897cf1289bc84743e61c67c0a8c09ab5b4aba0b7fc74809763",
|
||||
"Tools/cases_generator/cwriter.py": "fb65713b1ad11c5dc190f50f6bb0ed026d5b582c9e5bac623037c000e25c9403",
|
||||
"Tools/cases_generator/generators_common.py": "06912332943c5962c45314d92bb7a40d95802c165b3dc791d9680e1c5a450118",
|
||||
"Tools/cases_generator/lexer.py": "80f2c5b97e012e0acda4b0a3de38506dbdd926e64f6328344172246d34ee0c4a",
|
||||
"Tools/cases_generator/opcode_id_generator.py": "69535cc326af8b3b649000c96d7f609f79a3d7b0a737a56469a9b5c442914966",
|
||||
"Tools/cases_generator/opcode_metadata_generator.py": "48bc3b643528a6d9eee6a48361882b5a2e30024fe74ae9f1a062f1d9524dd2d3",
|
||||
"Tools/cases_generator/optimizer_generator.py": "3cf3e9e3881b33700a851732c18ab32db4dddd4488ecc7b83e6e404caffd7e2f",
|
||||
"Tools/cases_generator/parser.py": "9f88319c6f461740cbfda18feb338a04276cd6672b5475956b155ba62f8912f6",
|
||||
"Tools/cases_generator/parsing.py": "02f90f5a1dc0c9ded0fcadf74b2f703301b0e5038ec0f2fdc0f185942ed77928",
|
||||
"Tools/cases_generator/plexer.py": "c13ce4a0b558791b1843e0969ee6abfc3e094e2116cc817fc1101dee9ccbb876",
|
||||
"Tools/cases_generator/py_metadata_generator.py": "23ba6de30dfc06bc7189e7636c98604fdf1e654af504237699a5b68a19fbca20",
|
||||
"Tools/cases_generator/record_function_generator.py": "13b66491a9f50df69ede5af3cf67dfa3b1ad688baf85c17f418a1ac29a1b107a",
|
||||
"Tools/cases_generator/stack.py": "2e192c11d7264d322cf691a3f66ce687459869c9b132710868a69d225103a4a1",
|
||||
"Tools/cases_generator/target_generator.py": "c7087ffceda3ac08c58aeb756f6a1326fd3a2dac4514bc2124b4660327c3d8b5",
|
||||
"Tools/cases_generator/tier1_generator.py": "a0c2ae9eb48d6e84f9b5fe3857ffb2aabb32e6231995a5e2c2a32d0e44788b57",
|
||||
"Tools/cases_generator/tier2_generator.py": "cb44cf5c1493a79b01b779bbf9b8efcf78bee1d339c03d3afe4c050e576fb09d",
|
||||
"Tools/cases_generator/uop_id_generator.py": "3aec741aed85baa041a9f40e1bb6019dd4cc35ae4d4de0b0c5dd092011fa439f",
|
||||
"Tools/cases_generator/uop_metadata_generator.py": "d95e5001035eda47642ecd70d2884b39f7dd69547bcd9c77dd9a341e2e088f3b"
|
||||
},
|
||||
"tag": "v3.15.0"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Fail closed before importing any target CPython generator code."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def verify_source(root):
|
||||
caches = list((root / "Tools/cases_generator").rglob("*.pyc"))
|
||||
if caches:
|
||||
raise ValueError(
|
||||
"target generator source must be cache-free; found " + str(caches[0])
|
||||
)
|
||||
manifest = json.loads(Path(__file__).with_suffix(".json").read_text())
|
||||
for relative, expected in manifest["files"].items():
|
||||
path = root / relative
|
||||
actual = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
if actual != expected:
|
||||
raise ValueError(f"wrong {manifest['tag']} source: {relative}: {actual}")
|
||||
return manifest
|
||||
|
||||
|
||||
def verify_import_origins(root):
|
||||
manifest = json.loads(Path(__file__).with_suffix(".json").read_text())
|
||||
for relative in manifest["files"]:
|
||||
path = Path(relative)
|
||||
if path.parent != Path("Tools/cases_generator") or path.suffix != ".py":
|
||||
continue
|
||||
module = sys.modules.get(path.stem)
|
||||
if module is not None:
|
||||
origin = getattr(module, "__file__", None)
|
||||
if origin is None or Path(origin).resolve() != (root / path).resolve():
|
||||
raise ValueError(
|
||||
"generator dependency loaded outside pinned source: " + path.stem
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Verify every active public discriminant against pinned final opcode_ids.h."""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from target_source import verify_source
|
||||
|
||||
from utils import DEFAULT_INPUT, to_upper_snake_case
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--source", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
verify_source(args.source)
|
||||
ids = {
|
||||
name: int(value)
|
||||
for name, value in re.findall(
|
||||
r"^#define\s+(\w+)\s+(\d+)\s*$",
|
||||
(args.source / "Include/opcode_ids.h").read_text(),
|
||||
re.M,
|
||||
)
|
||||
}
|
||||
private = {
|
||||
"GetYieldFromIter": 219,
|
||||
"LoadConstImmortal": 220,
|
||||
"LoadConstMortal": 221,
|
||||
}
|
||||
entries = re.findall(
|
||||
r"^ (\w+)(?:\s*\{[^}]*\})?\s*=\s*(\d+),",
|
||||
DEFAULT_INPUT.read_text(),
|
||||
re.M,
|
||||
)
|
||||
assert len(entries) == 238
|
||||
assert len({int(value) for _, value in entries}) == len(entries)
|
||||
for name, value in entries:
|
||||
expected = private[name] if name in private else ids[to_upper_snake_case(name)]
|
||||
assert int(value) == expected, (name, value, expected)
|
||||
print("Verified 235 public and 3 explicitly private opcode discriminants")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Regenerate independent C stack goldens, or fail if checked-in rows differ."""
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from target_source import verify_source
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--source", type=Path, required=True)
|
||||
parser.add_argument("--build", type=Path, required=True)
|
||||
parser.add_argument("--write", action="store_true")
|
||||
args = parser.parse_args()
|
||||
verify_source(args.source)
|
||||
here = Path(__file__).resolve().parent
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
binary = Path(temporary) / "stack-contract"
|
||||
subprocess.run(
|
||||
[
|
||||
"cc",
|
||||
"-I" + str(args.build),
|
||||
"-I" + str(args.source / "Include"),
|
||||
"-I" + str(args.source / "Include/internal"),
|
||||
str(here / "stack_contract.c"),
|
||||
"-o",
|
||||
str(binary),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
output = subprocess.check_output([str(binary)], text=True)
|
||||
rows = [tuple(map(int, line.split())) for line in output.splitlines()]
|
||||
assert len(rows) == 3430 and len(set(rows)) == 3430
|
||||
contents = (
|
||||
"// Generated by tools/opcode_metadata/verify_stack_contract.py from pinned CPython C metadata.\n"
|
||||
"const TARGET_STACK_CASES: &[(u16, u32, u32, u32)] = &[\n"
|
||||
+ "".join(f" ({op}, {arg}, {pop}, {push}),\n" for op, arg, pop, push in rows)
|
||||
+ "];\n"
|
||||
)
|
||||
target = here.parents[1] / "crates/compiler-core/src/bytecode/target_stack_cases.rs"
|
||||
if args.write:
|
||||
target.write_text(contents)
|
||||
elif target.read_text() != contents:
|
||||
raise SystemExit("pinned C stack rows differ")
|
||||
print(f"Verified {len(rows)} exact C stack rows")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user