diff --git a/docs/LIMITATIONS.md b/docs/LIMITATIONS.md index 6860fd1b..c1c364e4 100644 --- a/docs/LIMITATIONS.md +++ b/docs/LIMITATIONS.md @@ -23,16 +23,11 @@ constant handling. On real code compiled with CPython 3.15.0 (pandas + scikit-learn, 2091 modules), **all 2091 recompile (100%)**. -One 3.15 edge remains outside that corpus: a `with` body whose only -statement is `return ` (`with c as x: return x`) renders the body as -`pass` and loses the returned value (the exit protocol is not sunk into -the body). Attribute and subscript as-targets (`with c as h.attr:`, +Attribute and subscript as-targets (`with c as h.attr:`, `with c as d[k]:`) are recovered on 3.14 and 3.15. 3.15 的 opcode 表由本机解释器直接生成,2.6–3.15 的行为用例全部通过。 真实代码(pandas + scikit-learn,2091 模块)**全部 2091 个可重编译(100%)**。 -语料之外仍有一个 3.15 边角:`with` 体只有 `return ` 时 -(`with c as x: return x`)体会渲染成 `pass` 并丢失返回值。 ## Class bodies with conditional method definitions (3.14/3.15) diff --git a/src/decompiler.rs b/src/decompiler.rs index 05a5036f..be8f8683 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -572,6 +572,9 @@ struct Ctx<'a> { /// chains, rendering `if AttributeError:` garbage and skipping the /// outer body end so the outer tail never fired) nested_tail_defer: bool, + /// An intervening live For owns this nested try. Its out-of-line + /// handler must not adopt the outer dispatch or enter a flat rebuild. + lexical_loop_handlers: FastMap, pending_try_handlers: Vec>, pending_loop: Vec<(Option, Option, Option, Vec, bool)>, pending_with: Vec>, @@ -872,6 +875,13 @@ pub fn decompile_in_scope( let Some(&hi) = idx_of.get(&e.target) else { return true; }; + if version.at_least(3, 14) + && instrs.get(hi).map(|instruction| instruction.op) == Some(Op::CLEANUP_THROW) + { + // Await/yield suspension cleanup is interpreter protocol, + // even when followed by a backward jump and user handlers. + return false; + } let mut saw_reraise = false; for ins in instrs.iter().skip(hi).take(14) { if matches!(ins.op, Op::RERAISE) { @@ -888,6 +898,44 @@ pub fn decompile_in_scope( }); } + if version.at_least(3, 14) { + // Await suspension splits one source try into fragments, and its + // out-of-line CLEANUP_THROW trampoline can itself be protected by + // the same source handler. Rebuild the lexical await region before + // deriving try contexts; keep the original table for VM glue. + exc_entries.retain(|entry| { + idx_of.get(&entry.start).and_then(|index| instrs.get(*index)) + .map_or(true, |instruction| instruction.op != Op::CLEANUP_THROW) + }); + let mut grouped: std::collections::BTreeMap<(usize, u32, bool), Vec> = + std::collections::BTreeMap::new(); + for entry in exc_entries.drain(..) { + grouped.entry((entry.target, entry.depth, entry.lasti)).or_default().push(entry); + } + for (_, mut entries) in grouped { + entries.sort_by_key(|entry| entry.start); + let mut merged: Vec = Vec::new(); + for entry in entries { + let join = merged.last().map_or(false, |previous| { + entry.start >= previous.end + && instrs.iter().filter(|instruction| { + instruction.offset >= previous.end && instruction.offset < entry.start + }).all(|instruction| matches!(instruction.op, + Op::SEND | Op::YIELD_VALUE | Op::RESUME | Op::RESUME_CHECK + | Op::JUMP_BACKWARD_NO_INTERRUPT | Op::END_SEND + | Op::NOP | Op::NOT_TAKEN | Op::CACHE)) + }); + if join { + merged.last_mut().unwrap().end = entry.end; + } else { + merged.push(entry); + } + } + exc_entries.extend(merged); + } + exc_entries.sort_by_key(|entry| entry.start); + } + // classify exception-table targets: handlers starting with PUSH_EXC_INFO // are out-of-line handler regions; CHECK_EXC_MATCH inside => except // dispatch, otherwise => finally handler. @@ -1049,11 +1097,22 @@ pub fn decompile_in_scope( | Op::CALL_FUNCTION | Op::RETURN_VALUE | Op::RETURN_CONST + | Op::LOAD_SMALL_INT + | Op::GET_AWAITABLE + | Op::SEND + | Op::END_SEND + | Op::YIELD_VALUE + | Op::YIELD_FROM + | Op::RESUME + | Op::RESUME_CHECK + | Op::JUMP_BACKWARD_NO_INTERRUPT ) || (x.op == Op::LOAD_CONST && matches!( code.consts.get(x.arg as usize).map(|o| &**o), Some(PyObject::None) - )) + )) || (version.at_least(3, 14) && exc_entries.iter().any(|entry| { + entry.start <= x.offset && x.offset < entry.end && entry.target != t + })) }) && gap.iter().any(|x| { matches!(x.op, Op::RETURN_VALUE | Op::RETURN_CONST) }); @@ -1169,6 +1228,7 @@ pub fn decompile_in_scope( sunk_pair_drop: None, sunk_while_lift: None, nested_tail_defer: false, + lexical_loop_handlers: FastMap::default(), pending_try_handlers: Vec::new(), pending_loop: Vec::new(), pending_with: Vec::new(), @@ -3462,7 +3522,7 @@ impl<'a> Ctx<'a> { // interact) and its skip_until strands the enclosing // region's tail if let Some(h) = tc.except_handler { - if self.try_ctxs.values().any(|r| { + if let Some(enclosing) = self.try_ctxs.values().filter(|r| { r.start < tc.start && r.region_end > tc.region_end && r.finally_handler.is_none() @@ -3470,8 +3530,20 @@ impl<'a> Ctx<'a> { && r.except_handler.map_or(false, |ho| { h >= r.body_end && h < ho }) - }) { - self.nested_tail_defer = true; + }).max_by_key(|r| r.start) { + let intervening_for = self.blocks.iter().any(|b| { + b.kind == BlockType::For + && b.start > enclosing.start + && b.start <= tc.start + && b.end >= tc.region_end + }); + if intervening_for { + self.lexical_loop_handlers.insert( + h, enclosing.except_handler.unwrap(), + ); + } else { + self.nested_tail_defer = true; + } } } let mut blk = Block::new(BlockType::Try, pos, tc.body_end); @@ -4150,6 +4222,9 @@ impl<'a> Ctx<'a> { && r.finally_handler.is_none() && r.except_handler .map_or(false, |h| h >= tc.body_end && h < h_outer) + && !r.except_handler.map_or(false, |h| { + self.lexical_loop_handlers.contains_key(&h) + }) }) .cloned(); if let Some(inner) = nested { @@ -4748,6 +4823,18 @@ impl<'a> Ctx<'a> { // handler chain — statements AFTER the copy (post-try code inside // a loop iteration) belong to the main walk, not the finally let mut fin_span_stop = stop; + if self.version.at_least(3, 14) + && self.blocks.iter().any(|block| block.kind == BlockType::With) + { + if let Some(tail) = self.instrs.iter().enumerate().find_map(|(index, instruction)| { + if instruction.offset >= pos && instruction.offset < fin_span_stop + && instruction.op == Op::SWAP && self.with_return_tail_end(index).is_some() + { Some(instruction.offset) } else { None } + }) { + stop = tail; + fin_span_stop = tail; + } + } // the inline-copy mirror verdict: false means the span at `pos` // does NOT opcode-mirror the finally chain's copy — there is no // inline copy here (an always-raising try body has no normal @@ -5071,6 +5158,8 @@ impl<'a> Ctx<'a> { let r = &self.instrs[ri]; if r.op == Op::RETURN_VALUE { Some(r.end()) + } else if self.version.at_least(3, 14) && r.op == Op::SWAP { + self.with_return_tail_end(ri) } else { None } @@ -5095,6 +5184,50 @@ impl<'a> Ctx<'a> { } } let mut body = self.pending_try_body.pop().unwrap_or_default(); + // A narrowed except-only range can end on padding immediately before + // RETURN_VALUE, while its calculated value is still live. Preserve + // that exact carried return inside the try without scanning onward + // into a following sibling branch or an out-of-line handler. + if self.version.at_least(3, 11) + && self.code.name != "" + && tc.except_handler.is_some() + && tc.finally_handler.is_none() + && tc.region_end == tc.body_end + && body.is_empty() + && orelse.is_empty() + && self.stack.len() == 1 + && matches!(self.stack.last(), Some(Sv::E(_))) + { + let tail = self.instrs.iter() + .skip_while(|instruction| instruction.offset < tc.body_end) + .find(|instruction| !matches!(instruction.op, + Op::NOP | Op::NOT_TAKEN | Op::CACHE)) + .copied(); + if let Some(ret) = tail { + let before_handler = tc.except_handler.map_or(false, |handler| ret.offset < handler); + let entered_from_outside = self.instrs.iter().any(|instruction| { + (instruction.offset < tc.start || instruction.offset > ret.offset) + && instruction.target.map_or(false, |target| { + target >= tc.body_end && target <= ret.offset + }) + }); + let has_context_protocol = self.instrs.iter().any(|instruction| { + instruction.offset >= tc.start && instruction.offset <= tc.body_end + && matches!(instruction.op, Op::BEFORE_WITH | Op::BEFORE_ASYNC_WITH + | Op::WITH_EXCEPT_START | Op::LOAD_SPECIAL) + }); + if ret.op == Op::RETURN_VALUE && before_handler + && !entered_from_outside && !has_context_protocol + && !self.return_offsets.contains(&ret.offset) + { + body.push(Stmt::Return(Some(self.pop_expr()))); + self.return_offsets.insert(ret.offset); + if self.skip_until.map_or(true, |skip| skip < ret.end()) { + self.skip_until = Some(ret.end()); + } + } + } + } // 3.12+ with-wrapped try: the body's tail `return None` sits in // the gap between body_end and the chain head, AFTER the inline // with success-exit sequence (POP_EXCEPT; LOAD None ×3; CALL; @@ -6061,7 +6194,27 @@ impl<'a> Ctx<'a> { }); if !has_with { if bare_tail_span { - body.push(Stmt::Return(None)); + // A padded RETURN_VALUE can consume the + // value that the protected body already + // calculated. The handler parser preserves + // that live stack; a fresh region walk has + // no value and used to fabricate a bare + // return instead. Keep only this exact + // single-expression stack and return tail. + let carried_return = self.version.at_least(3, 11) + && self.stack.len() == 1 + && matches!(self.stack.last(), Some(Sv::E(_))) + && self.instrs.iter().any(|x| { + x.offset >= span_start + && x.offset < else_end + && x.op == Op::RETURN_VALUE + }); + let value = if carried_return { + Some(self.pop_expr()) + } else { + None + }; + body.push(Stmt::Return(value)); } else { // 3.14 narrows protected ranges at // value-construction boundaries: the @@ -6478,6 +6631,32 @@ impl<'a> Ctx<'a> { stmts } + /// Bound a typed clause reached by the preceding match's false edge. + /// Only ordinary pattern loads followed by CHECK_EXC_MATCH and its + /// forward mismatch jump qualify; never cross a new handler or cleanup. + fn following_except_clause_end(&self, from: usize) -> Option { + let start = *self.idx_of.get(&from)?; + for (index, inst) in self.instrs.iter().enumerate().skip(start).take(32) { + if inst.op == Op::CHECK_EXC_MATCH { + let jump = self.instrs.get(index + 1)?; + if matches!(jump.op, Op::POP_JUMP_IF_FALSE | Op::POP_JUMP_FORWARD_IF_FALSE) + && jump.target.map_or(false, |target| target > jump.offset) + { + return Some(jump.end()); + } + return None; + } + if !matches!(inst.op, + Op::LOAD_GLOBAL | Op::LOAD_NAME | Op::LOAD_FAST | Op::LOAD_DEREF + | Op::LOAD_ATTR | Op::LOAD_METHOD | Op::BUILD_TUPLE + | Op::NOP | Op::NOT_TAKEN | Op::CACHE) + { + return None; + } + } + None + } + /// Parse an out-of-line except-dispatch handler region (3.11+): /// PUSH_EXC_INFO followed by `[pattern; CHECK_EXC_MATCH; PJIF next]*` /// clause bodies, ending in RERAISE. @@ -6486,6 +6665,11 @@ impl<'a> Ctx<'a> { let mut clause_offsets: Vec = Vec::new(); let end = self.dispatch_region_end(from); let region_end = self.handler_region_end(from); + let private_body_cutoff = end < region_end + && self.exc_entries.iter().any(|entry| { + entry.start == end && entry.target == region_end + }); + let mut dispatch_limit = end; // nesting: this chain's own cleanup region ends exactly where the // next chain begins — that chain handles re-raises FROM this one, // i.e. it is the ENCLOSING try's dispatch. Adjacency must be exact @@ -6557,6 +6741,7 @@ impl<'a> Ctx<'a> { None => false, }; let nested = region_end > from && adj_reraise && (extent == region_end || tramp_gap) + && !self.lexical_loop_handlers.contains_key(&from) // the exception table must confirm the enclosure: a chain // genuinely nested inside the adjacent one has (part of) its // own region protected by an entry targeting the adjacent @@ -6627,7 +6812,7 @@ impl<'a> Ctx<'a> { let mut eg_chain = false; while pc < self.instrs.len() { let inst = self.instrs[pc]; - if inst.offset >= end + if inst.offset >= dispatch_limit && !handlers.is_empty() && !(nested && inst.offset < self.chain_extent(from)) { @@ -6678,6 +6863,15 @@ impl<'a> Ctx<'a> { | Op::POP_JUMP_FORWARD_IF_NONE => { at_clause_head = false; let next = inst.target.unwrap_or(end); + if private_body_cutoff && pattern.is_some() && next > dispatch_limit { + // A private try bounds this clause's body subwalk, + // not the parent match's next typed clause. Retain + // that body bound while following only the explicit + // mismatch edge and its verified pattern protocol. + if let Some(limit) = self.following_except_clause_end(next) { + dispatch_limit = limit; + } + } clause_offsets.push(inst.offset); pc += 1; // 3.14 pads the clause head with NOT_TAKEN @@ -7553,6 +7747,31 @@ impl<'a> Ctx<'a> { | Op::DELETE_DEREF => { k2 += 1; } + Op::LOAD_COMMON_CONSTANT if self.loads_none(c.op, c.arg) => { + k2 += 1; + } + Op::POP_TOP => { + // A For-handler break drops the live iterator after + // POP_EXCEPT and any named cleanup, immediately before + // its jump to the registered loop exit. The iterator + // discard is machinery, but that jump still represents + // a source Break. Do not skip arbitrary body POP_TOPs. + // 3.15 keeps a second iterator slot on this path. + let pop_count = if self.version.at_least(3, 15) + && self.instrs.get(k2 + 1).map(|x| x.op) == Some(Op::POP_TOP) + { 2 } else { 1 }; + let next = self.instrs.get(k2 + pop_count); + let iterator_exit = next.map_or(false, |jump| { + matches!(jump.op, Op::JUMP_FORWARD | Op::JUMP_BACKWARD + | Op::JUMP_ABSOLUTE | Op::JUMP) + && jump.target.and_then(|target| self.find_loop_exit(target)) + .map_or(false, |index| self.blocks[index].kind == BlockType::For) + }); + if !iterator_exit { + break; + } + k2 += pop_count; + } Op::RETURN_VALUE | Op::RETURN_CONST => { // a sunk implicit tail `return None` (3.12+ // function-tail try: POP_EXCEPT; RETURN_CONST None — @@ -8380,6 +8599,12 @@ impl<'a> Ctx<'a> { fn dispatch_region_end(&self, from: usize) -> usize { let mut end = self.handler_region_end(from); for e in &self.exc_entries { + // A per-row clause's POP_EXCEPT can be protected by the + // enclosing dispatch. It does not introduce a nested clause + // body. All other genuine nested-chain bounds remain active. + if self.lexical_loop_handlers.get(&from) == Some(&e.target) { + continue; + } if e.start > from && e.start < end && e.target > from { // only a NESTED chain head bounds the region — an // except* clause body's lasti redirect targets in-chain @@ -11152,7 +11377,7 @@ impl<'a> Ctx<'a> { } /// 3.12+ inline with success-exit span: `LOAD None; LOAD None; - /// LOAD None; CALL; POP_TOP` starting at `off` — returns the offset + /// LOAD None; CALL; [async exit await;] POP_TOP` starting at `off` — returns the offset /// just past the POP_TOP. The callable is the VM-managed bound /// __exit__ (never modeled); the sequence is pure machinery. fn inline_with_exit_span(&self, off: usize) -> Option { @@ -11206,9 +11431,74 @@ impl<'a> Ctx<'a> { while matches!(self.instrs.get(k).map(|x| x.op), Some(op) if matches!(op, Op::NOP | Op::NOT_TAKEN | Op::CACHE)) { k += 1; } + let mut async_exit = false; + if self.version.at_least(3, 14) + && self.instrs.get(k).map(|x| (x.op, x.arg)) == Some((Op::GET_AWAITABLE, 2)) + { + // The compiler marks an __aexit__ await with operand 2. A + // user-authored await uses operand 0 and must remain visible. + // Consume only a complete SEND loop ending in END_SEND. + k += 1; + if self.version.at_least(3, 15) + && self.instrs.get(k).map(|x| x.op) == Some(Op::PUSH_NULL) + { + k += 1; + } + let none = self.instrs.get(k)?; + if !self.loads_none(none.op, none.arg) { + return None; + } + k += 1; + let send = self.instrs.get(k)?; + if send.op != Op::SEND { + return None; + } + let end = *self.idx_of.get(&send.target?)?; + if end <= k || end - k > 12 || self.instrs[end].op != Op::END_SEND { + return None; + } + let loop_body = &self.instrs[k + 1..end]; + if !loop_body.iter().any(|x| x.op == Op::YIELD_VALUE) + || !loop_body.iter().any(|x| { + x.op == Op::JUMP_BACKWARD_NO_INTERRUPT && x.target == Some(send.offset) + }) + || loop_body.iter().any(|x| { + !matches!(x.op, Op::YIELD_VALUE | Op::RESUME | Op::CLEANUP_THROW + | Op::JUMP_BACKWARD_NO_INTERRUPT | Op::NOP | Op::NOT_TAKEN | Op::CACHE) + || (x.op == Op::JUMP_BACKWARD_NO_INTERRUPT && x.target != Some(send.offset)) + }) + { + return None; + } + k = end + 1; + async_exit = true; + while matches!(self.instrs.get(k).map(|x| x.op), Some(op) if matches!(op, Op::NOP | Op::NOT_TAKEN | Op::CACHE)) { + k += 1; + } + } if self.instrs.get(k).map(|x| x.op) != Some(Op::POP_TOP) { return None; } + let mut tail = k + 1; + while self.instrs.get(tail).map_or(false, |x| is_pad(x)) { + tail += 1; + } + let mut return_at = tail + 1; + while self.instrs.get(return_at).map_or(false, |x| is_pad(x)) { + return_at += 1; + } + if async_exit && self.instrs.get(tail).map_or(false, |x| { + matches!(x.op, Op::RETURN_VALUE | Op::RETURN_CONST) + || (matches!(x.op, Op::LOAD_CONST | Op::LOAD_SMALL_INT | Op::LOAD_COMMON_CONSTANT) + && self.instrs.get(return_at).map_or(false, |next| { + matches!(next.op, Op::RETURN_VALUE | Op::RETURN_CONST) + })) + }) { + // A calculated return can carry its value through this same + // exit await. Keep the historical return protocol; closing the + // With here can discard that value or eject its source Return. + return None; + } Some(self.instrs[k].end()) } @@ -13683,6 +13973,54 @@ fn short(sv: &Sv) -> String { use crate::opcode::Op; impl<'a> Ctx<'a> { + /// Recognize only an exit owned by the current lexical walk. Both + /// constant encodings must preserve nested and displaced return slots. + fn owned_inline_with_exit_span(&self) -> Option { + self.inline_with_exit_span(self.cur_offset).filter(|after| { + let is_async_exit = self.instrs.iter().any(|instruction| { + instruction.offset >= self.cur_offset + && instruction.offset < *after + && instruction.op == Op::GET_AWAITABLE + && instruction.arg == 2 + }); + let split_protected_walk = self.blocks.iter().any(|block| block.kind == BlockType::Try); + if !is_async_exit { + return true; + } + if !split_protected_walk { + // A split handler walk can lose both its Try and + // lexical With blocks while the original exception + // table still protects a nested exit. Preserve the + // existing protocol in that displaced fragment; + // ordinary open-With walks keep their established + // success-exit handling. + let has_open_with = self.blocks.iter().any(|block| block.kind == BlockType::With); + let protected_by_with = self.exc_entries.iter().any(|entry| { + entry.start <= self.cur_offset + && self.cur_offset < entry.end + && self.with_region_handlers.values().any(|handler| *handler == entry.target) + }); + return has_open_with || !protected_by_with; + } + let Some(owner) = self.blocks.iter().rfind(|block| block.kind == BlockType::With) else { + // A displaced return must retain its protocol + // value for the source fidelity reconstruction. + return false; + }; + // An exit protected by the open With belongs to a + // nested context. Keep its existing CALL/accounting + // path; consuming it here also consumes the outer + // return's exit slot in a split protected walk. + !owner.with_handler.map_or(false, |handler| { + self.exc_entries.iter().any(|entry| { + entry.start <= self.cur_offset + && self.cur_offset < entry.end + && entry.target == handler + }) + }) + }) + } + /// Execute one instruction. Returns false when the instruction stream /// should stop being processed (unconditional exit). /// 3.15 spells the inline with's three None exit arguments as @@ -13692,7 +14030,7 @@ impl<'a> Ctx<'a> { if !self.version.at_least(3, 15) || self.with_exits == 0 { return false; } - let Some(after) = self.inline_with_exit_span(self.cur_offset) else { + let Some(after) = self.owned_inline_with_exit_span() else { return false; }; let nested_end_here = self.with_regions.iter().any(|(k, v)| { @@ -13758,7 +14096,7 @@ impl<'a> Ctx<'a> { // _maybe_compile: the leak rendered `None(None, None)` // and trapped the post-with tail return inside the with) if self.version.at_least(3, 11) && self.with_exits > 0 { - if let Some(after) = self.inline_with_exit_span(self.cur_offset) { + if let Some(after) = self.owned_inline_with_exit_span() { // close the With ONLY when it is topmost: a sunk // exit inside an open branch arm (`if quitting: // return` — bdb 3.12 trace_dispatch) must keep @@ -14398,6 +14736,10 @@ impl<'a> Ctx<'a> { | Op::END_SEND | Op::CLEANUP_THROW | Op::LOAD_CONST + // 3.15's await setup has a null SEND + // slot and a shared None constant. + | Op::PUSH_NULL + | Op::LOAD_COMMON_CONSTANT | Op::NOP | Op::NOT_TAKEN | Op::END_ASYNC_FOR @@ -23216,9 +23558,17 @@ impl<'a> Ctx<'a> { // rotated `while a or b:` — the body region loops back to this // condition; leave it to the while machinery if let (Some(&bi), Some(&ei)) = (self.idx_of.get(&target), self.idx_of.get(&exit)) { + let outer_for_top = self.blocks.iter().rev() + .find(|block| matches!(block.kind, BlockType::For | BlockType::While)) + .filter(|block| block.kind == BlockType::For && block.start < self.cur_offset) + .map(|block| block.start); if self.instrs[bi..ei] .iter() - .any(|x| x.is_backward && x.target.map_or(false, |t| t <= self.cur_offset)) + .any(|x| x.is_backward && x.target.map_or(false, |t| { + t <= self.cur_offset && !outer_for_top.map_or(false, |top| { + t == top || self.is_loop_top_prefix(t, top) + }) + })) { return None; } @@ -29212,6 +29562,27 @@ return None; Op::PUSH_NULL => { st.push(self.name_expr("\u{0}null")); } + Op::MAKE_FUNCTION if self.version.at_least(3, 13) => { + let value = st.pop()?; + let Expr::Const(value) = &*value else { return None; }; + let PyObject::Code(code) = &**value else { return None; }; + if code.name != "" { return None; } + st.push(Rc::new(Expr::Function(Rc::new(FunctionDef { + name: code.name.clone(), code: code.clone(), params: Parameters::empty(), + decorators: Vec::new(), returns: None, is_async: false, type_params: Vec::new(), + })))); + } + Op::SET_FUNCTION_ATTRIBUTE if ins.arg == 8 => { + let function = st.pop()?; + let closure = st.pop()?; + let Expr::Function(definition) = &*function else { return None; }; + let Expr::Tuple(cells) = &*closure else { return None; }; + if cells.len() != definition.code.freevars.len() { return None; } + // Generator captures refer to the enclosing lexical + // cells; retain the function after checking their count. + st.push(function); + } + Op::GET_ITER => { st.last()?; } Op::COPY => { if arg == 0 || arg > st.len() { return None; @@ -29268,6 +29639,23 @@ return None; } else { arg }; + if self.version.at_least(3, 13) && ins.op == Op::CALL && n == 0 && st.len() >= 2 { + if let Expr::Function(definition) = &*st[st.len() - 2] { + if definition.code.name == "" { + let code = definition.code.clone(); + let outer = st.pop()?; + st.pop(); + let decoded = decompile(&code, self.version).ok().filter(|result| result.clean)?; + let mut generators = Vec::new(); + let element = generator_parts_from_body(&decoded.body, &mut generators, &code.freevars)?; + generators.first_mut()?.iter = outer; + st.push(Rc::new(Expr::Comprehension { + kind: CompKind::Generator, elt: element, key: None, generators, + })); + continue; + } + } + } let mut args = Vec::new(); for _ in 0..n { args.push(pop1(&mut st)); @@ -29407,6 +29795,58 @@ return None; }; st.push(Rc::new(e)); } + Op::BUILD_MAP => { + let count = arg.checked_mul(2)?; + if st.len() < count { return None; } + let values = st.split_off(st.len() - count); + st.push(Rc::new(Expr::Dict(values.chunks_exact(2) + .map(|pair| (pair[0].clone(), pair[1].clone())).collect()))); + } + // Shared validation raises often construct an f-string + // message. Use the main parser's conversion/spec model, + // with exact stack consumption and no invented value ops. + Op::CONVERT_VALUE if self.version.at_least(3, 13) => { + let conversion = match ins.arg { + 1 => 's', 2 => 'r', 3 => 'a', _ => return None, + }; + let value = st.pop()?; + st.push(Rc::new(Expr::FString(Box::new(FString { + parts: vec![FStringPart::Value { value, conversion: Some(conversion), format_spec: None }], + })))); + } + Op::FORMAT_SIMPLE | Op::FORMAT_WITH_SPEC if self.version.at_least(3, 13) => { + let spec = if ins.op == Op::FORMAT_WITH_SPEC { Some(st.pop()?) } else { None }; + let value = st.pop()?; + let (value, conversion) = match &*value { + Expr::FString(text) if text.parts.len() == 1 => match &text.parts[0] { + FStringPart::Value { value: inner, conversion, format_spec: None } => + (inner.clone(), *conversion), + _ => (value, None), + }, + _ => (value, None), + }; + st.push(Rc::new(Expr::FString(Box::new(FString { + parts: vec![FStringPart::Value { + value, conversion, format_spec: spec.map(|value| Box::new(expr_to_fstring(value))), + }], + })))); + } + Op::BUILD_STRING if self.version.at_least(3, 13) => { + if st.len() < arg { return None; } + let values = st.split_off(st.len() - arg); + let mut parts = Vec::new(); + for value in values { + match &*value { + Expr::Const(value) => match &**value { + PyObject::Str(text) => parts.push(FStringPart::Literal(text.clone())), + _ => return None, + }, + Expr::FString(text) => parts.extend(text.parts.iter().cloned()), + _ => return None, + } + } + st.push(Rc::new(Expr::FString(Box::new(FString { parts })))); + } Op::RETURN_CONST => { st.push(Rc::new(Expr::Const( self.code.consts.get(arg).cloned().unwrap_or_else(|| Rc::new(PyObject::None)), @@ -30089,9 +30529,213 @@ return None; Some(body_start) } + /// Recover a shared rejection block reached by short-circuit tests. + /// CPython 3.14 puts the last test's success jump over the reject block. + /// Treating earlier None jumps as nested positive ifs loses rejection for + /// missing values. A negated comparison chain also has a POP_TOP cleanup + /// edge; retain its copied middle value to avoid evaluating it twice. + fn try_shared_rejection_guard( + &self, first: &ExprRef, first_jump_true: bool, target: usize, + ) -> Option<(ExprRef, Vec, usize)> { + if !self.version.at_least(3, 14) || !self.stack.is_empty() || target <= self.cur_offset { + return None; + } + let padding = |op| matches!(op, Op::NOP | Op::NOT_TAKEN | Op::CACHE | Op::EXTENDED_ARG); + let skip_padding = |mut index: usize| { + while self.instrs.get(index).map_or(false, |item| padding(item.op)) { index += 1; } + index + }; + let constant_at = |index: usize| -> Option { + let item = self.instrs.get(index)?; + match item.op { + Op::LOAD_CONST => self.code.consts.get(item.arg as usize).cloned(), + Op::LOAD_SMALL_INT => Some(Rc::new(PyObject::Int(item.arg as i32))), + Op::LOAD_COMMON_CONSTANT => match &*common_constant_expr(item.arg, self.version)? { + Expr::Const(value) => Some(value.clone()), + _ => None, + }, + _ => None, + } + }; + let reject_index = skip_padding(*self.idx_of.get(&target)?); + let reject = self.instrs.get(reject_index)?; + let (body, end_index) = if reject.is_backward && matches!(reject.op, Op::JUMP_BACKWARD | Op::JUMP_BACKWARD_NO_INTERRUPT) { + let top = reject.target?; + if !self.blocks.iter().any(|block| matches!(block.kind, BlockType::For | BlockType::While) + && (block.start == top || block.cond_end == top)) { return None; } + (vec![Stmt::Continue], reject_index + 1) + } else { + // A rejection can return a value or raise an exception with an + // optional explicit cause. Require exactly the terminator's + // operands, without branches, stores or exception cleanup. + let mut ret = reject_index; + while let Some(item) = self.instrs.get(ret) { + if item.op == Op::RETURN_VALUE + || (item.op == Op::RAISE_VARARGS && matches!(item.arg, 1 | 2)) { break; } + if item.is_jump() || matches!(item.op, Op::STORE_FAST | Op::STORE_NAME + | Op::STORE_DEREF | Op::STORE_ATTR | Op::STORE_SUBSCR | Op::POP_EXCEPT + | Op::PUSH_EXC_INFO | Op::RERAISE | Op::RAISE_VARARGS) { return None; } + ret += 1; + } + let terminal = self.instrs.get(ret)?; + let mut result = self.sim_stack_region(reject_index, ret, Vec::new())?; + let statement = match terminal.op { + Op::RETURN_VALUE if result.len() == 1 => Stmt::Return(Some(result.pop()?)), + Op::RAISE_VARARGS if matches!(terminal.arg, 1 | 2) + && result.len() == terminal.arg as usize => { + let cause = if terminal.arg == 2 { Some(result.pop()?) } else { None }; + Stmt::Raise { exc: Some(result.pop()?), cause, py2_inst: None, py2_tb: None } + } + _ => return None, + }; + (vec![statement], ret + 1) + }; + let equivalent_return = |index: usize| -> Option { + let Some(Stmt::Return(Some(expected))) = body.first() else { return None; }; + let Expr::Const(expected) = &**expected else { + // Short constructed returns can be duplicated too. Compare + // the complete branch-free instruction run, not just its + // first constant or the resulting expression's spelling. + let mut actual = skip_padding(index); + for expected in &self.instrs[reject_index..end_index] { + if padding(expected.op) { continue; } + let item = self.instrs.get(actual)?; + if item.op != expected.op || item.arg != expected.arg { return None; } + actual = skip_padding(actual + 1); + } + return Some(actual); + }; + let actual = constant_at(index)?; + if !Rc::ptr_eq(expected, &actual) && !const_value_eq(expected, &actual) { return None; } + let ret = skip_padding(index + 1); + if self.instrs.get(ret)?.op != Op::RETURN_VALUE { return None; } + Some(ret + 1) + }; + let mut body_index = skip_padding(end_index); + // A failed chained link may pop its retained middle value before a + // compiler-generated duplicate return. The successful jump skips + // both return copies, not merely the first one. + if matches!(body.first(), Some(Stmt::Return(_))) { + loop { + let mut copy = body_index; + if !self.instrs.get(copy).map_or(false, |item| item.op == Op::POP_TOP) { + break; + } + copy = skip_padding(copy + 1); + let Some(after_copy) = equivalent_return(copy) else { break; }; + body_index = skip_padding(after_copy); + } + } + let body_start = self.instrs.get(body_index)?.offset; + let reaches_reject = |offset: usize| -> bool { + let Some(&start) = self.idx_of.get(&offset) else { return false; }; + let mut index = skip_padding(start); + // A real success continuation may return the same constant. + // Equality alone does not make that statement a rejection copy. + if index >= body_index { return false; } + if self.instrs.get(index).map_or(false, |item| item.op == Op::POP_TOP) { + index = skip_padding(index + 1); + } + if index == reject_index { return true; } + if equivalent_return(index).is_some() { return true; } + if self.instrs.get(index).map_or(false, |item| + self.dup_copy_redirect.get(&item.offset).copied() == Some(reject.offset)) { return true; } + self.instrs.get(index).map_or(false, |item| item.op == Op::JUMP_FORWARD + && item.target.map_or(false, |dest| skip_padding(*self.idx_of.get(&dest).unwrap_or(&usize::MAX)) == reject_index)) + }; + let failure = |condition: ExprRef, truth: bool| -> ExprRef { + if truth { return condition; } + if let Expr::Compare { operands, ops } = &*condition { + if ops.len() == 1 && matches!(ops[0], CmpOp::Is | CmpOp::IsNot) { + return Rc::new(Expr::Compare { + operands: operands.clone(), ops: vec![if ops[0] == CmpOp::Is { CmpOp::IsNot } else { CmpOp::Is }], + }); + } + } + // Do not flip inequalities: NaN and custom rich comparisons + // make `not (a <= b)` different from `a > b`. + Rc::new(Expr::Unary { op: UnaryOp::Not, operand: condition }) + }; + let mut values = vec![failure(first.clone(), first_jump_true)]; + let mut start = *self.idx_of.get(&self.cur_offset)? + 1; + let mut carried = Vec::new(); + let mut complete = false; + while start < reject_index { + let mut end = start; + while end < reject_index && !matches!(self.instrs[end].op, + Op::POP_JUMP_IF_NONE | Op::POP_JUMP_IF_NOT_NONE | Op::POP_JUMP_IF_TRUE | Op::POP_JUMP_IF_FALSE) { + end += 1; + } + if end == reject_index { return None; } + let was_chain = !carried.is_empty(); + carried = self.sim_stack_region(start, end, carried)?; + let value = carried.pop()?; + let jump = self.instrs[end]; + let (condition, jump_true) = match jump.op { + Op::POP_JUMP_IF_NONE | Op::POP_JUMP_IF_NOT_NONE => (Rc::new(Expr::Compare { + operands: vec![value, Rc::new(Expr::Const(Rc::new(PyObject::None)))], + ops: vec![if jump.op == Op::POP_JUMP_IF_NONE { CmpOp::Is } else { CmpOp::IsNot }], + }), true), + Op::POP_JUMP_IF_TRUE => (value, true), + _ => (value, false), + }; + let destination = jump.target?; + let member = if reaches_reject(destination) { + failure(condition, jump_true) + } else if destination == body_start && reaches_reject(self.instrs.get(end + 1)?.offset) { + complete = true; + failure(condition, !jump_true) + } else { return None; }; + let merged = if was_chain { + match (values.last().map(|value| &**value), &*member) { + (Some(Expr::Unary { op: UnaryOp::Not, operand: left }), + Expr::Unary { op: UnaryOp::Not, operand: right }) => { + merge_chain_compare(left, right).map(|operand| Rc::new(Expr::Unary { op: UnaryOp::Not, operand })) + } + _ => None, + } + } else { None }; + let chain_merged = merged.is_some(); + if let Some(merged) = merged { values.pop(); values.push(merged); } + else { values.push(member); } + if complete { break; } + start = end + 1; + // A successful chained comparison can jump over its failed + // link's POP_TOP/JUMP_FORWARD cleanup into another predicate. + // Skip only that complete trampoline back to this rejection; + // simulating it on the success path consumes the middle value + // again or makes the remaining predicate region unparseable. + if chain_merged && carried.is_empty() { + let success = skip_padding(start); + if let Some(skip) = self.instrs.get(success) { + if skip.op == Op::JUMP_FORWARD { + if let Some(&destination) = skip.target.and_then(|offset| self.idx_of.get(&offset)) { + let cleanup = skip_padding(success + 1); + let jump = skip_padding(cleanup + 1); + if destination < reject_index + && self.instrs.get(cleanup).map_or(false, |item| item.op == Op::POP_TOP) + && self.instrs.get(jump).map_or(false, |item| item.op == Op::JUMP_FORWARD + && item.target.map_or(false, reaches_reject)) + && skip_padding(jump + 1) == destination { + start = destination; + } + } + } + } + } + } + if !complete || !carried.is_empty() || values.len() < 2 { return None; } + Some((Rc::new(Expr::BoolOp { op: BoolOpKind::Or, values }), body, body_start)) + } + fn handle_cond_jump(&mut self, cond: ExprRef, jump_if_true: bool, target: usize) { // tail-duplicated body copy: aim the jump at the original body let target = self.dup_copy_redirect.get(&target).copied().unwrap_or(target); + if let Some((guard, body, resume)) = self.try_shared_rejection_guard(&cond, jump_if_true, target) { + self.push_stmt(Stmt::If { cond: guard, body, orelse: Vec::new() }); + self.skip_until = Some(resume); + return; + } let raw_target = target; // 3.8+ `continue` threading: the jump may land ON the loop's back // edge instruction instead of the loop top (3.10 `if a or b: @@ -31523,6 +32167,51 @@ return None; if let Some((merged, body_start, body_end)) = self.try_stmt_chain_compare(&cond, target) { + // A chain can guard a comprehension element's ternary. + // Its success/else arms build values; opening a statement + // If here drops those values and the collection itself. + let ternary_merge = (|| { + let comp = self.inline_comp.as_ref()?; + let start = *self.idx_of.get(&body_start)?; + let end = *self.idx_of.get(&body_end)?; + let then = self.instrs[start..end].iter() + .filter(|item| !matches!(item.op, Op::NOP | Op::NOT_TAKEN | Op::CACHE)) + .collect::>(); + let hop = then.last()?; + let merge = hop.target?; + if hop.op != Op::JUMP_FORWARD || hop.is_backward + || merge <= body_end || merge >= comp.end + || !then[..then.len() - 1].iter().all(|item| is_pure_value_op(item.op)) + { return None; } + let merge_index = *self.idx_of.get(&merge)?; + if !self.instrs[end..merge_index].iter().all(|item| { + is_pure_value_op(item.op) || matches!(item.op, Op::NOP | Op::NOT_TAKEN | Op::CACHE) + }) { return None; } + let cleanup = *self.idx_of.get(&target)?; + let mut failed = self.instrs[cleanup..start].iter() + .filter(|item| !matches!(item.op, Op::NOP | Op::NOT_TAKEN | Op::CACHE)); + if failed.next()?.op != Op::POP_TOP { return None; } + let failed_hop = failed.next()?; + if failed_hop.op != Op::JUMP_FORWARD || failed_hop.target != Some(body_end) + || failed.next().is_some() + { return None; } + let Expr::Compare { operands, .. } = &*cond else { return None; }; + if !matches!(self.stack.last(), Some(Sv::E(value)) + if Rc::ptr_eq(value, &operands[1])) + { return None; } + Some(merge) + })(); + if let Some(merge) = ternary_merge { + // COPY retained this one middle operand for later links. + // The chain scan consumed those links without executing + // their stack operations; remove only that same value. + self.stack.pop(); + let comp = self.inline_comp.as_mut().unwrap(); + comp.pending_merges.push((merge, merged, None)); + comp.tern_chain_t = None; + self.skip_until = Some(body_start); + return; + } let mut blk = Block::new(BlockType::If, body_start, body_end); blk.cond = Some(merged); blk.cond_set = true; @@ -41695,6 +42384,10 @@ if split_cond { } fn with_return_tail_end(&self, k0: usize) -> Option { + self.with_return_tail_info(k0).map(|(after, _)| after) + } + + fn with_return_tail_info(&self, k0: usize) -> Option<(usize, usize)> { let first = self.instrs.get(k0)?; if !(first.op == Op::ROT_TWO || (first.op == Op::SWAP && (first.arg == 2 || first.arg == 3))) @@ -41721,6 +42414,8 @@ if split_cond { let mut call = false; let mut pop_after_call = false; let mut cleanup = false; + let mut remaining_exits = self.with_exits.saturating_sub(1); + let mut exit_groups = 1; while k < self.instrs.len() { let x = &self.instrs[k]; match x.op { @@ -41730,11 +42425,8 @@ if split_cond { k += 1; } Op::POP_FINALLY => k += 1, - Op::LOAD_CONST - if matches!( - self.code.consts.get(x.arg as usize).map(|o| &**o), - Some(PyObject::None) - ) => + Op::LOAD_CONST | Op::LOAD_COMMON_CONSTANT + if self.loads_none(x.op, x.arg) => { nones += 1; k += 1; @@ -41776,9 +42468,27 @@ if split_cond { pop_after_call = true; k += 1; } + // Nested modern contexts carry the same computed value + // across another complete exit group before one shared + // return. Only consume the exact SWAP pair after a finished + // exit, and never exceed the known context-exit slots. + Op::SWAP if (x.arg == 2 || x.arg == 3) && call && pop_after_call + && self.version.at_least(3, 11) + && remaining_exits > 0 + && (x.arg == 2 || matches!(self.instrs.get(k + 1).map(|next| (next.op, next.arg)), + Some((Op::SWAP, 2)))) => + { + remaining_exits -= 1; + exit_groups += 1; + nones = 0; + call = false; + pop_after_call = false; + cleanup = false; + k += if x.arg == 3 { 2 } else { 1 }; + } Op::RETURN_VALUE | Op::RETURN_CONST => { if cleanup || (call && pop_after_call) { - return Some(x.end()); + return Some((x.end(), exit_groups)); } return None; } @@ -41878,7 +42588,7 @@ if split_cond { let Some(&ki) = self.idx_of.get(&inst.offset) else { return false; }; - let Some(after) = self.with_return_tail_end(ki) else { + let Some((after, exit_groups)) = self.with_return_tail_info(ki) else { return false; }; // 3.14 guard-arm sunk exits: the With is still OPEN (its merged @@ -41920,6 +42630,43 @@ if split_cond { } return true; } + // The innermost context may have closed at the first exit group, + // leaving its With statement last in a still-open outer context. + // A proved multi-exit return belongs in that closed inner body. + // Contiguous outer contexts prove its nesting. Keep them open: + // their exit slots are still needed on the suppression path. + if exit_groups > 1 + && self.blocks.iter().rev().take(exit_groups - 1) + .all(|block| block.kind == BlockType::With) + && self.blocks.len() >= exit_groups + && matches!(self.blocks.last().and_then(|block| block.stmts.last()), Some(Stmt::With { .. })) + { + let value = self.pop_expr(); + let ret = match &*value { + Expr::Const(object) if matches!(&**object, PyObject::None) => Stmt::Return(None), + _ => Stmt::Return(Some(value)), + }; + if let Some(Stmt::With { body, .. }) = self.blocks.last_mut().and_then(|block| block.stmts.last_mut()) { + body.push(ret); + } + self.with_exits = self.with_exits.saturating_sub(1); + let skip_end = self.with_handler_skip_end(after); + if self.skip_until.map_or(true, |current| current < skip_end) { + self.skip_until = Some(skip_end); + } + return true; + } + // A loop's out-of-line except fragments can extend the With's + // region beyond its normal computed-return tail. The With is then + // still open here although its loop has closed. This pure exit + // protocol carries the body's existing result, not a user call. + // Close that With at the tail before simulating its cleanup slots. + if self.blocks.last().map_or(false, |block| { + block.kind == BlockType::With && block.end > inst.offset + }) { + self.fold_with_return(inst.offset, after); + return true; + } // probe the target shape BEFORE popping so a failed fold leaves // the stack untouched if !matches!( @@ -43055,7 +43802,12 @@ impl<'a> Ctx<'a> { && self .idx_of .get(&self.cur_offset) - .and_then(|&pi| (pi > 0).then(|| &self.instrs[pi - 1])) + // 3.14 inserts NOT_TAKEN after the conditional jump. + // It does not consume the retained operand: the first + // meaningful instruction in the arm is still its pop. + .and_then(|&pi| self.instrs[..pi].iter().rev().find(|p| { + !matches!(p.op, Op::NOP | Op::NOT_TAKEN | Op::CACHE) + })) .map_or(false, |p| { matches!(p.op, Op::POP_JUMP_IF_FALSE | Op::POP_JUMP_IF_TRUE) && !p.is_backward @@ -44384,6 +45136,19 @@ impl<'a> Ctx<'a> { } } + fn loads_none(&self, op: Op, arg: u32) -> bool { + match op { + Op::LOAD_CONST => matches!( + self.code.consts.get(arg as usize).map(|o| &**o), + Some(PyObject::None) + ), + Op::LOAD_COMMON_CONSTANT => common_constant_expr(arg, self.version) + .map_or(false, |value| matches!(&*value, + Expr::Const(object) if matches!(&**object, PyObject::None))), + _ => false, + } + } + /// 3.11+ function-tail try/except[/else] whose implicit `return None` /// the compiler SANK into both exits: the success path ends with a /// RETURN None right before the out-of-line chain, and the chain's @@ -44483,15 +45248,15 @@ impl<'a> Ctx<'a> { // the mainline and claimed the else arm's own return as the // mirror, losing it. A bare `LOAD None; RETURN` tail is // try_tail_sunk_pair's stricter domain - skip it here. - if tail.len() < 2 { + let meaningful_tail: Vec<_> = tail.iter().filter(|(op, _)| { + !matches!(op, Op::NOP | Op::NOT_TAKEN | Op::CACHE) + }).collect(); + if meaningful_tail.len() < 2 { return None; } - if tail.len() == 2 - && tail[0].0 == Op::LOAD_CONST - && matches!( - self.code.consts.get(tail[0].1 as usize).map(|o| &**o), - Some(PyObject::None) - ) + if meaningful_tail.len() == 2 + && meaningful_tail[1].0 == Op::RETURN_VALUE + && self.loads_none(meaningful_tail[0].0, meaningful_tail[0].1) { return None; } @@ -44608,11 +45373,7 @@ impl<'a> Ctx<'a> { ), Op::RETURN_VALUE => ri > 0 && { let p = &self.instrs[ri - 1]; - p.op == Op::LOAD_CONST - && matches!( - self.code.consts.get(p.arg as usize).map(|o| &**o), - Some(PyObject::None) - ) + self.loads_none(p.op, p.arg) }, _ => false, }; @@ -44634,13 +45395,7 @@ impl<'a> Ctx<'a> { self.idx_of .get(&x.offset) .and_then(|&xi| xi.checked_sub(1).map(|p| &self.instrs[p])) - .map(|p| { - p.op == Op::LOAD_CONST - && matches!( - self.code.consts.get(p.arg as usize).map(|o| &**o), - Some(PyObject::None) - ) - }) + .map(|p| self.loads_none(p.op, p.arg)) .unwrap_or(false) }, _ => false, @@ -47073,7 +47828,16 @@ impl<'a> Ctx<'a> { && star_args_none(&args) && args.iter().all(|a| matches!(&**a, Expr::Const(o) if matches!(&**o, PyObject::None))) { - self.with_exits -= 1; + // A constant early return has no SWAP-based return tail. Its + // copied __aexit__ call must not consume the enclosing with's + // bookkeeping needed by later branches and the final exit. + let early_exit = self.blocks.iter().any(|block| { + block.kind == BlockType::With && block.end > self.cur_offset + }) && matches!(self.blocks.last().map(|block| block.kind), + Some(BlockType::If) | Some(BlockType::Else)); + if !early_exit { + self.with_exits -= 1; + } let next_awaitable = self .idx_of .get(&self.cur_offset) @@ -47257,6 +48021,7 @@ impl<'a> Ctx<'a> { /// method is already `Attribute { value: receiver }`, so the marker is /// only validated, not consumed into the AST. fn call_311(&mut self, argc: usize, _is_method: bool) { + let clean_before_call = self.clean; // 3.11+ assert-with-message: the assertion class and the message // sit as [cls, msg] with NO null slot, and CALL 0 consumes msg as // the self_or_null position (an implicit first argument). Without @@ -47298,6 +48063,7 @@ impl<'a> Ctx<'a> { // the iterable sits ABOVE the function and becomes the marker // Peek before popping to choose the right order. let n = self.stack.len(); + let missing_callable_slot = n == 0; let genexpr_case = n >= 2 && matches!(&self.stack[n - 1], Sv::E(_)) && matches!(&self.stack[n - 2], Sv::E(f) if is_comp_callable(f)); @@ -47498,7 +48264,37 @@ impl<'a> Ctx<'a> { && none_args && (func_is_none || func_is_underflow) { - self.with_exits -= 1; + let protected_early_exit = self.blocks.iter().any(|block| { + block.kind == BlockType::With && block.end > self.cur_offset + }) && matches!(self.blocks.last().map(|block| block.kind), + Some(BlockType::If) | Some(BlockType::Else)); + // CPython 3.14+ can sink a constant-return protocol call + // immediately past the With's final protected fragment. The + // arm still owns it, and a later normal exit needs this slot. + let sunk_return_exit = self.version.at_least(3, 14) + && pos_args.len() == 3 + && self.blocks.iter().any(|block| block.kind == BlockType::With) + && self.blocks.last().filter(|block| { + matches!(block.kind, BlockType::If | BlockType::Else) + && block.end > self.cur_offset + }).zip(self.idx_of.get(&self.cur_offset)).map_or(false, |(arm, &index)| { + for item in self.instrs[index + 1..].iter().take_while(|item| item.offset < arm.end) { + if item.op == Op::RETURN_VALUE { return true; } + // 3.15 adds a null SEND slot after GET_AWAITABLE. + if item.op == Op::PUSH_NULL && self.version.at_least(3, 15) { continue; } + if !matches!(item.op, + Op::NOP | Op::NOT_TAKEN | Op::CACHE | Op::GET_AWAITABLE + | Op::LOAD_CONST | Op::LOAD_SMALL_INT | Op::LOAD_COMMON_CONSTANT + | Op::SEND | Op::YIELD_VALUE + | Op::RESUME | Op::JUMP_BACKWARD_NO_INTERRUPT | Op::END_SEND | Op::POP_TOP) + { return false; } + } + false + }); + let early_exit = protected_early_exit || sunk_return_exit; + if !early_exit { + self.with_exits -= 1; + } // async with (3.11+): the __aexit__ result is awaited right // after the swallowed call — the protocol has no awaitable on // our modeled stack, so drop it cleanly (no underflow) @@ -47513,6 +48309,14 @@ impl<'a> Ctx<'a> { }); if next_awaitable { self.with_exit_await_drop = true; + // The exit callable is deliberately absent from this + // modeled stack. Its exact three-None async protocol + // has been consumed, so that one expected underflow + // does not indicate missing user code. Preserve every + // incomplete diagnostic that preceded this call. + if missing_callable_slot && argc == 3 { + self.clean = clean_before_call; + } } return; } @@ -51591,11 +52395,12 @@ impl<'a> Ctx<'a> { // ===================== comprehensions ===================== -// The main statement parser already understands PEP 709's nested inline -// comprehensions. Reuse it for generator elements instead of flattening an -// inner dict/list/set comprehension into the generator's own clauses. +// The main statement parser understands nested inline comprehensions, +// optimized unpack targets, and nested closure calls. Reuse its For/If/Yield +// tree for eligible 3.12+ generators, retaining the legacy simulator as a +// fallback when the statement result is incomplete or not a generator tree. fn generator_parts_from_body( - body: &[Stmt], generators: &mut Vec, + body: &[Stmt], generators: &mut Vec, freevars: &[String], ) -> Option { let mut result = None; for statement in body { @@ -51606,13 +52411,21 @@ fn generator_parts_from_body( target: target.clone(), iter: iter.clone(), ifs: Vec::new(), is_async: *is_async, if_line_break: false, }); - result = generator_parts_from_body(body, generators); + result = generator_parts_from_body(body, generators, freevars); if result.is_none() { return None; } } + // A rejected iteration can be an inverted continue guard, + // followed by the yielded value. Keep the statement parser's + // expression instead of losing its COPY/TO_BOOL value merges. + Stmt::If { cond, body, orelse } + if orelse.is_empty() && matches!(body.as_slice(), [Stmt::Continue]) => { + if result.is_some() { return None; } + generators.last_mut()?.ifs.push(negate_cond(cond.clone())); + } Stmt::If { cond, body, orelse } if orelse.is_empty() => { if result.is_some() { return None; } generators.last_mut()?.ifs.push(cond.clone()); - result = generator_parts_from_body(body, generators); + result = generator_parts_from_body(body, generators, freevars); if result.is_none() { return None; } } Stmt::Expr(value) => { @@ -51625,6 +52438,10 @@ fn generator_parts_from_body( _ => return None, } } + // Generator walrus targets bind enclosing cells. Their Named + // expressions retain the stores; the parent supplies the scope. + // Never discard global or unrelated nonlocal declarations. + Stmt::Nonlocal(names) if names.iter().all(|name| freevars.contains(name)) => {}, Stmt::Pass | Stmt::Return(None) => {}, Stmt::Return(Some(value)) if matches!(&**value, Expr::Const(object) if matches!(&**object, PyObject::None)) => {}, @@ -51896,13 +52713,32 @@ fn genexpr_ternary_merge( let table = table_for(self.version).ok()?; let instrs = crate::bytecode::decode_instructions(code, table, self.version); - if self.version.at_least(3, 12) && matches!(_kind, CompKind::Generator) - && instrs.iter().any(|instruction| instruction.op == Op::LOAD_FAST_AND_CLEAR) - { + if self.version.at_least(3, 12) && matches!(_kind, CompKind::Generator) { if let Some(statements) = decompile(code, self.version).ok().filter(|result| result.clean) { let mut generators = Vec::new(); - if let Some(element) = generator_parts_from_body(&statements.body, &mut generators) { + if let Some(element) = generator_parts_from_body(&statements.body, &mut generators, &code.freevars) { if !generators.is_empty() { + // In 3.12, a filter on a separate source line jumps + // to the shared back edge instead of to the yield. + // Preserve the legacy walker's layout hint when the + // decoded loops map one-to-one to generator clauses. + if !self.version.at_least(3, 13) { + let loops: Vec<_> = instrs.iter() + .filter(|ins| ins.op == Op::FOR_ITER).collect(); + if loops.len() == generators.len() { + for (clause, loop_head) in generators.iter_mut().zip(loops.iter()) { + clause.if_line_break = !clause.ifs.is_empty() && instrs.iter().any(|ins| { + ins.op == Op::POP_JUMP_IF_FALSE + && ins.offset > loop_head.offset + && loop_head.target.map_or(false, |end| ins.offset < end) + && ins.target.and_then(|target| instrs.iter() + .find(|tail| tail.offset == target)) + .map_or(false, |tail| tail.op == Op::JUMP_BACKWARD + && tail.target == Some(loop_head.offset)) + }); + } + } + } generators[0].iter = outer_iter.clone(); return Some((element, None, generators)); } @@ -53963,18 +54799,61 @@ mod generator_parts_tests { } } + #[test] + fn extraction_omits_only_declarations_for_real_enclosing_cells() { + let declaration = Stmt::Nonlocal(vec!["change".into()]); + let body = vec![declaration.clone(), yielded_loop()]; + assert!(generator_parts_from_body(&body, &mut Vec::new(), &["change".into()]).is_some()); + assert!(generator_parts_from_body(&body, &mut Vec::new(), &[]).is_none()); + for declaration in [ + Stmt::Global(vec!["change".into()]), + Stmt::Nonlocal(vec!["change".into(), "unrelated".into()]), + ] { + assert!(generator_parts_from_body( + &[declaration, yielded_loop()], &mut Vec::new(), &["change".into()], + ).is_none()); + } + } + + #[test] + fn extraction_accepts_only_an_early_continue_guard() { + let guard = Stmt::If { + cond: Rc::new(Expr::Name("skip".into())), + body: vec![Stmt::Continue], orelse: vec![], + }; + let mut accepted = yielded_loop(); + if let Stmt::For { body, .. } = &mut accepted { body.insert(0, guard.clone()); } + let mut clauses = Vec::new(); + assert!(generator_parts_from_body(&[accepted], &mut clauses, &[]).is_some()); + assert_eq!(clauses[0].ifs.len(), 1); + assert!(matches!(&*clauses[0].ifs[0], Expr::Unary { op: UnaryOp::Not, .. })); + assert!(generator_parts_from_body(&[guard.clone()], &mut Vec::new(), &[]).is_none()); + let mut late = yielded_loop(); + if let Stmt::For { body, .. } = &mut late { body.push(guard.clone()); } + assert!(generator_parts_from_body(&[late], &mut Vec::new(), &[]).is_none()); + for add_else in [false, true] { + let mut rejected = guard.clone(); + if let Stmt::If { body, orelse, .. } = &mut rejected { + if add_else { orelse.push(Stmt::Pass); } else { body.push(Stmt::Pass); } + } + let mut enclosing = yielded_loop(); + if let Stmt::For { body, .. } = &mut enclosing { body.insert(0, rejected); } + assert!(generator_parts_from_body(&[enclosing], &mut Vec::new(), &[]).is_none()); + } + } + #[test] fn extraction_preserves_one_yield_path_and_rejects_siblings() { let body = vec![yielded_loop()]; let mut generators = Vec::new(); - let element = generator_parts_from_body(&body, &mut generators).unwrap(); + let element = generator_parts_from_body(&body, &mut generators, &[]).unwrap(); assert!(matches!(&*element, Expr::Name(name) if name == "row")); assert_eq!(generators.len(), 1); let mut generators = Vec::new(); assert!(generator_parts_from_body( &[yielded_loop(), yielded_loop()], &mut generators, - ).is_none()); + &[]).is_none()); let branch = Stmt::If { cond: Rc::new(Expr::Name("ready".into())), @@ -53987,7 +54866,7 @@ mod generator_parts_tests { if let Stmt::For { body, .. } = &mut body { *body = vec![branch.clone(), branch]; } - assert!(generator_parts_from_body(&[body], &mut Vec::new()).is_none()); + assert!(generator_parts_from_body(&[body], &mut Vec::new(), &[]).is_none()); } #[test] @@ -54000,12 +54879,12 @@ mod generator_parts_tests { }; assert!(generator_parts_from_body( &[assignment, yielded_loop()], &mut Vec::new(), - ).is_none()); + &[]).is_none()); } let mut body = yielded_loop(); if let Stmt::For { orelse, .. } = &mut body { *orelse = vec![Stmt::Pass]; } - assert!(generator_parts_from_body(&[body], &mut Vec::new()).is_none()); + assert!(generator_parts_from_body(&[body], &mut Vec::new(), &[]).is_none()); } } diff --git a/src/decompiler_stack_tests.rs b/src/decompiler_stack_tests.rs index 236a612e..7809c6a7 100644 --- a/src/decompiler_stack_tests.rs +++ b/src/decompiler_stack_tests.rs @@ -74,6 +74,7 @@ fn context(code: &CodeObject, version: PythonVersion, instrs: Vec) sunk_pair_drop: None, sunk_while_lift: None, nested_tail_defer: false, + lexical_loop_handlers: FastMap::default(), pending_try_handlers: Vec::new(), pending_loop: Vec::new(), pending_with: Vec::new(), @@ -249,3 +250,384 @@ fn store_fast_rejects_separate_evaluations_and_standalone_assignments() { assert!(ctx.sim_stack_region(0, 1, stack).is_none()); } } + +fn nested_code(code: &CodeObject, name: &str) -> Rc { + code.consts.iter().find_map(|value| match &**value { + PyObject::Code(code) if code.name == name => Some(code.clone()), + _ => None, + }).unwrap_or_else(|| panic!("missing code object {name}")) +} + +#[test] +fn bounded_generator_regions_preserve_iterables_and_captured_cells() { + for minor in 13..=15 { + let path = format!("{}/tests/pyc/bounded_generator_expressions.3.{minor}.pyc", + env!("CARGO_MANIFEST_DIR")); + let module = PycModule::from_bytes(&std::fs::read(path).unwrap()).unwrap(); + let version = module.version(); + for (name, expected) in [ + ("plain", "return collect((value for value in values))"), + ("captured", "return collect((value + offset for value in values))"), + ("multiple", "return collect(((value + first) * second for value in values))"), + ] { + let code = nested_code(&module.code, name); + let instrs = crate::bytecode::decode_instructions(&code, table_for(version).unwrap(), version); + let start = instrs.iter().position(|ins| ins.op == Op::LOAD_GLOBAL).unwrap(); + let end = instrs.iter().position(|ins| ins.op == Op::RETURN_VALUE).unwrap(); + let ctx = context(&code, version, instrs); + let result = ctx.sim_stack_region(start, end, vec![]) + .unwrap_or_else(|| panic!("3.{minor} {name}: generator construction rejected")); + assert_eq!(result.len(), 1, "3.{minor} {name}: {result:?}"); + let source = crate::codegen::generate(&[Stmt::Return(Some(result[0].clone()))], version, true); + assert_eq!(source.trim(), expected, "3.{minor} {name}"); + } + let ordinary = nested_code(&module.code, "ordinary_function"); + let instrs = crate::bytecode::decode_instructions(&ordinary, table_for(version).unwrap(), version); + let start = instrs.iter().position(|ins| ins.op == Op::LOAD_CONST).unwrap(); + let end = instrs.iter().position(|ins| ins.op == Op::RETURN_VALUE).unwrap(); + let ctx = context(&ordinary, version, instrs); + assert!(ctx.sim_stack_region(start, end, vec![]).is_none()); + } +} + +#[test] +fn bounded_generator_construction_rejects_invalid_closure_slots() { + let module = PycModule::from_bytes(include_bytes!( + "../tests/pyc/bounded_generator_expressions.3.13.pyc")).unwrap(); + let parent = nested_code(&module.code, "captured"); + let generator = nested_code(&parent, ""); + assert_eq!(generator.freevars, ["offset"]); + let constant = Rc::new(Expr::Const(Rc::new(PyObject::Code(generator)))); + let name = Rc::new(Expr::Name("offset".into())); + for closure in [name.clone(), Rc::new(Expr::Tuple(vec![])), + Rc::new(Expr::Tuple(vec![name.clone(), name.clone()]))] { + let ctx = context(&parent, module.version(), vec![ + instruction(Op::MAKE_FUNCTION, 0), instruction(Op::SET_FUNCTION_ATTRIBUTE, 8), + ]); + assert!(ctx.sim_stack_region(0, 2, vec![closure, constant.clone()]).is_none()); + } + let ctx = context(&parent, module.version(), vec![ + instruction(Op::MAKE_FUNCTION, 0), instruction(Op::SET_FUNCTION_ATTRIBUTE, 8), + ]); + let result = ctx.sim_stack_region(0, 2, + vec![Rc::new(Expr::Tuple(vec![name])), constant]).unwrap(); + assert!(matches!(&*result[0], Expr::Function(_))); + let ctx = context(&parent, module.version(), vec![instruction(Op::GET_ITER, 0)]); + assert!(ctx.sim_stack_region(0, 1, vec![]).is_none()); +} + +#[test] +fn expected_async_exit_underflow_preserves_prior_diagnostics() { + let module = fixture(); + for minor in [14, 15] { + for initial_clean in [true, false] { + let mut next = instruction(Op::GET_AWAITABLE, 2); + next.offset = 2; + let mut ctx = context(&module.code, PythonVersion { minor, ..module.version() }, + vec![instruction(Op::CALL, 3), next]); + ctx.idx_of.insert(0, 0); + ctx.with_exits = 1; + ctx.clean = initial_clean; + ctx.stack = vec![Sv::E(Rc::new(Expr::Const(Rc::new(PyObject::None)))); 3]; + ctx.call_311(3, false); + assert!(ctx.with_exit_await_drop); + assert_eq!(ctx.clean, initial_clean, "prior diagnostics must survive"); + assert_eq!(ctx.with_exits, 0); + } + for (argc, awaited) in [(2, true), (3, false)] { + let mut next = instruction(if awaited { Op::GET_AWAITABLE } else { Op::POP_TOP }, 2); + next.offset = 2; + let mut ctx = context(&module.code, PythonVersion { minor, ..module.version() }, + vec![instruction(Op::CALL, argc as u32), next]); + ctx.idx_of.insert(0, 0); + ctx.with_exits = 1; + ctx.stack = vec![Sv::E(Rc::new(Expr::Const(Rc::new(PyObject::None)))); argc]; + ctx.call_311(argc, false); + assert!(!ctx.clean, "only the exact three-None async exit forgives its absent callable"); + } + } +} + +#[test] +fn nested_context_return_matcher_requires_complete_exit_groups_and_slots() { + let module = fixture(); + let none = module.code.consts.iter().position(|o| matches!(&**o, PyObject::None)).unwrap() as u32; + for minor in 11..=15 { + for groups in [1, 2, 3] { + let mut instrs = Vec::new(); + for _ in 0..groups { + instrs.extend([ + instruction(Op::SWAP, 3), instruction(Op::SWAP, 2), + instruction(Op::LOAD_CONST, none), instruction(Op::LOAD_CONST, none), + instruction(Op::LOAD_CONST, none), instruction(Op::CALL, 3), + instruction(Op::POP_TOP, 0), + ]); + } + instrs.push(instruction(Op::RETURN_VALUE, 0)); + for (i, inst) in instrs.iter_mut().enumerate() { inst.offset = i * 2; } + let end = instrs.last().unwrap().end(); + let mut ctx = context(&module.code, PythonVersion { minor, ..module.version() }, instrs); + ctx.with_exits = groups; + assert_eq!(ctx.with_return_tail_info(0), Some((end, groups))); + if groups > 1 { + ctx.with_exits = groups - 1; + assert!(ctx.with_return_tail_info(0).is_none(), "must not consume unknown exits"); + ctx.with_exits = groups; + ctx.instrs[8].arg = 1; + assert!(ctx.with_return_tail_info(0).is_none(), "second SWAP pair must be exact"); + ctx.instrs[8].arg = 2; + ctx.instrs[13].op = Op::NOP; + assert!(ctx.with_return_tail_info(0).is_none(), "each exit must discard its call result"); + } + } + } +} + +#[test] +fn normal_async_exit_spans_recognize_actual_suspension_protocols() { + for minor in [14, 15] { + let path = format!("{}/tests/pyc/normal_async_exits.3.{minor}.pyc", env!("CARGO_MANIFEST_DIR")); + let module = PycModule::from_bytes(&std::fs::read(path).unwrap()).unwrap(); + let code = nested_code(&module.code, "normal_1"); + let version = module.version(); + let instrs = crate::bytecode::decode_instructions(&code, table_for(version).unwrap(), version); + let mut ctx = context(&code, version, instrs); + ctx.idx_of = ctx.instrs.iter().enumerate().map(|(i, x)| (x.offset, i)).collect(); + let starts: Vec<_> = ctx.instrs.windows(3).enumerate() + .filter(|(_, group)| group.iter().all(|x| ctx.loads_none(x.op, x.arg))) + .map(|(i, group)| (i, group[0].offset)).collect(); + assert_eq!(starts.len(), 2); + for (i, off) in starts { + assert!(ctx.inline_with_exit_span(off).is_some(), "3.{minor} exit at {off}: {:?}", &ctx.instrs[i..i+16]); + } + } +} + +#[test] +fn async_normal_exit_rejects_user_awaits_broken_loops_and_return_tails() { + for minor in [14, 15] { + let path = format!("{}/tests/pyc/normal_async_exits.3.{minor}.pyc", env!("CARGO_MANIFEST_DIR")); + let module = PycModule::from_bytes(&std::fs::read(path).unwrap()).unwrap(); + let code = nested_code(&module.code, "normal_1"); + let version = module.version(); + let instrs = crate::bytecode::decode_instructions(&code, table_for(version).unwrap(), version); + let make = |instrs| { + let mut ctx = context(&code, version, instrs); + ctx.idx_of = ctx.instrs.iter().enumerate().map(|(i, x)| (x.offset, i)).collect(); + ctx + }; + let ctx = make(instrs.clone()); + let start = ctx.instrs.windows(3).position(|group| group.iter().all(|x| ctx.loads_none(x.op, x.arg))).unwrap(); + let off = ctx.instrs[start].offset; + let after = ctx.inline_with_exit_span(off).unwrap(); + let end = ctx.idx_of[&after]; + for mutation in 0..4 { + let mut bad = instrs.clone(); + let span = &mut bad[start..end]; + match mutation { + 0 => span.iter_mut().find(|x| x.op == Op::GET_AWAITABLE).unwrap().arg = 0, + 1 => span.iter_mut().find(|x| x.op == Op::JUMP_BACKWARD_NO_INTERRUPT).unwrap().target = Some(off), + 2 => span.iter_mut().find(|x| x.op == Op::END_SEND).unwrap().op = Op::NOP, + _ => span.iter_mut().find(|x| x.op == Op::RESUME).unwrap().op = Op::LOAD_GLOBAL, + } + assert!(make(bad).inline_with_exit_span(off).is_none(), "3.{minor}: mutation {mutation}"); + } + for padding in [0, 1, 3, 9] { + for constant in [false, true] { + let mut tail = instrs[..end].to_vec(); + tail.extend((0..padding).map(|_| instruction(Op::NOP, 0))); + if constant { + tail.push(instruction(Op::LOAD_SMALL_INT, 4)); + tail.extend((0..padding).map(|_| instruction(Op::NOP, 0))); + } + tail.push(instruction(Op::RETURN_VALUE, 0)); + for (i, item) in tail[end..].iter_mut().enumerate() { item.offset = after + i * 2; } + assert!(make(tail).inline_with_exit_span(off).is_none(), "3.{minor}: carried return with {padding} pads"); + } + } + } +} + +#[test] +fn async_exit_ownership_preserves_displaced_and_nested_slots() { + for minor in [14, 15] { + let path = format!("{}/tests/pyc/normal_async_exits.3.{minor}.pyc", env!("CARGO_MANIFEST_DIR")); + let module = PycModule::from_bytes(&std::fs::read(path).unwrap()).unwrap(); + let code = nested_code(&module.code, "normal_1"); + let version = module.version(); + let instrs = crate::bytecode::decode_instructions(&code, table_for(version).unwrap(), version); + let mut ctx = context(&code, version, instrs); + ctx.idx_of = ctx.instrs.iter().enumerate().map(|(i, x)| (x.offset, i)).collect(); + ctx.cur_offset = ctx.instrs.windows(3).find(|group| group.iter().all(|x| ctx.loads_none(x.op, x.arg))).unwrap()[0].offset; + let after = ctx.inline_with_exit_span(ctx.cur_offset).unwrap(); + assert!(ctx.owned_inline_with_exit_span().is_some()); + ctx.exc_entries.push(crate::code::ExceptionEntry { start: ctx.cur_offset, end: after, target: 999, depth: 2, lasti: true }); + ctx.with_region_handlers.insert(1, 999); + assert!(ctx.owned_inline_with_exit_span().is_none(), "lost Try and With must retain the protected exit"); + ctx.blocks.push(Block::new(BlockType::Try, 0, after)); + assert!(ctx.owned_inline_with_exit_span().is_none(), "a displaced Try has no owning With"); + let mut owner = Block::new(BlockType::With, 0, after); + owner.with_handler = Some(999); + ctx.blocks.push(owner); + assert!(ctx.owned_inline_with_exit_span().is_none(), "the open owner's handler protects an inner exit"); + ctx.blocks.last_mut().unwrap().with_handler = Some(1001); + assert!(ctx.owned_inline_with_exit_span().is_some()); + ctx.blocks.remove(1); + assert!(ctx.owned_inline_with_exit_span().is_some(), "ordinary open-With walks keep their exit handling"); + } +} + +#[test] +fn following_exception_clauses_require_bounded_typed_mismatch_protocols() { + let module = fixture(); + let build = |mut prefix: Vec, jump_op, forward| { + prefix.push(instruction(Op::CHECK_EXC_MATCH, 0)); + let mut jump = instruction(jump_op, 0); + jump.target = Some(if forward { 1000 } else { 0 }); + prefix.push(jump); + for (i, x) in prefix.iter_mut().enumerate() { x.offset = i * 2; } + let mut ctx = context(&module.code, module.version(), prefix); + ctx.idx_of = ctx.instrs.iter().enumerate().map(|(i, x)| (x.offset, i)).collect(); + ctx + }; + for prefix in [ + vec![instruction(Op::LOAD_GLOBAL, 0)], + vec![instruction(Op::LOAD_NAME, 0)], + vec![instruction(Op::LOAD_FAST, 0), instruction(Op::LOAD_ATTR, 0)], + vec![instruction(Op::LOAD_DEREF, 0), instruction(Op::LOAD_ATTR, 0)], + vec![instruction(Op::LOAD_GLOBAL, 0), instruction(Op::LOAD_GLOBAL, 1), instruction(Op::BUILD_TUPLE, 2)], + ] { + let ctx = build(prefix.clone(), Op::POP_JUMP_IF_FALSE, true); + assert_eq!(ctx.following_except_clause_end(0), Some(ctx.instrs.last().unwrap().end())); + assert!(build(prefix.clone(), Op::POP_JUMP_IF_FALSE, false).following_except_clause_end(0).is_none()); + assert!(build(prefix, Op::POP_JUMP_IF_TRUE, true).following_except_clause_end(0).is_none()); + } + for barrier in [Op::PUSH_EXC_INFO, Op::POP_EXCEPT, Op::RERAISE, Op::CALL, Op::JUMP_FORWARD] { + let ctx = build(vec![instruction(Op::LOAD_GLOBAL, 0), instruction(barrier, 0)], Op::POP_JUMP_IF_FALSE, true); + assert!(ctx.following_except_clause_end(0).is_none(), "must stop at {barrier:?}"); + } + let ctx = build(vec![instruction(Op::NOP, 0); 32], Op::POP_JUMP_IF_FALSE, true); + assert!(ctx.following_except_clause_end(0).is_none()); +} + +#[test] +fn bounded_mapping_preserves_pair_order_prefix_and_rejects_underflow() { + let module = fixture(); + let values: Vec<_> = ["prefix", "key0", "value0", "key1", "value1"].iter() + .map(|name| Rc::new(Expr::Name((*name).into()))).collect(); + for minor in 8..=15 { + let version = PythonVersion { minor, ..module.version() }; + let ctx = context(&module.code, version, vec![instruction(Op::BUILD_MAP, 2)]); + let result = ctx.sim_stack_region(0, 1, values.clone()).unwrap(); + assert_eq!(result.len(), 2); + assert!(Rc::ptr_eq(&result[0], &values[0])); + let Expr::Dict(pairs) = &*result[1] else { panic!("expected dictionary"); }; + assert_eq!(pairs.len(), 2); + for (i, (key, value)) in pairs.iter().enumerate() { + assert!(Rc::ptr_eq(key, &values[1 + 2 * i])); + assert!(Rc::ptr_eq(value, &values[2 + 2 * i])); + } + assert!(ctx.sim_stack_region(0, 1, values[..3].to_vec()).is_none()); + let empty = context(&module.code, version, vec![instruction(Op::BUILD_MAP, 0)]); + let result = empty.sim_stack_region(0, 1, values[..1].to_vec()).unwrap(); + assert!(matches!(&*result[1], Expr::Dict(pairs) if pairs.is_empty())); + let oversized = context(&module.code, version, vec![instruction(Op::BUILD_MAP, u32::MAX)]); + assert!(oversized.sim_stack_region(0, 1, values.clone()).is_none()); + } +} + +#[test] +fn bounded_formatting_preserves_conversions_and_dynamic_specs() { + for minor in 13..=15 { + let path = format!("{}/tests/pyc/bounded_formatting.3.{minor}.pyc", env!("CARGO_MANIFEST_DIR")); + let module = PycModule::from_bytes(&std::fs::read(path).unwrap()).unwrap(); + let version = module.version(); + for (name, expected) in [ + ("simple", "return f'{value}'"), + ("conversions", "return f'{label!r}: {value!s:>{width}}'"), + ("ascii_spec", "return f'{label!a}: {value:{width}}'"), + ] { + let code = nested_code(&module.code, name); + let instrs = crate::bytecode::decode_instructions(&code, table_for(version).unwrap(), version); + let start = instrs.iter().position(|ins| ins.op == Op::RESUME).unwrap() + 1; + let end = instrs.iter().position(|ins| ins.op == Op::RETURN_VALUE).unwrap(); + let ctx = context(&code, version, instrs); + let prefix = Rc::new(Expr::Name("prefix".into())); + let result = ctx.sim_stack_region(start, end, vec![prefix.clone()]).unwrap(); + assert_eq!(result.len(), 2, "3.{minor}: {name}"); + assert!(Rc::ptr_eq(&result[0], &prefix)); + let source = crate::codegen::generate(&[Stmt::Return(Some(result[1].clone()))], version, true); + assert_eq!(source.trim(), expected, "3.{minor}: {name}"); + } + } +} + +#[test] +fn bounded_formatting_rejects_invalid_conversion_parts_and_missing_operands() { + let module = fixture(); + let value = Rc::new(Expr::Name("value".into())); + for minor in 13..=15 { + let version = PythonVersion { minor, ..module.version() }; + for (op, arg, stack) in [ + (Op::CONVERT_VALUE, 1, vec![]), + (Op::CONVERT_VALUE, 0, vec![value.clone()]), + (Op::CONVERT_VALUE, 4, vec![value.clone()]), + (Op::FORMAT_SIMPLE, 0, vec![]), + (Op::FORMAT_WITH_SPEC, 0, vec![]), + (Op::FORMAT_WITH_SPEC, 0, vec![value.clone()]), + (Op::BUILD_STRING, 2, vec![value.clone()]), + (Op::BUILD_STRING, 1, vec![value.clone()]), + (Op::BUILD_STRING, 1, vec![Rc::new(Expr::Const(Rc::new(PyObject::Int(3))))]), + ] { + let ctx = context(&module.code, version, vec![instruction(op, arg)]); + assert!(ctx.sim_stack_region(0, 1, stack).is_none(), "3.{minor}: {op:?} {arg}"); + } + for (arg, conversion) in [(1, 's'), (2, 'r'), (3, 'a')] { + let ctx = context(&module.code, version, vec![instruction(Op::CONVERT_VALUE, arg), instruction(Op::FORMAT_SIMPLE, 0)]); + let result = ctx.sim_stack_region(0, 2, vec![value.clone()]).unwrap(); + assert_eq!(result.len(), 1); + let Expr::FString(text) = &*result[0] else { panic!("expected f-string"); }; + assert!(matches!(&text.parts[..], [FStringPart::Value { conversion: Some(c), format_spec: None, .. }] if *c == conversion)); + } + } +} + +#[test] +fn shared_raise_regions_require_exact_operands_and_no_statements() { + for minor in [14, 15] { + let path = format!("{}/tests/pyc/shared_validation_raises.3.{minor}.pyc", env!("CARGO_MANIFEST_DIR")); + let module = PycModule::from_bytes(&std::fs::read(path).unwrap()).unwrap(); + let version = module.version(); + for name in ["plain", "cause", "suppress"] { + let code = nested_code(&module.code, name); + let instrs = crate::bytecode::decode_instructions(&code, table_for(version).unwrap(), version); + let first = instrs.iter().position(|item| item.op == Op::POP_JUMP_IF_NONE).unwrap(); + let target = instrs[first].target.unwrap(); + let terminal = instrs.iter().position(|item| item.op == Op::RAISE_VARARGS).unwrap(); + let mut ctx = context(&code, version, instrs); + ctx.idx_of = ctx.instrs.iter().enumerate().map(|(i, x)| (x.offset, i)).collect(); + ctx.cur_offset = ctx.instrs[first].offset; + let cond = Rc::new(Expr::Compare { + operands: vec![Rc::new(Expr::Name("value".into())), Rc::new(Expr::Const(Rc::new(PyObject::None)))], + ops: vec![CmpOp::Is], + }); + let (_, body, _) = ctx.try_shared_rejection_guard(&cond, true, target).unwrap(); + assert!(matches!(&body[..], [Stmt::Raise { exc: Some(_), cause, .. }] if cause.is_some() == (name != "plain"))); + let original = ctx.instrs[terminal].arg; + for arg in [0, 3, if original == 1 { 2 } else { 1 }] { + ctx.instrs[terminal].arg = arg; + assert!(ctx.try_shared_rejection_guard(&cond, true, target).is_none(), "3.{minor}: {name}, arg {arg}"); + } + ctx.instrs[terminal].arg = original; + let start = ctx.idx_of[&target]; + let original = ctx.instrs[start].op; + for op in [Op::STORE_FAST, Op::POP_EXCEPT, Op::PUSH_EXC_INFO, Op::RERAISE] { + ctx.instrs[start].op = op; + assert!(ctx.try_shared_rejection_guard(&cond, true, target).is_none(), "3.{minor}: {name}, {op:?}"); + } + ctx.instrs[start].op = original; + ctx.stack.push(Sv::E(cond.clone())); + assert!(ctx.try_shared_rejection_guard(&cond, true, target).is_none()); + } + } +} diff --git a/tests/behavior/cases/b_await_context_controls.py b/tests/behavior/cases/b_await_context_controls.py new file mode 100644 index 00000000..af81b5dc --- /dev/null +++ b/tests/behavior/cases/b_await_context_controls.py @@ -0,0 +1,72 @@ +# MIN_VERSION: 3.12 +import asyncio + +async def sequential_contexts(manager, work, record, flag): + async with manager: + a = await work("a") + async with manager: + b = await work("b") + return a, b + + +async def awaited_then_context(manager, work, record, flag): + try: + a = await work("a") + async with manager: + b = await work("b") + except ValueError: + return "handled" + return a, b + + +async def outer_handler(manager, work, record, flag): + try: + async with manager: + a = await work("a") + b = await work("b") + except ValueError: + return "handled" + return a, b + + +async def exercise(function, failure, suppress): + events = [] + + async def work(label): + events.append(label) + await asyncio.sleep(0) + if failure == label: + raise ValueError(label) + return 1 if label == "a" else 2 + + def record(label): + events.append(label) + + class Manager: + async def __aenter__(self): + events.append("enter") + await asyncio.sleep(0) + if failure == "enter": + raise ValueError("enter") + return self + + async def __aexit__(self, typ, value, traceback): + events.append(("exit", typ.__name__ if typ else None)) + await asyncio.sleep(0) + if failure == "exit": + raise ValueError("exit") + return suppress + + try: + result = await function(Manager(), work, record, False) + print("return", result) + except BaseException as error: + print(type(error).__name__, str(error)) + print(events) + + +for function in (sequential_contexts, awaited_then_context, outer_handler): + for failure in ("", "a", "b", "cleanup", "enter", "exit"): + for suppress in (False, True): + print(function.__name__, failure, suppress) + asyncio.run(exercise(function, failure, suppress)) diff --git a/tests/behavior/cases/b_await_exception_regions.py b/tests/behavior/cases/b_await_exception_regions.py new file mode 100644 index 00000000..fea42d06 --- /dev/null +++ b/tests/behavior/cases/b_await_exception_regions.py @@ -0,0 +1,65 @@ +# MIN_VERSION: 3.14 +import asyncio + +async def awaited_finally(manager, work, record, flag): + async with manager: + try: + await work("a") + finally: + record("cleanup") + await work("b") + return "tail" + + +async def awaited_else(manager, work, record, flag): + async with manager: + try: + a = await work("a") + except ValueError: + record("handled") + else: + await work("b") + return "tail" + + +async def exercise(function, failure, suppress): + events = [] + + async def work(label): + events.append(label) + await asyncio.sleep(0) + if failure == label: + raise ValueError(label) + return 1 if label == "a" else 2 + + def record(label): + events.append(label) + + class Manager: + async def __aenter__(self): + events.append("enter") + await asyncio.sleep(0) + if failure == "enter": + raise ValueError("enter") + return self + + async def __aexit__(self, typ, value, traceback): + events.append(("exit", typ.__name__ if typ else None)) + await asyncio.sleep(0) + if failure == "exit": + raise ValueError("exit") + return suppress + + try: + result = await function(Manager(), work, record, False) + print("return", result) + except BaseException as error: + print(type(error).__name__, str(error)) + print(events) + + +for function in (awaited_finally, awaited_else): + for failure in ("", "a", "b", "cleanup", "enter", "exit"): + for suppress in (False, True): + print(function.__name__, failure, suppress) + asyncio.run(exercise(function, failure, suppress)) diff --git a/tests/behavior/cases/b_bounded_generator_expressions.py b/tests/behavior/cases/b_bounded_generator_expressions.py new file mode 100644 index 00000000..b601762d --- /dev/null +++ b/tests/behavior/cases/b_bounded_generator_expressions.py @@ -0,0 +1,38 @@ +# MIN_VERSION: 3.13 + +collect = tuple + +def plain(values): + return collect(value for value in values) + + +def captured(values, offset): + return collect(value + offset for value in values) + + +def multiple(values, first, second): + return collect((value + first) * second for value in values) + + +def ordinary_function(values): + return (lambda value: value)(values) + + +for values in ([], [1, 2, 3], ["x"]): + for function, args in ((plain, (values,)), (captured, (values, 5)), + (multiple, (values, 5, 3))): + try: + print(function.__name__, function(*args)) + except TypeError as error: + print(function.__name__, type(error).__name__) + + +def values(): + print("outer") + for value in (1, 2): + print("yield", value) + yield value + + +print(captured(values(), 5)) +print(ordinary_function("unchanged")) diff --git a/tests/behavior/cases/b_bounded_try_returns.py b/tests/behavior/cases/b_bounded_try_returns.py new file mode 100644 index 00000000..11f3c2b5 --- /dev/null +++ b/tests/behavior/cases/b_bounded_try_returns.py @@ -0,0 +1,59 @@ +# MIN_VERSION: 3.11 + +# Regenerate with: python tools/compile_bounded_try_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + return datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") + + +for value in ( + None, True, False, 0, 123456789, float("inf"), float("nan"), "invalid", + "2026-10-12T12:00:00", "2026-10-12T12:00:00Z", + datetime(2026, 10, 12), datetime(2026, 10, 12, tzinfo=timezone.utc), +): + print(parse_timestamp(value)) + + +def read(where): + print("read", where) + if where == "outside": + raise ValueError("outside the protected body") + + +for function in (unprotected_return, else_return): + try: + function(read) + except ValueError as error: + print(function.__name__, str(error)) diff --git a/tests/behavior/cases/b_carried_value_returns.py b/tests/behavior/cases/b_carried_value_returns.py new file mode 100644 index 00000000..13f67652 --- /dev/null +++ b/tests/behavior/cases/b_carried_value_returns.py @@ -0,0 +1,87 @@ +# MIN_VERSION: 3.11 +from types import SimpleNamespace + +# A computed return can be outside the protected range but still use its value. + + +def call_return(value, make, fallback, default): + try: + return make(value) + except Exception: + pass + return fallback.read().result or default.result + + +def attribute_return(value, make, fallback, default): + try: + return make(value).result + except Exception: + pass + return fallback.read().result or default.result + + +def tuple_return(value, make, fallback, default): + try: + return (make(value), 7) + except Exception: + pass + return fallback.read().result or default.result + + +def mapping_return(value, make, fallback, default): + try: + return {"value": make(value)} + except Exception: + pass + return fallback.read().result or default.result + + +def bare_return(value, make, fallback, default): + try: + make(value) + return None + except Exception: + return "handled" + return fallback.read().result or default.result + +for function in (call_return, attribute_return, tuple_return, mapping_return, bare_return): + for truth in (False, True): + for result in (None, 0, "success"): + for failure in ("", "truth", "attribute", "make", "base", "fallback"): + events = [] + + class Value: + def __bool__(self): + events.append("truth") + if failure == "truth": + raise ValueError("truth failed") + return truth + + class Result: + @property + def result(self): + events.append("attribute") + if failure == "attribute": + raise AttributeError("attribute failed") + return result + + class Fallback: + def read(self): + events.append("fallback") + if failure == "fallback": + raise RuntimeError("fallback failed") + return SimpleNamespace(result="fallback-result") + + def make(value): + events.append("make") + if failure == "make": + raise KeyError("make failed") + if failure == "base": + raise KeyboardInterrupt("owned interrupt") + return Result() if function is attribute_return else result + + try: + outcome = function(Value(), make, Fallback(), SimpleNamespace(result="default")) + print(function.__name__, truth, result, failure, outcome, events) + except BaseException as error: + print(function.__name__, truth, result, failure, type(error).__name__, str(error), events) diff --git a/tests/behavior/cases/b_comprehension_chained_ternaries.py b/tests/behavior/cases/b_comprehension_chained_ternaries.py new file mode 100644 index 00000000..1536345d --- /dev/null +++ b/tests/behavior/cases/b_comprehension_chained_ternaries.py @@ -0,0 +1,56 @@ +# MIN_VERSION: 3.12 +# This case exercises the inline-comprehension reconstruction path. + +def mapping_value(values): + return {value: 1 if 6 <= value <= 8 else 0 for value in values} + + +def mapping_key(values): + return {(value if 6 <= value <= 8 else -value): value for value in values} + + +def arithmetic(values): + return [0.6 + 0.4 * (1.0 if 4 <= value <= 8 < 10 else 0.3) for value in values] + + +def filtered(values): + return [1 if value >= 6 else 0 for value in values if value <= 8] + + +def observed(values, base, coefficient, lower, probe, upper): + return {value: base(value) + coefficient(value) * (1.0 if lower() <= probe(value) <= upper() else 0.3) for value in values} + + +for values in ([], list(range(24)), [0, 6, 8, 9, float("inf")]): + for function in (mapping_value, mapping_key, arithmetic, filtered): + print(function.__name__, function(values)) + +for failure in (None, "base", "coefficient", "lower", "probe", "upper"): + events = [] + + def record(label, value=None): + events.append((label, value)) + if label == failure: + raise ValueError("owned " + label) + return {"base": 0.6, "coefficient": 0.4, "lower": 4, "upper": 8, "probe": value}[label] + + def base(value): + return record("base", value) + + def coefficient(value): + return record("coefficient", value) + + def lower(): + return record("lower") + + def probe(value): + return record("probe", value) + + def upper(): + return record("upper") + + try: + print(failure, observed(range(10), base, coefficient, lower, probe, upper)) + except ValueError as error: + print(failure, str(error)) + print(events) diff --git a/tests/behavior/cases/b_constructed_rejection_guards.py b/tests/behavior/cases/b_constructed_rejection_guards.py new file mode 100644 index 00000000..86d4a2c1 --- /dev/null +++ b/tests/behavior/cases/b_constructed_rejection_guards.py @@ -0,0 +1,124 @@ +# MIN_VERSION: 3.14 +def constructed(value, build): + if value is None or not 0.0 <= value <= 20.0: + return build("reject", amount=0) + return build("accept", amount=value) + + +def truth_guard(value, build): + if not value or len(value) < 2: + return build("reject", amount=0) + return build("accept", amount=len(value)) + + +def positive_guard(value, invalid, build): + if invalid(value) or value < 2: + return build("reject", amount=value) + return build("accept", amount=value) + + +def constant_truth(value): + if not value or len(value) < 2: + return False + return True + + +def list_result(value): + if value is None or not 0.0 <= value <= 20.0: + return ["reject", value] + return ["accept", value] + + +def mapping_result(value): + if value is None or not 0.0 <= value <= 20.0: + return {"kind": "reject", "value": value} + return {"kind": "accept", "value": value} + + +def generator_result(value, rows, collect, scale): + if value is None or not 0.0 <= value <= 20.0: + return collect(scale * item for item in rows) + return collect(rows) + + +def stored_result(value, build): + if not value or len(value) < 2: + result = build("reject", amount=0) + return result + return build("accept", amount=len(value)) + + +def effectful_body(value, build, log): + if not value or len(value) < 2: + log("rejected") + return build("reject", amount=0) + return build("accept", amount=len(value)) + +# Run the observation driver unchanged. +exec(""" +trace = [] +fail = None +def build(kind, **options): + trace.append(('build', kind, options)) + if fail == 'build': raise RuntimeError('build') + return kind, options + +def log(message): + trace.append(('log', message)) + if fail == 'log': raise RuntimeError('log') + +def outcome(function, *args): + try: return 'return', function(*args) + except Exception as error: return 'raise', type(error).__name__, str(error) + +class Values: + def __init__(self, count): self.count = count + def __bool__(self): + trace.append('bool') + if fail == 'bool': raise ValueError('bool') + return self.count != 0 + def __len__(self): + trace.append('len') + if fail == 'len': raise ValueError('len') + return self.count + +for function in (constructed, list_result, mapping_result): + for value in (None, -1.0, 0.0, 10.0, 20.0, 21.0, float('nan')): + for fail in (None, 'build'): + trace.clear() + args = (value, build) if function is constructed else (value,) + print(function.__name__, repr(value), fail, outcome(function, *args), trace) +for function in (truth_guard, constant_truth, stored_result, effectful_body): + for count in (0, 1, 2, 3): + for fail in (None, 'bool', 'len', 'build', 'log'): + trace.clear() + args = (Values(count),) + if function is not constant_truth: args += (build,) + if function is effectful_body: args += (log,) + print(function.__name__, count, fail, outcome(function, *args), trace) +for invalid_result in (False, True): + for value in (0, 2, 3): + for fail in (None, 'invalid', 'build'): + trace.clear() + def invalid(value): + trace.append(('invalid', value)) + if fail == 'invalid': raise RuntimeError('invalid') + return invalid_result + print('positive', invalid_result, value, fail, outcome(positive_guard, value, invalid, build), trace) +class Rows: + def __iter__(self): + trace.append('iter') + if fail == 'iter': raise RuntimeError('iter') + for value in (2, 3): + trace.append(('next', value)) + yield value + trace.append('exhausted') +def collect(values): + trace.append('collect') + if fail == 'collect': raise RuntimeError('collect') + return list(values) +for value in (None, -1.0, 0.0, 20.0, 21.0, float('nan')): + for fail in (None, 'iter', 'collect'): + trace.clear() + print('generator', repr(value), fail, outcome(generator_result, value, Rows(), collect, 10), trace) +""") diff --git a/tests/behavior/cases/b_context_exit_accounting.py b/tests/behavior/cases/b_context_exit_accounting.py new file mode 100644 index 00000000..c9f43438 --- /dev/null +++ b/tests/behavior/cases/b_context_exit_accounting.py @@ -0,0 +1,73 @@ +# MIN_VERSION: 3.14 +import asyncio + +async def guarded_return(manager, work, record, flag): + async with manager: + if flag: + return "early" + try: + return await work("a") + finally: + record("cleanup") + + +async def computed_return(manager, work, record, flag): + async with manager: + try: + return await work("a") + finally: + record("cleanup") + + +async def none_guard(manager, work, record, flag): + async with manager: + if flag is None: + return 3 + try: + return await work("a") + finally: + record("cleanup") + + +async def exercise(function, failure, suppress, flag): + events = [] + + async def work(label): + events.append(label) + await asyncio.sleep(0) + if failure == label: + raise ValueError(label) + return 1 if label == "a" else 2 + + def record(label): + events.append(label) + + class Manager: + async def __aenter__(self): + events.append("enter") + await asyncio.sleep(0) + if failure == "enter": + raise ValueError("enter") + return self + + async def __aexit__(self, typ, value, traceback): + events.append(("exit", typ.__name__ if typ else None)) + await asyncio.sleep(0) + if failure == "exit": + raise ValueError("exit") + return suppress + + try: + result = await function(Manager(), work, record, flag) + print("return", result) + except BaseException as error: + print(type(error).__name__, str(error)) + print(events) + + +for function in (guarded_return, computed_return, none_guard): + for failure in ("", "a", "cleanup", "enter", "exit"): + for suppress in (False, True): + for flag in (False, True, None): + print(function.__name__, failure, suppress, flag) + asyncio.run(exercise(function, failure, suppress, flag)) diff --git a/tests/behavior/cases/b_context_manager_returns.py b/tests/behavior/cases/b_context_manager_returns.py new file mode 100644 index 00000000..9e9228e2 --- /dev/null +++ b/tests/behavior/cases/b_context_manager_returns.py @@ -0,0 +1,72 @@ +# MIN_VERSION: 3.9 +# Controls for the modern exit protocol; Python 3.8 async returns have a separate gap. +import asyncio + +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result + + +events = [] + + +class Manager: + def __repr__(self): + return "manager" + + def __enter__(self): + events.append("enter") + if failure == "enter": + raise ValueError("enter") + return self + + def __exit__(self, typ, value, traceback): + events.append(("exit", typ.__name__ if typ else None)) + if failure == "exit": + raise ValueError("exit") + return suppress + + async def __aenter__(self): + await asyncio.sleep(0) + return self.__enter__() + + async def __aexit__(self, typ, value, traceback): + await asyncio.sleep(0) + return self.__exit__(typ, value, traceback) + + @property + def result(self): + events.append("read") + if failure == "read": + raise ValueError("read") + return 7 + + +for failure in ("", "enter", "exit", "read"): + for suppress in (False, True): + for function in (synchronous, synchronous_attribute, asynchronous, asynchronous_attribute): + events.clear() + print(function.__name__, failure, suppress) + try: + result = function(Manager()) + if asyncio.iscoroutine(result): + result = asyncio.run(result) + print("return", result) + except ValueError as error: + print("error", str(error)) + print(events) diff --git a/tests/behavior/cases/b_filtered_generator_boolean_values.py b/tests/behavior/cases/b_filtered_generator_boolean_values.py new file mode 100644 index 00000000..44c53633 --- /dev/null +++ b/tests/behavior/cases/b_filtered_generator_boolean_values.py @@ -0,0 +1,60 @@ +# MIN_VERSION: 3.12 +# The modern statement route preserves filters and yielded Boolean values. + +def or_value(rows): + return tuple(row[1] or 0 for row in rows if row[0]) + + +def and_value(rows): + return tuple(row[1] and row[2] for row in rows if row[0]) + + +def chained_or(rows): + return tuple(row[1] or row[2] or 0 for row in rows if row[0]) + + +def mixed_value(rows): + return tuple((row[1] and row[2]) or 0 for row in rows if row[0]) + + +def none_filter(rows): + return tuple(row.get("amount") or 0.0 for row in rows if row.get("amount") is not None) + + +for rows in ([], [(False, 3, 4)], [(True, 0, 2)], + [(True, 2, 3), (True, None, 4), (False, 5, 6)], + [(True, "", 7), (True, "kept", 8)]): + for function in (or_value, and_value, chained_or, mixed_value): + print(function.__name__, function(rows)) + + +events = [] + + +class Value: + def __init__(self, tag, truth): + self.tag = tag + self.truth = truth + + def __bool__(self): + events.append(("bool", self.tag)) + return self.truth + + +class Row: + def __init__(self, tag, value): + self.tag = tag + self.value = value + + def get(self, key): + events.append(("get", self.tag, key)) + return self.value + + +rows = [Row("missing", None), Row("zero", 0), Row("false", Value("false", False)), + Row("true", Value("true", True))] +result = [] +for value in none_filter(rows): + result.append(value.tag if isinstance(value, Value) else value) +print(result) +print(events) diff --git a/tests/behavior/cases/b_for_loop_disjunction_legacy.py b/tests/behavior/cases/b_for_loop_disjunction_legacy.py new file mode 100644 index 00000000..fec52108 --- /dev/null +++ b/tests/behavior/cases/b_for_loop_disjunction_legacy.py @@ -0,0 +1,22 @@ +# MIN_VERSION: 3.8 +# MAX_VERSION: 3.9 + + +def select(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +for values in ([], [0.1, 0.3, 0.1], [None, 0.1, 0.3], ["dry", 0.3]): + events = [] + + def test(value): + events.append(value) + return isinstance(value, (int, float)) + + print(select(values, test), events) diff --git a/tests/behavior/cases/b_for_loop_disjunctions.py b/tests/behavior/cases/b_for_loop_disjunctions.py new file mode 100644 index 00000000..b8ad1ede --- /dev/null +++ b/tests/behavior/cases/b_for_loop_disjunctions.py @@ -0,0 +1,161 @@ +# MIN_VERSION: 3.10 + +def call_only(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + return result + + +def call_else(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def truth_only(values, test): + result = [] + for value in values: + if not value or value < 0.2: + result.append("dry") + return result + + +def truth_else(values, test): + result = [] + for value in values: + if not value or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def none_only(values, test): + result = [] + for value in values: + if value is None or value < 0.2: + result.append("dry") + return result + + +def none_else(values, test): + result = [] + for value in values: + if value is None or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def positive_only(values, test): + result = [] + for value in values: + if test(value) or value < 0.2: + result.append("dry") + return result + + +def positive_else(values, test): + result = [] + for value in values: + if test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def rotated_while(values, test): + result = [] + value = len(values) + while value < 2 or value == 3: + result.append(value) + value += 2 + return result + + +def break_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + break + else: + result.append(value) + else: + result.append('complete') + result.append('tail') + return result + + +def continue_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + continue + result.append(value) + return result + + +def extended_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + else: + result.append("wet") + return result + + +for function in ( + call_only, call_else, truth_only, truth_else, none_only, none_else, + positive_only, positive_else, rotated_while, break_loop, continue_loop, + extended_loop, +): + for values in ([], [0.1, 0.3, 0.1], [None, 0.1, 0.3], ["dry", 0.3]): + events = [] + + def test(value): + events.append(value) + return isinstance(value, (int, float)) + + try: + result = ("return", function(values, test)) + except TypeError as error: + result = ("error", type(error).__name__) + print(function.__name__, result, events) diff --git a/tests/behavior/cases/b_generator_statement_reconstruction.py b/tests/behavior/cases/b_generator_statement_reconstruction.py new file mode 100644 index 00000000..2145bac7 --- /dev/null +++ b/tests/behavior/cases/b_generator_statement_reconstruction.py @@ -0,0 +1,41 @@ +# MIN_VERSION: 3.12 +# Exercise the modern generator statement-parser route. + +def repeated_targets(values): + return tuple(value is not None for value, discarded, discarded, discarded in values) + + +def real_underscore(values): + return tuple((value, discarded, _) for value, _, discarded, discarded in values) + + +def nested_calls(rows): + return sorted(sum(value for value in row) / len(row) for row in rows) + + +def captured_cells(rows): + deltas = rows + factory = sum + return sorted(sum(factory(deltas) for _ in deltas) / len(deltas) for _ in range(3)) + + +for values in ([], [(None, 1, 2, 3)], [(1, 2, 3, 4), (None, 5, 6, 7)], [(1, 2)], [None]): + for function in (repeated_targets, real_underscore): + try: + print(function.__name__, function(values)) + except (TypeError, ValueError) as error: + print(function.__name__, type(error).__name__, str(error)) + +for rows in ([], [[1, 2, 3], [5, 9], [3]]): + print(nested_calls(rows)) +print(captured_cells([1, 2, 4])) + + +def observed_rows(): + print("outer called") + for value in (1, None, 3): + print("yield", value) + yield (value, 1, 2, 3) + + +print(repeated_targets(observed_rows())) diff --git a/tests/behavior/cases/b_generator_walrus_cells.py b/tests/behavior/cases/b_generator_walrus_cells.py new file mode 100644 index 00000000..a8202cf0 --- /dev/null +++ b/tests/behavior/cases/b_generator_walrus_cells.py @@ -0,0 +1,36 @@ +# MIN_VERSION: 3.12 +# Walrus syntax starts in 3.8; this tests the modern generator route. + +def select(rows): + return tuple(isinstance(row, dict) and (change := row.get("change")) is not None and change >= 0 for row in rows) + + +def last_binding(rows): + change = "initial" + results = tuple((change := row.get("change")) for row in rows) + return results, change + + +for rows in ([], [None], [{}], [{"change": None}], [{"change": -1}], + [{"change": 0}, {"change": 7}, {"change": -3}, None], + [{"change": "invalid"}]): + try: + print(select(rows)) + except TypeError as error: + print(type(error).__name__, str(error)) + +for rows in ([], [{}], [{"change": 1}, {"change": 7}], [{"change": None}]): + print(last_binding(rows)) + + +events = [] + + +class Row(dict): + def get(self, key): + events.append(key) + return dict.get(self, key) + + +print(select([None, Row(change=None), Row(change=0), Row(change=7)])) +print(events) diff --git a/tests/behavior/cases/b_handler_iterator_breaks.py b/tests/behavior/cases/b_handler_iterator_breaks.py new file mode 100644 index 00000000..f69f7d2e --- /dev/null +++ b/tests/behavior/cases/b_handler_iterator_breaks.py @@ -0,0 +1,217 @@ +# MIN_VERSION: 3.12 +import asyncio + + +async def named_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +def sync_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + break + except Exception as error: + owner.log("unavailable", str(error)) + break + owner.log("finished", accepted) + return accepted + + +async def direct_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +async def unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + +async def direct_unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + +class Incompatible(ValueError): + pass + +class Owner: + + def __init__(self): + self.candidates = [] if case == 'empty' else ['first', 'second'] + self.can_close = case != 'no-close' + self.events = [] + + def check(self, value): + self.events.append(('check', value)) + if value == 'first': + failures = {'incompatible': Incompatible, 'no-close': Incompatible, 'close-failed': Incompatible, 'terminal': Incompatible, 'log-error': Incompatible, 'invalid': ValueError, 'io': OSError, 'timeout': asyncio.TimeoutError, 'unexpected': LookupError, 'cancel': asyncio.CancelledError} + if case in failures: + raise failures[case]('owned fault') + + def reconsider(self, candidate): + self.events.append(('reconsider', candidate)) + return True + + def close(self, candidate): + self.events.append(('close', candidate)) + if case == 'close-failed': + raise RuntimeError('owned close') + return case != 'terminal' + + def log(self, *values): + self.events.append(('log', *values)) + if case == 'log-error' and values[0] == 'incompatible': + raise KeyError('owned log') + if case == 'finish-error' and values[0] == 'finished': + raise RuntimeError('owned finish') + +class AsyncOwner(Owner): + async def check(self, value): + await asyncio.sleep(0) + return super().check(value) + + async def reconsider(self, candidate): + await asyncio.sleep(0) + return super().reconsider(candidate) + + async def close(self, candidate): + await asyncio.sleep(0) + return super().close(candidate) + + async def log(self, *values): + await asyncio.sleep(0) + return super().log(*values) + + +from types import SimpleNamespace +faults = SimpleNamespace(incompatible=Incompatible, invalid=ValueError, + operating=OSError, timeout=asyncio.TimeoutError, ordinary=Exception) +functions = [ + (named_break, True, False), + (sync_break, False, False), + (direct_break, True, False), + (unnamed_break, True, False), + (direct_unnamed_break, True, False), +] +for function, asynchronous, local in functions: + for case in ("normal", "empty", "incompatible", "no-close", "close-failed", "terminal", "invalid", "io", "timeout", "unexpected", "cancel", "log-error", "finish-error"): + owner = AsyncOwner() if asynchronous else Owner() + print(function.__name__, case) + try: + result = function(owner, faults) if local else function(owner) + if asynchronous: + result = asyncio.run(result) + print("return", result) + except BaseException as error: + print("error", type(error).__name__, str(error)) + print(owner.events) diff --git a/tests/behavior/cases/b_loop_context_returns.py b/tests/behavior/cases/b_loop_context_returns.py new file mode 100644 index 00000000..cc2db758 --- /dev/null +++ b/tests/behavior/cases/b_loop_context_returns.py @@ -0,0 +1,117 @@ +# MIN_VERSION: 3.9 +import asyncio + +def sync_value(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +def sync_length(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +def sync_mapping(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + +async def async_value(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +async def async_length(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +async def async_mapping(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + +events = [] + +class Lock: + def __enter__(self): + events.append("enter") + if mode == "open_error": + raise RuntimeError("enter") + return self + + def __exit__(self, kind, value, traceback): + events.append(("exit", kind.__name__ if kind else None)) + if mode == "exit_error": + raise RuntimeError("exit") + return mode == "suppressed" + + async def __aenter__(self): + await asyncio.sleep(0) + return self.__enter__() + + async def __aexit__(self, kind, value, traceback): + await asyncio.sleep(0) + return self.__exit__(kind, value, traceback) + + +def parse(value): + events.append(("parse", value)) + if mode == "rejected" and value <= 1: + raise ValueError("row") + if mode in ("failed", "suppressed"): + raise RuntimeError("body") + if mode == "cancelled": + raise asyncio.CancelledError("cancelled") + return value * 2 + + +for rows in ([], [1, 2], [-1, 0, 3]): + for mode in ("normal", "rejected", "failed", "cancelled", "open_error", "exit_error", "suppressed"): + for function in (sync_value, sync_length, sync_mapping, async_value, async_length, async_mapping): + events.clear() + print(function.__name__, rows, mode) + try: + result = function(Lock(), rows, parse) + if asyncio.iscoroutine(result): + result = asyncio.run(result) + print("return", result) + except (RuntimeError, asyncio.CancelledError) as error: + print(type(error).__name__, str(error)) + print(events) diff --git a/tests/behavior/cases/b_loop_handler_ownership.py b/tests/behavior/cases/b_loop_handler_ownership.py new file mode 100644 index 00000000..c629bd7a --- /dev/null +++ b/tests/behavior/cases/b_loop_handler_ownership.py @@ -0,0 +1,184 @@ +# MIN_VERSION: 3.12 +import asyncio + +def named(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def multiple(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + except KeyError as error: + owner.other(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def siblings(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + try: + owner.second(row) + except KeyError: + owner.other(row) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def same_type(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except Exception: + owner.failed(row) + owner.finish() + except Exception as error: + owner.fallback(str(error)) + return None + + +async def async_0(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_1(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_2(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + await owner.asecond(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +def outer_finally(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + finally: + owner.finish() + return None + + +def nonloop(owner): + try: + try: + owner.record(owner.row) + except ValueError: + owner.failed(owner.row) + except RuntimeError: + owner.fallback() + return None + + +class Owner: + def __init__(self): + self.rows = [] if case == "empty" else [1, 2, 3] + self.events = [] + self.row = 2 + + def record(self, row): + self.events.append(("record", row)) + if case in ("row-error", "handler-error") and row == 2: + raise ValueError("row") + if case == "key-error" and row == 2: + raise KeyError("key") + if case == "runtime-error": + raise RuntimeError("runtime") + if case == "cancel": + raise asyncio.CancelledError("cancelled") + + async def arecord(self, row): + await asyncio.sleep(0) + self.record(row) + + def second(self, row): + self.events.append(("second", row)) + if case == "second-error" and row == 2: + raise KeyError("second") + + async def asecond(self, row): + await asyncio.sleep(0) + self.second(row) + + def failed(self, row, *details): + self.events.append(("failed", row, details)) + if case == "handler-error": + raise RuntimeError("handler") + + def other(self, row, *details): + self.events.append(("other", row, details)) + + def finish(self): + self.events.append("finish") + if case == "finish-error": + raise RuntimeError("finish") + + def fallback(self, *details): + self.events.append(("fallback", details)) + + +functions = (named, multiple, siblings, same_type, async_0, async_1, async_2, outer_finally, nonloop) +for function in functions: + for case in ("normal", "empty", "row-error", "key-error", "second-error", "handler-error", "finish-error", "runtime-error", "cancel"): + owner = Owner() + print(function.__name__, case) + try: + result = function(owner) + if asyncio.iscoroutine(result): + result = asyncio.run(result) + print("return", result) + except BaseException as error: + print("error", type(error).__name__, str(error)) + print(owner.events) diff --git a/tests/behavior/cases/b_nested_context_returns.py b/tests/behavior/cases/b_nested_context_returns.py new file mode 100644 index 00000000..544de2cb --- /dev/null +++ b/tests/behavior/cases/b_nested_context_returns.py @@ -0,0 +1,124 @@ +# MIN_VERSION: 3.11 +import asyncio + +def sync_call(contexts, value): + with contexts[0]: + with contexts[1]: + return value() + + +def sync_mapping(contexts, value): + with contexts[0]: + with contexts[1]: + with contexts[2]: + return {"rows": value()} + + +async def async_call(contexts, value): + async with contexts[0]: + async with contexts[1]: + return value() + + +async def async_await(contexts, value): + async with contexts[0]: + async with contexts[1]: + async with contexts[2]: + return await value() + + +async def async_tuple(contexts, value): + async with contexts[0]: + async with contexts[1]: + return (value(),) + + +async def async_single(contexts, value): + async with contexts[0]: + return await value() + + +def sync_outer_return(contexts, value): + with contexts[0]: + with contexts[1]: + result = value() + return result + + + +async def async_outer_return(contexts, value): + async with contexts[0]: + async with contexts[1]: + result = await value() + return result + + +events = [] +opened = set() + +class Context: + def __init__(self, index): + self.index = index + + def __enter__(self): + events.append(("enter", self.index)) + if failure == "enter-" + str(self.index): + raise ValueError("entry") + opened.add(self.index) + return self + + def __exit__(self, kind, error, traceback): + events.append(("exit", self.index, kind.__name__ if kind else None)) + opened.remove(self.index) + if failure == "exit-" + str(self.index): + raise ValueError("exit") + return self.index == suppression and kind is ValueError + + async def __aenter__(self): + await asyncio.sleep(0) + return self.__enter__() + + async def __aexit__(self, kind, error, traceback): + await asyncio.sleep(0) + return self.__exit__(kind, error, traceback) + + +def value(): + events.append(("value", tuple(sorted(opened)))) + if failure == "body": + raise ValueError("body") + if failure == "cancelled": + raise asyncio.CancelledError("cancelled") + return 7 + + +async def async_value(): + await asyncio.sleep(0) + return value() + + +cases = [ + (sync_call, 2, False), + (sync_mapping, 3, False), + (async_call, 2, False), + (async_await, 3, True), + (async_tuple, 2, False), + (async_single, 1, True), + (sync_outer_return, 2, False), + (async_outer_return, 2, True), +] +for function, depth, isawait in cases: + for failure in ("", "body", "cancelled", "enter-0", "enter-1", "enter-2", "exit-0", "exit-1", "exit-2"): + for suppression in (-1, 0, 1, 2): + events.clear() + opened.clear() + print(function.__name__, failure, suppression) + contexts = [Context(index) for index in range(depth)] + try: + result = function(contexts, async_value if isawait else value) + if asyncio.iscoroutine(result): + result = asyncio.run(result) + print("return", result) + except (ValueError, asyncio.CancelledError, UnboundLocalError) as error: + print(type(error).__name__, str(error)) + print(events, tuple(sorted(opened))) diff --git a/tests/behavior/cases/b_none_rejection_guards.py b/tests/behavior/cases/b_none_rejection_guards.py new file mode 100644 index 00000000..a0ee0677 --- /dev/null +++ b/tests/behavior/cases/b_none_rejection_guards.py @@ -0,0 +1,66 @@ +# MIN_VERSION: 3.14 +def in_range(value): + if value is None or not 0.0 <= value <= 15.0: + return False + return True + + +def called(read): + if read is None or not 0.0 <= read() <= 15.0: + return False + return True + + +def multi(first, second): + if first is None or not 0.0 <= first() <= second() <= 15.0: + return False + return True + + +def separate(read): + if read is None or not (0.0 <= read() and read() <= 15.0): + return False + return True + + +def same_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return False + return False + + +def integer_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return 3 + return 5 + + +def string_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return "reject" + return "accept" + + +def collect(rows): + result = [] + for row in rows: + a = row.get("a") + b = row.get("b") + c = row.get("c") + d = row.get("d") + if a is None or b is None or c is None or d is None or d < 0.01: + continue + result.append(a + b + c + d) + return result + + +def unpacked(rows): + result = [] + for a, b, c, d in rows: + if a is None or b is None or c is None or d is None or d < 0.01: + continue + result.append(a + b + c + d) + return result + +# Keep the driver out of the source being reconstructed. +exec("trace = []\ndef outcome(function, *args):\n try:\n return ('return', function(*args))\n except Exception as error:\n return ('raise', type(error).__name__, str(error))\n\nclass Number:\n def __init__(self, label, value, fail=None):\n self.label, self.value, self.fail = label, value, fail\n def compare(self, op, other):\n trace.append((self.label, op, getattr(other, 'label', other)))\n if self.fail == op:\n raise RuntimeError(self.label + ':' + op)\n other = getattr(other, 'value', other)\n return self.value <= other if op == 'le' else self.value >= other\n def __le__(self, other): return self.compare('le', other)\n def __ge__(self, other): return self.compare('ge', other)\n def __lt__(self, other): raise AssertionError('rewritten lt')\n def __gt__(self, other): raise AssertionError('rewritten gt')\n def __bool__(self): raise AssertionError('unexpected truth conversion')\n\ndef reader(label, values):\n pending = iter(values)\n def read():\n trace.append(label)\n result = next(pending)\n if isinstance(result, Exception): raise result\n return result\n return read\n\nfor function in (in_range, same_tail, integer_tail, string_tail):\n for value in (None, -1.0, 0.0, 10.0, 15.0, 16.0, float('nan'), float('inf')):\n print(function.__name__, repr(value), outcome(function, value))\n for value in (-1.0, 3.0, 17.0, float('nan')):\n for fail in (None, 'ge', 'le'):\n trace.clear()\n result = outcome(function, Number('value', value, fail))\n print(function.__name__, repr(value), fail, result, trace)\nfor function in (called, separate):\n print(function.__name__, 'none', outcome(function, None))\n for values in ((1.0, 16.0), (-1.0, 5.0), (float('nan'), 2.0), (2.0, float('nan')), (ValueError('first'),), (2.0, ValueError('second'))):\n trace.clear()\n result = outcome(function, reader('read', values))\n print(function.__name__, repr(values), result, trace)\nfor a, b in ((1.0, 2.0), (-1.0, 2.0), (3.0, 2.0), (2.0, 16.0), (float('nan'), 2.0), (2.0, float('nan')), (ValueError('first'), 2.0), (2.0, ValueError('second'))):\n trace.clear()\n result = outcome(multi, reader('first', (a,)), reader('second', (b,)))\n print('multi', repr((a,b)), result, trace)\ntrace.clear()\nprint('multi-none', outcome(multi, None, reader('unused', (1,))), trace)\nclass Row(dict):\n def get(self, key):\n trace.append(key)\n return super().get(key)\nrows = [(1,2,3,4), (1,2,3,0), (None,2,3,4), (1,None,3,4), (1,2,None,4), (1,2,3,None), (4,5,6,7)]\nfor values in ([], rows, rows[::-1]):\n trace.clear()\n mapped = [Row(zip('abcd', value)) for value in values]\n print('collect', outcome(collect, mapped), trace)\n print('unpacked', outcome(unpacked, values))\n") diff --git a/tests/behavior/cases/b_none_return_sibling_branches.py b/tests/behavior/cases/b_none_return_sibling_branches.py new file mode 100644 index 00000000..a244a81c --- /dev/null +++ b/tests/behavior/cases/b_none_return_sibling_branches.py @@ -0,0 +1,60 @@ +# MIN_VERSION: 3.11 + +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + stamp = datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + return stamp + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") + + +for value in ( + None, True, False, 0, 123456789, float("inf"), float("nan"), "invalid", + "2026-10-12T12:00:00", "2026-10-12T12:00:00Z", + datetime(2026, 10, 12), datetime(2026, 10, 12, tzinfo=timezone.utc), +): + print(parse_timestamp(value)) + + +def read(where): + print("read", where) + if where == "outside": + raise ValueError("outside the protected body") + + +for function in (unprotected_return, else_return): + try: + function(read) + except ValueError as error: + print(function.__name__, str(error)) diff --git a/tests/behavior/cases/b_normal_async_exits.py b/tests/behavior/cases/b_normal_async_exits.py new file mode 100644 index 00000000..8998d35e --- /dev/null +++ b/tests/behavior/cases/b_normal_async_exits.py @@ -0,0 +1,189 @@ +# MIN_VERSION: 3.14 +import asyncio + +async def normal_1(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + value = owner.build(row0, row0) + return {"value": value, "active": owner.active} + + +async def normal_2(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + value = owner.build(row0, row1) + return {"value": value, "active": owner.active} + + +async def normal_4(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + async with owner.query("row2") as query: + row2 = await query.read() + async with owner.query("row3") as query: + row3 = await query.read() + value = owner.build(row0, row3) + return {"value": value, "active": owner.active} + + +async def constant_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return 4 + + +async def constant_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return 4 + + +async def carried_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return row0 + + +async def computed_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return owner.build(row0, row0) + + +async def mapping_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return {"value": row0} + + +async def user_await(owner): + async with owner: + await owner.user(None, None, None) + return owner.build(1, 2) + + +async def branch_return(owner): + async with owner: + if owner.ready: + return 4 + await owner.write(1, 2) + return 2 + + +class Owner: + active = False + + def __init__(self): + self.events = [] + self.ready = ready + + async def __aenter__(self): + await asyncio.sleep(0) + self.events.append('outer-enter') + if fault == 'outer-enter': + raise ValueError('owned outer enter') + self.active = True + return self + + async def __aexit__(self, kind, value, trace): + await asyncio.sleep(0) + self.events.append(('outer-exit', None if kind is None else kind.__name__)) + self.active = False + if fault == 'outer-exit': + raise ValueError('owned outer exit') + return suppression == 'outer' + + def query(self, name): + self.events.append(('query', name)) + return Query(self, name) + + def build(self, first, last): + self.events.append(('build', self.active, first, last)) + if fault == 'build': + raise ValueError('owned build') + if fault == 'build-cancel': + raise asyncio.CancelledError('owned build cancellation') + return first + last + + async def user(self, *values): + await asyncio.sleep(0) + self.events.append(('user', self.active, values)) + if fault == 'read': + raise ValueError('owned user') + if fault == 'read-cancel': + raise asyncio.CancelledError('owned user cancellation') + + async def write(self, first, last): + await asyncio.sleep(0) + await self.user(first, last) + +class Query: + + def __init__(self, owner, name): + self.owner, self.name = (owner, name) + self.selected = name == f'row{count // 2}' + + async def __aenter__(self): + await asyncio.sleep(0) + self.owner.events.append(('query-enter', self.name, self.owner.active)) + if self.selected and fault == 'query-enter': + raise ValueError('owned query enter') + return self + + async def read(self): + await asyncio.sleep(0) + self.owner.events.append(('read', self.name, self.owner.active)) + if self.selected and fault == 'read': + raise ValueError('owned read') + if self.selected and fault == 'read-cancel': + raise asyncio.CancelledError('owned read cancellation') + return int(self.name[3:]) + + async def __aexit__(self, kind, value, trace): + await asyncio.sleep(0) + self.owner.events.append(('query-exit', self.name, None if kind is None else kind.__name__, self.owner.active)) + if self.selected and fault == 'query-exit': + raise ValueError('owned query exit') + return suppression == 'query' + +functions = [ + (normal_1, 1), + (normal_2, 2), + (normal_4, 4), + (constant_one, 1), + (constant_two, 2), + (carried_one, 1), + (computed_two, 2), + (mapping_two, 2), + (user_await, 1), + (branch_return, 1), +] +for function, count in functions: + for ready in (False, True): + for fault in ("", "outer-enter", "outer-exit", "query-enter", "query-exit", "read", "read-cancel", "build", "build-cancel"): + for suppression in ("", "outer", "query"): + owner = Owner() + print(function.__name__, ready, fault, suppression) + try: + print("return", asyncio.run(function(owner))) + except BaseException as error: + print("error", type(error).__name__, str(error)) + print(owner.events, owner.active) diff --git a/tests/behavior/cases/b_private_handler_clauses.py b/tests/behavior/cases/b_private_handler_clauses.py new file mode 100644 index 00000000..a2129022 --- /dev/null +++ b/tests/behavior/cases/b_private_handler_clauses.py @@ -0,0 +1,241 @@ +# MIN_VERSION: 3.12 +import asyncio + + +async def named(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +def synchronous(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + continue + except Exception as error: + owner.log("unavailable", str(error)) + continue + owner.log("finished", accepted) + return accepted + + +async def attributes(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def local_attributes(owner, faults): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def single_clause(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + await owner.log("finished", accepted) + return accepted + + +async def no_private_try(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +class Incompatible(ValueError): + pass + +class Owner: + + def __init__(self): + self.candidates = [] if case == 'empty' else ['first', 'second'] + self.can_close = case != 'no-close' + self.events = [] + + def check(self, value): + self.events.append(('check', value)) + if value == 'first': + failures = {'incompatible': Incompatible, 'no-close': Incompatible, 'close-failed': Incompatible, 'terminal': Incompatible, 'log-error': Incompatible, 'invalid': ValueError, 'io': OSError, 'timeout': asyncio.TimeoutError, 'unexpected': LookupError, 'cancel': asyncio.CancelledError} + if case in failures: + raise failures[case]('owned fault') + + def reconsider(self, candidate): + self.events.append(('reconsider', candidate)) + return True + + def close(self, candidate): + self.events.append(('close', candidate)) + if case == 'close-failed': + raise RuntimeError('owned close') + return case != 'terminal' + + def log(self, *values): + self.events.append(('log', *values)) + if case == 'log-error' and values[0] == 'incompatible': + raise KeyError('owned log') + if case == 'finish-error' and values[0] == 'finished': + raise RuntimeError('owned finish') + +class AsyncOwner(Owner): + async def check(self, value): + await asyncio.sleep(0) + return super().check(value) + + async def reconsider(self, candidate): + await asyncio.sleep(0) + return super().reconsider(candidate) + + async def close(self, candidate): + await asyncio.sleep(0) + return super().close(candidate) + + async def log(self, *values): + await asyncio.sleep(0) + return super().log(*values) + + +from types import SimpleNamespace +faults = SimpleNamespace(incompatible=Incompatible, invalid=ValueError, + operating=OSError, timeout=asyncio.TimeoutError, ordinary=Exception) +functions = [ + (named, True, False), + (synchronous, False, False), + (attributes, True, False), + (local_attributes, True, True), + (single_clause, True, False), + (no_private_try, True, False), +] +for function, asynchronous, local in functions: + for case in ("normal", "empty", "incompatible", "no-close", "close-failed", "terminal", "invalid", "io", "timeout", "unexpected", "cancel", "log-error", "finish-error"): + owner = AsyncOwner() if asynchronous else Owner() + print(function.__name__, case) + try: + result = function(owner, faults) if local else function(owner) + if asynchronous: + result = asyncio.run(result) + print("return", result) + except BaseException as error: + print("error", type(error).__name__, str(error)) + print(owner.events) diff --git a/tests/behavior/cases/b_shared_validation_raises.py b/tests/behavior/cases/b_shared_validation_raises.py new file mode 100644 index 00000000..9b34d3c5 --- /dev/null +++ b/tests/behavior/cases/b_shared_validation_raises.py @@ -0,0 +1,166 @@ +# MIN_VERSION: 3.14 +def plain(value, bad, ready, make_error): + if value is None or bad(value) or not ready(value): + raise make_error("invalid", value=value) + return value + + +def chained(value, read, invalid, make_error): + if value is None or not 0 <= read(value) <= 10 or invalid(value): + raise make_error("range failed") + return value + + +def cause(value, reject, make_error, make_cause): + if value is None or reject(value): + raise make_error("invalid", value=value) from make_cause(value) + return value + + +def suppress(value, reject, make_error): + if value is None or reject(value): + raise make_error("invalid", value=value) from None + return value + + +def formatted(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") from make_cause() + return value + + +def ascii_spec(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!a}: {value:{width}}") from make_cause() + return value + + +def formatted_plain(value, label, width, read, reject, make_error): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") + return value + + +def constant_error(value, reject): + if value is None or reject(value): + raise ValueError + return value + +# Run the observation driver unchanged. +exec(""" +from itertools import product + +def outcome(function, *args): + try: + # Retain an active exception to check implicit context and suppression. + try: + raise LookupError('outer context') + except LookupError: + value = function(*args) + return ('value', 'original object' if value is args[0] else value) + except Exception as error: + cause = error.__cause__ + context = error.__context__ + return (type(error).__name__, str(error), + None if cause is None else (type(cause).__name__, str(cause)), + None if context is None else (type(context).__name__, str(context)), + error.__suppress_context__) + +trace = [] +for value, bad_value, ready_value, failure in product((None, 0, 7), (False, True), (False, True), ('', 'bad', 'ready', 'constructor')): + trace.clear() + def bad(item): + trace.append(('bad', item)) + if failure == 'bad': raise KeyError('bad failed') + return bad_value + def ready(item): + trace.append(('ready', item)) + if failure == 'ready': raise KeyError('ready failed') + return ready_value + def make_error(message, **keywords): + trace.append(('error', message, keywords)) + if failure == 'constructor': raise TypeError('constructor failed') + return ValueError(message) + print('plain', value, bad_value, ready_value, failure, outcome(plain, value, bad, ready, make_error), trace) + +for value, number, invalid_value, failure in product((None, 'value'), (-1, 0, 5, 10, 11, float('nan')), (False, True), ('', 'read', 'ge', 'le', 'invalid')): + trace.clear() + class Number: + def __ge__(self, other): + trace.append(('ge', other)) + if failure == 'ge': raise TypeError('ge failed') + return number >= other + def __le__(self, other): + trace.append(('le', other)) + if failure == 'le': raise TypeError('le failed') + return number <= other + def __lt__(self, other): raise AssertionError('inverted comparison') + def __gt__(self, other): raise AssertionError('inverted comparison') + def read(item): + trace.append(('read', item)) + if failure == 'read': raise TypeError('read failed') + return Number() + def invalid(item): + trace.append(('invalid', item)) + if failure == 'invalid': raise TypeError('invalid failed') + return invalid_value + def make_error(message): + trace.append(('error', message)) + return ValueError(message) + print('chain', value, repr(number), invalid_value, failure, outcome(chained, value, read, invalid, make_error), trace) + +for function, value, rejects, cause_mode in product((cause, suppress, constant_error), (None, 0, 7), (False, True), ('none', 'exception', 'class', 'invalid', 'failure')): + trace.clear() + def reject(item): + trace.append(('reject', item)); return rejects + def make_error(message, **keywords): + trace.append(('error', message, keywords)); return ValueError(message) + def make_cause(item): + trace.append(('cause', item)) + if cause_mode == 'failure': raise KeyError('cause factory failed') + if cause_mode == 'invalid': return 'invalid cause' + if cause_mode == 'class': return RuntimeError + return RuntimeError('original cause') if cause_mode == 'exception' else None + args = (value, reject) + if function is not constant_error: args += (make_error,) + if function is cause: args += (make_cause,) + print(function.__name__, value, rejects, cause_mode, outcome(function, *args), trace) + +for function, number, rejects, failure in product((formatted, ascii_spec, formatted_plain), (None, 5, 11), (False, True), ('', 'repr', 'str', 'format', 'width', 'constructor', 'cause')): + trace.clear() + class Label: + def __repr__(self): + trace.append('repr') + if failure == 'repr': raise TypeError('repr failed') + return 'label-α' + class Value: + def __str__(self): + trace.append('str') + if failure == 'str': raise TypeError('str failed') + return 'value' + def __format__(self, spec): + trace.append(('format', spec)) + if failure == 'format': raise TypeError('format failed') + return 'formatted-' + spec + class Width: + def __format__(self, spec): + trace.append(('width', spec)) + if failure == 'width': raise TypeError('width failed') + return '6' + def read(value): + trace.append('read'); return number + def reject(value): + trace.append('reject'); return rejects + def make_error(message): + trace.append(('error', message)) + if failure == 'constructor': raise ValueError('constructor failed') + return ValueError(message) + def make_cause(): + trace.append('cause') + if failure == 'cause': raise KeyError('cause failed') + return RuntimeError('cause') + value = None if number is None else Value() + args = (value, Label(), Width(), read, reject, make_error) + if function is not formatted_plain: args += (make_cause,) + print(function.__name__, number, rejects, failure, outcome(function, *args), trace) +""") diff --git a/tests/behavior/cases/b_sunk_context_exits.py b/tests/behavior/cases/b_sunk_context_exits.py new file mode 100644 index 00000000..fc179ff4 --- /dev/null +++ b/tests/behavior/cases/b_sunk_context_exits.py @@ -0,0 +1,100 @@ +# MIN_VERSION: 3.14 +import asyncio + +async def false(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return False + record("outside") + return True + + +async def bare(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return + record("outside") + return True + + +async def twice(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return False + if release: + return False + record("outside") + return True + + +async def value(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return release + record("outside") + return True + + +async def user_await(lock, pending, release, user_call): + async with lock: + record("body") + await user_call(None, None, None) + record("outside") + return True + + +async def normal(lock, pending, release, user_call): + async with lock: + record("body") + record("ready") + record("outside") + return True + + +events = [] + +class Lock: + async def __aenter__(self): + await asyncio.sleep(0) + events.append("enter") + if mode == "enter_error": + raise RuntimeError("enter") + return self + + async def __aexit__(self, kind, value, traceback): + await asyncio.sleep(0) + events.append(("exit", kind.__name__ if kind else None)) + if mode == "exit_error": + raise RuntimeError("exit") + return mode == "suppressed" + + +def record(value): + events.append(value) + if value == "body" and mode in ("body_error", "suppressed"): + raise ValueError("body") + if value == "body" and mode == "cancelled": + raise asyncio.CancelledError("cancelled") + + +async def user_call(*args): + await asyncio.sleep(0) + events.append(("user_call", args)) + if mode == "user_error": + raise RuntimeError("user") + + +for pending, release in ((False, False), (True, False), (False, True), (True, True)): + for mode in ("normal", "body_error", "suppressed", "cancelled", "enter_error", "exit_error", "user_error"): + for function in (false, bare, twice, value, user_await, normal): + events.clear() + print(function.__name__, pending, release, mode) + try: + print("return", asyncio.run(function(Lock(), pending, release, user_call))) + except (ValueError, RuntimeError, asyncio.CancelledError) as error: + print(type(error).__name__, str(error)) + print(events) diff --git a/tests/corpus/3.10.19/context_manager_returns.py b/tests/corpus/3.10.19/context_manager_returns.py new file mode 100644 index 00000000..02534690 --- /dev/null +++ b/tests/corpus/3.10.19/context_manager_returns.py @@ -0,0 +1,18 @@ +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result diff --git a/tests/corpus/3.10.19/context_manager_returns.pyc b/tests/corpus/3.10.19/context_manager_returns.pyc new file mode 100644 index 00000000..7c4af4f7 Binary files /dev/null and b/tests/corpus/3.10.19/context_manager_returns.pyc differ diff --git a/tests/corpus/3.10.19/loop_context_returns.py b/tests/corpus/3.10.19/loop_context_returns.py new file mode 100644 index 00000000..703d904c --- /dev/null +++ b/tests/corpus/3.10.19/loop_context_returns.py @@ -0,0 +1,64 @@ +def sync_value(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +def sync_length(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +def sync_mapping(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + +async def async_value(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +async def async_length(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +async def async_mapping(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} diff --git a/tests/corpus/3.10.19/loop_context_returns.pyc b/tests/corpus/3.10.19/loop_context_returns.pyc new file mode 100644 index 00000000..a7fcf9ac Binary files /dev/null and b/tests/corpus/3.10.19/loop_context_returns.pyc differ diff --git a/tests/corpus/3.11.14/bounded_try_returns_3.py b/tests/corpus/3.11.14/bounded_try_returns_3.py new file mode 100644 index 00000000..b1b14365 --- /dev/null +++ b/tests/corpus/3.11.14/bounded_try_returns_3.py @@ -0,0 +1,36 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + return datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.11.14/bounded_try_returns_3.pyc b/tests/corpus/3.11.14/bounded_try_returns_3.pyc new file mode 100644 index 00000000..b1bbf9f1 Binary files /dev/null and b/tests/corpus/3.11.14/bounded_try_returns_3.pyc differ diff --git a/tests/corpus/3.11.14/carried_value_returns_3.py b/tests/corpus/3.11.14/carried_value_returns_3.py new file mode 100644 index 00000000..7c0f657e --- /dev/null +++ b/tests/corpus/3.11.14/carried_value_returns_3.py @@ -0,0 +1,47 @@ +# A computed return can be outside the protected range but still use its value. + + +def call_return(value, make, fallback, default): + if value: + try: + return make(value) + except Exception: + pass + return fallback.read().result or default.result + + +def attribute_return(value, make, fallback, default): + if value: + try: + return make(value).result + except Exception: + pass + return fallback.read().result or default.result + + +def tuple_return(value, make, fallback, default): + if value: + try: + return (make(value), 7) + except Exception: + pass + return fallback.read().result or default.result + + +def mapping_return(value, make, fallback, default): + if value: + try: + return {"value": make(value)} + except Exception: + pass + return fallback.read().result or default.result + + +def bare_return(value, make, fallback, default): + if value: + try: + make(value) + return None + except Exception: + return "handled" + return fallback.read().result or default.result diff --git a/tests/corpus/3.11.14/carried_value_returns_3.pyc b/tests/corpus/3.11.14/carried_value_returns_3.pyc new file mode 100644 index 00000000..1f16c355 Binary files /dev/null and b/tests/corpus/3.11.14/carried_value_returns_3.pyc differ diff --git a/tests/corpus/3.11.14/context_manager_returns.py b/tests/corpus/3.11.14/context_manager_returns.py new file mode 100644 index 00000000..02534690 --- /dev/null +++ b/tests/corpus/3.11.14/context_manager_returns.py @@ -0,0 +1,18 @@ +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result diff --git a/tests/corpus/3.11.14/context_manager_returns.pyc b/tests/corpus/3.11.14/context_manager_returns.pyc new file mode 100644 index 00000000..3d183ede Binary files /dev/null and b/tests/corpus/3.11.14/context_manager_returns.pyc differ diff --git a/tests/corpus/3.11.14/loop_context_returns.py b/tests/corpus/3.11.14/loop_context_returns.py new file mode 100644 index 00000000..703d904c --- /dev/null +++ b/tests/corpus/3.11.14/loop_context_returns.py @@ -0,0 +1,64 @@ +def sync_value(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +def sync_length(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +def sync_mapping(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + +async def async_value(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +async def async_length(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +async def async_mapping(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} diff --git a/tests/corpus/3.11.14/loop_context_returns.pyc b/tests/corpus/3.11.14/loop_context_returns.pyc new file mode 100644 index 00000000..727ceda6 Binary files /dev/null and b/tests/corpus/3.11.14/loop_context_returns.pyc differ diff --git a/tests/corpus/3.11.14/nested_context_returns.py b/tests/corpus/3.11.14/nested_context_returns.py new file mode 100644 index 00000000..5040cd90 --- /dev/null +++ b/tests/corpus/3.11.14/nested_context_returns.py @@ -0,0 +1,49 @@ +def sync_call(contexts, value): + with contexts[0]: + with contexts[1]: + return value() + + +def sync_mapping(contexts, value): + with contexts[0]: + with contexts[1]: + with contexts[2]: + return {"rows": value()} + + +async def async_call(contexts, value): + async with contexts[0]: + async with contexts[1]: + return value() + + +async def async_await(contexts, value): + async with contexts[0]: + async with contexts[1]: + async with contexts[2]: + return await value() + + +async def async_tuple(contexts, value): + async with contexts[0]: + async with contexts[1]: + return (value(),) + + +async def async_single(contexts, value): + async with contexts[0]: + return await value() + + +def sync_outer_return(contexts, value): + with contexts[0]: + with contexts[1]: + result = value() + return result + + +async def async_outer_return(contexts, value): + async with contexts[0]: + async with contexts[1]: + result = await value() + return result diff --git a/tests/corpus/3.11.14/nested_context_returns.pyc b/tests/corpus/3.11.14/nested_context_returns.pyc new file mode 100644 index 00000000..f621cf44 Binary files /dev/null and b/tests/corpus/3.11.14/nested_context_returns.pyc differ diff --git a/tests/corpus/3.11.14/padded_none_returns_3.py b/tests/corpus/3.11.14/padded_none_returns_3.py new file mode 100644 index 00000000..c23f4191 --- /dev/null +++ b/tests/corpus/3.11.14/padded_none_returns_3.py @@ -0,0 +1,37 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + stamp = datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + return stamp + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.11.14/padded_none_returns_3.pyc b/tests/corpus/3.11.14/padded_none_returns_3.pyc new file mode 100644 index 00000000..e0882574 Binary files /dev/null and b/tests/corpus/3.11.14/padded_none_returns_3.pyc differ diff --git a/tests/corpus/3.12.3/await_context_controls.py b/tests/corpus/3.12.3/await_context_controls.py new file mode 100644 index 00000000..cfa905b1 --- /dev/null +++ b/tests/corpus/3.12.3/await_context_controls.py @@ -0,0 +1,26 @@ +async def sequential_contexts(manager, work, record, flag): + async with manager: + a = await work("a") + async with manager: + b = await work("b") + return a, b + + +async def awaited_then_context(manager, work, record, flag): + try: + a = await work("a") + async with manager: + b = await work("b") + except ValueError: + return "handled" + return a, b + + +async def outer_handler(manager, work, record, flag): + try: + async with manager: + a = await work("a") + b = await work("b") + except ValueError: + return "handled" + return a, b diff --git a/tests/corpus/3.12.3/await_context_controls.pyc b/tests/corpus/3.12.3/await_context_controls.pyc new file mode 100644 index 00000000..ed6a6878 Binary files /dev/null and b/tests/corpus/3.12.3/await_context_controls.pyc differ diff --git a/tests/corpus/3.12.3/bounded_try_returns_3.py b/tests/corpus/3.12.3/bounded_try_returns_3.py new file mode 100644 index 00000000..b1b14365 --- /dev/null +++ b/tests/corpus/3.12.3/bounded_try_returns_3.py @@ -0,0 +1,36 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + return datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.12.3/bounded_try_returns_3.pyc b/tests/corpus/3.12.3/bounded_try_returns_3.pyc new file mode 100644 index 00000000..7e839f77 Binary files /dev/null and b/tests/corpus/3.12.3/bounded_try_returns_3.pyc differ diff --git a/tests/corpus/3.12.3/carried_value_returns_3.py b/tests/corpus/3.12.3/carried_value_returns_3.py new file mode 100644 index 00000000..7c0f657e --- /dev/null +++ b/tests/corpus/3.12.3/carried_value_returns_3.py @@ -0,0 +1,47 @@ +# A computed return can be outside the protected range but still use its value. + + +def call_return(value, make, fallback, default): + if value: + try: + return make(value) + except Exception: + pass + return fallback.read().result or default.result + + +def attribute_return(value, make, fallback, default): + if value: + try: + return make(value).result + except Exception: + pass + return fallback.read().result or default.result + + +def tuple_return(value, make, fallback, default): + if value: + try: + return (make(value), 7) + except Exception: + pass + return fallback.read().result or default.result + + +def mapping_return(value, make, fallback, default): + if value: + try: + return {"value": make(value)} + except Exception: + pass + return fallback.read().result or default.result + + +def bare_return(value, make, fallback, default): + if value: + try: + make(value) + return None + except Exception: + return "handled" + return fallback.read().result or default.result diff --git a/tests/corpus/3.12.3/carried_value_returns_3.pyc b/tests/corpus/3.12.3/carried_value_returns_3.pyc new file mode 100644 index 00000000..c5fb5bfb Binary files /dev/null and b/tests/corpus/3.12.3/carried_value_returns_3.pyc differ diff --git a/tests/corpus/3.12.3/comprehension_chained_ternaries.py b/tests/corpus/3.12.3/comprehension_chained_ternaries.py new file mode 100644 index 00000000..4130711f --- /dev/null +++ b/tests/corpus/3.12.3/comprehension_chained_ternaries.py @@ -0,0 +1,18 @@ +def mapping_value(values): + return {value: 1 if 6 <= value <= 8 else 0 for value in values} + + +def mapping_key(values): + return {(value if 6 <= value <= 8 else -value): value for value in values} + + +def arithmetic(values): + return [0.6 + 0.4 * (1.0 if 4 <= value <= 8 < 10 else 0.3) for value in values] + + +def filtered(values): + return [1 if value >= 6 else 0 for value in values if value <= 8] + + +def observed(values, base, coefficient, lower, probe, upper): + return {value: base(value) + coefficient(value) * (1.0 if lower() <= probe(value) <= upper() else 0.3) for value in values} diff --git a/tests/corpus/3.12.3/comprehension_chained_ternaries.pyc b/tests/corpus/3.12.3/comprehension_chained_ternaries.pyc new file mode 100644 index 00000000..75ee69b7 Binary files /dev/null and b/tests/corpus/3.12.3/comprehension_chained_ternaries.pyc differ diff --git a/tests/corpus/3.12.3/context_manager_returns.py b/tests/corpus/3.12.3/context_manager_returns.py new file mode 100644 index 00000000..02534690 --- /dev/null +++ b/tests/corpus/3.12.3/context_manager_returns.py @@ -0,0 +1,18 @@ +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result diff --git a/tests/corpus/3.12.3/context_manager_returns.pyc b/tests/corpus/3.12.3/context_manager_returns.pyc new file mode 100644 index 00000000..ddd21f3e Binary files /dev/null and b/tests/corpus/3.12.3/context_manager_returns.pyc differ diff --git a/tests/corpus/3.12.3/filtered_generator_boolean_values.py b/tests/corpus/3.12.3/filtered_generator_boolean_values.py new file mode 100644 index 00000000..b80c25db --- /dev/null +++ b/tests/corpus/3.12.3/filtered_generator_boolean_values.py @@ -0,0 +1,18 @@ +def or_value(rows): + return tuple(row[1] or 0 for row in rows if row[0]) + + +def and_value(rows): + return tuple(row[1] and row[2] for row in rows if row[0]) + + +def chained_or(rows): + return tuple(row[1] or row[2] or 0 for row in rows if row[0]) + + +def mixed_value(rows): + return tuple((row[1] and row[2]) or 0 for row in rows if row[0]) + + +def none_filter(rows): + return tuple(row.get("amount") or 0.0 for row in rows if row.get("amount") is not None) diff --git a/tests/corpus/3.12.3/filtered_generator_boolean_values.pyc b/tests/corpus/3.12.3/filtered_generator_boolean_values.pyc new file mode 100644 index 00000000..b67ed1c9 Binary files /dev/null and b/tests/corpus/3.12.3/filtered_generator_boolean_values.pyc differ diff --git a/tests/corpus/3.12.3/generator_multiline_filter_312.py b/tests/corpus/3.12.3/generator_multiline_filter_312.py new file mode 100644 index 00000000..b6d92ec8 --- /dev/null +++ b/tests/corpus/3.12.3/generator_multiline_filter_312.py @@ -0,0 +1,3 @@ +def multiline_filter(values, excluded): + return tuple(value for value in values + if value not in excluded) diff --git a/tests/corpus/3.12.3/generator_multiline_filter_312.pyc b/tests/corpus/3.12.3/generator_multiline_filter_312.pyc new file mode 100644 index 00000000..daf24490 Binary files /dev/null and b/tests/corpus/3.12.3/generator_multiline_filter_312.pyc differ diff --git a/tests/corpus/3.12.3/generator_statement_reconstruction.py b/tests/corpus/3.12.3/generator_statement_reconstruction.py new file mode 100644 index 00000000..44c9a23a --- /dev/null +++ b/tests/corpus/3.12.3/generator_statement_reconstruction.py @@ -0,0 +1,16 @@ +def repeated_targets(values): + return tuple(value is not None for value, discarded, discarded, discarded in values) + + +def real_underscore(values): + return tuple((value, discarded, _) for value, _, discarded, discarded in values) + + +def nested_calls(rows): + return sorted(sum(value for value in row) / len(row) for row in rows) + + +def captured_cells(rows): + deltas = rows + factory = sum + return sorted(sum(factory(deltas) for _ in deltas) / len(deltas) for _ in range(3)) diff --git a/tests/corpus/3.12.3/generator_statement_reconstruction.pyc b/tests/corpus/3.12.3/generator_statement_reconstruction.pyc new file mode 100644 index 00000000..e23d9844 Binary files /dev/null and b/tests/corpus/3.12.3/generator_statement_reconstruction.pyc differ diff --git a/tests/corpus/3.12.3/generator_walrus_cells.py b/tests/corpus/3.12.3/generator_walrus_cells.py new file mode 100644 index 00000000..c2aea4ad --- /dev/null +++ b/tests/corpus/3.12.3/generator_walrus_cells.py @@ -0,0 +1,8 @@ +def select(rows): + return tuple(isinstance(row, dict) and (change := row.get("change")) is not None and change >= 0 for row in rows) + + +def last_binding(rows): + change = "initial" + results = tuple((change := row.get("change")) for row in rows) + return results, change diff --git a/tests/corpus/3.12.3/generator_walrus_cells.pyc b/tests/corpus/3.12.3/generator_walrus_cells.pyc new file mode 100644 index 00000000..2c8c69c9 Binary files /dev/null and b/tests/corpus/3.12.3/generator_walrus_cells.pyc differ diff --git a/tests/corpus/3.12.3/handler_iterator_breaks.py b/tests/corpus/3.12.3/handler_iterator_breaks.py new file mode 100644 index 00000000..97ae7406 --- /dev/null +++ b/tests/corpus/3.12.3/handler_iterator_breaks.py @@ -0,0 +1,140 @@ +import asyncio + + +async def named_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +def sync_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + break + except Exception as error: + owner.log("unavailable", str(error)) + break + owner.log("finished", accepted) + return accepted + + +async def direct_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +async def unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + +async def direct_unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted diff --git a/tests/corpus/3.12.3/handler_iterator_breaks.pyc b/tests/corpus/3.12.3/handler_iterator_breaks.pyc new file mode 100644 index 00000000..43585ae5 Binary files /dev/null and b/tests/corpus/3.12.3/handler_iterator_breaks.pyc differ diff --git a/tests/corpus/3.12.3/loop_context_returns.py b/tests/corpus/3.12.3/loop_context_returns.py new file mode 100644 index 00000000..703d904c --- /dev/null +++ b/tests/corpus/3.12.3/loop_context_returns.py @@ -0,0 +1,64 @@ +def sync_value(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +def sync_length(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +def sync_mapping(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + +async def async_value(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +async def async_length(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +async def async_mapping(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} diff --git a/tests/corpus/3.12.3/loop_context_returns.pyc b/tests/corpus/3.12.3/loop_context_returns.pyc new file mode 100644 index 00000000..46287a41 Binary files /dev/null and b/tests/corpus/3.12.3/loop_context_returns.pyc differ diff --git a/tests/corpus/3.12.3/loop_handler_ownership.py b/tests/corpus/3.12.3/loop_handler_ownership.py new file mode 100644 index 00000000..c69d4d26 --- /dev/null +++ b/tests/corpus/3.12.3/loop_handler_ownership.py @@ -0,0 +1,119 @@ +def named(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def multiple(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + except KeyError as error: + owner.other(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def siblings(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + try: + owner.second(row) + except KeyError: + owner.other(row) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def same_type(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except Exception: + owner.failed(row) + owner.finish() + except Exception as error: + owner.fallback(str(error)) + return None + + +async def async_0(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_1(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_2(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + await owner.asecond(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +def outer_finally(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + finally: + owner.finish() + return None + + +def nonloop(owner): + try: + try: + owner.record(owner.row) + except ValueError: + owner.failed(owner.row) + except RuntimeError: + owner.fallback() + return None diff --git a/tests/corpus/3.12.3/loop_handler_ownership.pyc b/tests/corpus/3.12.3/loop_handler_ownership.pyc new file mode 100644 index 00000000..a7c511ae Binary files /dev/null and b/tests/corpus/3.12.3/loop_handler_ownership.pyc differ diff --git a/tests/corpus/3.12.3/nested_context_returns.py b/tests/corpus/3.12.3/nested_context_returns.py new file mode 100644 index 00000000..5040cd90 --- /dev/null +++ b/tests/corpus/3.12.3/nested_context_returns.py @@ -0,0 +1,49 @@ +def sync_call(contexts, value): + with contexts[0]: + with contexts[1]: + return value() + + +def sync_mapping(contexts, value): + with contexts[0]: + with contexts[1]: + with contexts[2]: + return {"rows": value()} + + +async def async_call(contexts, value): + async with contexts[0]: + async with contexts[1]: + return value() + + +async def async_await(contexts, value): + async with contexts[0]: + async with contexts[1]: + async with contexts[2]: + return await value() + + +async def async_tuple(contexts, value): + async with contexts[0]: + async with contexts[1]: + return (value(),) + + +async def async_single(contexts, value): + async with contexts[0]: + return await value() + + +def sync_outer_return(contexts, value): + with contexts[0]: + with contexts[1]: + result = value() + return result + + +async def async_outer_return(contexts, value): + async with contexts[0]: + async with contexts[1]: + result = await value() + return result diff --git a/tests/corpus/3.12.3/nested_context_returns.pyc b/tests/corpus/3.12.3/nested_context_returns.pyc new file mode 100644 index 00000000..0db7fe80 Binary files /dev/null and b/tests/corpus/3.12.3/nested_context_returns.pyc differ diff --git a/tests/corpus/3.12.3/padded_none_returns_3.py b/tests/corpus/3.12.3/padded_none_returns_3.py new file mode 100644 index 00000000..c23f4191 --- /dev/null +++ b/tests/corpus/3.12.3/padded_none_returns_3.py @@ -0,0 +1,37 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + stamp = datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + return stamp + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.12.3/padded_none_returns_3.pyc b/tests/corpus/3.12.3/padded_none_returns_3.pyc new file mode 100644 index 00000000..3a066d43 Binary files /dev/null and b/tests/corpus/3.12.3/padded_none_returns_3.pyc differ diff --git a/tests/corpus/3.12.3/private_handler_clauses.py b/tests/corpus/3.12.3/private_handler_clauses.py new file mode 100644 index 00000000..082f02b7 --- /dev/null +++ b/tests/corpus/3.12.3/private_handler_clauses.py @@ -0,0 +1,163 @@ +import asyncio + + +async def named(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +def synchronous(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + continue + except Exception as error: + owner.log("unavailable", str(error)) + continue + owner.log("finished", accepted) + return accepted + + +async def attributes(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def local_attributes(owner, faults): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def single_clause(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + await owner.log("finished", accepted) + return accepted + + +async def no_private_try(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted diff --git a/tests/corpus/3.12.3/private_handler_clauses.pyc b/tests/corpus/3.12.3/private_handler_clauses.pyc new file mode 100644 index 00000000..c2a695c5 Binary files /dev/null and b/tests/corpus/3.12.3/private_handler_clauses.pyc differ diff --git a/tests/corpus/3.13.7/await_context_controls.py b/tests/corpus/3.13.7/await_context_controls.py new file mode 100644 index 00000000..cfa905b1 --- /dev/null +++ b/tests/corpus/3.13.7/await_context_controls.py @@ -0,0 +1,26 @@ +async def sequential_contexts(manager, work, record, flag): + async with manager: + a = await work("a") + async with manager: + b = await work("b") + return a, b + + +async def awaited_then_context(manager, work, record, flag): + try: + a = await work("a") + async with manager: + b = await work("b") + except ValueError: + return "handled" + return a, b + + +async def outer_handler(manager, work, record, flag): + try: + async with manager: + a = await work("a") + b = await work("b") + except ValueError: + return "handled" + return a, b diff --git a/tests/corpus/3.13.7/await_context_controls.pyc b/tests/corpus/3.13.7/await_context_controls.pyc new file mode 100644 index 00000000..7a3f5eff Binary files /dev/null and b/tests/corpus/3.13.7/await_context_controls.pyc differ diff --git a/tests/corpus/3.13.7/bounded_formatting.py b/tests/corpus/3.13.7/bounded_formatting.py new file mode 100644 index 00000000..a03afc74 --- /dev/null +++ b/tests/corpus/3.13.7/bounded_formatting.py @@ -0,0 +1,10 @@ +def simple(value): + return f"{value}" + + +def conversions(label, value, width): + return f"{label!r}: {value!s:>{width}}" + + +def ascii_spec(label, value, width): + return f"{label!a}: {value:{width}}" diff --git a/tests/corpus/3.13.7/bounded_formatting.pyc b/tests/corpus/3.13.7/bounded_formatting.pyc new file mode 100644 index 00000000..dbb8e759 Binary files /dev/null and b/tests/corpus/3.13.7/bounded_formatting.pyc differ diff --git a/tests/corpus/3.13.7/bounded_generator_expressions.py b/tests/corpus/3.13.7/bounded_generator_expressions.py new file mode 100644 index 00000000..24dd6749 --- /dev/null +++ b/tests/corpus/3.13.7/bounded_generator_expressions.py @@ -0,0 +1,14 @@ +def plain(values): + return collect(value for value in values) + + +def captured(values, offset): + return collect(value + offset for value in values) + + +def multiple(values, first, second): + return collect((value + first) * second for value in values) + + +def ordinary_function(values): + return (lambda value: value)(values) diff --git a/tests/corpus/3.13.7/bounded_generator_expressions.pyc b/tests/corpus/3.13.7/bounded_generator_expressions.pyc new file mode 100644 index 00000000..283b3e27 Binary files /dev/null and b/tests/corpus/3.13.7/bounded_generator_expressions.pyc differ diff --git a/tests/corpus/3.13.7/bounded_try_returns_3.py b/tests/corpus/3.13.7/bounded_try_returns_3.py new file mode 100644 index 00000000..b1b14365 --- /dev/null +++ b/tests/corpus/3.13.7/bounded_try_returns_3.py @@ -0,0 +1,36 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + return datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.13.7/bounded_try_returns_3.pyc b/tests/corpus/3.13.7/bounded_try_returns_3.pyc new file mode 100644 index 00000000..da87932a Binary files /dev/null and b/tests/corpus/3.13.7/bounded_try_returns_3.pyc differ diff --git a/tests/corpus/3.13.7/carried_value_returns_3.py b/tests/corpus/3.13.7/carried_value_returns_3.py new file mode 100644 index 00000000..7c0f657e --- /dev/null +++ b/tests/corpus/3.13.7/carried_value_returns_3.py @@ -0,0 +1,47 @@ +# A computed return can be outside the protected range but still use its value. + + +def call_return(value, make, fallback, default): + if value: + try: + return make(value) + except Exception: + pass + return fallback.read().result or default.result + + +def attribute_return(value, make, fallback, default): + if value: + try: + return make(value).result + except Exception: + pass + return fallback.read().result or default.result + + +def tuple_return(value, make, fallback, default): + if value: + try: + return (make(value), 7) + except Exception: + pass + return fallback.read().result or default.result + + +def mapping_return(value, make, fallback, default): + if value: + try: + return {"value": make(value)} + except Exception: + pass + return fallback.read().result or default.result + + +def bare_return(value, make, fallback, default): + if value: + try: + make(value) + return None + except Exception: + return "handled" + return fallback.read().result or default.result diff --git a/tests/corpus/3.13.7/carried_value_returns_3.pyc b/tests/corpus/3.13.7/carried_value_returns_3.pyc new file mode 100644 index 00000000..1d3cb3f3 Binary files /dev/null and b/tests/corpus/3.13.7/carried_value_returns_3.pyc differ diff --git a/tests/corpus/3.13.7/comprehension_chained_ternaries.py b/tests/corpus/3.13.7/comprehension_chained_ternaries.py new file mode 100644 index 00000000..4130711f --- /dev/null +++ b/tests/corpus/3.13.7/comprehension_chained_ternaries.py @@ -0,0 +1,18 @@ +def mapping_value(values): + return {value: 1 if 6 <= value <= 8 else 0 for value in values} + + +def mapping_key(values): + return {(value if 6 <= value <= 8 else -value): value for value in values} + + +def arithmetic(values): + return [0.6 + 0.4 * (1.0 if 4 <= value <= 8 < 10 else 0.3) for value in values] + + +def filtered(values): + return [1 if value >= 6 else 0 for value in values if value <= 8] + + +def observed(values, base, coefficient, lower, probe, upper): + return {value: base(value) + coefficient(value) * (1.0 if lower() <= probe(value) <= upper() else 0.3) for value in values} diff --git a/tests/corpus/3.13.7/comprehension_chained_ternaries.pyc b/tests/corpus/3.13.7/comprehension_chained_ternaries.pyc new file mode 100644 index 00000000..45cc60e2 Binary files /dev/null and b/tests/corpus/3.13.7/comprehension_chained_ternaries.pyc differ diff --git a/tests/corpus/3.13.7/context_manager_returns.py b/tests/corpus/3.13.7/context_manager_returns.py new file mode 100644 index 00000000..02534690 --- /dev/null +++ b/tests/corpus/3.13.7/context_manager_returns.py @@ -0,0 +1,18 @@ +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result diff --git a/tests/corpus/3.13.7/context_manager_returns.pyc b/tests/corpus/3.13.7/context_manager_returns.pyc new file mode 100644 index 00000000..de88bd8b Binary files /dev/null and b/tests/corpus/3.13.7/context_manager_returns.pyc differ diff --git a/tests/corpus/3.13.7/filtered_generator_boolean_values.py b/tests/corpus/3.13.7/filtered_generator_boolean_values.py new file mode 100644 index 00000000..b80c25db --- /dev/null +++ b/tests/corpus/3.13.7/filtered_generator_boolean_values.py @@ -0,0 +1,18 @@ +def or_value(rows): + return tuple(row[1] or 0 for row in rows if row[0]) + + +def and_value(rows): + return tuple(row[1] and row[2] for row in rows if row[0]) + + +def chained_or(rows): + return tuple(row[1] or row[2] or 0 for row in rows if row[0]) + + +def mixed_value(rows): + return tuple((row[1] and row[2]) or 0 for row in rows if row[0]) + + +def none_filter(rows): + return tuple(row.get("amount") or 0.0 for row in rows if row.get("amount") is not None) diff --git a/tests/corpus/3.13.7/filtered_generator_boolean_values.pyc b/tests/corpus/3.13.7/filtered_generator_boolean_values.pyc new file mode 100644 index 00000000..25782ac0 Binary files /dev/null and b/tests/corpus/3.13.7/filtered_generator_boolean_values.pyc differ diff --git a/tests/corpus/3.13.7/generator_statement_reconstruction.py b/tests/corpus/3.13.7/generator_statement_reconstruction.py new file mode 100644 index 00000000..44c9a23a --- /dev/null +++ b/tests/corpus/3.13.7/generator_statement_reconstruction.py @@ -0,0 +1,16 @@ +def repeated_targets(values): + return tuple(value is not None for value, discarded, discarded, discarded in values) + + +def real_underscore(values): + return tuple((value, discarded, _) for value, _, discarded, discarded in values) + + +def nested_calls(rows): + return sorted(sum(value for value in row) / len(row) for row in rows) + + +def captured_cells(rows): + deltas = rows + factory = sum + return sorted(sum(factory(deltas) for _ in deltas) / len(deltas) for _ in range(3)) diff --git a/tests/corpus/3.13.7/generator_statement_reconstruction.pyc b/tests/corpus/3.13.7/generator_statement_reconstruction.pyc new file mode 100644 index 00000000..fc1be9cf Binary files /dev/null and b/tests/corpus/3.13.7/generator_statement_reconstruction.pyc differ diff --git a/tests/corpus/3.13.7/generator_walrus_cells.py b/tests/corpus/3.13.7/generator_walrus_cells.py new file mode 100644 index 00000000..c2aea4ad --- /dev/null +++ b/tests/corpus/3.13.7/generator_walrus_cells.py @@ -0,0 +1,8 @@ +def select(rows): + return tuple(isinstance(row, dict) and (change := row.get("change")) is not None and change >= 0 for row in rows) + + +def last_binding(rows): + change = "initial" + results = tuple((change := row.get("change")) for row in rows) + return results, change diff --git a/tests/corpus/3.13.7/generator_walrus_cells.pyc b/tests/corpus/3.13.7/generator_walrus_cells.pyc new file mode 100644 index 00000000..5a99f589 Binary files /dev/null and b/tests/corpus/3.13.7/generator_walrus_cells.pyc differ diff --git a/tests/corpus/3.13.7/handler_iterator_breaks.py b/tests/corpus/3.13.7/handler_iterator_breaks.py new file mode 100644 index 00000000..97ae7406 --- /dev/null +++ b/tests/corpus/3.13.7/handler_iterator_breaks.py @@ -0,0 +1,140 @@ +import asyncio + + +async def named_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +def sync_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + break + except Exception as error: + owner.log("unavailable", str(error)) + break + owner.log("finished", accepted) + return accepted + + +async def direct_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +async def unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + +async def direct_unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted diff --git a/tests/corpus/3.13.7/handler_iterator_breaks.pyc b/tests/corpus/3.13.7/handler_iterator_breaks.pyc new file mode 100644 index 00000000..89aadc4b Binary files /dev/null and b/tests/corpus/3.13.7/handler_iterator_breaks.pyc differ diff --git a/tests/corpus/3.13.7/loop_context_returns.py b/tests/corpus/3.13.7/loop_context_returns.py new file mode 100644 index 00000000..703d904c --- /dev/null +++ b/tests/corpus/3.13.7/loop_context_returns.py @@ -0,0 +1,64 @@ +def sync_value(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +def sync_length(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +def sync_mapping(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + +async def async_value(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +async def async_length(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +async def async_mapping(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} diff --git a/tests/corpus/3.13.7/loop_context_returns.pyc b/tests/corpus/3.13.7/loop_context_returns.pyc new file mode 100644 index 00000000..e581bea9 Binary files /dev/null and b/tests/corpus/3.13.7/loop_context_returns.pyc differ diff --git a/tests/corpus/3.13.7/loop_handler_ownership.py b/tests/corpus/3.13.7/loop_handler_ownership.py new file mode 100644 index 00000000..c69d4d26 --- /dev/null +++ b/tests/corpus/3.13.7/loop_handler_ownership.py @@ -0,0 +1,119 @@ +def named(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def multiple(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + except KeyError as error: + owner.other(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def siblings(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + try: + owner.second(row) + except KeyError: + owner.other(row) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def same_type(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except Exception: + owner.failed(row) + owner.finish() + except Exception as error: + owner.fallback(str(error)) + return None + + +async def async_0(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_1(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_2(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + await owner.asecond(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +def outer_finally(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + finally: + owner.finish() + return None + + +def nonloop(owner): + try: + try: + owner.record(owner.row) + except ValueError: + owner.failed(owner.row) + except RuntimeError: + owner.fallback() + return None diff --git a/tests/corpus/3.13.7/loop_handler_ownership.pyc b/tests/corpus/3.13.7/loop_handler_ownership.pyc new file mode 100644 index 00000000..ce130150 Binary files /dev/null and b/tests/corpus/3.13.7/loop_handler_ownership.pyc differ diff --git a/tests/corpus/3.13.7/nested_context_returns.py b/tests/corpus/3.13.7/nested_context_returns.py new file mode 100644 index 00000000..5040cd90 --- /dev/null +++ b/tests/corpus/3.13.7/nested_context_returns.py @@ -0,0 +1,49 @@ +def sync_call(contexts, value): + with contexts[0]: + with contexts[1]: + return value() + + +def sync_mapping(contexts, value): + with contexts[0]: + with contexts[1]: + with contexts[2]: + return {"rows": value()} + + +async def async_call(contexts, value): + async with contexts[0]: + async with contexts[1]: + return value() + + +async def async_await(contexts, value): + async with contexts[0]: + async with contexts[1]: + async with contexts[2]: + return await value() + + +async def async_tuple(contexts, value): + async with contexts[0]: + async with contexts[1]: + return (value(),) + + +async def async_single(contexts, value): + async with contexts[0]: + return await value() + + +def sync_outer_return(contexts, value): + with contexts[0]: + with contexts[1]: + result = value() + return result + + +async def async_outer_return(contexts, value): + async with contexts[0]: + async with contexts[1]: + result = await value() + return result diff --git a/tests/corpus/3.13.7/nested_context_returns.pyc b/tests/corpus/3.13.7/nested_context_returns.pyc new file mode 100644 index 00000000..b408426c Binary files /dev/null and b/tests/corpus/3.13.7/nested_context_returns.pyc differ diff --git a/tests/corpus/3.13.7/padded_none_returns_3.py b/tests/corpus/3.13.7/padded_none_returns_3.py new file mode 100644 index 00000000..c23f4191 --- /dev/null +++ b/tests/corpus/3.13.7/padded_none_returns_3.py @@ -0,0 +1,37 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + stamp = datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + return stamp + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.13.7/padded_none_returns_3.pyc b/tests/corpus/3.13.7/padded_none_returns_3.pyc new file mode 100644 index 00000000..3e33a197 Binary files /dev/null and b/tests/corpus/3.13.7/padded_none_returns_3.pyc differ diff --git a/tests/corpus/3.13.7/private_handler_clauses.py b/tests/corpus/3.13.7/private_handler_clauses.py new file mode 100644 index 00000000..082f02b7 --- /dev/null +++ b/tests/corpus/3.13.7/private_handler_clauses.py @@ -0,0 +1,163 @@ +import asyncio + + +async def named(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +def synchronous(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + continue + except Exception as error: + owner.log("unavailable", str(error)) + continue + owner.log("finished", accepted) + return accepted + + +async def attributes(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def local_attributes(owner, faults): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def single_clause(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + await owner.log("finished", accepted) + return accepted + + +async def no_private_try(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted diff --git a/tests/corpus/3.13.7/private_handler_clauses.pyc b/tests/corpus/3.13.7/private_handler_clauses.pyc new file mode 100644 index 00000000..7a1fdb42 Binary files /dev/null and b/tests/corpus/3.13.7/private_handler_clauses.pyc differ diff --git a/tests/corpus/3.14.2/async_context_return_padding_1.py b/tests/corpus/3.14.2/async_context_return_padding_1.py new file mode 100644 index 00000000..d4ef2843 --- /dev/null +++ b/tests/corpus/3.14.2/async_context_return_padding_1.py @@ -0,0 +1,12 @@ +async def constant_one(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return 4 + + +async def padded_mapping(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return {'value': row0} diff --git a/tests/corpus/3.14.2/async_context_return_padding_1.pyc b/tests/corpus/3.14.2/async_context_return_padding_1.pyc new file mode 100644 index 00000000..7dbd40f4 Binary files /dev/null and b/tests/corpus/3.14.2/async_context_return_padding_1.pyc differ diff --git a/tests/corpus/3.14.2/async_context_return_padding_3.py b/tests/corpus/3.14.2/async_context_return_padding_3.py new file mode 100644 index 00000000..d4ef2843 --- /dev/null +++ b/tests/corpus/3.14.2/async_context_return_padding_3.py @@ -0,0 +1,12 @@ +async def constant_one(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return 4 + + +async def padded_mapping(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return {'value': row0} diff --git a/tests/corpus/3.14.2/async_context_return_padding_3.pyc b/tests/corpus/3.14.2/async_context_return_padding_3.pyc new file mode 100644 index 00000000..6973a933 Binary files /dev/null and b/tests/corpus/3.14.2/async_context_return_padding_3.pyc differ diff --git a/tests/corpus/3.14.2/async_context_return_padding_9.py b/tests/corpus/3.14.2/async_context_return_padding_9.py new file mode 100644 index 00000000..d4ef2843 --- /dev/null +++ b/tests/corpus/3.14.2/async_context_return_padding_9.py @@ -0,0 +1,12 @@ +async def constant_one(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return 4 + + +async def padded_mapping(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return {'value': row0} diff --git a/tests/corpus/3.14.2/async_context_return_padding_9.pyc b/tests/corpus/3.14.2/async_context_return_padding_9.pyc new file mode 100644 index 00000000..e1485469 Binary files /dev/null and b/tests/corpus/3.14.2/async_context_return_padding_9.pyc differ diff --git a/tests/corpus/3.14.2/await_context_controls.py b/tests/corpus/3.14.2/await_context_controls.py new file mode 100644 index 00000000..cfa905b1 --- /dev/null +++ b/tests/corpus/3.14.2/await_context_controls.py @@ -0,0 +1,26 @@ +async def sequential_contexts(manager, work, record, flag): + async with manager: + a = await work("a") + async with manager: + b = await work("b") + return a, b + + +async def awaited_then_context(manager, work, record, flag): + try: + a = await work("a") + async with manager: + b = await work("b") + except ValueError: + return "handled" + return a, b + + +async def outer_handler(manager, work, record, flag): + try: + async with manager: + a = await work("a") + b = await work("b") + except ValueError: + return "handled" + return a, b diff --git a/tests/corpus/3.14.2/await_context_controls.pyc b/tests/corpus/3.14.2/await_context_controls.pyc new file mode 100644 index 00000000..9d4103b1 Binary files /dev/null and b/tests/corpus/3.14.2/await_context_controls.pyc differ diff --git a/tests/corpus/3.14.2/await_exception_regions.py b/tests/corpus/3.14.2/await_exception_regions.py new file mode 100644 index 00000000..383e0fa6 --- /dev/null +++ b/tests/corpus/3.14.2/await_exception_regions.py @@ -0,0 +1,19 @@ +async def awaited_finally(manager, work, record, flag): + async with manager: + try: + await work("a") + finally: + record("cleanup") + await work("b") + return "tail" + + +async def awaited_else(manager, work, record, flag): + async with manager: + try: + a = await work("a") + except ValueError: + record("handled") + else: + await work("b") + return "tail" diff --git a/tests/corpus/3.14.2/await_exception_regions.pyc b/tests/corpus/3.14.2/await_exception_regions.pyc new file mode 100644 index 00000000..caf0d367 Binary files /dev/null and b/tests/corpus/3.14.2/await_exception_regions.pyc differ diff --git a/tests/corpus/3.14.2/bounded_formatting.py b/tests/corpus/3.14.2/bounded_formatting.py new file mode 100644 index 00000000..a03afc74 --- /dev/null +++ b/tests/corpus/3.14.2/bounded_formatting.py @@ -0,0 +1,10 @@ +def simple(value): + return f"{value}" + + +def conversions(label, value, width): + return f"{label!r}: {value!s:>{width}}" + + +def ascii_spec(label, value, width): + return f"{label!a}: {value:{width}}" diff --git a/tests/corpus/3.14.2/bounded_formatting.pyc b/tests/corpus/3.14.2/bounded_formatting.pyc new file mode 100644 index 00000000..7dee5da5 Binary files /dev/null and b/tests/corpus/3.14.2/bounded_formatting.pyc differ diff --git a/tests/corpus/3.14.2/bounded_generator_expressions.py b/tests/corpus/3.14.2/bounded_generator_expressions.py new file mode 100644 index 00000000..24dd6749 --- /dev/null +++ b/tests/corpus/3.14.2/bounded_generator_expressions.py @@ -0,0 +1,14 @@ +def plain(values): + return collect(value for value in values) + + +def captured(values, offset): + return collect(value + offset for value in values) + + +def multiple(values, first, second): + return collect((value + first) * second for value in values) + + +def ordinary_function(values): + return (lambda value: value)(values) diff --git a/tests/corpus/3.14.2/bounded_generator_expressions.pyc b/tests/corpus/3.14.2/bounded_generator_expressions.pyc new file mode 100644 index 00000000..740ccff9 Binary files /dev/null and b/tests/corpus/3.14.2/bounded_generator_expressions.pyc differ diff --git a/tests/corpus/3.14.2/bounded_try_returns_3.py b/tests/corpus/3.14.2/bounded_try_returns_3.py new file mode 100644 index 00000000..b1b14365 --- /dev/null +++ b/tests/corpus/3.14.2/bounded_try_returns_3.py @@ -0,0 +1,36 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + return datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.14.2/bounded_try_returns_3.pyc b/tests/corpus/3.14.2/bounded_try_returns_3.pyc new file mode 100644 index 00000000..b0c8297b Binary files /dev/null and b/tests/corpus/3.14.2/bounded_try_returns_3.pyc differ diff --git a/tests/corpus/3.14.2/carried_value_returns_3.py b/tests/corpus/3.14.2/carried_value_returns_3.py new file mode 100644 index 00000000..7c0f657e --- /dev/null +++ b/tests/corpus/3.14.2/carried_value_returns_3.py @@ -0,0 +1,47 @@ +# A computed return can be outside the protected range but still use its value. + + +def call_return(value, make, fallback, default): + if value: + try: + return make(value) + except Exception: + pass + return fallback.read().result or default.result + + +def attribute_return(value, make, fallback, default): + if value: + try: + return make(value).result + except Exception: + pass + return fallback.read().result or default.result + + +def tuple_return(value, make, fallback, default): + if value: + try: + return (make(value), 7) + except Exception: + pass + return fallback.read().result or default.result + + +def mapping_return(value, make, fallback, default): + if value: + try: + return {"value": make(value)} + except Exception: + pass + return fallback.read().result or default.result + + +def bare_return(value, make, fallback, default): + if value: + try: + make(value) + return None + except Exception: + return "handled" + return fallback.read().result or default.result diff --git a/tests/corpus/3.14.2/carried_value_returns_3.pyc b/tests/corpus/3.14.2/carried_value_returns_3.pyc new file mode 100644 index 00000000..44fa1cc6 Binary files /dev/null and b/tests/corpus/3.14.2/carried_value_returns_3.pyc differ diff --git a/tests/corpus/3.14.2/comprehension_chained_ternaries.py b/tests/corpus/3.14.2/comprehension_chained_ternaries.py new file mode 100644 index 00000000..4130711f --- /dev/null +++ b/tests/corpus/3.14.2/comprehension_chained_ternaries.py @@ -0,0 +1,18 @@ +def mapping_value(values): + return {value: 1 if 6 <= value <= 8 else 0 for value in values} + + +def mapping_key(values): + return {(value if 6 <= value <= 8 else -value): value for value in values} + + +def arithmetic(values): + return [0.6 + 0.4 * (1.0 if 4 <= value <= 8 < 10 else 0.3) for value in values] + + +def filtered(values): + return [1 if value >= 6 else 0 for value in values if value <= 8] + + +def observed(values, base, coefficient, lower, probe, upper): + return {value: base(value) + coefficient(value) * (1.0 if lower() <= probe(value) <= upper() else 0.3) for value in values} diff --git a/tests/corpus/3.14.2/comprehension_chained_ternaries.pyc b/tests/corpus/3.14.2/comprehension_chained_ternaries.pyc new file mode 100644 index 00000000..6169659a Binary files /dev/null and b/tests/corpus/3.14.2/comprehension_chained_ternaries.pyc differ diff --git a/tests/corpus/3.14.2/constructed_rejection_guards.py b/tests/corpus/3.14.2/constructed_rejection_guards.py new file mode 100644 index 00000000..7696b0e5 --- /dev/null +++ b/tests/corpus/3.14.2/constructed_rejection_guards.py @@ -0,0 +1,54 @@ +def constructed(value, build): + if value is None or not 0.0 <= value <= 20.0: + return build("reject", amount=0) + return build("accept", amount=value) + + +def truth_guard(value, build): + if not value or len(value) < 2: + return build("reject", amount=0) + return build("accept", amount=len(value)) + + +def positive_guard(value, invalid, build): + if invalid(value) or value < 2: + return build("reject", amount=value) + return build("accept", amount=value) + + +def constant_truth(value): + if not value or len(value) < 2: + return False + return True + + +def list_result(value): + if value is None or not 0.0 <= value <= 20.0: + return ["reject", value] + return ["accept", value] + + +def mapping_result(value): + if value is None or not 0.0 <= value <= 20.0: + return {"kind": "reject", "value": value} + return {"kind": "accept", "value": value} + + +def generator_result(value, rows, collect, scale): + if value is None or not 0.0 <= value <= 20.0: + return collect(scale * item for item in rows) + return collect(rows) + + +def stored_result(value, build): + if not value or len(value) < 2: + result = build("reject", amount=0) + return result + return build("accept", amount=len(value)) + + +def effectful_body(value, build, log): + if not value or len(value) < 2: + log("rejected") + return build("reject", amount=0) + return build("accept", amount=len(value)) diff --git a/tests/corpus/3.14.2/constructed_rejection_guards.pyc b/tests/corpus/3.14.2/constructed_rejection_guards.pyc new file mode 100644 index 00000000..26f1339a Binary files /dev/null and b/tests/corpus/3.14.2/constructed_rejection_guards.pyc differ diff --git a/tests/corpus/3.14.2/context_exit_accounting.py b/tests/corpus/3.14.2/context_exit_accounting.py new file mode 100644 index 00000000..aa8d62ca --- /dev/null +++ b/tests/corpus/3.14.2/context_exit_accounting.py @@ -0,0 +1,26 @@ +async def guarded_return(manager, work, record, flag): + async with manager: + if flag: + return "early" + try: + return await work("a") + finally: + record("cleanup") + + +async def computed_return(manager, work, record, flag): + async with manager: + try: + return await work("a") + finally: + record("cleanup") + + +async def none_guard(manager, work, record, flag): + async with manager: + if flag is None: + return 3 + try: + return await work("a") + finally: + record("cleanup") diff --git a/tests/corpus/3.14.2/context_exit_accounting.pyc b/tests/corpus/3.14.2/context_exit_accounting.pyc new file mode 100644 index 00000000..2a4e8420 Binary files /dev/null and b/tests/corpus/3.14.2/context_exit_accounting.pyc differ diff --git a/tests/corpus/3.14.2/context_manager_returns.py b/tests/corpus/3.14.2/context_manager_returns.py new file mode 100644 index 00000000..02534690 --- /dev/null +++ b/tests/corpus/3.14.2/context_manager_returns.py @@ -0,0 +1,18 @@ +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result diff --git a/tests/corpus/3.14.2/context_manager_returns.pyc b/tests/corpus/3.14.2/context_manager_returns.pyc new file mode 100644 index 00000000..8ba11d23 Binary files /dev/null and b/tests/corpus/3.14.2/context_manager_returns.pyc differ diff --git a/tests/corpus/3.14.2/filtered_generator_boolean_values.py b/tests/corpus/3.14.2/filtered_generator_boolean_values.py new file mode 100644 index 00000000..b80c25db --- /dev/null +++ b/tests/corpus/3.14.2/filtered_generator_boolean_values.py @@ -0,0 +1,18 @@ +def or_value(rows): + return tuple(row[1] or 0 for row in rows if row[0]) + + +def and_value(rows): + return tuple(row[1] and row[2] for row in rows if row[0]) + + +def chained_or(rows): + return tuple(row[1] or row[2] or 0 for row in rows if row[0]) + + +def mixed_value(rows): + return tuple((row[1] and row[2]) or 0 for row in rows if row[0]) + + +def none_filter(rows): + return tuple(row.get("amount") or 0.0 for row in rows if row.get("amount") is not None) diff --git a/tests/corpus/3.14.2/filtered_generator_boolean_values.pyc b/tests/corpus/3.14.2/filtered_generator_boolean_values.pyc new file mode 100644 index 00000000..4602839a Binary files /dev/null and b/tests/corpus/3.14.2/filtered_generator_boolean_values.pyc differ diff --git a/tests/corpus/3.14.2/for_loop_disjunctions.py b/tests/corpus/3.14.2/for_loop_disjunctions.py new file mode 100644 index 00000000..795af584 --- /dev/null +++ b/tests/corpus/3.14.2/for_loop_disjunctions.py @@ -0,0 +1,131 @@ +def call_only(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + return result + + +def call_else(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def truth_only(values, test): + result = [] + for value in values: + if not value or value < 0.2: + result.append("dry") + return result + + +def truth_else(values, test): + result = [] + for value in values: + if not value or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def none_only(values, test): + result = [] + for value in values: + if value is None or value < 0.2: + result.append("dry") + return result + + +def none_else(values, test): + result = [] + for value in values: + if value is None or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def positive_only(values, test): + result = [] + for value in values: + if test(value) or value < 0.2: + result.append("dry") + return result + + +def positive_else(values, test): + result = [] + for value in values: + if test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def rotated_while(values, test): + result = [] + value = len(values) + while value < 2 or value == 3: + result.append(value) + value += 2 + return result + + +def break_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + break + else: + result.append(value) + else: + result.append('complete') + result.append('tail') + return result + + +def extended_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + else: + result.append("wet") + return result diff --git a/tests/corpus/3.14.2/for_loop_disjunctions.pyc b/tests/corpus/3.14.2/for_loop_disjunctions.pyc new file mode 100644 index 00000000..eec355fe Binary files /dev/null and b/tests/corpus/3.14.2/for_loop_disjunctions.pyc differ diff --git a/tests/corpus/3.14.2/generator_statement_reconstruction.py b/tests/corpus/3.14.2/generator_statement_reconstruction.py new file mode 100644 index 00000000..44c9a23a --- /dev/null +++ b/tests/corpus/3.14.2/generator_statement_reconstruction.py @@ -0,0 +1,16 @@ +def repeated_targets(values): + return tuple(value is not None for value, discarded, discarded, discarded in values) + + +def real_underscore(values): + return tuple((value, discarded, _) for value, _, discarded, discarded in values) + + +def nested_calls(rows): + return sorted(sum(value for value in row) / len(row) for row in rows) + + +def captured_cells(rows): + deltas = rows + factory = sum + return sorted(sum(factory(deltas) for _ in deltas) / len(deltas) for _ in range(3)) diff --git a/tests/corpus/3.14.2/generator_statement_reconstruction.pyc b/tests/corpus/3.14.2/generator_statement_reconstruction.pyc new file mode 100644 index 00000000..fbd66df9 Binary files /dev/null and b/tests/corpus/3.14.2/generator_statement_reconstruction.pyc differ diff --git a/tests/corpus/3.14.2/generator_walrus_cells.py b/tests/corpus/3.14.2/generator_walrus_cells.py new file mode 100644 index 00000000..c2aea4ad --- /dev/null +++ b/tests/corpus/3.14.2/generator_walrus_cells.py @@ -0,0 +1,8 @@ +def select(rows): + return tuple(isinstance(row, dict) and (change := row.get("change")) is not None and change >= 0 for row in rows) + + +def last_binding(rows): + change = "initial" + results = tuple((change := row.get("change")) for row in rows) + return results, change diff --git a/tests/corpus/3.14.2/generator_walrus_cells.pyc b/tests/corpus/3.14.2/generator_walrus_cells.pyc new file mode 100644 index 00000000..37df60df Binary files /dev/null and b/tests/corpus/3.14.2/generator_walrus_cells.pyc differ diff --git a/tests/corpus/3.14.2/handler_iterator_breaks.py b/tests/corpus/3.14.2/handler_iterator_breaks.py new file mode 100644 index 00000000..97ae7406 --- /dev/null +++ b/tests/corpus/3.14.2/handler_iterator_breaks.py @@ -0,0 +1,140 @@ +import asyncio + + +async def named_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +def sync_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + break + except Exception as error: + owner.log("unavailable", str(error)) + break + owner.log("finished", accepted) + return accepted + + +async def direct_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +async def unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + +async def direct_unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted diff --git a/tests/corpus/3.14.2/handler_iterator_breaks.pyc b/tests/corpus/3.14.2/handler_iterator_breaks.pyc new file mode 100644 index 00000000..e0c5484d Binary files /dev/null and b/tests/corpus/3.14.2/handler_iterator_breaks.pyc differ diff --git a/tests/corpus/3.14.2/loop_context_returns.py b/tests/corpus/3.14.2/loop_context_returns.py new file mode 100644 index 00000000..703d904c --- /dev/null +++ b/tests/corpus/3.14.2/loop_context_returns.py @@ -0,0 +1,64 @@ +def sync_value(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +def sync_length(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +def sync_mapping(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + +async def async_value(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +async def async_length(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +async def async_mapping(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} diff --git a/tests/corpus/3.14.2/loop_context_returns.pyc b/tests/corpus/3.14.2/loop_context_returns.pyc new file mode 100644 index 00000000..8b24a6bc Binary files /dev/null and b/tests/corpus/3.14.2/loop_context_returns.pyc differ diff --git a/tests/corpus/3.14.2/loop_handler_ownership.py b/tests/corpus/3.14.2/loop_handler_ownership.py new file mode 100644 index 00000000..c69d4d26 --- /dev/null +++ b/tests/corpus/3.14.2/loop_handler_ownership.py @@ -0,0 +1,119 @@ +def named(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def multiple(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + except KeyError as error: + owner.other(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def siblings(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + try: + owner.second(row) + except KeyError: + owner.other(row) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def same_type(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except Exception: + owner.failed(row) + owner.finish() + except Exception as error: + owner.fallback(str(error)) + return None + + +async def async_0(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_1(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_2(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + await owner.asecond(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +def outer_finally(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + finally: + owner.finish() + return None + + +def nonloop(owner): + try: + try: + owner.record(owner.row) + except ValueError: + owner.failed(owner.row) + except RuntimeError: + owner.fallback() + return None diff --git a/tests/corpus/3.14.2/loop_handler_ownership.pyc b/tests/corpus/3.14.2/loop_handler_ownership.pyc new file mode 100644 index 00000000..c40f2b9d Binary files /dev/null and b/tests/corpus/3.14.2/loop_handler_ownership.pyc differ diff --git a/tests/corpus/3.14.2/nested_context_returns.py b/tests/corpus/3.14.2/nested_context_returns.py new file mode 100644 index 00000000..5040cd90 --- /dev/null +++ b/tests/corpus/3.14.2/nested_context_returns.py @@ -0,0 +1,49 @@ +def sync_call(contexts, value): + with contexts[0]: + with contexts[1]: + return value() + + +def sync_mapping(contexts, value): + with contexts[0]: + with contexts[1]: + with contexts[2]: + return {"rows": value()} + + +async def async_call(contexts, value): + async with contexts[0]: + async with contexts[1]: + return value() + + +async def async_await(contexts, value): + async with contexts[0]: + async with contexts[1]: + async with contexts[2]: + return await value() + + +async def async_tuple(contexts, value): + async with contexts[0]: + async with contexts[1]: + return (value(),) + + +async def async_single(contexts, value): + async with contexts[0]: + return await value() + + +def sync_outer_return(contexts, value): + with contexts[0]: + with contexts[1]: + result = value() + return result + + +async def async_outer_return(contexts, value): + async with contexts[0]: + async with contexts[1]: + result = await value() + return result diff --git a/tests/corpus/3.14.2/nested_context_returns.pyc b/tests/corpus/3.14.2/nested_context_returns.pyc new file mode 100644 index 00000000..97baf745 Binary files /dev/null and b/tests/corpus/3.14.2/nested_context_returns.pyc differ diff --git a/tests/corpus/3.14.2/none_rejection_guards.py b/tests/corpus/3.14.2/none_rejection_guards.py new file mode 100644 index 00000000..c0c4e441 --- /dev/null +++ b/tests/corpus/3.14.2/none_rejection_guards.py @@ -0,0 +1,62 @@ +def in_range(value): + if value is None or not 0.0 <= value <= 15.0: + return False + return True + + +def called(read): + if read is None or not 0.0 <= read() <= 15.0: + return False + return True + + +def multi(first, second): + if first is None or not 0.0 <= first() <= second() <= 15.0: + return False + return True + + +def separate(read): + if read is None or not (0.0 <= read() and read() <= 15.0): + return False + return True + + +def same_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return False + return False + + +def integer_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return 3 + return 5 + + +def string_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return "reject" + return "accept" + + +def collect(rows): + result = [] + for row in rows: + a = row.get("a") + b = row.get("b") + c = row.get("c") + d = row.get("d") + if a is None or b is None or c is None or d is None or d < 0.01: + continue + result.append(a + b + c + d) + return result + + +def unpacked(rows): + result = [] + for a, b, c, d in rows: + if a is None or b is None or c is None or d is None or d < 0.01: + continue + result.append(a + b + c + d) + return result diff --git a/tests/corpus/3.14.2/none_rejection_guards.pyc b/tests/corpus/3.14.2/none_rejection_guards.pyc new file mode 100644 index 00000000..4b95f7ac Binary files /dev/null and b/tests/corpus/3.14.2/none_rejection_guards.pyc differ diff --git a/tests/corpus/3.14.2/normal_async_exits.py b/tests/corpus/3.14.2/normal_async_exits.py new file mode 100644 index 00000000..78cc6e22 --- /dev/null +++ b/tests/corpus/3.14.2/normal_async_exits.py @@ -0,0 +1,85 @@ +async def normal_1(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + value = owner.build(row0, row0) + return {"value": value, "active": owner.active} + + +async def normal_2(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + value = owner.build(row0, row1) + return {"value": value, "active": owner.active} + + +async def normal_4(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + async with owner.query("row2") as query: + row2 = await query.read() + async with owner.query("row3") as query: + row3 = await query.read() + value = owner.build(row0, row3) + return {"value": value, "active": owner.active} + + +async def constant_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return 4 + + +async def constant_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return 4 + + +async def carried_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return row0 + + +async def computed_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return owner.build(row0, row0) + + +async def mapping_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return {"value": row0} + + +async def user_await(owner): + async with owner: + await owner.user(None, None, None) + return owner.build(1, 2) + + +async def branch_return(owner): + async with owner: + if owner.ready: + return 4 + await owner.write(1, 2) + return 2 diff --git a/tests/corpus/3.14.2/normal_async_exits.pyc b/tests/corpus/3.14.2/normal_async_exits.pyc new file mode 100644 index 00000000..dd9c8435 Binary files /dev/null and b/tests/corpus/3.14.2/normal_async_exits.pyc differ diff --git a/tests/corpus/3.14.2/padded_none_returns_3.py b/tests/corpus/3.14.2/padded_none_returns_3.py new file mode 100644 index 00000000..c23f4191 --- /dev/null +++ b/tests/corpus/3.14.2/padded_none_returns_3.py @@ -0,0 +1,37 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + stamp = datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + return stamp + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.14.2/padded_none_returns_3.pyc b/tests/corpus/3.14.2/padded_none_returns_3.pyc new file mode 100644 index 00000000..abf44cf6 Binary files /dev/null and b/tests/corpus/3.14.2/padded_none_returns_3.pyc differ diff --git a/tests/corpus/3.14.2/private_handler_clauses.py b/tests/corpus/3.14.2/private_handler_clauses.py new file mode 100644 index 00000000..082f02b7 --- /dev/null +++ b/tests/corpus/3.14.2/private_handler_clauses.py @@ -0,0 +1,163 @@ +import asyncio + + +async def named(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +def synchronous(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + continue + except Exception as error: + owner.log("unavailable", str(error)) + continue + owner.log("finished", accepted) + return accepted + + +async def attributes(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def local_attributes(owner, faults): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def single_clause(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + await owner.log("finished", accepted) + return accepted + + +async def no_private_try(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted diff --git a/tests/corpus/3.14.2/private_handler_clauses.pyc b/tests/corpus/3.14.2/private_handler_clauses.pyc new file mode 100644 index 00000000..8577f731 Binary files /dev/null and b/tests/corpus/3.14.2/private_handler_clauses.pyc differ diff --git a/tests/corpus/3.14.2/shared_validation_raises.py b/tests/corpus/3.14.2/shared_validation_raises.py new file mode 100644 index 00000000..eb634f66 --- /dev/null +++ b/tests/corpus/3.14.2/shared_validation_raises.py @@ -0,0 +1,46 @@ +def plain(value, bad, ready, make_error): + if value is None or bad(value) or not ready(value): + raise make_error("invalid", value=value) + return value + + +def chained(value, read, invalid, make_error): + if value is None or not 0 <= read(value) <= 10 or invalid(value): + raise make_error("range failed") + return value + + +def cause(value, reject, make_error, make_cause): + if value is None or reject(value): + raise make_error("invalid", value=value) from make_cause(value) + return value + + +def suppress(value, reject, make_error): + if value is None or reject(value): + raise make_error("invalid", value=value) from None + return value + + +def formatted(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") from make_cause() + return value + + +def ascii_spec(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!a}: {value:{width}}") from make_cause() + return value + + +def formatted_plain(value, label, width, read, reject, make_error): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") + return value + + +def constant_error(value, reject): + if value is None or reject(value): + raise ValueError + return value diff --git a/tests/corpus/3.14.2/shared_validation_raises.pyc b/tests/corpus/3.14.2/shared_validation_raises.pyc new file mode 100644 index 00000000..44d77043 Binary files /dev/null and b/tests/corpus/3.14.2/shared_validation_raises.pyc differ diff --git a/tests/corpus/3.14.2/sunk_context_exits.py b/tests/corpus/3.14.2/sunk_context_exits.py new file mode 100644 index 00000000..37288190 --- /dev/null +++ b/tests/corpus/3.14.2/sunk_context_exits.py @@ -0,0 +1,52 @@ +async def false(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return False + record("outside") + return True + + +async def bare(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return + record("outside") + return True + + +async def twice(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return False + if release: + return False + record("outside") + return True + + +async def value(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return release + record("outside") + return True + + +async def user_await(lock, pending, release, user_call): + async with lock: + record("body") + await user_call(None, None, None) + record("outside") + return True + + +async def normal(lock, pending, release, user_call): + async with lock: + record("body") + record("ready") + record("outside") + return True diff --git a/tests/corpus/3.14.2/sunk_context_exits.pyc b/tests/corpus/3.14.2/sunk_context_exits.pyc new file mode 100644 index 00000000..4751ce12 Binary files /dev/null and b/tests/corpus/3.14.2/sunk_context_exits.pyc differ diff --git a/tests/corpus/3.15.2/async_context_return_padding_1.py b/tests/corpus/3.15.2/async_context_return_padding_1.py new file mode 100644 index 00000000..d4ef2843 --- /dev/null +++ b/tests/corpus/3.15.2/async_context_return_padding_1.py @@ -0,0 +1,12 @@ +async def constant_one(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return 4 + + +async def padded_mapping(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return {'value': row0} diff --git a/tests/corpus/3.15.2/async_context_return_padding_1.pyc b/tests/corpus/3.15.2/async_context_return_padding_1.pyc new file mode 100644 index 00000000..431ffd48 Binary files /dev/null and b/tests/corpus/3.15.2/async_context_return_padding_1.pyc differ diff --git a/tests/corpus/3.15.2/async_context_return_padding_3.py b/tests/corpus/3.15.2/async_context_return_padding_3.py new file mode 100644 index 00000000..d4ef2843 --- /dev/null +++ b/tests/corpus/3.15.2/async_context_return_padding_3.py @@ -0,0 +1,12 @@ +async def constant_one(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return 4 + + +async def padded_mapping(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return {'value': row0} diff --git a/tests/corpus/3.15.2/async_context_return_padding_3.pyc b/tests/corpus/3.15.2/async_context_return_padding_3.pyc new file mode 100644 index 00000000..ba3c1b2a Binary files /dev/null and b/tests/corpus/3.15.2/async_context_return_padding_3.pyc differ diff --git a/tests/corpus/3.15.2/async_context_return_padding_9.py b/tests/corpus/3.15.2/async_context_return_padding_9.py new file mode 100644 index 00000000..d4ef2843 --- /dev/null +++ b/tests/corpus/3.15.2/async_context_return_padding_9.py @@ -0,0 +1,12 @@ +async def constant_one(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return 4 + + +async def padded_mapping(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return {'value': row0} diff --git a/tests/corpus/3.15.2/async_context_return_padding_9.pyc b/tests/corpus/3.15.2/async_context_return_padding_9.pyc new file mode 100644 index 00000000..00258ee2 Binary files /dev/null and b/tests/corpus/3.15.2/async_context_return_padding_9.pyc differ diff --git a/tests/corpus/3.15.2/await_context_controls.py b/tests/corpus/3.15.2/await_context_controls.py new file mode 100644 index 00000000..cfa905b1 --- /dev/null +++ b/tests/corpus/3.15.2/await_context_controls.py @@ -0,0 +1,26 @@ +async def sequential_contexts(manager, work, record, flag): + async with manager: + a = await work("a") + async with manager: + b = await work("b") + return a, b + + +async def awaited_then_context(manager, work, record, flag): + try: + a = await work("a") + async with manager: + b = await work("b") + except ValueError: + return "handled" + return a, b + + +async def outer_handler(manager, work, record, flag): + try: + async with manager: + a = await work("a") + b = await work("b") + except ValueError: + return "handled" + return a, b diff --git a/tests/corpus/3.15.2/await_context_controls.pyc b/tests/corpus/3.15.2/await_context_controls.pyc new file mode 100644 index 00000000..9f98af22 Binary files /dev/null and b/tests/corpus/3.15.2/await_context_controls.pyc differ diff --git a/tests/corpus/3.15.2/await_exception_regions.py b/tests/corpus/3.15.2/await_exception_regions.py new file mode 100644 index 00000000..383e0fa6 --- /dev/null +++ b/tests/corpus/3.15.2/await_exception_regions.py @@ -0,0 +1,19 @@ +async def awaited_finally(manager, work, record, flag): + async with manager: + try: + await work("a") + finally: + record("cleanup") + await work("b") + return "tail" + + +async def awaited_else(manager, work, record, flag): + async with manager: + try: + a = await work("a") + except ValueError: + record("handled") + else: + await work("b") + return "tail" diff --git a/tests/corpus/3.15.2/await_exception_regions.pyc b/tests/corpus/3.15.2/await_exception_regions.pyc new file mode 100644 index 00000000..f8ad7890 Binary files /dev/null and b/tests/corpus/3.15.2/await_exception_regions.pyc differ diff --git a/tests/corpus/3.15.2/bounded_formatting.py b/tests/corpus/3.15.2/bounded_formatting.py new file mode 100644 index 00000000..a03afc74 --- /dev/null +++ b/tests/corpus/3.15.2/bounded_formatting.py @@ -0,0 +1,10 @@ +def simple(value): + return f"{value}" + + +def conversions(label, value, width): + return f"{label!r}: {value!s:>{width}}" + + +def ascii_spec(label, value, width): + return f"{label!a}: {value:{width}}" diff --git a/tests/corpus/3.15.2/bounded_formatting.pyc b/tests/corpus/3.15.2/bounded_formatting.pyc new file mode 100644 index 00000000..215391f2 Binary files /dev/null and b/tests/corpus/3.15.2/bounded_formatting.pyc differ diff --git a/tests/corpus/3.15.2/bounded_generator_expressions.py b/tests/corpus/3.15.2/bounded_generator_expressions.py new file mode 100644 index 00000000..24dd6749 --- /dev/null +++ b/tests/corpus/3.15.2/bounded_generator_expressions.py @@ -0,0 +1,14 @@ +def plain(values): + return collect(value for value in values) + + +def captured(values, offset): + return collect(value + offset for value in values) + + +def multiple(values, first, second): + return collect((value + first) * second for value in values) + + +def ordinary_function(values): + return (lambda value: value)(values) diff --git a/tests/corpus/3.15.2/bounded_generator_expressions.pyc b/tests/corpus/3.15.2/bounded_generator_expressions.pyc new file mode 100644 index 00000000..de3d0495 Binary files /dev/null and b/tests/corpus/3.15.2/bounded_generator_expressions.pyc differ diff --git a/tests/corpus/3.15.2/bounded_try_returns_3.py b/tests/corpus/3.15.2/bounded_try_returns_3.py new file mode 100644 index 00000000..b1b14365 --- /dev/null +++ b/tests/corpus/3.15.2/bounded_try_returns_3.py @@ -0,0 +1,36 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + return datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.15.2/bounded_try_returns_3.pyc b/tests/corpus/3.15.2/bounded_try_returns_3.pyc new file mode 100644 index 00000000..fc066ad6 Binary files /dev/null and b/tests/corpus/3.15.2/bounded_try_returns_3.pyc differ diff --git a/tests/corpus/3.15.2/carried_value_returns_3.py b/tests/corpus/3.15.2/carried_value_returns_3.py new file mode 100644 index 00000000..7c0f657e --- /dev/null +++ b/tests/corpus/3.15.2/carried_value_returns_3.py @@ -0,0 +1,47 @@ +# A computed return can be outside the protected range but still use its value. + + +def call_return(value, make, fallback, default): + if value: + try: + return make(value) + except Exception: + pass + return fallback.read().result or default.result + + +def attribute_return(value, make, fallback, default): + if value: + try: + return make(value).result + except Exception: + pass + return fallback.read().result or default.result + + +def tuple_return(value, make, fallback, default): + if value: + try: + return (make(value), 7) + except Exception: + pass + return fallback.read().result or default.result + + +def mapping_return(value, make, fallback, default): + if value: + try: + return {"value": make(value)} + except Exception: + pass + return fallback.read().result or default.result + + +def bare_return(value, make, fallback, default): + if value: + try: + make(value) + return None + except Exception: + return "handled" + return fallback.read().result or default.result diff --git a/tests/corpus/3.15.2/carried_value_returns_3.pyc b/tests/corpus/3.15.2/carried_value_returns_3.pyc new file mode 100644 index 00000000..c21c38b3 Binary files /dev/null and b/tests/corpus/3.15.2/carried_value_returns_3.pyc differ diff --git a/tests/corpus/3.15.2/comprehension_chained_ternaries.py b/tests/corpus/3.15.2/comprehension_chained_ternaries.py new file mode 100644 index 00000000..4130711f --- /dev/null +++ b/tests/corpus/3.15.2/comprehension_chained_ternaries.py @@ -0,0 +1,18 @@ +def mapping_value(values): + return {value: 1 if 6 <= value <= 8 else 0 for value in values} + + +def mapping_key(values): + return {(value if 6 <= value <= 8 else -value): value for value in values} + + +def arithmetic(values): + return [0.6 + 0.4 * (1.0 if 4 <= value <= 8 < 10 else 0.3) for value in values] + + +def filtered(values): + return [1 if value >= 6 else 0 for value in values if value <= 8] + + +def observed(values, base, coefficient, lower, probe, upper): + return {value: base(value) + coefficient(value) * (1.0 if lower() <= probe(value) <= upper() else 0.3) for value in values} diff --git a/tests/corpus/3.15.2/comprehension_chained_ternaries.pyc b/tests/corpus/3.15.2/comprehension_chained_ternaries.pyc new file mode 100644 index 00000000..91931c0a Binary files /dev/null and b/tests/corpus/3.15.2/comprehension_chained_ternaries.pyc differ diff --git a/tests/corpus/3.15.2/constructed_rejection_guards.py b/tests/corpus/3.15.2/constructed_rejection_guards.py new file mode 100644 index 00000000..7696b0e5 --- /dev/null +++ b/tests/corpus/3.15.2/constructed_rejection_guards.py @@ -0,0 +1,54 @@ +def constructed(value, build): + if value is None or not 0.0 <= value <= 20.0: + return build("reject", amount=0) + return build("accept", amount=value) + + +def truth_guard(value, build): + if not value or len(value) < 2: + return build("reject", amount=0) + return build("accept", amount=len(value)) + + +def positive_guard(value, invalid, build): + if invalid(value) or value < 2: + return build("reject", amount=value) + return build("accept", amount=value) + + +def constant_truth(value): + if not value or len(value) < 2: + return False + return True + + +def list_result(value): + if value is None or not 0.0 <= value <= 20.0: + return ["reject", value] + return ["accept", value] + + +def mapping_result(value): + if value is None or not 0.0 <= value <= 20.0: + return {"kind": "reject", "value": value} + return {"kind": "accept", "value": value} + + +def generator_result(value, rows, collect, scale): + if value is None or not 0.0 <= value <= 20.0: + return collect(scale * item for item in rows) + return collect(rows) + + +def stored_result(value, build): + if not value or len(value) < 2: + result = build("reject", amount=0) + return result + return build("accept", amount=len(value)) + + +def effectful_body(value, build, log): + if not value or len(value) < 2: + log("rejected") + return build("reject", amount=0) + return build("accept", amount=len(value)) diff --git a/tests/corpus/3.15.2/constructed_rejection_guards.pyc b/tests/corpus/3.15.2/constructed_rejection_guards.pyc new file mode 100644 index 00000000..4bdb6822 Binary files /dev/null and b/tests/corpus/3.15.2/constructed_rejection_guards.pyc differ diff --git a/tests/corpus/3.15.2/context_exit_accounting.py b/tests/corpus/3.15.2/context_exit_accounting.py new file mode 100644 index 00000000..aa8d62ca --- /dev/null +++ b/tests/corpus/3.15.2/context_exit_accounting.py @@ -0,0 +1,26 @@ +async def guarded_return(manager, work, record, flag): + async with manager: + if flag: + return "early" + try: + return await work("a") + finally: + record("cleanup") + + +async def computed_return(manager, work, record, flag): + async with manager: + try: + return await work("a") + finally: + record("cleanup") + + +async def none_guard(manager, work, record, flag): + async with manager: + if flag is None: + return 3 + try: + return await work("a") + finally: + record("cleanup") diff --git a/tests/corpus/3.15.2/context_exit_accounting.pyc b/tests/corpus/3.15.2/context_exit_accounting.pyc new file mode 100644 index 00000000..544bfe7b Binary files /dev/null and b/tests/corpus/3.15.2/context_exit_accounting.pyc differ diff --git a/tests/corpus/3.15.2/context_manager_returns.py b/tests/corpus/3.15.2/context_manager_returns.py new file mode 100644 index 00000000..02534690 --- /dev/null +++ b/tests/corpus/3.15.2/context_manager_returns.py @@ -0,0 +1,18 @@ +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result diff --git a/tests/corpus/3.15.2/context_manager_returns.pyc b/tests/corpus/3.15.2/context_manager_returns.pyc new file mode 100644 index 00000000..aa391bdd Binary files /dev/null and b/tests/corpus/3.15.2/context_manager_returns.pyc differ diff --git a/tests/corpus/3.15.2/filtered_generator_boolean_values.py b/tests/corpus/3.15.2/filtered_generator_boolean_values.py new file mode 100644 index 00000000..b80c25db --- /dev/null +++ b/tests/corpus/3.15.2/filtered_generator_boolean_values.py @@ -0,0 +1,18 @@ +def or_value(rows): + return tuple(row[1] or 0 for row in rows if row[0]) + + +def and_value(rows): + return tuple(row[1] and row[2] for row in rows if row[0]) + + +def chained_or(rows): + return tuple(row[1] or row[2] or 0 for row in rows if row[0]) + + +def mixed_value(rows): + return tuple((row[1] and row[2]) or 0 for row in rows if row[0]) + + +def none_filter(rows): + return tuple(row.get("amount") or 0.0 for row in rows if row.get("amount") is not None) diff --git a/tests/corpus/3.15.2/filtered_generator_boolean_values.pyc b/tests/corpus/3.15.2/filtered_generator_boolean_values.pyc new file mode 100644 index 00000000..990451f5 Binary files /dev/null and b/tests/corpus/3.15.2/filtered_generator_boolean_values.pyc differ diff --git a/tests/corpus/3.15.2/for_loop_disjunctions.py b/tests/corpus/3.15.2/for_loop_disjunctions.py new file mode 100644 index 00000000..795af584 --- /dev/null +++ b/tests/corpus/3.15.2/for_loop_disjunctions.py @@ -0,0 +1,131 @@ +def call_only(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + return result + + +def call_else(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def truth_only(values, test): + result = [] + for value in values: + if not value or value < 0.2: + result.append("dry") + return result + + +def truth_else(values, test): + result = [] + for value in values: + if not value or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def none_only(values, test): + result = [] + for value in values: + if value is None or value < 0.2: + result.append("dry") + return result + + +def none_else(values, test): + result = [] + for value in values: + if value is None or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def positive_only(values, test): + result = [] + for value in values: + if test(value) or value < 0.2: + result.append("dry") + return result + + +def positive_else(values, test): + result = [] + for value in values: + if test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def rotated_while(values, test): + result = [] + value = len(values) + while value < 2 or value == 3: + result.append(value) + value += 2 + return result + + +def break_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + break + else: + result.append(value) + else: + result.append('complete') + result.append('tail') + return result + + +def extended_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + else: + result.append("wet") + return result diff --git a/tests/corpus/3.15.2/for_loop_disjunctions.pyc b/tests/corpus/3.15.2/for_loop_disjunctions.pyc new file mode 100644 index 00000000..60d9f6c2 Binary files /dev/null and b/tests/corpus/3.15.2/for_loop_disjunctions.pyc differ diff --git a/tests/corpus/3.15.2/generator_statement_reconstruction.py b/tests/corpus/3.15.2/generator_statement_reconstruction.py new file mode 100644 index 00000000..44c9a23a --- /dev/null +++ b/tests/corpus/3.15.2/generator_statement_reconstruction.py @@ -0,0 +1,16 @@ +def repeated_targets(values): + return tuple(value is not None for value, discarded, discarded, discarded in values) + + +def real_underscore(values): + return tuple((value, discarded, _) for value, _, discarded, discarded in values) + + +def nested_calls(rows): + return sorted(sum(value for value in row) / len(row) for row in rows) + + +def captured_cells(rows): + deltas = rows + factory = sum + return sorted(sum(factory(deltas) for _ in deltas) / len(deltas) for _ in range(3)) diff --git a/tests/corpus/3.15.2/generator_statement_reconstruction.pyc b/tests/corpus/3.15.2/generator_statement_reconstruction.pyc new file mode 100644 index 00000000..4ef14ad8 Binary files /dev/null and b/tests/corpus/3.15.2/generator_statement_reconstruction.pyc differ diff --git a/tests/corpus/3.15.2/generator_walrus_cells.py b/tests/corpus/3.15.2/generator_walrus_cells.py new file mode 100644 index 00000000..c2aea4ad --- /dev/null +++ b/tests/corpus/3.15.2/generator_walrus_cells.py @@ -0,0 +1,8 @@ +def select(rows): + return tuple(isinstance(row, dict) and (change := row.get("change")) is not None and change >= 0 for row in rows) + + +def last_binding(rows): + change = "initial" + results = tuple((change := row.get("change")) for row in rows) + return results, change diff --git a/tests/corpus/3.15.2/generator_walrus_cells.pyc b/tests/corpus/3.15.2/generator_walrus_cells.pyc new file mode 100644 index 00000000..d325ca8d Binary files /dev/null and b/tests/corpus/3.15.2/generator_walrus_cells.pyc differ diff --git a/tests/corpus/3.15.2/handler_iterator_breaks.py b/tests/corpus/3.15.2/handler_iterator_breaks.py new file mode 100644 index 00000000..97ae7406 --- /dev/null +++ b/tests/corpus/3.15.2/handler_iterator_breaks.py @@ -0,0 +1,140 @@ +import asyncio + + +async def named_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +def sync_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + break + except Exception as error: + owner.log("unavailable", str(error)) + break + owner.log("finished", accepted) + return accepted + + +async def direct_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +async def unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + +async def direct_unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted diff --git a/tests/corpus/3.15.2/handler_iterator_breaks.pyc b/tests/corpus/3.15.2/handler_iterator_breaks.pyc new file mode 100644 index 00000000..cf260ed1 Binary files /dev/null and b/tests/corpus/3.15.2/handler_iterator_breaks.pyc differ diff --git a/tests/corpus/3.15.2/loop_context_returns.py b/tests/corpus/3.15.2/loop_context_returns.py new file mode 100644 index 00000000..703d904c --- /dev/null +++ b/tests/corpus/3.15.2/loop_context_returns.py @@ -0,0 +1,64 @@ +def sync_value(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +def sync_length(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +def sync_mapping(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + +async def async_value(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +async def async_length(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +async def async_mapping(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} diff --git a/tests/corpus/3.15.2/loop_context_returns.pyc b/tests/corpus/3.15.2/loop_context_returns.pyc new file mode 100644 index 00000000..fd2011ba Binary files /dev/null and b/tests/corpus/3.15.2/loop_context_returns.pyc differ diff --git a/tests/corpus/3.15.2/loop_handler_ownership.py b/tests/corpus/3.15.2/loop_handler_ownership.py new file mode 100644 index 00000000..c69d4d26 --- /dev/null +++ b/tests/corpus/3.15.2/loop_handler_ownership.py @@ -0,0 +1,119 @@ +def named(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def multiple(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + except KeyError as error: + owner.other(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def siblings(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + try: + owner.second(row) + except KeyError: + owner.other(row) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def same_type(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except Exception: + owner.failed(row) + owner.finish() + except Exception as error: + owner.fallback(str(error)) + return None + + +async def async_0(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_1(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_2(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + await owner.asecond(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +def outer_finally(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + finally: + owner.finish() + return None + + +def nonloop(owner): + try: + try: + owner.record(owner.row) + except ValueError: + owner.failed(owner.row) + except RuntimeError: + owner.fallback() + return None diff --git a/tests/corpus/3.15.2/loop_handler_ownership.pyc b/tests/corpus/3.15.2/loop_handler_ownership.pyc new file mode 100644 index 00000000..98019758 Binary files /dev/null and b/tests/corpus/3.15.2/loop_handler_ownership.pyc differ diff --git a/tests/corpus/3.15.2/nested_context_returns.py b/tests/corpus/3.15.2/nested_context_returns.py new file mode 100644 index 00000000..5040cd90 --- /dev/null +++ b/tests/corpus/3.15.2/nested_context_returns.py @@ -0,0 +1,49 @@ +def sync_call(contexts, value): + with contexts[0]: + with contexts[1]: + return value() + + +def sync_mapping(contexts, value): + with contexts[0]: + with contexts[1]: + with contexts[2]: + return {"rows": value()} + + +async def async_call(contexts, value): + async with contexts[0]: + async with contexts[1]: + return value() + + +async def async_await(contexts, value): + async with contexts[0]: + async with contexts[1]: + async with contexts[2]: + return await value() + + +async def async_tuple(contexts, value): + async with contexts[0]: + async with contexts[1]: + return (value(),) + + +async def async_single(contexts, value): + async with contexts[0]: + return await value() + + +def sync_outer_return(contexts, value): + with contexts[0]: + with contexts[1]: + result = value() + return result + + +async def async_outer_return(contexts, value): + async with contexts[0]: + async with contexts[1]: + result = await value() + return result diff --git a/tests/corpus/3.15.2/nested_context_returns.pyc b/tests/corpus/3.15.2/nested_context_returns.pyc new file mode 100644 index 00000000..cbb56edc Binary files /dev/null and b/tests/corpus/3.15.2/nested_context_returns.pyc differ diff --git a/tests/corpus/3.15.2/none_rejection_guards.py b/tests/corpus/3.15.2/none_rejection_guards.py new file mode 100644 index 00000000..c0c4e441 --- /dev/null +++ b/tests/corpus/3.15.2/none_rejection_guards.py @@ -0,0 +1,62 @@ +def in_range(value): + if value is None or not 0.0 <= value <= 15.0: + return False + return True + + +def called(read): + if read is None or not 0.0 <= read() <= 15.0: + return False + return True + + +def multi(first, second): + if first is None or not 0.0 <= first() <= second() <= 15.0: + return False + return True + + +def separate(read): + if read is None or not (0.0 <= read() and read() <= 15.0): + return False + return True + + +def same_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return False + return False + + +def integer_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return 3 + return 5 + + +def string_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return "reject" + return "accept" + + +def collect(rows): + result = [] + for row in rows: + a = row.get("a") + b = row.get("b") + c = row.get("c") + d = row.get("d") + if a is None or b is None or c is None or d is None or d < 0.01: + continue + result.append(a + b + c + d) + return result + + +def unpacked(rows): + result = [] + for a, b, c, d in rows: + if a is None or b is None or c is None or d is None or d < 0.01: + continue + result.append(a + b + c + d) + return result diff --git a/tests/corpus/3.15.2/none_rejection_guards.pyc b/tests/corpus/3.15.2/none_rejection_guards.pyc new file mode 100644 index 00000000..93d96024 Binary files /dev/null and b/tests/corpus/3.15.2/none_rejection_guards.pyc differ diff --git a/tests/corpus/3.15.2/normal_async_exits.py b/tests/corpus/3.15.2/normal_async_exits.py new file mode 100644 index 00000000..78cc6e22 --- /dev/null +++ b/tests/corpus/3.15.2/normal_async_exits.py @@ -0,0 +1,85 @@ +async def normal_1(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + value = owner.build(row0, row0) + return {"value": value, "active": owner.active} + + +async def normal_2(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + value = owner.build(row0, row1) + return {"value": value, "active": owner.active} + + +async def normal_4(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + async with owner.query("row2") as query: + row2 = await query.read() + async with owner.query("row3") as query: + row3 = await query.read() + value = owner.build(row0, row3) + return {"value": value, "active": owner.active} + + +async def constant_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return 4 + + +async def constant_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return 4 + + +async def carried_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return row0 + + +async def computed_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return owner.build(row0, row0) + + +async def mapping_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return {"value": row0} + + +async def user_await(owner): + async with owner: + await owner.user(None, None, None) + return owner.build(1, 2) + + +async def branch_return(owner): + async with owner: + if owner.ready: + return 4 + await owner.write(1, 2) + return 2 diff --git a/tests/corpus/3.15.2/normal_async_exits.pyc b/tests/corpus/3.15.2/normal_async_exits.pyc new file mode 100644 index 00000000..456685ae Binary files /dev/null and b/tests/corpus/3.15.2/normal_async_exits.pyc differ diff --git a/tests/corpus/3.15.2/padded_none_returns_3.py b/tests/corpus/3.15.2/padded_none_returns_3.py new file mode 100644 index 00000000..c23f4191 --- /dev/null +++ b/tests/corpus/3.15.2/padded_none_returns_3.py @@ -0,0 +1,37 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + stamp = datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + return stamp + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/corpus/3.15.2/padded_none_returns_3.pyc b/tests/corpus/3.15.2/padded_none_returns_3.pyc new file mode 100644 index 00000000..d66ceba8 Binary files /dev/null and b/tests/corpus/3.15.2/padded_none_returns_3.pyc differ diff --git a/tests/corpus/3.15.2/private_handler_clauses.py b/tests/corpus/3.15.2/private_handler_clauses.py new file mode 100644 index 00000000..082f02b7 --- /dev/null +++ b/tests/corpus/3.15.2/private_handler_clauses.py @@ -0,0 +1,163 @@ +import asyncio + + +async def named(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +def synchronous(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + continue + except Exception as error: + owner.log("unavailable", str(error)) + continue + owner.log("finished", accepted) + return accepted + + +async def attributes(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def local_attributes(owner, faults): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def single_clause(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + await owner.log("finished", accepted) + return accepted + + +async def no_private_try(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted diff --git a/tests/corpus/3.15.2/private_handler_clauses.pyc b/tests/corpus/3.15.2/private_handler_clauses.pyc new file mode 100644 index 00000000..1c2b8d6a Binary files /dev/null and b/tests/corpus/3.15.2/private_handler_clauses.pyc differ diff --git a/tests/corpus/3.15.2/shared_validation_raises.py b/tests/corpus/3.15.2/shared_validation_raises.py new file mode 100644 index 00000000..eb634f66 --- /dev/null +++ b/tests/corpus/3.15.2/shared_validation_raises.py @@ -0,0 +1,46 @@ +def plain(value, bad, ready, make_error): + if value is None or bad(value) or not ready(value): + raise make_error("invalid", value=value) + return value + + +def chained(value, read, invalid, make_error): + if value is None or not 0 <= read(value) <= 10 or invalid(value): + raise make_error("range failed") + return value + + +def cause(value, reject, make_error, make_cause): + if value is None or reject(value): + raise make_error("invalid", value=value) from make_cause(value) + return value + + +def suppress(value, reject, make_error): + if value is None or reject(value): + raise make_error("invalid", value=value) from None + return value + + +def formatted(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") from make_cause() + return value + + +def ascii_spec(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!a}: {value:{width}}") from make_cause() + return value + + +def formatted_plain(value, label, width, read, reject, make_error): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") + return value + + +def constant_error(value, reject): + if value is None or reject(value): + raise ValueError + return value diff --git a/tests/corpus/3.15.2/shared_validation_raises.pyc b/tests/corpus/3.15.2/shared_validation_raises.pyc new file mode 100644 index 00000000..f43b00a3 Binary files /dev/null and b/tests/corpus/3.15.2/shared_validation_raises.pyc differ diff --git a/tests/corpus/3.15.2/sunk_context_exits.py b/tests/corpus/3.15.2/sunk_context_exits.py new file mode 100644 index 00000000..37288190 --- /dev/null +++ b/tests/corpus/3.15.2/sunk_context_exits.py @@ -0,0 +1,52 @@ +async def false(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return False + record("outside") + return True + + +async def bare(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return + record("outside") + return True + + +async def twice(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return False + if release: + return False + record("outside") + return True + + +async def value(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return release + record("outside") + return True + + +async def user_await(lock, pending, release, user_call): + async with lock: + record("body") + await user_call(None, None, None) + record("outside") + return True + + +async def normal(lock, pending, release, user_call): + async with lock: + record("body") + record("ready") + record("outside") + return True diff --git a/tests/corpus/3.15.2/sunk_context_exits.pyc b/tests/corpus/3.15.2/sunk_context_exits.pyc new file mode 100644 index 00000000..4c1c856c Binary files /dev/null and b/tests/corpus/3.15.2/sunk_context_exits.pyc differ diff --git a/tests/corpus/3.9.25/context_manager_returns.py b/tests/corpus/3.9.25/context_manager_returns.py new file mode 100644 index 00000000..02534690 --- /dev/null +++ b/tests/corpus/3.9.25/context_manager_returns.py @@ -0,0 +1,18 @@ +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result diff --git a/tests/corpus/3.9.25/context_manager_returns.pyc b/tests/corpus/3.9.25/context_manager_returns.pyc new file mode 100644 index 00000000..90ed8f60 Binary files /dev/null and b/tests/corpus/3.9.25/context_manager_returns.pyc differ diff --git a/tests/data/async_context_return_padding.py b/tests/data/async_context_return_padding.py new file mode 100644 index 00000000..d4ef2843 --- /dev/null +++ b/tests/data/async_context_return_padding.py @@ -0,0 +1,12 @@ +async def constant_one(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return 4 + + +async def padded_mapping(owner): + async with owner: + async with owner.query('row0') as query: + row0 = await query.read() + return {'value': row0} diff --git a/tests/data/await_context_controls.py b/tests/data/await_context_controls.py new file mode 100644 index 00000000..cfa905b1 --- /dev/null +++ b/tests/data/await_context_controls.py @@ -0,0 +1,26 @@ +async def sequential_contexts(manager, work, record, flag): + async with manager: + a = await work("a") + async with manager: + b = await work("b") + return a, b + + +async def awaited_then_context(manager, work, record, flag): + try: + a = await work("a") + async with manager: + b = await work("b") + except ValueError: + return "handled" + return a, b + + +async def outer_handler(manager, work, record, flag): + try: + async with manager: + a = await work("a") + b = await work("b") + except ValueError: + return "handled" + return a, b diff --git a/tests/data/await_exception_regions.py b/tests/data/await_exception_regions.py new file mode 100644 index 00000000..383e0fa6 --- /dev/null +++ b/tests/data/await_exception_regions.py @@ -0,0 +1,19 @@ +async def awaited_finally(manager, work, record, flag): + async with manager: + try: + await work("a") + finally: + record("cleanup") + await work("b") + return "tail" + + +async def awaited_else(manager, work, record, flag): + async with manager: + try: + a = await work("a") + except ValueError: + record("handled") + else: + await work("b") + return "tail" diff --git a/tests/data/bounded_formatting.py b/tests/data/bounded_formatting.py new file mode 100644 index 00000000..a03afc74 --- /dev/null +++ b/tests/data/bounded_formatting.py @@ -0,0 +1,10 @@ +def simple(value): + return f"{value}" + + +def conversions(label, value, width): + return f"{label!r}: {value!s:>{width}}" + + +def ascii_spec(label, value, width): + return f"{label!a}: {value:{width}}" diff --git a/tests/data/bounded_generator_expressions.py b/tests/data/bounded_generator_expressions.py new file mode 100644 index 00000000..24dd6749 --- /dev/null +++ b/tests/data/bounded_generator_expressions.py @@ -0,0 +1,14 @@ +def plain(values): + return collect(value for value in values) + + +def captured(values, offset): + return collect(value + offset for value in values) + + +def multiple(values, first, second): + return collect((value + first) * second for value in values) + + +def ordinary_function(values): + return (lambda value: value)(values) diff --git a/tests/data/bounded_try_returns.py b/tests/data/bounded_try_returns.py new file mode 100644 index 00000000..b1b14365 --- /dev/null +++ b/tests/data/bounded_try_returns.py @@ -0,0 +1,36 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + return datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/data/carried_value_returns.py b/tests/data/carried_value_returns.py new file mode 100644 index 00000000..7c0f657e --- /dev/null +++ b/tests/data/carried_value_returns.py @@ -0,0 +1,47 @@ +# A computed return can be outside the protected range but still use its value. + + +def call_return(value, make, fallback, default): + if value: + try: + return make(value) + except Exception: + pass + return fallback.read().result or default.result + + +def attribute_return(value, make, fallback, default): + if value: + try: + return make(value).result + except Exception: + pass + return fallback.read().result or default.result + + +def tuple_return(value, make, fallback, default): + if value: + try: + return (make(value), 7) + except Exception: + pass + return fallback.read().result or default.result + + +def mapping_return(value, make, fallback, default): + if value: + try: + return {"value": make(value)} + except Exception: + pass + return fallback.read().result or default.result + + +def bare_return(value, make, fallback, default): + if value: + try: + make(value) + return None + except Exception: + return "handled" + return fallback.read().result or default.result diff --git a/tests/data/comprehension_chained_ternaries.py b/tests/data/comprehension_chained_ternaries.py new file mode 100644 index 00000000..4130711f --- /dev/null +++ b/tests/data/comprehension_chained_ternaries.py @@ -0,0 +1,18 @@ +def mapping_value(values): + return {value: 1 if 6 <= value <= 8 else 0 for value in values} + + +def mapping_key(values): + return {(value if 6 <= value <= 8 else -value): value for value in values} + + +def arithmetic(values): + return [0.6 + 0.4 * (1.0 if 4 <= value <= 8 < 10 else 0.3) for value in values] + + +def filtered(values): + return [1 if value >= 6 else 0 for value in values if value <= 8] + + +def observed(values, base, coefficient, lower, probe, upper): + return {value: base(value) + coefficient(value) * (1.0 if lower() <= probe(value) <= upper() else 0.3) for value in values} diff --git a/tests/data/constructed_rejection_guards.py b/tests/data/constructed_rejection_guards.py new file mode 100644 index 00000000..7696b0e5 --- /dev/null +++ b/tests/data/constructed_rejection_guards.py @@ -0,0 +1,54 @@ +def constructed(value, build): + if value is None or not 0.0 <= value <= 20.0: + return build("reject", amount=0) + return build("accept", amount=value) + + +def truth_guard(value, build): + if not value or len(value) < 2: + return build("reject", amount=0) + return build("accept", amount=len(value)) + + +def positive_guard(value, invalid, build): + if invalid(value) or value < 2: + return build("reject", amount=value) + return build("accept", amount=value) + + +def constant_truth(value): + if not value or len(value) < 2: + return False + return True + + +def list_result(value): + if value is None or not 0.0 <= value <= 20.0: + return ["reject", value] + return ["accept", value] + + +def mapping_result(value): + if value is None or not 0.0 <= value <= 20.0: + return {"kind": "reject", "value": value} + return {"kind": "accept", "value": value} + + +def generator_result(value, rows, collect, scale): + if value is None or not 0.0 <= value <= 20.0: + return collect(scale * item for item in rows) + return collect(rows) + + +def stored_result(value, build): + if not value or len(value) < 2: + result = build("reject", amount=0) + return result + return build("accept", amount=len(value)) + + +def effectful_body(value, build, log): + if not value or len(value) < 2: + log("rejected") + return build("reject", amount=0) + return build("accept", amount=len(value)) diff --git a/tests/data/context_exit_accounting.py b/tests/data/context_exit_accounting.py new file mode 100644 index 00000000..aa8d62ca --- /dev/null +++ b/tests/data/context_exit_accounting.py @@ -0,0 +1,26 @@ +async def guarded_return(manager, work, record, flag): + async with manager: + if flag: + return "early" + try: + return await work("a") + finally: + record("cleanup") + + +async def computed_return(manager, work, record, flag): + async with manager: + try: + return await work("a") + finally: + record("cleanup") + + +async def none_guard(manager, work, record, flag): + async with manager: + if flag is None: + return 3 + try: + return await work("a") + finally: + record("cleanup") diff --git a/tests/data/filtered_generator_boolean_values.py b/tests/data/filtered_generator_boolean_values.py new file mode 100644 index 00000000..b80c25db --- /dev/null +++ b/tests/data/filtered_generator_boolean_values.py @@ -0,0 +1,18 @@ +def or_value(rows): + return tuple(row[1] or 0 for row in rows if row[0]) + + +def and_value(rows): + return tuple(row[1] and row[2] for row in rows if row[0]) + + +def chained_or(rows): + return tuple(row[1] or row[2] or 0 for row in rows if row[0]) + + +def mixed_value(rows): + return tuple((row[1] and row[2]) or 0 for row in rows if row[0]) + + +def none_filter(rows): + return tuple(row.get("amount") or 0.0 for row in rows if row.get("amount") is not None) diff --git a/tests/data/generator_multiline_filter_312.py b/tests/data/generator_multiline_filter_312.py new file mode 100644 index 00000000..b6d92ec8 --- /dev/null +++ b/tests/data/generator_multiline_filter_312.py @@ -0,0 +1,3 @@ +def multiline_filter(values, excluded): + return tuple(value for value in values + if value not in excluded) diff --git a/tests/data/generator_statement_reconstruction.py b/tests/data/generator_statement_reconstruction.py new file mode 100644 index 00000000..44c9a23a --- /dev/null +++ b/tests/data/generator_statement_reconstruction.py @@ -0,0 +1,16 @@ +def repeated_targets(values): + return tuple(value is not None for value, discarded, discarded, discarded in values) + + +def real_underscore(values): + return tuple((value, discarded, _) for value, _, discarded, discarded in values) + + +def nested_calls(rows): + return sorted(sum(value for value in row) / len(row) for row in rows) + + +def captured_cells(rows): + deltas = rows + factory = sum + return sorted(sum(factory(deltas) for _ in deltas) / len(deltas) for _ in range(3)) diff --git a/tests/data/generator_walrus_cells.py b/tests/data/generator_walrus_cells.py new file mode 100644 index 00000000..c2aea4ad --- /dev/null +++ b/tests/data/generator_walrus_cells.py @@ -0,0 +1,8 @@ +def select(rows): + return tuple(isinstance(row, dict) and (change := row.get("change")) is not None and change >= 0 for row in rows) + + +def last_binding(rows): + change = "initial" + results = tuple((change := row.get("change")) for row in rows) + return results, change diff --git a/tests/data/handler_iterator_breaks.py b/tests/data/handler_iterator_breaks.py new file mode 100644 index 00000000..97ae7406 --- /dev/null +++ b/tests/data/handler_iterator_breaks.py @@ -0,0 +1,140 @@ +import asyncio + + +async def named_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +def sync_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + break + except Exception as error: + owner.log("unavailable", str(error)) + break + owner.log("finished", accepted) + return accepted + + +async def direct_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + break + except Exception as error: + await owner.log("unavailable", str(error)) + break + await owner.log("finished", accepted) + return accepted + + +async def unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted + + +async def direct_unnamed_break(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError): + await owner.log("transient", "owned fault") + break + except Exception: + await owner.log("unavailable", "owned fault") + break + await owner.log("finished", accepted) + return accepted diff --git a/tests/data/loop_context_returns.py b/tests/data/loop_context_returns.py new file mode 100644 index 00000000..703d904c --- /dev/null +++ b/tests/data/loop_context_returns.py @@ -0,0 +1,64 @@ +def sync_value(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +def sync_length(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +def sync_mapping(lock, rows, parse): + with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} + + +async def async_value(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return result + + +async def async_length(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return len(result) + + +async def async_mapping(lock, rows, parse): + async with lock: + result = [] + for row in rows: + try: + result.append(parse(row)) + except ValueError: + continue + return {"rows": result} diff --git a/tests/data/loop_handler_ownership.py b/tests/data/loop_handler_ownership.py new file mode 100644 index 00000000..c69d4d26 --- /dev/null +++ b/tests/data/loop_handler_ownership.py @@ -0,0 +1,119 @@ +def named(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def multiple(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError as error: + owner.failed(row, str(error)) + except KeyError as error: + owner.other(row, str(error)) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def siblings(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + try: + owner.second(row) + except KeyError: + owner.other(row) + owner.finish() + except RuntimeError as error: + owner.fallback(str(error)) + return None + + +def same_type(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except Exception: + owner.failed(row) + owner.finish() + except Exception as error: + owner.fallback(str(error)) + return None + + +async def async_0(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_1(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +async def async_2(owner): + try: + for row in owner.rows: + try: + await owner.arecord(row) + await owner.asecond(row) + except ValueError: + owner.failed(row) + owner.finish() + except RuntimeError: + owner.fallback() + return None + + +def outer_finally(owner): + try: + for row in owner.rows: + try: + owner.record(row) + except ValueError: + owner.failed(row) + finally: + owner.finish() + return None + + +def nonloop(owner): + try: + try: + owner.record(owner.row) + except ValueError: + owner.failed(owner.row) + except RuntimeError: + owner.fallback() + return None diff --git a/tests/data/nested_context_returns.py b/tests/data/nested_context_returns.py new file mode 100644 index 00000000..5040cd90 --- /dev/null +++ b/tests/data/nested_context_returns.py @@ -0,0 +1,49 @@ +def sync_call(contexts, value): + with contexts[0]: + with contexts[1]: + return value() + + +def sync_mapping(contexts, value): + with contexts[0]: + with contexts[1]: + with contexts[2]: + return {"rows": value()} + + +async def async_call(contexts, value): + async with contexts[0]: + async with contexts[1]: + return value() + + +async def async_await(contexts, value): + async with contexts[0]: + async with contexts[1]: + async with contexts[2]: + return await value() + + +async def async_tuple(contexts, value): + async with contexts[0]: + async with contexts[1]: + return (value(),) + + +async def async_single(contexts, value): + async with contexts[0]: + return await value() + + +def sync_outer_return(contexts, value): + with contexts[0]: + with contexts[1]: + result = value() + return result + + +async def async_outer_return(contexts, value): + async with contexts[0]: + async with contexts[1]: + result = await value() + return result diff --git a/tests/data/none_rejection_guards.py b/tests/data/none_rejection_guards.py new file mode 100644 index 00000000..c0c4e441 --- /dev/null +++ b/tests/data/none_rejection_guards.py @@ -0,0 +1,62 @@ +def in_range(value): + if value is None or not 0.0 <= value <= 15.0: + return False + return True + + +def called(read): + if read is None or not 0.0 <= read() <= 15.0: + return False + return True + + +def multi(first, second): + if first is None or not 0.0 <= first() <= second() <= 15.0: + return False + return True + + +def separate(read): + if read is None or not (0.0 <= read() and read() <= 15.0): + return False + return True + + +def same_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return False + return False + + +def integer_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return 3 + return 5 + + +def string_tail(value): + if value is None or not 0.0 <= value <= 15.0: + return "reject" + return "accept" + + +def collect(rows): + result = [] + for row in rows: + a = row.get("a") + b = row.get("b") + c = row.get("c") + d = row.get("d") + if a is None or b is None or c is None or d is None or d < 0.01: + continue + result.append(a + b + c + d) + return result + + +def unpacked(rows): + result = [] + for a, b, c, d in rows: + if a is None or b is None or c is None or d is None or d < 0.01: + continue + result.append(a + b + c + d) + return result diff --git a/tests/data/normal_async_exits.py b/tests/data/normal_async_exits.py new file mode 100644 index 00000000..78cc6e22 --- /dev/null +++ b/tests/data/normal_async_exits.py @@ -0,0 +1,85 @@ +async def normal_1(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + value = owner.build(row0, row0) + return {"value": value, "active": owner.active} + + +async def normal_2(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + value = owner.build(row0, row1) + return {"value": value, "active": owner.active} + + +async def normal_4(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + async with owner.query("row2") as query: + row2 = await query.read() + async with owner.query("row3") as query: + row3 = await query.read() + value = owner.build(row0, row3) + return {"value": value, "active": owner.active} + + +async def constant_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return 4 + + +async def constant_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return 4 + + +async def carried_one(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + return row0 + + +async def computed_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return owner.build(row0, row0) + + +async def mapping_two(owner): + async with owner: + async with owner.query("row0") as query: + row0 = await query.read() + async with owner.query("row1") as query: + row1 = await query.read() + return {"value": row0} + + +async def user_await(owner): + async with owner: + await owner.user(None, None, None) + return owner.build(1, 2) + + +async def branch_return(owner): + async with owner: + if owner.ready: + return 4 + await owner.write(1, 2) + return 2 diff --git a/tests/data/padded_none_returns.py b/tests/data/padded_none_returns.py new file mode 100644 index 00000000..c23f4191 --- /dev/null +++ b/tests/data/padded_none_returns.py @@ -0,0 +1,37 @@ +# Regenerate with: python tools/compile_padded_none_returns.py +# The compiled inputs add NOPs before returns and relocate exception ranges. +from datetime import datetime, timezone + + +def parse_timestamp(value): + if isinstance(value, datetime): + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + stamp = datetime.fromtimestamp(value, timezone.utc) + except (OverflowError, OSError, ValueError): + return + return stamp + if isinstance(value, str): + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + except ValueError: + return + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + + +def unprotected_return(read): + try: + read("inside") + except ValueError: + return + return read("outside") + + +def else_return(read): + try: + read("inside") + except ValueError: + return + else: + return read("outside") diff --git a/tests/data/private_handler_clauses.py b/tests/data/private_handler_clauses.py new file mode 100644 index 00000000..082f02b7 --- /dev/null +++ b/tests/data/private_handler_clauses.py @@ -0,0 +1,163 @@ +import asyncio + + +async def named(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +def synchronous(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + owner.check(value) + accepted = bool(owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + owner.log("close-failed", str(inner)) + owner.log("incompatible", details) + except ValueError as error: + owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + owner.log("transient", str(error)) + continue + except Exception as error: + owner.log("unavailable", str(error)) + continue + owner.log("finished", accepted) + return accepted + + +async def attributes(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def local_attributes(owner, faults): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except faults.incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + except faults.invalid as error: + await owner.log("invalid", str(error)) + continue + except (faults.operating, faults.timeout) as error: + await owner.log("transient", str(error)) + continue + except faults.ordinary as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted + + +async def single_clause(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + try: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + except RuntimeError as inner: + await owner.log("close-failed", str(inner)) + await owner.log("incompatible", details) + await owner.log("finished", accepted) + return accepted + + +async def no_private_try(owner): + accepted = False + for candidate in owner.candidates: + try: + for value in (candidate, "keeper"): + await owner.check(value) + accepted = bool(await owner.reconsider(candidate)) or accepted + except Incompatible as error: + details = "reviewable" + if owner.can_close: + changed = bool(await owner.close(candidate)) + details = "closed" if changed else "terminal" + await owner.log("incompatible", details) + except ValueError as error: + await owner.log("invalid", str(error)) + continue + except (OSError, asyncio.TimeoutError) as error: + await owner.log("transient", str(error)) + continue + except Exception as error: + await owner.log("unavailable", str(error)) + continue + await owner.log("finished", accepted) + return accepted diff --git a/tests/data/shared_validation_raises.py b/tests/data/shared_validation_raises.py new file mode 100644 index 00000000..eb634f66 --- /dev/null +++ b/tests/data/shared_validation_raises.py @@ -0,0 +1,46 @@ +def plain(value, bad, ready, make_error): + if value is None or bad(value) or not ready(value): + raise make_error("invalid", value=value) + return value + + +def chained(value, read, invalid, make_error): + if value is None or not 0 <= read(value) <= 10 or invalid(value): + raise make_error("range failed") + return value + + +def cause(value, reject, make_error, make_cause): + if value is None or reject(value): + raise make_error("invalid", value=value) from make_cause(value) + return value + + +def suppress(value, reject, make_error): + if value is None or reject(value): + raise make_error("invalid", value=value) from None + return value + + +def formatted(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") from make_cause() + return value + + +def ascii_spec(value, label, width, read, reject, make_error, make_cause): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!a}: {value:{width}}") from make_cause() + return value + + +def formatted_plain(value, label, width, read, reject, make_error): + if value is None or not 0 <= read(value) <= 10 or reject(value): + raise make_error(f"{label!r}: {value!s:>{width}}") + return value + + +def constant_error(value, reject): + if value is None or reject(value): + raise ValueError + return value diff --git a/tests/data/sunk_context_exits.py b/tests/data/sunk_context_exits.py new file mode 100644 index 00000000..37288190 --- /dev/null +++ b/tests/data/sunk_context_exits.py @@ -0,0 +1,52 @@ +async def false(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return False + record("outside") + return True + + +async def bare(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return + record("outside") + return True + + +async def twice(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return False + if release: + return False + record("outside") + return True + + +async def value(lock, pending, release, user_call): + async with lock: + record("body") + if pending: + return release + record("outside") + return True + + +async def user_await(lock, pending, release, user_call): + async with lock: + record("body") + await user_call(None, None, None) + record("outside") + return True + + +async def normal(lock, pending, release, user_call): + async with lock: + record("body") + record("ready") + record("outside") + return True diff --git a/tests/fixtures/context_manager_returns.py b/tests/fixtures/context_manager_returns.py new file mode 100644 index 00000000..02534690 --- /dev/null +++ b/tests/fixtures/context_manager_returns.py @@ -0,0 +1,18 @@ +def synchronous(manager): + with manager as value: + return value + + +def synchronous_attribute(manager): + with manager as value: + return value.result + + +async def asynchronous(manager): + async with manager as value: + return value + + +async def asynchronous_attribute(manager): + async with manager as value: + return value.result diff --git a/tests/fixtures/for_loop_disjunctions.py b/tests/fixtures/for_loop_disjunctions.py new file mode 100644 index 00000000..2ee0f33f --- /dev/null +++ b/tests/fixtures/for_loop_disjunctions.py @@ -0,0 +1,99 @@ +def call_else(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def truth_else(values, test): + result = [] + for value in values: + if not value or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def none_else(values, test): + result = [] + for value in values: + if value is None or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def positive_else(values, test): + result = [] + for value in values: + if test(value) or value < 0.2: + result.append("dry") + else: + result.append("wet") + return result + + +def rotated_while(values, test): + result = [] + value = len(values) + while value < 2 or value == 3: + result.append(value) + value += 2 + return result + + +def break_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + break + else: + result.append(value) + else: + result.append('complete') + result.append('tail') + return result + + +def extended_loop(values, test): + result = [] + for value in values: + if not test(value) or value < 0.2: + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + result.append("dry") + else: + result.append("wet") + return result diff --git a/tests/integration.rs b/tests/integration.rs index 62a3b6f0..c4f15e3a 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -25,6 +25,364 @@ fn assert_contains(haystack: &str, needles: &[&str], ctx: &str) { } } +#[test] +fn disjunction_inside_for_preserves_each_operand_and_else() { + for version in ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("for_loop_disjunctions.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + assert_contains( + &src, + &[ + "for value in values:\n if not test(value) or value < 0.2:", + "for value in values:\n if not value or value < 0.2:", + "for value in values:\n if value is None or value < 0.2:", + "for value in values:\n if test(value) or value < 0.2:", + "else:\n result.append('wet')", + "while value < 2 or value == 3:", + "result.append('complete')", + "result.append('tail')", + ], + version, + ); + } +} + +#[test] +fn padded_none_return_does_not_absorb_a_following_branch() { + for version in ["3.11", "3.12", "3.13", "3.14", "3.15"] { + for padding in [0, 1, 3] { + let (src, clean) = decompile_fixture( + &format!("padded_none_returns_{padding}.{version}.pyc")); + assert!(clean, "Python {version}, padding {padding}: incomplete:\n{src}"); + assert_contains( + &src, + &[ + "try:\n stamp = datetime.fromtimestamp(value, timezone.utc)", + "return stamp\n if isinstance(value, str):", + "try:\n parsed = datetime.fromisoformat(value.replace('Z', '+00:00'))", + "parsed.astimezone(timezone.utc)", + "def unprotected_return(read):", + "def else_return(read):", + "read('inside')", + "return read('outside')", + ], + version, + ); + } + } +} + +#[test] +fn padded_return_consumes_the_existing_calculated_value_once() { + for version in ["3.11", "3.12", "3.13", "3.14", "3.15"] { + for padding in [1, 3, 9] { + let (src, clean) = decompile_fixture( + &format!("carried_value_returns_{padding}.{version}.pyc")); + assert!(clean, "Python {version}, padding {padding}: incomplete:\n{src}"); + for value in ["make(value)", "make(value).result", "make(value), 7", + "{'value': make(value)}"] { + assert!(src.contains(&format!("try:\n return {value}\n")), + "Python {version}, padding {padding}: lost protected value {value}:\n{src}"); + } + assert_eq!(src.matches("make(value)").count(), 5, "{version}: {src}"); + assert_eq!(src.matches("return fallback.read().result or default.result").count(), 5, + "{version}: {src}"); + assert_contains(&src, &["try:\n make(value)\n", "return 'handled'"], version); + } + } +} + +#[test] +fn bounded_try_return_keeps_its_handler_and_following_sibling() { + for version in ["3.11", "3.12", "3.13", "3.14", "3.15"] { + for padding in [0, 1, 3] { + let (src, clean) = decompile_fixture( + &format!("bounded_try_returns_{padding}.{version}.pyc")); + assert!(clean, "Python {version}, padding {padding}: incomplete:\n{src}"); + assert_contains(&src, &[ + "try:\n return datetime.fromtimestamp(value, timezone.utc)", + "except (OverflowError, OSError, ValueError):\n return\n if isinstance(value, str):", + "try:\n parsed = datetime.fromisoformat(value.replace('Z', '+00:00'))", + "parsed.astimezone(timezone.utc)", + "def unprotected_return(read):", "def else_return(read):", + "read('inside')", "return read('outside')", + ], version); + assert_eq!(src.matches("datetime.fromtimestamp(value, timezone.utc)").count(), 1, + "{version}: {src}"); + } + } +} + +#[test] +fn chained_comparison_ternaries_keep_collection_values_and_filters_distinct() { + for version in ["3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture( + &format!("comprehension_chained_ternaries.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "return {value: 1 if 6 <= value <= 8 else 0 for value in values}", + "return {value if 6 <= value <= 8 else -value: value for value in values}", + "return [0.6 + 0.4 * (1.0 if 4 <= value <= 8 < 10 else 0.3) for value in values]", + "return [1 if value >= 6 else 0 for value in values if value <= 8]", + "base(value) + coefficient(value) * (1.0 if lower() <= probe(value) <= upper() else 0.3)", + ], version); + } +} + +#[test] +fn generator_statement_route_preserves_repeated_targets_and_closure_calls() { + for version in ["3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture( + &format!("generator_statement_reconstruction.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "value is not None for value, discarded, discarded, discarded in values", + "(value, discarded, _) for value, _, discarded, discarded in values", + "sum((value for value in row)) / len(row) for row in rows", + "sum((factory(deltas) for _ in deltas)) / len(deltas) for _ in range(3)", + ], version); + } +} + +#[test] +fn generator_statement_route_keeps_python312_multiline_filter_layout() { + let (src, clean) = decompile_fixture("generator_multiline_filter_312.3.12.pyc"); + assert!(clean, "{src}"); + assert_contains(&src, &["value for value in values\n if value not in excluded"], + "3.12 multiline filter"); +} + +#[test] +fn filtered_generators_keep_boolean_values_separate_from_rejection_guards() { + for version in ["3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture( + &format!("filtered_generator_boolean_values.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "row[1] or 0 for row in rows if row[0]", + "row[1] and row[2] for row in rows if row[0]", + "row[1] or row[2] or 0 for row in rows if row[0]", + "row[1] and row[2] or 0 for row in rows if row[0]", + "row.get('amount') or 0.0 for row in rows if row.get('amount') is not None", + ], version); + } +} + +#[test] +fn generator_walrus_keeps_its_parent_cell_and_short_circuit_value() { + for version in ["3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("generator_walrus_cells.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "isinstance(row, dict) and (change := row.get('change')) is not None and change >= 0", + "change = 'initial'", "return results, change", + ], version); + assert_eq!(src.matches("change :=").count(), 2, "{version}: {src}"); + assert!(!src.contains("nonlocal "), "{version}: declaration escaped its generator:\n{src}"); + } +} + +#[test] +fn suspension_cleanup_does_not_split_async_with_exception_suites() { + for version in ["3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("await_exception_regions.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "async with manager:\n try:\n await work('a')\n finally:\n record('cleanup')\n await work('b')\n return 'tail'", + "async with manager:\n try:\n a = await work('a')\n except ValueError:\n record('handled')\n else:\n await work('b')\n return 'tail'", + ], version); + } +} + +#[test] +fn await_region_reconstruction_preserves_adjacent_contexts_and_outer_handlers() { + for version in ["3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("await_context_controls.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "async with manager:\n a = await work('a')\n async with manager:\n b = await work('b')\n return a, b", + "try:\n a = await work('a')\n async with manager:", + "try:\n async with manager:\n a = await work('a')\n b = await work('b')", + "except ValueError:\n return 'handled'\n return a, b", + ], version); + } +} + +#[test] +fn early_context_returns_preserve_finally_and_remaining_exit_bookkeeping() { + for version in ["3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("context_exit_accounting.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "async with manager:\n if flag:\n return 'early'\n try:\n return await work('a')\n finally:\n record('cleanup')", + "async with manager:\n try:\n return await work('a')\n finally:\n record('cleanup')", + "async with manager:\n if flag is None:\n return 3\n try:\n return await work('a')\n finally:\n record('cleanup')", + ], version); + } +} + +#[test] +fn context_managers_keep_return_values_inside_the_body() { + for version in ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("context_manager_returns.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "def synchronous(manager):\n with manager as value:\n return value", + "def synchronous_attribute(manager):\n with manager as value:\n return value.result", + "async def asynchronous(manager):\n async with manager as value:\n return value", + "async def asynchronous_attribute(manager):\n async with manager as value:\n return value.result", + ], version); + } +} + +#[test] +fn handler_loops_keep_computed_returns_inside_their_context() { + for version in ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("loop_context_returns.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + for prefix in ["", "async "] { + for (suffix, value) in [("value", "result"), ("length", "len(result)"), + ("mapping", "{'rows': result}")] { + let name = if prefix.is_empty() { "sync" } else { "async" }; + // 3.10 optimizes this final continue into the loop's fallthrough. + let handler = if version == "3.10" && suffix == "value" { "pass" } else { "continue" }; + let expected = format!("{prefix}def {name}_{suffix}(lock, rows, parse):\n {prefix}with lock:\n result = []\n for row in rows:\n try:\n result.append(parse(row))\n except ValueError:\n {handler}\n return {value}"); + assert!(src.contains(&expected), "Python {version}: {expected} missing:\n{src}"); + } + } + } +} + +#[test] +fn sunk_early_context_exits_leave_the_normal_exit_available() { + for version in ["3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("sunk_context_exits.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "if pending:\n return False\n record('outside')", + "if pending:\n return\n record('outside')", + "if pending:\n return False\n if release:\n return False\n record('outside')", + "if pending:\n return release\n record('outside')", + "await user_call(None, None, None)\n record('outside')", + "record('ready')\n record('outside')", + ], version); + assert!(!src.contains("await None("), "Python {version}: leaked exit call:\n{src}"); + } +} + +#[test] +fn nested_contexts_carry_one_return_through_every_exit() { + for version in ["3.11", "3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("nested_context_returns.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "with contexts[0]:\n with contexts[1]:\n return value()", + "with contexts[0]:\n with contexts[1]:\n with contexts[2]:\n return {'rows': value()}", + "async with contexts[0]:\n async with contexts[1]:\n return value()", + "async with contexts[0]:\n async with contexts[1]:\n async with contexts[2]:\n return await value()", + "async with contexts[0]:\n async with contexts[1]:\n return (value(),)", + "async with contexts[0]:\n return await value()", + "result = value()\n return result", + "result = await value()\n return result", + ], version); + } +} + +#[test] +fn per_row_handlers_remain_owned_by_their_loop_and_outer_try() { + for version in ["3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("loop_handler_ownership.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "try:\n for row in owner.rows:\n try:\n owner.record(row)\n except ValueError as error:", + " except KeyError as error:\n owner.other(row, str(error))", + " try:\n owner.second(row)\n except KeyError:", + " except Exception:\n owner.failed(row)", + " owner.finish()\n except RuntimeError as error:", + " owner.finish()\n except Exception as error:", + " await owner.arecord(row)\n await owner.asecond(row)\n except ValueError:", + " finally:\n owner.finish()", + "def nonloop(owner):\n try:\n try:\n owner.record(owner.row)", + " except ValueError:\n owner.failed(owner.row)", + ], version); + // The compiler may sink implicit None returns into either arm. + let nonloop = src.split("def nonloop(owner):").nth(1).unwrap(); + assert!(nonloop.contains(" except RuntimeError:\n owner.fallback()"), "{src}"); + } +} + +#[test] +fn normal_async_exits_close_only_their_own_context() { + for version in ["3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("normal_async_exits.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "row0 = await query.read()\n value = owner.build(row0, row0)", + "row3 = await query.read()\n value = owner.build(row0, row3)", + "row0 = await query.read()\n async with owner.query('row1') as query:", + "row0 = await query.read()\n return 4", + "row0 = await query.read()\n return row0", + "row1 = await query.read()\n return owner.build(row0, row0)", + "row1 = await query.read()\n return {'value': row0}", + "await owner.user(None, None, None)\n return owner.build(1, 2)", + "if owner.ready:\n return 4\n await owner.write(1, 2)\n return 2", + ], version); + } +} + +#[test] +fn async_return_padding_never_turns_a_carried_exit_into_a_normal_exit() { + for version in ["3.14", "3.15"] { + for padding in [1, 3, 9] { + let (src, clean) = decompile_fixture(&format!("async_context_return_padding_{padding}.{version}.pyc")); + assert!(clean, "Python {version}, padding {padding}: incomplete:\n{src}"); + assert_contains(&src, &[ + "row0 = await query.read()\n return 4", + "row0 = await query.read()\n return {'value': row0}", + ], version); + } + } +} + +#[test] +fn private_handler_bodies_do_not_hide_later_typed_clauses() { + for version in ["3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("private_handler_clauses.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "except RuntimeError as inner:\n await owner.log('close-failed', str(inner))", + " except ValueError as error:\n await owner.log('invalid', str(error))", + " except (OSError, asyncio.TimeoutError) as error:\n await owner.log('transient', str(error))", + " except Exception as error:\n await owner.log('unavailable', str(error))", + " except ValueError as error:\n owner.log('invalid', str(error))", + " except faults.invalid as error:", + " except (faults.operating, faults.timeout) as error:", + " except faults.ordinary as error:", + " await owner.log('finished', accepted)\n return accepted", + "async def single_clause(owner):", "async def no_private_try(owner):", + ], version); + assert_eq!(src.matches("except (faults.operating, faults.timeout)").count(), 2); + } +} + +#[test] +fn handler_iterator_disposal_preserves_break_and_post_loop_work() { + for version in ["3.12", "3.13", "3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("handler_iterator_breaks.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "except (OSError, asyncio.TimeoutError) as error:\n await owner.log('transient', str(error))\n break", + "except Exception as error:\n await owner.log('unavailable', str(error))\n break", + "except (OSError, asyncio.TimeoutError) as error:\n owner.log('transient', str(error))\n break", + "except (OSError, asyncio.TimeoutError):\n await owner.log('transient', 'owned fault')\n break", + "except Exception:\n await owner.log('unavailable', 'owned fault')\n break", + " await owner.log('finished', accepted)\n return accepted", + " owner.log('finished', accepted)\n return accepted", + ], version); + assert_eq!(src.matches("\n break").count(), 10, "Python {version}: {src}"); + } +} + #[test] fn py27_if_elif_else() { let (src, clean) = decompile_fixture("if_elif_else.2.7.pyc"); @@ -217,8 +575,16 @@ fn generator_expanded_calls() { &src, &[ "def positional(rows, factory):\n return tuple((factory(*row) for row in rows))", - "def keywords(rows, factory):\n return tuple((factory(*(), **{**dict(row)}) for row in rows))", - "def both(rows, factory):\n return tuple((factory(*row[0], **{**row[1]}) for row in rows))", + if version == "3.11" { + "def keywords(rows, factory):\n return tuple((factory(*(), **{**dict(row)}) for row in rows))" + } else { + "def keywords(rows, factory):\n return tuple((factory(**dict(row)) for row in rows))" + }, + if version == "3.11" { + "def both(rows, factory):\n return tuple((factory(*row[0], **{**row[1]}) for row in rows))" + } else { + "def both(rows, factory):\n return tuple((factory(*row[0], **row[1]) for row in rows))" + }, ], &fixture, ); @@ -859,3 +1225,58 @@ fn python315_shapes_recover_changed_encodings() { assert!(!src.contains("/*with-result*/"), "unresolved with target:\n{src}"); assert!(!src.contains('\0'), "PUSH_NULL marker leaked:\n{src}"); } + +#[test] +fn none_rejection_guards_preserve_chains_calls_and_success_returns() { + for version in ["3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("none_rejection_guards.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "if value is None or not 0.0 <= value <= 15.0:", + "if read is None or not 0.0 <= read() <= 15.0:", + "if first is None or not 0.0 <= first() <= second() <= 15.0:", + "if read is None or not 0.0 <= read() or not read() <= 15.0:", + "def same_tail(value):\n if value is None or not 0.0 <= value <= 15.0:\n return False\n return False", + "return 3\n return 5", + "return 'reject'\n return 'accept'", + "if a is None or b is None or c is None or d is None or d < 0.01:\n continue", + ], version); + assert_eq!(src.matches(" continue").count(), 2, "{version}: {src}"); + } +} + +#[test] +fn constructed_rejections_keep_selected_values_and_statement_boundaries() { + for version in ["3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("constructed_rejection_guards.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "if value is None or not 0.0 <= value <= 20.0:\n return build('reject', amount=0)", + "if not value or len(value) < 2:\n return build('reject', amount=0)", + "if invalid(value) or value < 2:", + "if not value or len(value) < 2:\n return False\n return True", + "if value is None or not 0.0 <= value <= 20.0:\n return ['reject', value]", + "if value is None or not 0.0 <= value <= 20.0:\n return {'kind': 'reject', 'value': value}", + "return collect((scale * item for item in rows))", + "result = build('reject', amount=0)\n return result", + "log('rejected')\n return build('reject', amount=0)", + ], version); + } +} + +#[test] +fn shared_validation_raises_keep_causes_and_formatted_error_paths() { + for version in ["3.14", "3.15"] { + let (src, clean) = decompile_fixture(&format!("shared_validation_raises.{version}.pyc")); + assert!(clean, "Python {version}: incomplete:\n{src}"); + assert_contains(&src, &[ + "if value is None or bad(value) or not ready(value):", + "if value is None or not 0 <= read(value) <= 10 or invalid(value):", + "raise make_error('invalid', value=value) from make_cause(value)", + "raise make_error('invalid', value=value) from None", + "raise make_error(f'{label!r}: {value!s:>{width}}') from make_cause()", + "raise make_error(f'{label!a}: {value:{width}}') from make_cause()", + "if value is None or reject(value):\n raise ValueError", + ], version); + } +} diff --git a/tests/pyc/async_context_return_padding_1.3.14.pyc b/tests/pyc/async_context_return_padding_1.3.14.pyc new file mode 100644 index 00000000..7dbd40f4 Binary files /dev/null and b/tests/pyc/async_context_return_padding_1.3.14.pyc differ diff --git a/tests/pyc/async_context_return_padding_1.3.15.pyc b/tests/pyc/async_context_return_padding_1.3.15.pyc new file mode 100644 index 00000000..431ffd48 Binary files /dev/null and b/tests/pyc/async_context_return_padding_1.3.15.pyc differ diff --git a/tests/pyc/async_context_return_padding_3.3.14.pyc b/tests/pyc/async_context_return_padding_3.3.14.pyc new file mode 100644 index 00000000..6973a933 Binary files /dev/null and b/tests/pyc/async_context_return_padding_3.3.14.pyc differ diff --git a/tests/pyc/async_context_return_padding_3.3.15.pyc b/tests/pyc/async_context_return_padding_3.3.15.pyc new file mode 100644 index 00000000..ba3c1b2a Binary files /dev/null and b/tests/pyc/async_context_return_padding_3.3.15.pyc differ diff --git a/tests/pyc/async_context_return_padding_9.3.14.pyc b/tests/pyc/async_context_return_padding_9.3.14.pyc new file mode 100644 index 00000000..e1485469 Binary files /dev/null and b/tests/pyc/async_context_return_padding_9.3.14.pyc differ diff --git a/tests/pyc/async_context_return_padding_9.3.15.pyc b/tests/pyc/async_context_return_padding_9.3.15.pyc new file mode 100644 index 00000000..00258ee2 Binary files /dev/null and b/tests/pyc/async_context_return_padding_9.3.15.pyc differ diff --git a/tests/pyc/await_context_controls.3.12.pyc b/tests/pyc/await_context_controls.3.12.pyc new file mode 100644 index 00000000..ed6a6878 Binary files /dev/null and b/tests/pyc/await_context_controls.3.12.pyc differ diff --git a/tests/pyc/await_context_controls.3.13.pyc b/tests/pyc/await_context_controls.3.13.pyc new file mode 100644 index 00000000..7a3f5eff Binary files /dev/null and b/tests/pyc/await_context_controls.3.13.pyc differ diff --git a/tests/pyc/await_context_controls.3.14.pyc b/tests/pyc/await_context_controls.3.14.pyc new file mode 100644 index 00000000..9d4103b1 Binary files /dev/null and b/tests/pyc/await_context_controls.3.14.pyc differ diff --git a/tests/pyc/await_context_controls.3.15.pyc b/tests/pyc/await_context_controls.3.15.pyc new file mode 100644 index 00000000..9f98af22 Binary files /dev/null and b/tests/pyc/await_context_controls.3.15.pyc differ diff --git a/tests/pyc/await_exception_regions.3.14.pyc b/tests/pyc/await_exception_regions.3.14.pyc new file mode 100644 index 00000000..caf0d367 Binary files /dev/null and b/tests/pyc/await_exception_regions.3.14.pyc differ diff --git a/tests/pyc/await_exception_regions.3.15.pyc b/tests/pyc/await_exception_regions.3.15.pyc new file mode 100644 index 00000000..f8ad7890 Binary files /dev/null and b/tests/pyc/await_exception_regions.3.15.pyc differ diff --git a/tests/pyc/bounded_formatting.3.13.pyc b/tests/pyc/bounded_formatting.3.13.pyc new file mode 100644 index 00000000..dbb8e759 Binary files /dev/null and b/tests/pyc/bounded_formatting.3.13.pyc differ diff --git a/tests/pyc/bounded_formatting.3.14.pyc b/tests/pyc/bounded_formatting.3.14.pyc new file mode 100644 index 00000000..7dee5da5 Binary files /dev/null and b/tests/pyc/bounded_formatting.3.14.pyc differ diff --git a/tests/pyc/bounded_formatting.3.15.pyc b/tests/pyc/bounded_formatting.3.15.pyc new file mode 100644 index 00000000..215391f2 Binary files /dev/null and b/tests/pyc/bounded_formatting.3.15.pyc differ diff --git a/tests/pyc/bounded_generator_expressions.3.13.pyc b/tests/pyc/bounded_generator_expressions.3.13.pyc new file mode 100644 index 00000000..283b3e27 Binary files /dev/null and b/tests/pyc/bounded_generator_expressions.3.13.pyc differ diff --git a/tests/pyc/bounded_generator_expressions.3.14.pyc b/tests/pyc/bounded_generator_expressions.3.14.pyc new file mode 100644 index 00000000..740ccff9 Binary files /dev/null and b/tests/pyc/bounded_generator_expressions.3.14.pyc differ diff --git a/tests/pyc/bounded_generator_expressions.3.15.pyc b/tests/pyc/bounded_generator_expressions.3.15.pyc new file mode 100644 index 00000000..de3d0495 Binary files /dev/null and b/tests/pyc/bounded_generator_expressions.3.15.pyc differ diff --git a/tests/pyc/bounded_try_returns_0.3.11.pyc b/tests/pyc/bounded_try_returns_0.3.11.pyc new file mode 100644 index 00000000..9ead619d Binary files /dev/null and b/tests/pyc/bounded_try_returns_0.3.11.pyc differ diff --git a/tests/pyc/bounded_try_returns_0.3.12.pyc b/tests/pyc/bounded_try_returns_0.3.12.pyc new file mode 100644 index 00000000..14dbecf9 Binary files /dev/null and b/tests/pyc/bounded_try_returns_0.3.12.pyc differ diff --git a/tests/pyc/bounded_try_returns_0.3.13.pyc b/tests/pyc/bounded_try_returns_0.3.13.pyc new file mode 100644 index 00000000..e66bb1a7 Binary files /dev/null and b/tests/pyc/bounded_try_returns_0.3.13.pyc differ diff --git a/tests/pyc/bounded_try_returns_0.3.14.pyc b/tests/pyc/bounded_try_returns_0.3.14.pyc new file mode 100644 index 00000000..6c9fbd29 Binary files /dev/null and b/tests/pyc/bounded_try_returns_0.3.14.pyc differ diff --git a/tests/pyc/bounded_try_returns_0.3.15.pyc b/tests/pyc/bounded_try_returns_0.3.15.pyc new file mode 100644 index 00000000..9a2549dc Binary files /dev/null and b/tests/pyc/bounded_try_returns_0.3.15.pyc differ diff --git a/tests/pyc/bounded_try_returns_1.3.11.pyc b/tests/pyc/bounded_try_returns_1.3.11.pyc new file mode 100644 index 00000000..79879354 Binary files /dev/null and b/tests/pyc/bounded_try_returns_1.3.11.pyc differ diff --git a/tests/pyc/bounded_try_returns_1.3.12.pyc b/tests/pyc/bounded_try_returns_1.3.12.pyc new file mode 100644 index 00000000..7a5877ba Binary files /dev/null and b/tests/pyc/bounded_try_returns_1.3.12.pyc differ diff --git a/tests/pyc/bounded_try_returns_1.3.13.pyc b/tests/pyc/bounded_try_returns_1.3.13.pyc new file mode 100644 index 00000000..9e31953b Binary files /dev/null and b/tests/pyc/bounded_try_returns_1.3.13.pyc differ diff --git a/tests/pyc/bounded_try_returns_1.3.14.pyc b/tests/pyc/bounded_try_returns_1.3.14.pyc new file mode 100644 index 00000000..c3b9f77c Binary files /dev/null and b/tests/pyc/bounded_try_returns_1.3.14.pyc differ diff --git a/tests/pyc/bounded_try_returns_1.3.15.pyc b/tests/pyc/bounded_try_returns_1.3.15.pyc new file mode 100644 index 00000000..60f48441 Binary files /dev/null and b/tests/pyc/bounded_try_returns_1.3.15.pyc differ diff --git a/tests/pyc/bounded_try_returns_3.3.11.pyc b/tests/pyc/bounded_try_returns_3.3.11.pyc new file mode 100644 index 00000000..b1bbf9f1 Binary files /dev/null and b/tests/pyc/bounded_try_returns_3.3.11.pyc differ diff --git a/tests/pyc/bounded_try_returns_3.3.12.pyc b/tests/pyc/bounded_try_returns_3.3.12.pyc new file mode 100644 index 00000000..7e839f77 Binary files /dev/null and b/tests/pyc/bounded_try_returns_3.3.12.pyc differ diff --git a/tests/pyc/bounded_try_returns_3.3.13.pyc b/tests/pyc/bounded_try_returns_3.3.13.pyc new file mode 100644 index 00000000..da87932a Binary files /dev/null and b/tests/pyc/bounded_try_returns_3.3.13.pyc differ diff --git a/tests/pyc/bounded_try_returns_3.3.14.pyc b/tests/pyc/bounded_try_returns_3.3.14.pyc new file mode 100644 index 00000000..b0c8297b Binary files /dev/null and b/tests/pyc/bounded_try_returns_3.3.14.pyc differ diff --git a/tests/pyc/bounded_try_returns_3.3.15.pyc b/tests/pyc/bounded_try_returns_3.3.15.pyc new file mode 100644 index 00000000..fc066ad6 Binary files /dev/null and b/tests/pyc/bounded_try_returns_3.3.15.pyc differ diff --git a/tests/pyc/carried_value_returns_1.3.11.pyc b/tests/pyc/carried_value_returns_1.3.11.pyc new file mode 100644 index 00000000..81e07361 Binary files /dev/null and b/tests/pyc/carried_value_returns_1.3.11.pyc differ diff --git a/tests/pyc/carried_value_returns_1.3.12.pyc b/tests/pyc/carried_value_returns_1.3.12.pyc new file mode 100644 index 00000000..85a51d29 Binary files /dev/null and b/tests/pyc/carried_value_returns_1.3.12.pyc differ diff --git a/tests/pyc/carried_value_returns_1.3.13.pyc b/tests/pyc/carried_value_returns_1.3.13.pyc new file mode 100644 index 00000000..b248c87d Binary files /dev/null and b/tests/pyc/carried_value_returns_1.3.13.pyc differ diff --git a/tests/pyc/carried_value_returns_1.3.14.pyc b/tests/pyc/carried_value_returns_1.3.14.pyc new file mode 100644 index 00000000..f555cf55 Binary files /dev/null and b/tests/pyc/carried_value_returns_1.3.14.pyc differ diff --git a/tests/pyc/carried_value_returns_1.3.15.pyc b/tests/pyc/carried_value_returns_1.3.15.pyc new file mode 100644 index 00000000..a2d2df74 Binary files /dev/null and b/tests/pyc/carried_value_returns_1.3.15.pyc differ diff --git a/tests/pyc/carried_value_returns_3.3.11.pyc b/tests/pyc/carried_value_returns_3.3.11.pyc new file mode 100644 index 00000000..1f16c355 Binary files /dev/null and b/tests/pyc/carried_value_returns_3.3.11.pyc differ diff --git a/tests/pyc/carried_value_returns_3.3.12.pyc b/tests/pyc/carried_value_returns_3.3.12.pyc new file mode 100644 index 00000000..c5fb5bfb Binary files /dev/null and b/tests/pyc/carried_value_returns_3.3.12.pyc differ diff --git a/tests/pyc/carried_value_returns_3.3.13.pyc b/tests/pyc/carried_value_returns_3.3.13.pyc new file mode 100644 index 00000000..1d3cb3f3 Binary files /dev/null and b/tests/pyc/carried_value_returns_3.3.13.pyc differ diff --git a/tests/pyc/carried_value_returns_3.3.14.pyc b/tests/pyc/carried_value_returns_3.3.14.pyc new file mode 100644 index 00000000..44fa1cc6 Binary files /dev/null and b/tests/pyc/carried_value_returns_3.3.14.pyc differ diff --git a/tests/pyc/carried_value_returns_3.3.15.pyc b/tests/pyc/carried_value_returns_3.3.15.pyc new file mode 100644 index 00000000..c21c38b3 Binary files /dev/null and b/tests/pyc/carried_value_returns_3.3.15.pyc differ diff --git a/tests/pyc/carried_value_returns_9.3.11.pyc b/tests/pyc/carried_value_returns_9.3.11.pyc new file mode 100644 index 00000000..14952439 Binary files /dev/null and b/tests/pyc/carried_value_returns_9.3.11.pyc differ diff --git a/tests/pyc/carried_value_returns_9.3.12.pyc b/tests/pyc/carried_value_returns_9.3.12.pyc new file mode 100644 index 00000000..26acf48a Binary files /dev/null and b/tests/pyc/carried_value_returns_9.3.12.pyc differ diff --git a/tests/pyc/carried_value_returns_9.3.13.pyc b/tests/pyc/carried_value_returns_9.3.13.pyc new file mode 100644 index 00000000..d1134419 Binary files /dev/null and b/tests/pyc/carried_value_returns_9.3.13.pyc differ diff --git a/tests/pyc/carried_value_returns_9.3.14.pyc b/tests/pyc/carried_value_returns_9.3.14.pyc new file mode 100644 index 00000000..25b2ec82 Binary files /dev/null and b/tests/pyc/carried_value_returns_9.3.14.pyc differ diff --git a/tests/pyc/carried_value_returns_9.3.15.pyc b/tests/pyc/carried_value_returns_9.3.15.pyc new file mode 100644 index 00000000..760729aa Binary files /dev/null and b/tests/pyc/carried_value_returns_9.3.15.pyc differ diff --git a/tests/pyc/comprehension_chained_ternaries.3.12.pyc b/tests/pyc/comprehension_chained_ternaries.3.12.pyc new file mode 100644 index 00000000..75ee69b7 Binary files /dev/null and b/tests/pyc/comprehension_chained_ternaries.3.12.pyc differ diff --git a/tests/pyc/comprehension_chained_ternaries.3.13.pyc b/tests/pyc/comprehension_chained_ternaries.3.13.pyc new file mode 100644 index 00000000..45cc60e2 Binary files /dev/null and b/tests/pyc/comprehension_chained_ternaries.3.13.pyc differ diff --git a/tests/pyc/comprehension_chained_ternaries.3.14.pyc b/tests/pyc/comprehension_chained_ternaries.3.14.pyc new file mode 100644 index 00000000..6169659a Binary files /dev/null and b/tests/pyc/comprehension_chained_ternaries.3.14.pyc differ diff --git a/tests/pyc/comprehension_chained_ternaries.3.15.pyc b/tests/pyc/comprehension_chained_ternaries.3.15.pyc new file mode 100644 index 00000000..91931c0a Binary files /dev/null and b/tests/pyc/comprehension_chained_ternaries.3.15.pyc differ diff --git a/tests/pyc/constructed_rejection_guards.3.14.pyc b/tests/pyc/constructed_rejection_guards.3.14.pyc new file mode 100644 index 00000000..26f1339a Binary files /dev/null and b/tests/pyc/constructed_rejection_guards.3.14.pyc differ diff --git a/tests/pyc/constructed_rejection_guards.3.15.pyc b/tests/pyc/constructed_rejection_guards.3.15.pyc new file mode 100644 index 00000000..4bdb6822 Binary files /dev/null and b/tests/pyc/constructed_rejection_guards.3.15.pyc differ diff --git a/tests/pyc/context_exit_accounting.3.14.pyc b/tests/pyc/context_exit_accounting.3.14.pyc new file mode 100644 index 00000000..2a4e8420 Binary files /dev/null and b/tests/pyc/context_exit_accounting.3.14.pyc differ diff --git a/tests/pyc/context_exit_accounting.3.15.pyc b/tests/pyc/context_exit_accounting.3.15.pyc new file mode 100644 index 00000000..544bfe7b Binary files /dev/null and b/tests/pyc/context_exit_accounting.3.15.pyc differ diff --git a/tests/pyc/context_manager_returns.3.10.pyc b/tests/pyc/context_manager_returns.3.10.pyc new file mode 100644 index 00000000..7c4af4f7 Binary files /dev/null and b/tests/pyc/context_manager_returns.3.10.pyc differ diff --git a/tests/pyc/context_manager_returns.3.11.pyc b/tests/pyc/context_manager_returns.3.11.pyc new file mode 100644 index 00000000..3d183ede Binary files /dev/null and b/tests/pyc/context_manager_returns.3.11.pyc differ diff --git a/tests/pyc/context_manager_returns.3.12.pyc b/tests/pyc/context_manager_returns.3.12.pyc new file mode 100644 index 00000000..ddd21f3e Binary files /dev/null and b/tests/pyc/context_manager_returns.3.12.pyc differ diff --git a/tests/pyc/context_manager_returns.3.13.pyc b/tests/pyc/context_manager_returns.3.13.pyc new file mode 100644 index 00000000..de88bd8b Binary files /dev/null and b/tests/pyc/context_manager_returns.3.13.pyc differ diff --git a/tests/pyc/context_manager_returns.3.14.pyc b/tests/pyc/context_manager_returns.3.14.pyc new file mode 100644 index 00000000..8ba11d23 Binary files /dev/null and b/tests/pyc/context_manager_returns.3.14.pyc differ diff --git a/tests/pyc/context_manager_returns.3.15.pyc b/tests/pyc/context_manager_returns.3.15.pyc new file mode 100644 index 00000000..aa391bdd Binary files /dev/null and b/tests/pyc/context_manager_returns.3.15.pyc differ diff --git a/tests/pyc/context_manager_returns.3.9.pyc b/tests/pyc/context_manager_returns.3.9.pyc new file mode 100644 index 00000000..90ed8f60 Binary files /dev/null and b/tests/pyc/context_manager_returns.3.9.pyc differ diff --git a/tests/pyc/filtered_generator_boolean_values.3.12.pyc b/tests/pyc/filtered_generator_boolean_values.3.12.pyc new file mode 100644 index 00000000..b67ed1c9 Binary files /dev/null and b/tests/pyc/filtered_generator_boolean_values.3.12.pyc differ diff --git a/tests/pyc/filtered_generator_boolean_values.3.13.pyc b/tests/pyc/filtered_generator_boolean_values.3.13.pyc new file mode 100644 index 00000000..25782ac0 Binary files /dev/null and b/tests/pyc/filtered_generator_boolean_values.3.13.pyc differ diff --git a/tests/pyc/filtered_generator_boolean_values.3.14.pyc b/tests/pyc/filtered_generator_boolean_values.3.14.pyc new file mode 100644 index 00000000..4602839a Binary files /dev/null and b/tests/pyc/filtered_generator_boolean_values.3.14.pyc differ diff --git a/tests/pyc/filtered_generator_boolean_values.3.15.pyc b/tests/pyc/filtered_generator_boolean_values.3.15.pyc new file mode 100644 index 00000000..990451f5 Binary files /dev/null and b/tests/pyc/filtered_generator_boolean_values.3.15.pyc differ diff --git a/tests/pyc/for_loop_disjunctions.3.10.pyc b/tests/pyc/for_loop_disjunctions.3.10.pyc new file mode 100644 index 00000000..da87c741 Binary files /dev/null and b/tests/pyc/for_loop_disjunctions.3.10.pyc differ diff --git a/tests/pyc/for_loop_disjunctions.3.11.pyc b/tests/pyc/for_loop_disjunctions.3.11.pyc new file mode 100644 index 00000000..bcfa57b1 Binary files /dev/null and b/tests/pyc/for_loop_disjunctions.3.11.pyc differ diff --git a/tests/pyc/for_loop_disjunctions.3.12.pyc b/tests/pyc/for_loop_disjunctions.3.12.pyc new file mode 100644 index 00000000..19c4e946 Binary files /dev/null and b/tests/pyc/for_loop_disjunctions.3.12.pyc differ diff --git a/tests/pyc/for_loop_disjunctions.3.13.pyc b/tests/pyc/for_loop_disjunctions.3.13.pyc new file mode 100644 index 00000000..63fa3266 Binary files /dev/null and b/tests/pyc/for_loop_disjunctions.3.13.pyc differ diff --git a/tests/pyc/for_loop_disjunctions.3.14.pyc b/tests/pyc/for_loop_disjunctions.3.14.pyc new file mode 100644 index 00000000..bba080ff Binary files /dev/null and b/tests/pyc/for_loop_disjunctions.3.14.pyc differ diff --git a/tests/pyc/for_loop_disjunctions.3.15.pyc b/tests/pyc/for_loop_disjunctions.3.15.pyc new file mode 100644 index 00000000..672ee873 Binary files /dev/null and b/tests/pyc/for_loop_disjunctions.3.15.pyc differ diff --git a/tests/pyc/for_loop_disjunctions.3.8.pyc b/tests/pyc/for_loop_disjunctions.3.8.pyc new file mode 100644 index 00000000..0ace3a8e Binary files /dev/null and b/tests/pyc/for_loop_disjunctions.3.8.pyc differ diff --git a/tests/pyc/for_loop_disjunctions.3.9.pyc b/tests/pyc/for_loop_disjunctions.3.9.pyc new file mode 100644 index 00000000..a5843e7e Binary files /dev/null and b/tests/pyc/for_loop_disjunctions.3.9.pyc differ diff --git a/tests/pyc/generator_multiline_filter_312.3.12.pyc b/tests/pyc/generator_multiline_filter_312.3.12.pyc new file mode 100644 index 00000000..daf24490 Binary files /dev/null and b/tests/pyc/generator_multiline_filter_312.3.12.pyc differ diff --git a/tests/pyc/generator_statement_reconstruction.3.12.pyc b/tests/pyc/generator_statement_reconstruction.3.12.pyc new file mode 100644 index 00000000..e23d9844 Binary files /dev/null and b/tests/pyc/generator_statement_reconstruction.3.12.pyc differ diff --git a/tests/pyc/generator_statement_reconstruction.3.13.pyc b/tests/pyc/generator_statement_reconstruction.3.13.pyc new file mode 100644 index 00000000..fc1be9cf Binary files /dev/null and b/tests/pyc/generator_statement_reconstruction.3.13.pyc differ diff --git a/tests/pyc/generator_statement_reconstruction.3.14.pyc b/tests/pyc/generator_statement_reconstruction.3.14.pyc new file mode 100644 index 00000000..fbd66df9 Binary files /dev/null and b/tests/pyc/generator_statement_reconstruction.3.14.pyc differ diff --git a/tests/pyc/generator_statement_reconstruction.3.15.pyc b/tests/pyc/generator_statement_reconstruction.3.15.pyc new file mode 100644 index 00000000..4ef14ad8 Binary files /dev/null and b/tests/pyc/generator_statement_reconstruction.3.15.pyc differ diff --git a/tests/pyc/generator_walrus_cells.3.12.pyc b/tests/pyc/generator_walrus_cells.3.12.pyc new file mode 100644 index 00000000..2c8c69c9 Binary files /dev/null and b/tests/pyc/generator_walrus_cells.3.12.pyc differ diff --git a/tests/pyc/generator_walrus_cells.3.13.pyc b/tests/pyc/generator_walrus_cells.3.13.pyc new file mode 100644 index 00000000..5a99f589 Binary files /dev/null and b/tests/pyc/generator_walrus_cells.3.13.pyc differ diff --git a/tests/pyc/generator_walrus_cells.3.14.pyc b/tests/pyc/generator_walrus_cells.3.14.pyc new file mode 100644 index 00000000..37df60df Binary files /dev/null and b/tests/pyc/generator_walrus_cells.3.14.pyc differ diff --git a/tests/pyc/generator_walrus_cells.3.15.pyc b/tests/pyc/generator_walrus_cells.3.15.pyc new file mode 100644 index 00000000..d325ca8d Binary files /dev/null and b/tests/pyc/generator_walrus_cells.3.15.pyc differ diff --git a/tests/pyc/handler_iterator_breaks.3.12.pyc b/tests/pyc/handler_iterator_breaks.3.12.pyc new file mode 100644 index 00000000..43585ae5 Binary files /dev/null and b/tests/pyc/handler_iterator_breaks.3.12.pyc differ diff --git a/tests/pyc/handler_iterator_breaks.3.13.pyc b/tests/pyc/handler_iterator_breaks.3.13.pyc new file mode 100644 index 00000000..89aadc4b Binary files /dev/null and b/tests/pyc/handler_iterator_breaks.3.13.pyc differ diff --git a/tests/pyc/handler_iterator_breaks.3.14.pyc b/tests/pyc/handler_iterator_breaks.3.14.pyc new file mode 100644 index 00000000..e0c5484d Binary files /dev/null and b/tests/pyc/handler_iterator_breaks.3.14.pyc differ diff --git a/tests/pyc/handler_iterator_breaks.3.15.pyc b/tests/pyc/handler_iterator_breaks.3.15.pyc new file mode 100644 index 00000000..cf260ed1 Binary files /dev/null and b/tests/pyc/handler_iterator_breaks.3.15.pyc differ diff --git a/tests/pyc/loop_context_returns.3.10.pyc b/tests/pyc/loop_context_returns.3.10.pyc new file mode 100644 index 00000000..a7fcf9ac Binary files /dev/null and b/tests/pyc/loop_context_returns.3.10.pyc differ diff --git a/tests/pyc/loop_context_returns.3.11.pyc b/tests/pyc/loop_context_returns.3.11.pyc new file mode 100644 index 00000000..727ceda6 Binary files /dev/null and b/tests/pyc/loop_context_returns.3.11.pyc differ diff --git a/tests/pyc/loop_context_returns.3.12.pyc b/tests/pyc/loop_context_returns.3.12.pyc new file mode 100644 index 00000000..46287a41 Binary files /dev/null and b/tests/pyc/loop_context_returns.3.12.pyc differ diff --git a/tests/pyc/loop_context_returns.3.13.pyc b/tests/pyc/loop_context_returns.3.13.pyc new file mode 100644 index 00000000..e581bea9 Binary files /dev/null and b/tests/pyc/loop_context_returns.3.13.pyc differ diff --git a/tests/pyc/loop_context_returns.3.14.pyc b/tests/pyc/loop_context_returns.3.14.pyc new file mode 100644 index 00000000..8b24a6bc Binary files /dev/null and b/tests/pyc/loop_context_returns.3.14.pyc differ diff --git a/tests/pyc/loop_context_returns.3.15.pyc b/tests/pyc/loop_context_returns.3.15.pyc new file mode 100644 index 00000000..fd2011ba Binary files /dev/null and b/tests/pyc/loop_context_returns.3.15.pyc differ diff --git a/tests/pyc/loop_handler_ownership.3.12.pyc b/tests/pyc/loop_handler_ownership.3.12.pyc new file mode 100644 index 00000000..a7c511ae Binary files /dev/null and b/tests/pyc/loop_handler_ownership.3.12.pyc differ diff --git a/tests/pyc/loop_handler_ownership.3.13.pyc b/tests/pyc/loop_handler_ownership.3.13.pyc new file mode 100644 index 00000000..ce130150 Binary files /dev/null and b/tests/pyc/loop_handler_ownership.3.13.pyc differ diff --git a/tests/pyc/loop_handler_ownership.3.14.pyc b/tests/pyc/loop_handler_ownership.3.14.pyc new file mode 100644 index 00000000..c40f2b9d Binary files /dev/null and b/tests/pyc/loop_handler_ownership.3.14.pyc differ diff --git a/tests/pyc/loop_handler_ownership.3.15.pyc b/tests/pyc/loop_handler_ownership.3.15.pyc new file mode 100644 index 00000000..98019758 Binary files /dev/null and b/tests/pyc/loop_handler_ownership.3.15.pyc differ diff --git a/tests/pyc/nested_context_returns.3.11.pyc b/tests/pyc/nested_context_returns.3.11.pyc new file mode 100644 index 00000000..f621cf44 Binary files /dev/null and b/tests/pyc/nested_context_returns.3.11.pyc differ diff --git a/tests/pyc/nested_context_returns.3.12.pyc b/tests/pyc/nested_context_returns.3.12.pyc new file mode 100644 index 00000000..0db7fe80 Binary files /dev/null and b/tests/pyc/nested_context_returns.3.12.pyc differ diff --git a/tests/pyc/nested_context_returns.3.13.pyc b/tests/pyc/nested_context_returns.3.13.pyc new file mode 100644 index 00000000..b408426c Binary files /dev/null and b/tests/pyc/nested_context_returns.3.13.pyc differ diff --git a/tests/pyc/nested_context_returns.3.14.pyc b/tests/pyc/nested_context_returns.3.14.pyc new file mode 100644 index 00000000..97baf745 Binary files /dev/null and b/tests/pyc/nested_context_returns.3.14.pyc differ diff --git a/tests/pyc/nested_context_returns.3.15.pyc b/tests/pyc/nested_context_returns.3.15.pyc new file mode 100644 index 00000000..cbb56edc Binary files /dev/null and b/tests/pyc/nested_context_returns.3.15.pyc differ diff --git a/tests/pyc/none_rejection_guards.3.14.pyc b/tests/pyc/none_rejection_guards.3.14.pyc new file mode 100644 index 00000000..4b95f7ac Binary files /dev/null and b/tests/pyc/none_rejection_guards.3.14.pyc differ diff --git a/tests/pyc/none_rejection_guards.3.15.pyc b/tests/pyc/none_rejection_guards.3.15.pyc new file mode 100644 index 00000000..93d96024 Binary files /dev/null and b/tests/pyc/none_rejection_guards.3.15.pyc differ diff --git a/tests/pyc/normal_async_exits.3.14.pyc b/tests/pyc/normal_async_exits.3.14.pyc new file mode 100644 index 00000000..dd9c8435 Binary files /dev/null and b/tests/pyc/normal_async_exits.3.14.pyc differ diff --git a/tests/pyc/normal_async_exits.3.15.pyc b/tests/pyc/normal_async_exits.3.15.pyc new file mode 100644 index 00000000..456685ae Binary files /dev/null and b/tests/pyc/normal_async_exits.3.15.pyc differ diff --git a/tests/pyc/padded_none_returns_0.3.11.pyc b/tests/pyc/padded_none_returns_0.3.11.pyc new file mode 100644 index 00000000..71192f68 Binary files /dev/null and b/tests/pyc/padded_none_returns_0.3.11.pyc differ diff --git a/tests/pyc/padded_none_returns_0.3.12.pyc b/tests/pyc/padded_none_returns_0.3.12.pyc new file mode 100644 index 00000000..ba1f0bdd Binary files /dev/null and b/tests/pyc/padded_none_returns_0.3.12.pyc differ diff --git a/tests/pyc/padded_none_returns_0.3.13.pyc b/tests/pyc/padded_none_returns_0.3.13.pyc new file mode 100644 index 00000000..b054dda5 Binary files /dev/null and b/tests/pyc/padded_none_returns_0.3.13.pyc differ diff --git a/tests/pyc/padded_none_returns_0.3.14.pyc b/tests/pyc/padded_none_returns_0.3.14.pyc new file mode 100644 index 00000000..692e41b6 Binary files /dev/null and b/tests/pyc/padded_none_returns_0.3.14.pyc differ diff --git a/tests/pyc/padded_none_returns_0.3.15.pyc b/tests/pyc/padded_none_returns_0.3.15.pyc new file mode 100644 index 00000000..a0a547e6 Binary files /dev/null and b/tests/pyc/padded_none_returns_0.3.15.pyc differ diff --git a/tests/pyc/padded_none_returns_1.3.11.pyc b/tests/pyc/padded_none_returns_1.3.11.pyc new file mode 100644 index 00000000..25d134e7 Binary files /dev/null and b/tests/pyc/padded_none_returns_1.3.11.pyc differ diff --git a/tests/pyc/padded_none_returns_1.3.12.pyc b/tests/pyc/padded_none_returns_1.3.12.pyc new file mode 100644 index 00000000..e2d2792d Binary files /dev/null and b/tests/pyc/padded_none_returns_1.3.12.pyc differ diff --git a/tests/pyc/padded_none_returns_1.3.13.pyc b/tests/pyc/padded_none_returns_1.3.13.pyc new file mode 100644 index 00000000..0555877b Binary files /dev/null and b/tests/pyc/padded_none_returns_1.3.13.pyc differ diff --git a/tests/pyc/padded_none_returns_1.3.14.pyc b/tests/pyc/padded_none_returns_1.3.14.pyc new file mode 100644 index 00000000..48e6587e Binary files /dev/null and b/tests/pyc/padded_none_returns_1.3.14.pyc differ diff --git a/tests/pyc/padded_none_returns_1.3.15.pyc b/tests/pyc/padded_none_returns_1.3.15.pyc new file mode 100644 index 00000000..c89e1c38 Binary files /dev/null and b/tests/pyc/padded_none_returns_1.3.15.pyc differ diff --git a/tests/pyc/padded_none_returns_3.3.11.pyc b/tests/pyc/padded_none_returns_3.3.11.pyc new file mode 100644 index 00000000..e0882574 Binary files /dev/null and b/tests/pyc/padded_none_returns_3.3.11.pyc differ diff --git a/tests/pyc/padded_none_returns_3.3.12.pyc b/tests/pyc/padded_none_returns_3.3.12.pyc new file mode 100644 index 00000000..3a066d43 Binary files /dev/null and b/tests/pyc/padded_none_returns_3.3.12.pyc differ diff --git a/tests/pyc/padded_none_returns_3.3.13.pyc b/tests/pyc/padded_none_returns_3.3.13.pyc new file mode 100644 index 00000000..3e33a197 Binary files /dev/null and b/tests/pyc/padded_none_returns_3.3.13.pyc differ diff --git a/tests/pyc/padded_none_returns_3.3.14.pyc b/tests/pyc/padded_none_returns_3.3.14.pyc new file mode 100644 index 00000000..abf44cf6 Binary files /dev/null and b/tests/pyc/padded_none_returns_3.3.14.pyc differ diff --git a/tests/pyc/padded_none_returns_3.3.15.pyc b/tests/pyc/padded_none_returns_3.3.15.pyc new file mode 100644 index 00000000..d66ceba8 Binary files /dev/null and b/tests/pyc/padded_none_returns_3.3.15.pyc differ diff --git a/tests/pyc/private_handler_clauses.3.12.pyc b/tests/pyc/private_handler_clauses.3.12.pyc new file mode 100644 index 00000000..c2a695c5 Binary files /dev/null and b/tests/pyc/private_handler_clauses.3.12.pyc differ diff --git a/tests/pyc/private_handler_clauses.3.13.pyc b/tests/pyc/private_handler_clauses.3.13.pyc new file mode 100644 index 00000000..7a1fdb42 Binary files /dev/null and b/tests/pyc/private_handler_clauses.3.13.pyc differ diff --git a/tests/pyc/private_handler_clauses.3.14.pyc b/tests/pyc/private_handler_clauses.3.14.pyc new file mode 100644 index 00000000..8577f731 Binary files /dev/null and b/tests/pyc/private_handler_clauses.3.14.pyc differ diff --git a/tests/pyc/private_handler_clauses.3.15.pyc b/tests/pyc/private_handler_clauses.3.15.pyc new file mode 100644 index 00000000..1c2b8d6a Binary files /dev/null and b/tests/pyc/private_handler_clauses.3.15.pyc differ diff --git a/tests/pyc/shared_validation_raises.3.14.pyc b/tests/pyc/shared_validation_raises.3.14.pyc new file mode 100644 index 00000000..44d77043 Binary files /dev/null and b/tests/pyc/shared_validation_raises.3.14.pyc differ diff --git a/tests/pyc/shared_validation_raises.3.15.pyc b/tests/pyc/shared_validation_raises.3.15.pyc new file mode 100644 index 00000000..f43b00a3 Binary files /dev/null and b/tests/pyc/shared_validation_raises.3.15.pyc differ diff --git a/tests/pyc/sunk_context_exits.3.14.pyc b/tests/pyc/sunk_context_exits.3.14.pyc new file mode 100644 index 00000000..4751ce12 Binary files /dev/null and b/tests/pyc/sunk_context_exits.3.14.pyc differ diff --git a/tests/pyc/sunk_context_exits.3.15.pyc b/tests/pyc/sunk_context_exits.3.15.pyc new file mode 100644 index 00000000..4c1c856c Binary files /dev/null and b/tests/pyc/sunk_context_exits.3.15.pyc differ diff --git a/tools/compile_bounded_try_returns.py b/tools/compile_bounded_try_returns.py new file mode 100644 index 00000000..30c4ba9e --- /dev/null +++ b/tools/compile_bounded_try_returns.py @@ -0,0 +1,35 @@ +"""Generate Bounded try-return fixtures with CPython 3.11–3.15. + +The zero-padding inputs and RETURN_CONST versions are compatibility controls. +This reuses the existing relocation helper rather than changing return values. +""" +import importlib.util +import marshal +from pathlib import Path +import sys +import types + +from compile_padded_value_returns import pad_return_values + + +if __name__ == "__main__": + if not (3, 11) <= sys.version_info[:2] <= (3, 15): + raise SystemExit("Use CPython 3.11 through 3.15") + root = Path(__file__).resolve().parents[1] + source_path = Path("tests/data/bounded_try_returns.py") + source = (root / source_path).read_bytes() + module = compile(source, source_path.as_posix(), "exec", dont_inherit=True) + version = f"{sys.version_info.major}.{sys.version_info.minor}" + header = (importlib.util.MAGIC_NUMBER + (3).to_bytes(4, "little") + + importlib.util.source_hash(source)) + for count in (0, 1, 3): + padded = module.replace(co_consts=tuple( + pad_return_values(value, count) if isinstance(value, types.CodeType) else value + for value in module.co_consts)) + payload = header + marshal.dumps(padded) + (root / f"tests/pyc/bounded_try_returns_{count}.{version}.pyc").write_bytes(payload) + if count == 3: + corpus = root / "tests/corpus" / ".".join(map(str, sys.version_info[:3])) + corpus.mkdir(parents=True, exist_ok=True) + (corpus / "bounded_try_returns_3.py").write_bytes(source) + (corpus / "bounded_try_returns_3.pyc").write_bytes(payload) diff --git a/tools/compile_carried_value_returns.py b/tools/compile_carried_value_returns.py new file mode 100644 index 00000000..1e9c3d5a --- /dev/null +++ b/tools/compile_carried_value_returns.py @@ -0,0 +1,35 @@ +"""Generate Carried-value return fixtures with CPython 3.11–3.15. + +Python 3.11 is a compatibility control for the protected-range boundary. +This reuses the existing relocation helper rather than changing return values. +""" +import importlib.util +import marshal +from pathlib import Path +import sys +import types + +from compile_padded_value_returns import pad_return_values + + +if __name__ == "__main__": + if not (3, 11) <= sys.version_info[:2] <= (3, 15): + raise SystemExit("Use CPython 3.11 through 3.15") + root = Path(__file__).resolve().parents[1] + source_path = Path("tests/data/carried_value_returns.py") + source = (root / source_path).read_bytes() + module = compile(source, source_path.as_posix(), "exec", dont_inherit=True) + version = f"{sys.version_info.major}.{sys.version_info.minor}" + header = (importlib.util.MAGIC_NUMBER + (3).to_bytes(4, "little") + + importlib.util.source_hash(source)) + for count in (1, 3, 9): + padded = module.replace(co_consts=tuple( + pad_return_values(value, count) if isinstance(value, types.CodeType) else value + for value in module.co_consts)) + payload = header + marshal.dumps(padded) + (root / f"tests/pyc/carried_value_returns_{count}.{version}.pyc").write_bytes(payload) + if count == 3: + corpus = root / "tests/corpus" / ".".join(map(str, sys.version_info[:3])) + corpus.mkdir(parents=True, exist_ok=True) + (corpus / "carried_value_returns_3.py").write_bytes(source) + (corpus / "carried_value_returns_3.pyc").write_bytes(payload) diff --git a/tools/compile_padded_none_returns.py b/tools/compile_padded_none_returns.py new file mode 100644 index 00000000..590def00 --- /dev/null +++ b/tools/compile_padded_none_returns.py @@ -0,0 +1,35 @@ +"""Generate None-return fixtures with CPython 3.11–3.15. + +The zero-padding inputs and RETURN_CONST versions are compatibility controls. +This reuses the existing relocation helper rather than changing return values. +""" +import importlib.util +import marshal +from pathlib import Path +import sys +import types + +from compile_padded_value_returns import pad_return_values + + +if __name__ == "__main__": + if not (3, 11) <= sys.version_info[:2] <= (3, 15): + raise SystemExit("Use CPython 3.11 through 3.15") + root = Path(__file__).resolve().parents[1] + source_path = Path("tests/data/padded_none_returns.py") + source = (root / source_path).read_bytes() + module = compile(source, source_path.as_posix(), "exec", dont_inherit=True) + version = f"{sys.version_info.major}.{sys.version_info.minor}" + header = (importlib.util.MAGIC_NUMBER + (3).to_bytes(4, "little") + + importlib.util.source_hash(source)) + for count in (0, 1, 3): + padded = module.replace(co_consts=tuple( + pad_return_values(value, count) if isinstance(value, types.CodeType) else value + for value in module.co_consts)) + payload = header + marshal.dumps(padded) + (root / f"tests/pyc/padded_none_returns_{count}.{version}.pyc").write_bytes(payload) + if count == 3: + corpus = root / "tests/corpus" / ".".join(map(str, sys.version_info[:3])) + corpus.mkdir(parents=True, exist_ok=True) + (corpus / "padded_none_returns_3.py").write_bytes(source) + (corpus / "padded_none_returns_3.pyc").write_bytes(payload)