Add BUILD_LIST_FROM_ARG opcode behind CompileOpts::build_list_from_arg (#9104)

Add Instruction::BuildListFromArg = 219 (stack effect: pop 1, push 2): pop
the iterator, push an empty list whose capacity is the iterator's length
hint, push the iterator back. A failing length hint is ignored unless it is
SystemExit or KeyboardInterrupt.

With CompileOpts::build_list_from_arg set, a list comprehension with one
generator, no `if` and no `async for` emits it in place of BUILD_LIST 0 on
the non-inlined path and in place of BUILD_LIST 0; SWAP 2 on the inlined
path. The option defaults to false, so default output is unchanged.

Assisted-by: Claude
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-10-15 10:05:35 +09:00
1 parent f9b07b786c
commit 2ba57dae68
7 files changed
+101 -3

No files matched your search

Generated Vendored
+1
View File
@@ -349,6 +349,7 @@ opmap = frozendict(
UNPACK_EX=117,
UNPACK_SEQUENCE=118,
YIELD_VALUE=119,
BUILD_LIST_FROM_ARG=219,
INSTRUMENTED_END_FOR=233,
INSTRUMENTED_POP_ITER=234,
INSTRUMENTED_END_SEND=235,
+75 -3
View File
@@ -314,6 +314,10 @@ pub struct CompileOpts {
pub dont_imply_dedent: bool,
/// Recursion limit used by compiler tree walks, matching Py_EnterRecursiveCall.
pub recursion_limit: usize,
/// Emit `BUILD_LIST_FROM_ARG` for a list comprehension with one
/// generator and no `if`, sizing the result from the iterator's length
/// hint. Off by default: the opcode is not part of the CPython set.
pub build_list_from_arg: bool,
}
impl Default for CompileOpts {
@@ -326,6 +330,7 @@ impl Default for CompileOpts {
future_features: bytecode::CodeFlags::empty(),
dont_imply_dedent: false,
recursion_limit: 1000,
build_list_from_arg: false,
}
}
}
@@ -10712,6 +10717,24 @@ impl<'warnings> Compiler<'warnings> {
Ok(())
}
/// ast.ListComp.build_container_and_load_iter: one generator, no ifs,
/// not an `async for`. The iterator is already on the stack; BUILD_LIST_FROM_ARG
/// sizes the list from its length hint.
fn listcomp_uses_build_list_from_arg(
&self,
init_collection: Option<&AnyInstruction>,
generators: &[ast::Comprehension],
) -> bool {
self.opts.build_list_from_arg
&& matches!(
init_collection,
Some(AnyInstruction::Real(Instruction::BuildList { .. }))
)
&& generators.len() == 1
&& generators[0].ifs.is_empty()
&& !generators[0].is_async
}
#[expect(clippy::too_many_arguments, reason = "ignore warning for now")]
fn compile_comprehension(
&mut self,
@@ -10845,8 +10868,15 @@ impl<'warnings> Compiler<'warnings> {
None
};
// ast.ListComp.build_container_and_load_iter: one generator and no
// ifs loads the iterator first, then BUILD_LIST_FROM_ARG.
let use_build_list_from_arg =
self.listcomp_uses_build_list_from_arg(init_collection.as_ref(), generators);
// Create empty object of proper type:
if let Some(init_collection) = init_collection {
if let Some(init_collection) = init_collection
&& !use_build_list_from_arg
{
self._emit(init_collection, OpArg::new(0), BlockIdx::NULL)
}
@@ -10896,6 +10926,9 @@ impl<'warnings> Compiler<'warnings> {
} else {
emit!(self, Instruction::GetIter { mode: 0 });
}
if use_build_list_from_arg {
emit!(self, Instruction::BuildListFromArg);
}
}
} else {
// Evaluate iterated item:
@@ -11217,8 +11250,14 @@ impl<'warnings> Compiler<'warnings> {
None
};
// Step 4: Create the collection (list/set/dict)
if let Some(init_collection) = init_collection {
// Step 4: Create the collection (list/set/dict).
// BUILD_LIST_FROM_ARG pops the iterator, pushes the list, then
// the iterator: the same stack as BUILD_LIST 0; SWAP 2.
let use_build_list_from_arg =
self.listcomp_uses_build_list_from_arg(init_collection.as_ref(), generators);
if use_build_list_from_arg {
emit!(self, Instruction::BuildListFromArg);
} else if let Some(init_collection) = init_collection {
self._emit(init_collection, OpArg::new(0), BlockIdx::NULL);
// SWAP to get iterator on top
emit!(self, Instruction::Swap { i: 2 });
@@ -17552,6 +17591,39 @@ def f(self):
);
}
#[test]
fn listcomp_build_list_from_arg_option() {
let has = |source: &str, build_list_from_arg: bool| {
let code = compile_exec_with_options(
source,
CompileOpts {
build_list_from_arg,
..CompileOpts::default()
},
);
let f = find_code(&code, "f").expect("missing f");
(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BuildListFromArg)),
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BuildList { .. })),
)
};
let single = "def f(n):\n return [i for i in range(n)]\n";
// (BUILD_LIST_FROM_ARG, BUILD_LIST)
assert_eq!(has(single, false), (false, true));
assert_eq!(has(single, true), (true, false));
for source in [
"def f(xs):\n return [x for x in xs if x]\n",
"def f(xss):\n return [x for xs in xss for x in xs]\n",
"async def f(xs):\n return [x async for x in xs]\n",
] {
assert_eq!(has(source, true), (false, true), "source={source:?}");
}
}
#[test]
fn or_return_not_taken_before_jump_target_splits_exception_table_like_cpython() {
let code = compile_exec_with_options(
@@ -680,6 +680,7 @@ define_opcodes!(
UnpackSequenceList = 216,
UnpackSequenceTuple = 217,
UnpackSequenceTwoTuple = 218,
BuildListFromArg = 219,
InstrumentedEndFor = 233,
InstrumentedPopIter = 234,
InstrumentedEndSend = 235,
+4
View File
@@ -822,6 +822,10 @@ impl super::Opcode {
Self::UnpackSequenceList => (oparg, 1),
Self::UnpackSequenceTuple => (oparg, 1),
Self::UnpackSequenceTwoTuple => (2, 1),
Self::BuildListFromArg => (
2, // TODO: Differs from CPython `None`
1, // TODO: Differs from CPython `None`
),
Self::InstrumentedEndFor => (2, 3),
Self::InstrumentedPopIter => (0, 2),
Self::InstrumentedEndSend => (1, 3),
+17
View File
@@ -4585,6 +4585,23 @@ impl ExecutingFrame<'_> {
self.push_value(list_obj.into());
Ok(None)
}
Instruction::BuildListFromArg => {
let last_val = self.pop_value();
let length_hint = match last_val.clone().length_hint(0, vm) {
Ok(hint) => hint,
Err(exc)
if exc.fast_isinstance(vm.ctx.exceptions.system_exit)
|| exc.fast_isinstance(vm.ctx.exceptions.keyboard_interrupt) =>
{
return Err(exc);
}
Err(_) => 0,
};
let list_obj = vm.ctx.new_list(Vec::with_capacity(length_hint));
self.push_value(list_obj.into());
self.push_value(last_val);
Ok(None)
}
Instruction::BuildMap { count: size } => self.execute_build_map(vm, size.get(arg)),
Instruction::BuildSet { count: size } => {
let set = PySet::default().into_ref(&vm.ctx);
+1
View File
@@ -3119,6 +3119,7 @@ impl VirtualMachine {
future_features: crate::bytecode::CodeFlags::empty(),
dont_imply_dedent: false,
recursion_limit: self.recursion_limit.get(),
build_list_from_arg: false,
}
}
+2
View File
@@ -0,0 +1,2 @@
[BuildListFromArg]
stack_effect = { pushed = "2", popped = "1" }