Skip to content

Commit ed87ab6

Browse files
committed
Compiler parity: docstring dedent, StopIteration wrapper, constant folding
- Fix clean_doc to skip first line in margin calculation (match CPython) - Add PEP 479 StopIteration handler to generator/coroutine bodies (SETUP_CLEANUP + CALL_INTRINSIC_1(StopIterationError) + RERAISE) - Remove yield-from/await -1 depth compensation in RESUME DEPTH1 - Add constant condition elimination for if/while (if False, while True) - Add POP_JUMP_IF_NONE/NOT_NONE for `x is None` patterns - Add binary op constant folding (*, **, +, -, <<, >>, etc.) - Add expr_constant() for compile-time boolean evaluation
1 parent 1a9b10e commit ed87ab6

File tree

2 files changed

+411
-79
lines changed

2 files changed

+411
-79
lines changed

crates/codegen/src/compile.rs

Lines changed: 218 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2360,43 +2360,7 @@ impl Compiler {
23602360
..
23612361
}) => {
23622362
self.enter_conditional_block();
2363-
match elif_else_clauses.as_slice() {
2364-
// Only if
2365-
[] => {
2366-
let after_block = self.new_block();
2367-
self.compile_jump_if(test, false, after_block)?;
2368-
self.compile_statements(body)?;
2369-
self.switch_to_block(after_block);
2370-
}
2371-
// If, elif*, elif/else
2372-
[rest @ .., tail] => {
2373-
let after_block = self.new_block();
2374-
let mut next_block = self.new_block();
2375-
2376-
self.compile_jump_if(test, false, next_block)?;
2377-
self.compile_statements(body)?;
2378-
emit!(self, PseudoInstruction::Jump { delta: after_block });
2379-
2380-
for clause in rest {
2381-
self.switch_to_block(next_block);
2382-
next_block = self.new_block();
2383-
if let Some(test) = &clause.test {
2384-
self.compile_jump_if(test, false, next_block)?;
2385-
} else {
2386-
unreachable!() // must be elif
2387-
}
2388-
self.compile_statements(&clause.body)?;
2389-
emit!(self, PseudoInstruction::Jump { delta: after_block });
2390-
}
2391-
2392-
self.switch_to_block(next_block);
2393-
if let Some(test) = &tail.test {
2394-
self.compile_jump_if(test, false, after_block)?;
2395-
}
2396-
self.compile_statements(&tail.body)?;
2397-
self.switch_to_block(after_block);
2398-
}
2399-
}
2363+
self.compile_if(test, body, elif_else_clauses)?;
24002364
self.leave_conditional_block();
24012365
}
24022366
ast::Stmt::While(ast::StmtWhile {
@@ -3899,6 +3863,22 @@ impl Compiler {
38993863
// Set qualname
39003864
self.set_qualname();
39013865

3866+
// PEP 479: Wrap generator/coroutine body with StopIteration handler
3867+
let is_gen = is_async || self.current_symbol_table().is_generator;
3868+
let stop_iteration_block = if is_gen {
3869+
let handler_block = self.new_block();
3870+
emit!(
3871+
self,
3872+
PseudoInstruction::SetupCleanup {
3873+
delta: handler_block
3874+
}
3875+
);
3876+
self.push_fblock(FBlockType::StopIteration, handler_block, handler_block)?;
3877+
Some(handler_block)
3878+
} else {
3879+
None
3880+
};
3881+
39023882
// Handle docstring - store in co_consts[0] if present
39033883
let (doc_str, body) = split_doc(body, &self.opts);
39043884
if let Some(doc) = &doc_str {
@@ -3929,6 +3909,20 @@ impl Compiler {
39293909
self.arg_constant(ConstantData::None);
39303910
}
39313911

3912+
// Close StopIteration handler and emit handler code
3913+
if let Some(handler_block) = stop_iteration_block {
3914+
emit!(self, PseudoInstruction::PopBlock);
3915+
self.pop_fblock(FBlockType::StopIteration);
3916+
self.switch_to_block(handler_block);
3917+
emit!(
3918+
self,
3919+
Instruction::CallIntrinsic1 {
3920+
func: oparg::IntrinsicFunction1::StopIterationError
3921+
}
3922+
);
3923+
emit!(self, Instruction::Reraise { depth: 1u32 });
3924+
}
3925+
39323926
// Exit scope and create function object
39333927
let code = self.exit_scope();
39343928
self.ctx = prev_ctx;
@@ -5182,6 +5176,84 @@ impl Compiler {
51825176
self.store_name(name)
51835177
}
51845178

5179+
/// Compile an if statement with constant condition elimination.
5180+
/// = compiler_if in CPython codegen.c
5181+
fn compile_if(
5182+
&mut self,
5183+
test: &ast::Expr,
5184+
body: &[ast::Stmt],
5185+
elif_else_clauses: &[ast::ElifElseClause],
5186+
) -> CompileResult<()> {
5187+
let constant = Self::expr_constant(test);
5188+
5189+
// If the test is constant false, skip the body entirely
5190+
if constant == Some(false) {
5191+
// The NOP is emitted by CPython to keep line number info
5192+
self.emit_nop();
5193+
// Compile the elif/else chain (if any)
5194+
match elif_else_clauses {
5195+
[] => {} // nothing to do
5196+
[first, rest @ ..] => {
5197+
if let Some(elif_test) = &first.test {
5198+
// elif: recursively compile with elif as the new if
5199+
self.compile_if(elif_test, &first.body, rest)?;
5200+
} else {
5201+
// else: compile the else body directly
5202+
self.compile_statements(&first.body)?;
5203+
}
5204+
}
5205+
}
5206+
return Ok(());
5207+
}
5208+
5209+
// If the test is constant true, compile body directly
5210+
if constant == Some(true) {
5211+
self.emit_nop();
5212+
self.compile_statements(body)?;
5213+
return Ok(());
5214+
}
5215+
5216+
// Non-constant test: normal compilation
5217+
match elif_else_clauses {
5218+
// Only if
5219+
[] => {
5220+
let after_block = self.new_block();
5221+
self.compile_jump_if(test, false, after_block)?;
5222+
self.compile_statements(body)?;
5223+
self.switch_to_block(after_block);
5224+
}
5225+
// If, elif*, elif/else
5226+
[rest @ .., tail] => {
5227+
let after_block = self.new_block();
5228+
let mut next_block = self.new_block();
5229+
5230+
self.compile_jump_if(test, false, next_block)?;
5231+
self.compile_statements(body)?;
5232+
emit!(self, PseudoInstruction::Jump { delta: after_block });
5233+
5234+
for clause in rest {
5235+
self.switch_to_block(next_block);
5236+
next_block = self.new_block();
5237+
if let Some(test) = &clause.test {
5238+
self.compile_jump_if(test, false, next_block)?;
5239+
} else {
5240+
unreachable!() // must be elif
5241+
}
5242+
self.compile_statements(&clause.body)?;
5243+
emit!(self, PseudoInstruction::Jump { delta: after_block });
5244+
}
5245+
5246+
self.switch_to_block(next_block);
5247+
if let Some(test) = &tail.test {
5248+
self.compile_jump_if(test, false, after_block)?;
5249+
}
5250+
self.compile_statements(&tail.body)?;
5251+
self.switch_to_block(after_block);
5252+
}
5253+
}
5254+
Ok(())
5255+
}
5256+
51855257
fn compile_while(
51865258
&mut self,
51875259
test: &ast::Expr,
@@ -5190,27 +5262,37 @@ impl Compiler {
51905262
) -> CompileResult<()> {
51915263
self.enter_conditional_block();
51925264

5265+
let constant = Self::expr_constant(test);
5266+
5267+
// while False: body → skip body, compile orelse only
5268+
if constant == Some(false) {
5269+
self.emit_nop();
5270+
self.compile_statements(orelse)?;
5271+
self.leave_conditional_block();
5272+
return Ok(());
5273+
}
5274+
51935275
let while_block = self.new_block();
51945276
let else_block = self.new_block();
51955277
let after_block = self.new_block();
51965278

5197-
// Note: SetupLoop is no longer emitted (break/continue use direct jumps)
51985279
self.switch_to_block(while_block);
5199-
5200-
// Push fblock for while loop
52015280
self.push_fblock(FBlockType::WhileLoop, while_block, after_block)?;
52025281

5203-
self.compile_jump_if(test, false, else_block)?;
5282+
// while True: → no condition test, just NOP
5283+
if constant == Some(true) {
5284+
self.emit_nop();
5285+
} else {
5286+
self.compile_jump_if(test, false, else_block)?;
5287+
}
52045288

52055289
let was_in_loop = self.ctx.loop_data.replace((while_block, after_block));
52065290
self.compile_statements(body)?;
52075291
self.ctx.loop_data = was_in_loop;
52085292
emit!(self, PseudoInstruction::Jump { delta: while_block });
52095293
self.switch_to_block(else_block);
52105294

5211-
// Pop fblock
52125295
self.pop_fblock(FBlockType::WhileLoop);
5213-
// Note: PopBlock is no longer emitted for loops
52145296
self.compile_statements(orelse)?;
52155297
self.switch_to_block(after_block);
52165298

@@ -6998,6 +7080,40 @@ impl Compiler {
69987080
}) => {
69997081
self.compile_jump_if(operand, !condition, target_block)?;
70007082
}
7083+
// `x is None` / `x is not None` → POP_JUMP_IF_NONE / POP_JUMP_IF_NOT_NONE
7084+
ast::Expr::Compare(ast::ExprCompare {
7085+
left,
7086+
ops,
7087+
comparators,
7088+
..
7089+
}) if ops.len() == 1
7090+
&& matches!(ops[0], ast::CmpOp::Is | ast::CmpOp::IsNot)
7091+
&& comparators.len() == 1
7092+
&& matches!(&comparators[0], ast::Expr::NoneLiteral(_)) =>
7093+
{
7094+
self.compile_expression(left)?;
7095+
let is_not = matches!(ops[0], ast::CmpOp::IsNot);
7096+
// is None + jump_if_false → POP_JUMP_IF_NOT_NONE
7097+
// is None + jump_if_true → POP_JUMP_IF_NONE
7098+
// is not None + jump_if_false → POP_JUMP_IF_NONE
7099+
// is not None + jump_if_true → POP_JUMP_IF_NOT_NONE
7100+
let jump_if_none = condition != is_not;
7101+
if jump_if_none {
7102+
emit!(
7103+
self,
7104+
Instruction::PopJumpIfNone {
7105+
delta: target_block,
7106+
}
7107+
);
7108+
} else {
7109+
emit!(
7110+
self,
7111+
Instruction::PopJumpIfNotNone {
7112+
delta: target_block,
7113+
}
7114+
);
7115+
}
7116+
}
70017117
_ => {
70027118
// Fall back case which always will work!
70037119
self.compile_expression(expression)?;
@@ -8801,6 +8917,38 @@ impl Compiler {
88018917
self.code_stack.last_mut().expect("no code on stack")
88028918
}
88038919

8920+
/// Evaluate whether an expression is a compile-time constant boolean.
8921+
/// Returns Some(true) for truthy constants, Some(false) for falsy constants,
8922+
/// None for non-constant expressions.
8923+
/// = expr_constant in CPython compile.c
8924+
fn expr_constant(expr: &ast::Expr) -> Option<bool> {
8925+
match expr {
8926+
ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) => Some(*value),
8927+
ast::Expr::NoneLiteral(_) => Some(false),
8928+
ast::Expr::EllipsisLiteral(_) => Some(true),
8929+
ast::Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => match value {
8930+
ast::Number::Int(i) => {
8931+
let n: i64 = i.as_i64().unwrap_or(1);
8932+
Some(n != 0)
8933+
}
8934+
ast::Number::Float(f) => Some(*f != 0.0),
8935+
ast::Number::Complex { real, imag, .. } => Some(*real != 0.0 || *imag != 0.0),
8936+
},
8937+
ast::Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
8938+
Some(!value.to_str().is_empty())
8939+
}
8940+
ast::Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => {
8941+
Some(value.bytes().next().is_some())
8942+
}
8943+
ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => Some(!elts.is_empty()),
8944+
_ => None,
8945+
}
8946+
}
8947+
8948+
fn emit_nop(&mut self) {
8949+
emit!(self, Instruction::Nop);
8950+
}
8951+
88048952
/// Enter a conditional block (if/for/while/match/try/with)
88058953
/// PEP 649: Track conditional annotation context
88068954
fn enter_conditional_block(&mut self) {
@@ -9411,32 +9559,34 @@ impl EmitArg<bytecode::Label> for BlockIdx {
94119559
// = _PyCompile_CleanDoc
94129560
fn clean_doc(doc: &str) -> String {
94139561
let doc = expandtabs(doc, 8);
9414-
// First pass: find minimum indentation of any non-blank lines
9415-
// after first line.
9562+
// First pass: find minimum indentation of non-blank lines AFTER the first line.
9563+
// A "blank line" is one containing only spaces (or empty).
94169564
let margin = doc
9417-
.lines()
9418-
// Find the non-blank lines
9419-
.filter(|line| !line.trim().is_empty())
9420-
// get the one with the least indentation
9421-
.map(|line| line.chars().take_while(|c| c == &' ').count())
9422-
.min();
9423-
if let Some(margin) = margin {
9424-
let mut cleaned = String::with_capacity(doc.len());
9425-
// copy first line without leading whitespace
9426-
if let Some(first_line) = doc.lines().next() {
9427-
cleaned.push_str(first_line.trim_start());
9428-
}
9429-
// copy subsequent lines without margin.
9430-
for line in doc.split('\n').skip(1) {
9431-
cleaned.push('\n');
9432-
let cleaned_line = line.chars().skip(margin).collect::<String>();
9433-
cleaned.push_str(&cleaned_line);
9434-
}
9435-
9436-
cleaned
9437-
} else {
9438-
doc.to_owned()
9439-
}
9565+
.split('\n')
9566+
.skip(1) // skip first line
9567+
.filter(|line| line.chars().any(|c| c != ' ')) // non-blank lines only
9568+
.map(|line| line.chars().take_while(|c| *c == ' ').count())
9569+
.min()
9570+
.unwrap_or(0);
9571+
9572+
let mut cleaned = String::with_capacity(doc.len());
9573+
// Strip all leading spaces from the first line
9574+
if let Some(first_line) = doc.split('\n').next() {
9575+
let trimmed = first_line.trim_start();
9576+
// Early exit: no leading spaces on first line AND margin == 0
9577+
if trimmed.len() == first_line.len() && margin == 0 {
9578+
return doc.to_owned();
9579+
}
9580+
cleaned.push_str(trimmed);
9581+
}
9582+
// Subsequent lines: skip up to `margin` leading spaces
9583+
for line in doc.split('\n').skip(1) {
9584+
cleaned.push('\n');
9585+
let skip = line.chars().take(margin).take_while(|c| *c == ' ').count();
9586+
cleaned.push_str(&line[skip..]);
9587+
}
9588+
9589+
cleaned
94409590
}
94419591

94429592
// copied from rustpython_common::str, so we don't have to depend on it just for this function

0 commit comments

Comments
 (0)