Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,23 @@ work.
> `Lib/test/` checkout into the `regrtest` harness (`--cpython-dir`,
> crash-isolated `--mode subprocess`, `--jobs`) and rewrites the touched
> rows of `tests/regrtest/expectations.toml` from guesses to a **measured**
> baseline (`unexpected 0` on a fresh sweep); the full allowlist is still
> being worked through file by file. Expect small breaking changes
> around the edges as the long tail catches up.
> baseline (`unexpected 0` on a fresh sweep). `RFC 0049` (wave 5)
> retires the curated allowlist as a scope mechanism: discovery now
> schedules **every** vendored `test_*.py` file and `test_*/` package
> (504 labels, up from 227), and `expectations.toml` is a measured
> whole-suite baseline — 226 of the 427 vendored-CPython labels pass
> under the sweep budget (plus all 77 bundled fixtures), and every red
> row carries a measured first-failure reason. The same
> wave landed the `SETUP_ANNOTATIONS` opcode (block-entry
> `__annotations__`, lazy type/module getsets), CPython-strict
> `bool()`/`__bool__`/`__len__` semantics, `str` argument-clinic arity
> across ~30 native methods, full-mapping-protocol `str.format_map`,
> saturating int shift semantics, code-object value equality
> (`code_richcompare`), `Py_ReprEnter`-style recursive-repr guards on
> dict views and the io stack (fixing two native stack overflows), a
> CPython-shaped `codeop`, verbatim `configparser`, and the six
> built-in `codecs` error-handler callables. Expect small breaking
> changes around the edges as the long tail catches up.
>
> `RFC 0033` makes WeavePy *introspectable like CPython*. It ships a
> CPython-faithful **code-object surface** (`co_code`, `co_linetable`
Expand Down
8 changes: 8 additions & 0 deletions crates/weavepy-compiler/src/bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,13 @@ pub enum OpCode {
/// (which is left at TOS for further updates). Used for `{**d}`
/// dict-literal spreads.
DictUpdate,
/// CPython `SETUP_ANNOTATIONS`: bind `__annotations__` to a fresh
/// empty dict in the current scope *unless it is already bound*.
/// Emitted once at the top of any module/class body that contains
/// an annotated statement, so code preceding the first annotation
/// can already read `__annotations__` (ann_module.py does
/// `__annotations__[1] = 2` at module top).
SetupAnnotations,

// Functions / closures
/// Build a function object from the code object on TOS.
Expand Down Expand Up @@ -536,6 +543,7 @@ impl OpCode {
OpCode::UnpackSequence => "UNPACK_SEQUENCE",
OpCode::UnpackEx => "UNPACK_EX",
OpCode::DictUpdate => "DICT_UPDATE",
OpCode::SetupAnnotations => "SETUP_ANNOTATIONS",
OpCode::MakeFunction => "MAKE_FUNCTION",
OpCode::BuildSlice => "BUILD_SLICE",
OpCode::LoadBuildClass => "LOAD_BUILD_CLASS",
Expand Down
13 changes: 12 additions & 1 deletion crates/weavepy-compiler/src/cpython_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ pub mod op {
pub const DELETE_GLOBAL: u8 = 66;
pub const DELETE_NAME: u8 = 67;
pub const DICT_UPDATE: u8 = 69;
pub const SETUP_ANNOTATIONS: u8 = 37;
pub const EXTENDED_ARG: u8 = 71;
pub const FOR_ITER: u8 = 72;
pub const GET_AWAITABLE: u8 = 73;
Expand Down Expand Up @@ -304,6 +305,7 @@ fn map_to_cpython(ins: Instruction, nlocals: u32) -> MappedOp {
O::UnpackSequence => (op::UNPACK_SEQUENCE, ins.arg),
O::UnpackEx => (op::UNPACK_EX, ins.arg),
O::DictUpdate => (op::DICT_UPDATE, ins.arg),
O::SetupAnnotations => (op::SETUP_ANNOTATIONS, 0),
O::MakeFunction => (op::MAKE_FUNCTION, ins.arg),
O::BuildSlice => (op::BUILD_SLICE, ins.arg),
O::LoadBuildClass => (op::LOAD_BUILD_CLASS, 0),
Expand Down Expand Up @@ -493,7 +495,15 @@ pub fn encode(code: &CodeObject) -> CpythonCode {
let mut co_code: Vec<u8> = Vec::with_capacity(starts[n] * 2);
let mut positions: Vec<Position> = Vec::with_capacity(starts[n]);
let mut inst_offsets: Vec<u32> = Vec::with_capacity(n);
let firstlineno = code.linetable.first().copied().unwrap_or(1);
// A module code object always reports `co_firstlineno == 1` in CPython
// regardless of where its first statement sits (leading blank lines,
// comments — test_opcodes `test_setup_annotations_line`). Other code
// objects start at their first instruction's line.
let firstlineno = if code.name == "<module>" {
1
} else {
code.linetable.first().copied().unwrap_or(1)
};
for i in 0..n {
let line = code.linetable.get(i).copied().unwrap_or(firstlineno) as i32;
// PEP-657 columns, when the compiler tracked them for this
Expand Down Expand Up @@ -1072,6 +1082,7 @@ fn map_from_cpython(cp_op: u8, arg: u32, nlocals: u32) -> Option<(OpCode, u32)>
op::BUILD_TUPLE => (O::BuildTuple, arg),
op::BUILD_SET => (O::BuildSet, arg),
op::BUILD_MAP => (O::BuildMap, arg),
op::SETUP_ANNOTATIONS => (O::SetupAnnotations, 0),
op::BUILD_STRING => (O::BuildString, arg),
op::LIST_APPEND => (O::ListAppend, arg),
op::SET_ADD => (O::SetAdd, arg),
Expand Down
55 changes: 55 additions & 0 deletions crates/weavepy-compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,15 @@ impl Compiler {
fn compile_module_body(&mut self, module: &Module) -> Result<(), CompileError> {
self.analyze_scope_module(module);
self.emit(OpCode::Resume, 0);
// CPython's symtable marks a module block containing any annotated
// statement (at the block's own level) and the compiler emits
// SETUP_ANNOTATIONS as its first real instruction — code preceding
// the first annotation can already read `__annotations__`
// (ann_module.py does `__annotations__[1] = 2` at module top).
if block_has_annotations(&module.body) {
self.emit(OpCode::SetupAnnotations, 0);
self.annotations_initialized = true;
}
for stmt in &module.body {
self.compile_stmt(stmt)?;
}
Expand Down Expand Up @@ -2689,6 +2698,15 @@ impl Compiler {
inner.emit(OpCode::StoreName, doc_name);
}

// SETUP_ANNOTATIONS before the first body statement when the class
// block contains an annotated statement at its own level (CPython
// symtable `ste_annotations_used`), so a read of `__annotations__`
// preceding the first annotation sees the dict.
if block_has_annotations(body) {
inner.emit(OpCode::SetupAnnotations, 0);
inner.annotations_initialized = true;
}

for s in body {
inner.compile_stmt(s)?;
}
Expand Down Expand Up @@ -6170,6 +6188,43 @@ fn collect_self_attr_stores(stmts: &[Stmt], self_name: &str, out: &mut HashSet<S
}
}

/// Does this block contain an annotated statement *at its own scope level*?
/// Mirrors CPython's symtable `ste_annotations_used`: `AnnAssign` anywhere
/// in the block — including inside `if`/`for`/`while`/`with`/`try`/`match`
/// bodies — counts, but nested function/class scopes do not (they set up
/// their own annotations).
fn block_has_annotations(body: &[Stmt]) -> bool {
fn stmt_has(s: &Stmt) -> bool {
match &s.kind {
StmtKind::AnnAssign { .. } => true,
StmtKind::If { body, orelse, .. } | StmtKind::While { body, orelse, .. } => {
block_has_annotations(body) || block_has_annotations(orelse)
}
StmtKind::For { body, orelse, .. } | StmtKind::AsyncFor { body, orelse, .. } => {
block_has_annotations(body) || block_has_annotations(orelse)
}
StmtKind::With { body, .. } | StmtKind::AsyncWith { body, .. } => {
block_has_annotations(body)
}
StmtKind::Try {
body,
handlers,
orelse,
finalbody,
..
} => {
block_has_annotations(body)
|| block_has_annotations(orelse)
|| block_has_annotations(finalbody)
|| handlers.iter().any(|h| block_has_annotations(&h.body))
}
StmtKind::Match { cases, .. } => cases.iter().any(|c| block_has_annotations(&c.body)),
_ => false,
}
}
body.iter().any(stmt_has)
}

fn collect_assigned(stmt: &Stmt, out: &mut HashSet<String>) {
match &stmt.kind {
StmtKind::Assign { targets, .. } => {
Expand Down
11 changes: 10 additions & 1 deletion crates/weavepy-conformance/src/regrtest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,17 @@ pub fn discover_with(
let Some(name) = p.file_name().and_then(|n| n.to_str()) else {
continue;
};
if name.starts_with("test_") && name.to_ascii_lowercase().ends_with(".py") {
if !name.starts_with("test_") {
continue;
}
if p.is_file() && name.to_ascii_lowercase().ends_with(".py") {
allowlist.insert(name.to_owned());
} else if p.is_dir() && p.join("__init__.py").is_file() {
// Test *packages* (`test_asyncio/`, `test_json/`, …) are
// scheduled under the same `<name>.py` label convention
// the curated allowlist uses, so an expectations row keys
// identically whether the target is a file or a package.
allowlist.insert(format!("{name}.py"));
}
}
}
Expand Down
37 changes: 36 additions & 1 deletion crates/weavepy-vm/src/builtin_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3940,13 +3940,48 @@ fn install_value_type_new(bt: &BuiltinTypes) {
&bt.set_,
&bt.frozenset_,
] {
// `int.__new__` carries its owner so the `tp_new_wrapper` safety
// check can reject `int.__new__(bool, 0)` (bool's tp_new differs
// from int's in CPython) while `bool.__new__(bool, 0)` still works.
let wrapper = if Rc::ptr_eq(ty, &bt.int_) {
make_owned_new("int")
} else {
make_default_new()
};
ty.dict
.borrow_mut()
.insert(DictKey(Object::from_static("__new__")), make_default_new());
.insert(DictKey(Object::from_static("__new__")), wrapper);
}
install_mutable_container_init(bt);
}

/// Like [`make_default_new`], but the wrapper knows which built-in type it
/// was installed on, mirroring CPython's `tp_new_wrapper` "staticbase"
/// check: calling `A.__new__(B, …)` when `B` overrides `A`'s `tp_new`
/// raises "A.__new__(B) is not safe, use B.__new__()". Only the
/// `int`/`bool` pair matters among WeavePy's built-ins (bool is the one
/// built-in subclass with its own constructor semantics).
fn make_owned_new(owner: &'static str) -> Object {
use crate::object::BuiltinFn;
Object::StaticMethod(MethodWrapper::new(Object::Builtin(Rc::new(BuiltinFn {
name: "__new__",
binds_instance: true,
call: Box::new(move |args| {
if owner == "int" {
if let Some(Object::Type(cls)) = args.first() {
if cls.flags.is_builtin && cls.name == "bool" {
return Err(crate::error::type_error(
"int.__new__(bool) is not safe, use bool.__new__()".to_owned(),
));
}
}
}
object_new(args)
}),
call_kw: None,
}))))
}

/// The mutable containers own a real `tp_init` in CPython: `dict.__init__`
/// merges a mapping/iterable + kwargs, `list.__init__` clears and extends,
/// `set.__init__` clears and unions. `super().__init__(src)` from a
Expand Down
Loading
Loading