diff --git a/AGENTS.md b/AGENTS.md index 824c00dc071..33595089b01 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -143,6 +143,30 @@ The `t` is the **free-threaded** build: "CPython does X" is an answer only once holds without the GIL. Correct-because-a-global-lock-serialises-it is not on-spec. +### Module presence follows PyPy + +**Do not implement a module that PyPy does not have merely to unskip a CPython +test.** Whether a module exists, and which layer owns it, is part of the PyPy +implementation shape. Before adding any builtin, extension, compatibility, or +test-support module, verify that the real `pypy3` oracle imports it and locate +its owner in `pypy/`, `rpython/`, or `lib_pypy/`. If neither exists, the module +is not a pyre porting target unless the user explicitly expands the scope. +This applies to builtin-type and CPython-test-suite sweeps too: exclude an +absent module from the backlog instead of treating its skipped tests as missing +implementation work. The goal is to complete PyPy's module/type surface in the +PyPy way, not to reproduce CPython's private extension-module inventory. + +In particular, CPython-only test helpers such as `_testlimitedcapi` are not +product modules. PyPy also has no `_datetime` extension module: it provides +`datetime` through its pure-Python implementation, so pyre must preserve the +working pure-Python path rather than invent an `_datetime` stub or accelerator. +A CPython test that directly imports one of these absent private modules may +skip or remain blocked; that is not by itself an implementation gap. If the +public stdlib feature is broken, port PyPy's actual owner/fallback and fix that +root cause. Never add an empty or partial module just to make the import +succeed, because that suppresses the intended fallback and turns a clean skip +into misleading failures. + **The spec governs only what a caller can observe** — return value, exception type/message/attributes, identity, encoding-and-errors contract, accepted argument shapes. Everything else follows PyPy **unconditionally**: names, module diff --git a/Cargo.lock b/Cargo.lock index ee455ca5fe7..c6c877b6ad1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3241,7 +3241,7 @@ dependencies = [ [[package]] name = "rustpython-codegen" version = "0.5.0" -source = "git+https://github.com/RustPython/RustPython.git?rev=91a725fe833dbc06912a2619e0c39fffa0c61232#91a725fe833dbc06912a2619e0c39fffa0c61232" +source = "git+https://github.com/RustPython/RustPython.git?rev=dd2cc4d77a625661e5ca189f1cf9be199ecaa434#dd2cc4d77a625661e5ca189f1cf9be199ecaa434" dependencies = [ "bitflags 2.13.0", "indexmap", @@ -3264,7 +3264,7 @@ dependencies = [ [[package]] name = "rustpython-common" version = "0.5.0" -source = "git+https://github.com/RustPython/RustPython.git?rev=91a725fe833dbc06912a2619e0c39fffa0c61232#91a725fe833dbc06912a2619e0c39fffa0c61232" +source = "git+https://github.com/RustPython/RustPython.git?rev=dd2cc4d77a625661e5ca189f1cf9be199ecaa434#dd2cc4d77a625661e5ca189f1cf9be199ecaa434" dependencies = [ "ascii", "bitflags 2.13.0", @@ -3287,7 +3287,7 @@ dependencies = [ [[package]] name = "rustpython-compiler" version = "0.5.0" -source = "git+https://github.com/RustPython/RustPython.git?rev=91a725fe833dbc06912a2619e0c39fffa0c61232#91a725fe833dbc06912a2619e0c39fffa0c61232" +source = "git+https://github.com/RustPython/RustPython.git?rev=dd2cc4d77a625661e5ca189f1cf9be199ecaa434#dd2cc4d77a625661e5ca189f1cf9be199ecaa434" dependencies = [ "rustpython-codegen", "rustpython-compiler-core", @@ -3301,7 +3301,7 @@ dependencies = [ [[package]] name = "rustpython-compiler-core" version = "0.5.0" -source = "git+https://github.com/RustPython/RustPython.git?rev=91a725fe833dbc06912a2619e0c39fffa0c61232#91a725fe833dbc06912a2619e0c39fffa0c61232" +source = "git+https://github.com/RustPython/RustPython.git?rev=dd2cc4d77a625661e5ca189f1cf9be199ecaa434#dd2cc4d77a625661e5ca189f1cf9be199ecaa434" dependencies = [ "bitflags 2.13.0", "bitflagset", @@ -3317,7 +3317,7 @@ dependencies = [ [[package]] name = "rustpython-host_env" version = "0.5.0" -source = "git+https://github.com/RustPython/RustPython.git?rev=91a725fe833dbc06912a2619e0c39fffa0c61232#91a725fe833dbc06912a2619e0c39fffa0c61232" +source = "git+https://github.com/RustPython/RustPython.git?rev=dd2cc4d77a625661e5ca189f1cf9be199ecaa434#dd2cc4d77a625661e5ca189f1cf9be199ecaa434" dependencies = [ "bitflags 2.13.0", "cc", @@ -3351,7 +3351,7 @@ dependencies = [ [[package]] name = "rustpython-literal" version = "0.5.0" -source = "git+https://github.com/RustPython/RustPython.git?rev=91a725fe833dbc06912a2619e0c39fffa0c61232#91a725fe833dbc06912a2619e0c39fffa0c61232" +source = "git+https://github.com/RustPython/RustPython.git?rev=dd2cc4d77a625661e5ca189f1cf9be199ecaa434#dd2cc4d77a625661e5ca189f1cf9be199ecaa434" dependencies = [ "hexf-parse", "is-macro", @@ -3430,7 +3430,7 @@ dependencies = [ [[package]] name = "rustpython-sre_engine" version = "0.5.0" -source = "git+https://github.com/RustPython/RustPython.git?rev=91a725fe833dbc06912a2619e0c39fffa0c61232#91a725fe833dbc06912a2619e0c39fffa0c61232" +source = "git+https://github.com/RustPython/RustPython.git?rev=dd2cc4d77a625661e5ca189f1cf9be199ecaa434#dd2cc4d77a625661e5ca189f1cf9be199ecaa434" dependencies = [ "bitflags 2.13.0", "num_enum", @@ -3442,7 +3442,7 @@ dependencies = [ [[package]] name = "rustpython-unicode" version = "0.5.0" -source = "git+https://github.com/RustPython/RustPython.git?rev=91a725fe833dbc06912a2619e0c39fffa0c61232#91a725fe833dbc06912a2619e0c39fffa0c61232" +source = "git+https://github.com/RustPython/RustPython.git?rev=dd2cc4d77a625661e5ca189f1cf9be199ecaa434#dd2cc4d77a625661e5ca189f1cf9be199ecaa434" dependencies = [ "icu_casemap", "icu_locale", @@ -3456,7 +3456,7 @@ dependencies = [ [[package]] name = "rustpython-wtf8" version = "0.5.0" -source = "git+https://github.com/RustPython/RustPython.git?rev=91a725fe833dbc06912a2619e0c39fffa0c61232#91a725fe833dbc06912a2619e0c39fffa0c61232" +source = "git+https://github.com/RustPython/RustPython.git?rev=dd2cc4d77a625661e5ca189f1cf9be199ecaa434#dd2cc4d77a625661e5ca189f1cf9be199ecaa434" dependencies = [ "ascii", "bstr", diff --git a/Cargo.toml b/Cargo.toml index 361703b68ad..950bdf1484f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,19 +64,22 @@ pyre-jit-trace = { version = "0.0.2", path = "pyre/pyre-jit-trace" } pyre-wasm = { version = "0.0.2", path = "pyre/pyre-wasm" } pyrex = { version = "0.0.2", path = "pyre/pyrex" } -# Pinned to upstream main after the `Constants` mutable-access impls from -# #8557 merged; `fix_code_filenames` needs `IndexMut` to edit nested code -# constants in place. -rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "91a725fe833dbc06912a2619e0c39fffa0c61232" } -rustpython-literal = { git = "https://github.com/RustPython/RustPython.git", rev = "91a725fe833dbc06912a2619e0c39fffa0c61232" } -rustpython-compiler = { git = "https://github.com/RustPython/RustPython.git", rev = "91a725fe833dbc06912a2619e0c39fffa0c61232" } -rustpython-compiler-core = { git = "https://github.com/RustPython/RustPython.git", rev = "91a725fe833dbc06912a2619e0c39fffa0c61232" } +# Pinned to upstream main after the pyre compatibility fixes from #8390 and +# #8506, the memoryview fixes from #8553, the runtime marshal code-byte hook +# from #8552, the `Constants` mutable-access impls from #8557, and the compiler +# parity fixes from #8550 were merged. `fix_code_filenames` needs `IndexMut` +# to edit nested code constants in place, and `loads` relies on the #8552 hook +# to preserve invalid code bytes, so this pin must not move below either. +rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "dd2cc4d77a625661e5ca189f1cf9be199ecaa434" } +rustpython-literal = { git = "https://github.com/RustPython/RustPython.git", rev = "dd2cc4d77a625661e5ca189f1cf9be199ecaa434" } +rustpython-compiler = { git = "https://github.com/RustPython/RustPython.git", rev = "dd2cc4d77a625661e5ca189f1cf9be199ecaa434" } +rustpython-compiler-core = { git = "https://github.com/RustPython/RustPython.git", rev = "dd2cc4d77a625661e5ca189f1cf9be199ecaa434" } ruff-text-size = { package = "rustpython-ruff_text_size", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" } num-complex = "0.4" -rustpython-host_env = { git = "https://github.com/RustPython/RustPython.git", rev = "91a725fe833dbc06912a2619e0c39fffa0c61232" } -rustpython-wtf8 = { git = "https://github.com/RustPython/RustPython.git", rev = "91a725fe833dbc06912a2619e0c39fffa0c61232" } -rustpython-unicode = { git = "https://github.com/RustPython/RustPython.git", rev = "91a725fe833dbc06912a2619e0c39fffa0c61232" } -sre-engine = { package = "rustpython-sre_engine", git = "https://github.com/RustPython/RustPython.git", rev = "91a725fe833dbc06912a2619e0c39fffa0c61232" } +rustpython-host_env = { git = "https://github.com/RustPython/RustPython.git", rev = "dd2cc4d77a625661e5ca189f1cf9be199ecaa434" } +rustpython-wtf8 = { git = "https://github.com/RustPython/RustPython.git", rev = "dd2cc4d77a625661e5ca189f1cf9be199ecaa434" } +rustpython-unicode = { git = "https://github.com/RustPython/RustPython.git", rev = "dd2cc4d77a625661e5ca189f1cf9be199ecaa434" } +sre-engine = { package = "rustpython-sre_engine", git = "https://github.com/RustPython/RustPython.git", rev = "dd2cc4d77a625661e5ca189f1cf9be199ecaa434" } # majit JIT framework majit = { version = "0.0.2", path = "majit/majit" } diff --git a/lib-python/3/test/test_frame.py b/lib-python/3/test/test_frame.py index 18ade18d1a1..4adbb56385a 100644 --- a/lib-python/3/test/test_frame.py +++ b/lib-python/3/test/test_frame.py @@ -677,6 +677,7 @@ def test_eq(self): pass +@support.cpython_only class TestFrameCApi(unittest.TestCase): def test_basic(self): x = 1 @@ -792,6 +793,7 @@ def f(): # Call from C, so there is a shim frame directly above f: weak = operator.call(f) # BOOM! # Cool, we didn't crash. Check that the callback actually happened: + support.gc_collect() # For PyPy or other GCs. self.assertIs(catcher.unraisable.exc_type, TypeError) self.assertIsNone(weak()) diff --git a/lib-python/3/test/test_weakref.py b/lib-python/3/test/test_weakref.py index 4c7c900eb56..f7f67407efb 100644 --- a/lib-python/3/test/test_weakref.py +++ b/lib-python/3/test/test_weakref.py @@ -1781,6 +1781,7 @@ def test_weak_valued_union_operators(self): self.assertIs(type(tmp4), weakref.WeakValueDictionary) del a + gc_collect() # For PyPy or other GCs. self.assertNotIn(2, tmp1) self.assertNotIn(2, tmp2) self.assertNotIn(1, tmp3) @@ -1833,6 +1834,7 @@ def test_weak_keyed_union_operators(self): self.assertIs(type(tmp4), weakref.WeakKeyDictionary) del o1 + gc_collect() # For PyPy or other GCs. self.assertNotIn(4, tmp1.values()) self.assertNotIn(4, tmp2.values()) self.assertNotIn(1, tmp3.values()) diff --git a/majit/majit-backend-wasm/js/jit_glue.js b/majit/majit-backend-wasm/js/jit_glue.js index 82460e90ef5..9b979f8b688 100644 --- a/majit/majit-backend-wasm/js/jit_glue.js +++ b/majit/majit-backend-wasm/js/jit_glue.js @@ -55,8 +55,8 @@ function jitCallTrampoline(framePtr, callAreaOfs = CALL_RESULT_OFS) { } export function jit_compile_wasm(bytesPtr, bytesLen) { - const trace = instantiateTrace(bytesPtr, bytesLen); - return registerTrace(trace); + const entries = instantiateTrace(bytesPtr, bytesLen); + return registerTrace(entries); } function instantiateTrace(bytesPtr, bytesLen) { @@ -74,16 +74,24 @@ function instantiateTrace(bytesPtr, bytesLen) { const instance = new WebAssembly.Instance(module, { env: { memory: mainMemory, jit_call: jitCallTrampoline, jit_call_compact: jitCallTrampoline, __indirect_function_table: mainTable } }); - return instance.exports.trace; + return traceEntries(instance); } catch (e) { // Retry without jit_call (for traces without CALL ops) const instance = new WebAssembly.Instance(module, { env: { memory: mainMemory } }); - return instance.exports.trace; + return traceEntries(instance); } } +// A resumable peeled loop exports a fixed-arity `trace_wide` beside the narrow +// `trace` shim, and the backend publishes its table slot as `handle + 1`. Both +// hosts must therefore install the pair adjacently or that published slot names +// an unrelated trace. +function traceEntries(instance) { + return { trace: instance.exports.trace, wide: instance.exports.trace_wide }; +} + // Compile and instantiate a trace, then overwrite an existing shared-table // slot. A caller already running the old function retains that invocation; // later indirect calls use the replacement. @@ -92,11 +100,29 @@ export function jit_replace_wasm(funcId, bytesPtr, bytesLen) { if (!funcTable[funcId]) { return 0; } - const trace = instantiateTrace(bytesPtr, bytesLen); + const { trace, wide } = instantiateTrace(bytesPtr, bytesLen); + // Modules emitted while this slot was wide carry `call_indirect funcId + 1` + // baked in. A narrow replacement cannot retract those, so accepting one + // would leave the pair straddling two compiles: `funcId` on the new trace + // and `funcId + 1` still on the old. `funcTable` records the wide entry + // only when one existed, so it is the discriminator the shared table + // cannot be — its spare slot holds the narrow function either way. Reject + // the shape change before touching either table; narrow-to-wide stays + // allowed, mirroring the wasmtime host. + if (!wide && funcTable[funcId + 1] !== undefined) { + console.error('[jit_replace_wasm] refused: id', funcId, 'has a published wide entry the replacement does not'); + return 0; + } if (mainTable) { mainTable.set(funcId, trace); + if (wide) { + mainTable.set(funcId + 1, wide); + } } funcTable[funcId] = trace; + if (wide) { + funcTable[funcId + 1] = wide; + } return funcId; } catch (e) { console.error('[jit_replace_wasm] failed:', e); @@ -108,15 +134,27 @@ export function jit_replace_wasm(funcId, bytesPtr, bytesLen) { // table slot as the id, mirroring the wasmtime host. The slot is both the // jit_execute_wasm handle and the index an in-module call_indirect targets. // Falls back to a private counter when no table is available. -function registerTrace(traceFn) { +function registerTrace(entries) { + const { trace, wide } = entries; let id; + // The pair is appended even for a narrow module, mirroring the wasmtime + // host. An emitted module names its wide entry `id + 1`, and a later + // `jit_replace_wasm` may install one where this compile had none; without + // the reservation that write would land on the next trace's own entry. if (mainTable) { - id = mainTable.grow(1); - mainTable.set(id, traceFn); + id = mainTable.grow(2, trace); + mainTable.set(id, trace); + if (wide) { + mainTable.set(id + 1, wide); + } } else { - id = nextFuncId++; + id = nextFuncId; + nextFuncId += 2; + } + funcTable[id] = trace; + if (wide) { + funcTable[id + 1] = wide; } - funcTable[id] = traceFn; return id; } diff --git a/majit/majit-backend-wasm/src/codegen.rs b/majit/majit-backend-wasm/src/codegen.rs index 5153906a494..a00b225c52b 100644 --- a/majit/majit-backend-wasm/src/codegen.rs +++ b/majit/majit-backend-wasm/src/codegen.rs @@ -2703,7 +2703,12 @@ pub fn build_wasm_module( ))); } - let entry_param_count = 1 + bridge_entry_arity.unwrap_or(0) as u32; + let label_param_entry = has_label_param_entry(inputargs, ops, *frame, *bridge_entry_arity); + let entry_param_count = 1 + if label_param_entry { + crate::FROZEN_LABEL_PARAM_ARITY + } else { + bridge_entry_arity.unwrap_or(0) + } as u32; if let Some(arity) = bridge_entry_arity && *arity != inputargs.len() { @@ -2852,6 +2857,20 @@ pub fn build_wasm_module( ); idx }); + let label_param_entry_type_idx = label_param_entry.then(|| { + let idx = next_type_idx; + next_type_idx += 1; + types.ty().function( + std::iter::once(ValType::I32) + .chain(std::iter::repeat_n( + ValType::I64, + crate::FROZEN_LABEL_PARAM_ARITY, + )) + .collect::>(), + vec![ValType::I32], + ); + idx + }); let mut bridge_param_type_indices = indexmap::IndexMap::new(); if let (Some(arity), Some(idx)) = (*bridge_entry_arity, bridge_entry_type_idx) { bridge_param_type_indices.insert(arity, idx); @@ -2970,7 +2989,12 @@ pub fn build_wasm_module( // Function section let mut functions = FunctionSection::new(); - functions.function(bridge_entry_type_idx.unwrap_or(0)); + if let Some(idx) = label_param_entry_type_idx { + functions.function(0); + functions.function(idx); + } else { + functions.function(bridge_entry_type_idx.unwrap_or(0)); + } module.section(&functions); // Only armed modules carry this global. The runner reads it after @@ -2993,6 +3017,9 @@ pub fn build_wasm_module( let trace_func_idx = if needs_call { 1 } else { 0 }; let mut exports = ExportSection::new(); exports.export("trace", ExportKind::Func, trace_func_idx); + if label_param_entry { + exports.export("trace_wide", ExportKind::Func, trace_func_idx + 1); + } if trace_entry_census.is_some() { exports.export("trace_entry_census_id", ExportKind::Global, 0); } @@ -3036,13 +3063,35 @@ pub fn build_wasm_module( ca.clone(), ca_helper_type_idx, *trace_entry_census, + label_param_entry, )?; + if label_param_entry { + codes.function(&build_label_param_shim(trace_func_idx + 1)); + } codes.function(&func); module.section(&codes); Ok((module.finish(), guards, num_ref_homes)) } +fn build_label_param_shim(wide_func_idx: u32) -> Function { + let mut func = Function::new(Vec::new()); + let mut raw_sink = func.instructions(); + let mut sink = PeepSink::new(&mut raw_sink); + + sink.local_get(0); + for k in 0..crate::FROZEN_LABEL_PARAM_ARITY { + sink.local_get(0); + sink.i64_load(mem64(FRAME_SLOT_BASE + k as u64 * SLOT_SIZE)); + } + sink.return_call(wide_func_idx); + sink.end(); + sink.flush(); + drop(sink); + + func +} + #[allow(clippy::too_many_arguments)] fn build_function( entry_inputargs: &[InputArg], @@ -3098,6 +3147,7 @@ fn build_function( // `ca.deopt_helper_slot` for a deopted callee. ca_helper_type_idx: u32, trace_entry_census: Option, + label_param_entry: bool, ) -> Result { // The CA arm requires residual types (the setup above forces arity >= 2 // while `WASM_DIRECT_RESIDUAL_CALL` is enabled). Its `jit_call` fallback @@ -3397,7 +3447,7 @@ fn build_function( // resume loader sets the live label-arg homes. for (k, ia) in entry_inputargs.iter().enumerate() { let local_idx = value_types.local(ia.index); - if bridge_entry_arity.is_some() { + if bridge_entry_arity.is_some() || label_param_entry { // Parameter entries carry raw i64 words after frame_ptr. Float // values use their IEEE bit pattern, matching the guard boundary. sink.local_get(k as u32 + 1); @@ -3467,8 +3517,12 @@ fn build_function( // homes, mirroring the JUMP's ref-home refresh below. The // fall-through path skipped this via the `br 1` above. for (i, la) in all_label_args[labels_passed].iter().enumerate() { - sink.local_get(0); - sink.i64_load(mem64(FRAME_SLOT_BASE + i as u64 * SLOT_SIZE)); + if label_param_entry { + sink.local_get(i as u32 + 1); + } else { + sink.local_get(0); + sink.i64_load(mem64(FRAME_SLOT_BASE + i as u64 * SLOT_SIZE)); + } if value_types.ty(la.raw()) == ValType::F64 { sink.f64_reinterpret_i64(); } @@ -6418,6 +6472,29 @@ pub fn label_arg_counts(ops: &[Op]) -> Vec { .collect() } +pub fn has_label_param_entry( + inputargs: &[InputArg], + ops: &[Op], + frame: FrameGeometry, + bridge_entry_arity: Option, +) -> bool { + if bridge_entry_arity.is_some() || !is_resumable_peeled(ops) { + return false; + } + let resumable = resumable_label_count(ops); + let labels_fit = label_arg_counts(ops) + .into_iter() + .take(resumable) + .all(|arity| arity <= crate::FROZEN_LABEL_PARAM_ARITY); + labels_fit + && inputargs.len() <= crate::FROZEN_LABEL_PARAM_ARITY + // The shim loads from `FRAME_SLOT_BASE`, so its `FROZEN_LABEL_PARAM_ARITY` + // reads occupy slots 1..=FROZEN_LABEL_PARAM_ARITY — slot 0 is the + // dispatch key. A frame with exactly that many slots would let the last + // load run off the end. + && frame.value_slots >= crate::FROZEN_LABEL_PARAM_ARITY + 1 +} + /// Per-label `(resume_safe, requires_own_frame)` metadata in ordinal order. /// Missing pre-LABEL live-ins are safe when the frozen geometry contains the /// capture plan. Such a plan is tied to the physical frame on which the owning diff --git a/majit/majit-backend-wasm/src/failguard.rs b/majit/majit-backend-wasm/src/failguard.rs index d98b000855b..b7d8d82e876 100644 --- a/majit/majit-backend-wasm/src/failguard.rs +++ b/majit/majit-backend-wasm/src/failguard.rs @@ -324,6 +324,8 @@ mod tests { pub struct LabelTarget { /// Table slot of the owning loop's compiled function. pub func_handle: u32, + /// Table slot of the fixed-arity label-parameter entry, or 0 when absent. + pub wide_slot: u32, /// Resume dispatch key (`label ordinal + 1`) the bridge's JUMP writes. pub key: u32, /// The label's arg count — the resume loader reads exactly this many diff --git a/majit/majit-backend-wasm/src/lib.rs b/majit/majit-backend-wasm/src/lib.rs index 20c2210d2e3..fceaf3e827f 100644 --- a/majit/majit-backend-wasm/src/lib.rs +++ b/majit/majit-backend-wasm/src/lib.rs @@ -420,6 +420,12 @@ fn diag_bump(i: usize) { const FROZEN_CHAIN_VALUE_SLOTS: usize = 64; const FROZEN_CHAIN_REF_HOMES: usize = 128; const FROZEN_CHAIN_LABEL_REF_SLOTS: usize = 2; +/// Words a label-parameter entry accepts after `frame_ptr`. One value for +/// every such entry in the process: a loop-closing JUMP reaches its target +/// with `return_call_indirect` on the shared table, and that type-checks the +/// callee against the *calling* module's type index, so two modules cannot +/// each pick their own width. +pub const FROZEN_LABEL_PARAM_ARITY: usize = 16; /// An op whose result advances loop-carried state. A value produced inside the /// re-running region by arithmetic or by a heap load is fresh on each pass, so @@ -580,6 +586,18 @@ fn stamp_and_publish_label_targets( // trace remainder. let label_num_args = codegen::label_arg_counts(ops); let label_resume_info = codegen::label_resume_info(inputargs, ops, frame); + // The wide entry occupies the slot the host appended right after this + // module's narrow one, so it exists only once a handle does. Where + // `func_handle` is 0 — a native build, which has no host at all — + // `func_handle + 1` would name slot 1, another trace's entry rather than + // an absent one, so both fields share the 0-means-absent encoding. + let wide_slot = if func_handle != 0 + && codegen::has_label_param_entry(inputargs, ops, frame, bridge_entry_arity) + { + func_handle + 1 + } else { + 0 + }; let mut published_descrs = Vec::new(); // A parameter entry with no fail values remains structurally `(i32) -> // i32`, so type-0 indirect calls may enter it. Only a nonzero parameter @@ -612,6 +630,7 @@ fn stamp_and_publish_label_targets( id, LabelTarget { func_handle, + wide_slot, key: j as u32 + 1, num_args: label_num_args[j], resume_safe: label_resume_info[j].0, @@ -652,6 +671,7 @@ fn stamp_and_publish_label_targets( id, LabelTarget { func_handle, + wide_slot, key: 0, num_args: inputargs.len(), resume_safe: true, diff --git a/majit/majit-backend-wasm/tests/codegen_test.rs b/majit/majit-backend-wasm/tests/codegen_test.rs index 5db613e31c4..c6169a90404 100644 --- a/majit/majit-backend-wasm/tests/codegen_test.rs +++ b/majit/majit-backend-wasm/tests/codegen_test.rs @@ -509,6 +509,11 @@ fn build_module_default( ) } +/// Locals of the module's FIRST code entry. +/// +/// A trace that emits a label-parameter entry puts the narrow shim first and +/// the real body second, and the shim declares no locals. Use this only for +/// shapes `has_label_param_entry` rejects, or it reports the shim's zero. fn emitted_local_count(bytes: &[u8]) -> u32 { wasmparser::Parser::new(0) .parse_all(bytes) @@ -525,6 +530,43 @@ fn emitted_local_count(bytes: &[u8]) -> u32 { .expect("generated module must contain its trace function") } +/// `(type arities, defined-function type indices, exported name -> func index)`. +/// +/// Each type is reduced to `(params, results)` because that is all the entry +/// shape assertions need, and it keeps them readable next to the wasm text. +fn module_shape(bytes: &[u8]) -> (Vec<(usize, usize)>, Vec, HashMap) { + let mut types = Vec::new(); + let mut functions = Vec::new(); + let mut exports = HashMap::new(); + for payload in wasmparser::Parser::new(0).parse_all(bytes) { + match payload.unwrap() { + wasmparser::Payload::TypeSection(reader) => { + for group in reader { + for ty in group.unwrap().into_types() { + let func = ty.unwrap_func(); + types.push((func.params().len(), func.results().len())); + } + } + } + wasmparser::Payload::FunctionSection(reader) => { + for idx in reader { + functions.push(idx.unwrap()); + } + } + wasmparser::Payload::ExportSection(reader) => { + for export in reader { + let export = export.unwrap(); + if export.kind == wasmparser::ExternalKind::Func { + exports.insert(export.name.to_string(), export.index); + } + } + } + _ => {} + } + } + (types, functions, exports) +} + #[test] fn sparse_value_ids_declare_only_addressable_value_locals() { let inputargs = vec![ @@ -2855,6 +2897,94 @@ fn test_multi_label_peeled_resumes_at_last_label_validates() { assert!(!guards[0].is_finish); } +/// A resumable-peeled loop within the fixed arity emits two functions: the +/// narrow `trace` shim the host and CALL_ASSEMBLER keep entering, and the +/// `trace_wide` body a loop-closing JUMP can pass its arguments to. +/// +/// `trace` must stay structurally `(i32) -> i32`: a loop's table slot is also +/// what the CALL_ASSEMBLER path calls as type 0, so widening it in place would +/// turn every such call into a trap rather than a decline. +#[test] +fn peeled_loop_exports_a_narrow_shim_beside_its_wide_entry() { + let inputargs = vec![InputArg::from_type(Type::Int, 0)]; + let const_1 = OpRef::const_int(1); + let const_100 = OpRef::const_int(100); + let constants: indexmap::IndexMap = indexmap::IndexMap::new(); + + let ops = vec![ + make_op( + OpCode::IntAdd, + &[OpRef::input_arg_int(0), const_1], + OpRef::int_op(1), + ), + Op::new(OpCode::Label, &[rb(OpRef::int_op(1))]), + make_op( + OpCode::IntAdd, + &[OpRef::int_op(1), const_1], + OpRef::int_op(2), + ), + make_op( + OpCode::IntLt, + &[OpRef::int_op(2), const_100], + OpRef::int_op(3), + ), + make_guard(OpCode::GuardTrue, &[OpRef::int_op(3)], &[OpRef::int_op(2)]), + Op::new(OpCode::Jump, &[rb(OpRef::int_op(2))]), + ]; + + let frame = codegen::FrameGeometry::fixed(); + assert!( + codegen::has_label_param_entry(&inputargs, &ops, frame, None), + "this shape is the one the wide entry exists for; if the gate stops \ + accepting it the rest of these assertions prove nothing" + ); + + let (bytes, _) = build_module_default(&inputargs, &ops, &constants); + validate_wasm(&bytes); + + let (types, functions, exports) = module_shape(&bytes); + let narrow = exports.get("trace").copied().expect("narrow entry export"); + let wide = exports + .get("trace_wide") + .copied() + .expect("wide entry export"); + assert_eq!(wide, narrow + 1, "the wide entry follows the shim"); + + // Exports index the whole function space, imports first; the type section + // is indexed by defined function only. + let imported = narrow; + assert_eq!( + types[functions[(narrow - imported) as usize] as usize], + (1, 1), + "trace must stay (i32) -> i32" + ); + assert_eq!( + types[functions[(wide - imported) as usize] as usize], + (1 + majit_backend_wasm::FROZEN_LABEL_PARAM_ARITY, 1), + "trace_wide takes frame_ptr plus one word per fixed label parameter" + ); + + // The shim reads from `FRAME_SLOT_BASE`, so its loads land in slots + // 1..=FROZEN_LABEL_PARAM_ARITY and a frame holding exactly that many slots + // is one short — its last load would run off the end. + let exact = codegen::FrameGeometry { + value_slots: majit_backend_wasm::FROZEN_LABEL_PARAM_ARITY, + ..frame + }; + assert!( + !codegen::has_label_param_entry(&inputargs, &ops, exact, None), + "a frame with FROZEN_LABEL_PARAM_ARITY slots must be rejected" + ); + let one_more = codegen::FrameGeometry { + value_slots: majit_backend_wasm::FROZEN_LABEL_PARAM_ARITY + 1, + ..frame + }; + assert!( + codegen::has_label_param_entry(&inputargs, &ops, one_more, None), + "one slot past the arity is the smallest frame the shim can read" + ); +} + #[test] fn test_non_last_label_backedge_validates() { // Quasi-immutable invalidation can re-trace a loop with a wide entry diff --git a/majit/majit-gc/src/collector.rs b/majit/majit-gc/src/collector.rs index ce1605eeed7..2bab68bf5f4 100644 --- a/majit/majit-gc/src/collector.rs +++ b/majit/majit-gc/src/collector.rs @@ -4597,6 +4597,34 @@ impl MiniMarkGC { } } + /// Return the inherited frontend-only edge that app-level inspection must + /// omit. RPython's class/type pointer is header metadata rather than a GC + /// referent; pyre's managed mirror is declared once on the OBJECT root. + fn app_level_inspector_hidden_edge_offset(&self, mut type_id: u32) -> Option { + loop { + let info = self.types.get(type_id); + if let Some(offset) = info.app_level_inspector_hidden_edge_offset { + return Some(offset); + } + type_id = info.parent?; + } + } + + /// Inspector trace for a value that may be a translated prebuilt/foreign + /// object. RPython gives those objects a typeid through the prebuilt GC + /// layout; pyre recovers it from the registered vtable mapping. Unlike + /// the collector trace, this omits the frontend's managed type-pointer + /// mirror by its slot address, so a real item equal to the class object is + /// still reported. + fn visit_actual_inspector_referents(&self, obj: GcRef, visitor: &mut dyn FnMut(GcRef)) -> bool { + let Some(type_id) = self.get_actual_typeid(obj) else { + return false; + }; + let hidden_offset = self.app_level_inspector_hidden_edge_offset(type_id); + self.visit_referents_with_type_id_filtered(obj.0, type_id, hidden_offset, visitor); + true + } + /// `visit_referents` for a caller that already resolved the type id, /// because a prebuilt object carries no header to read it back out of. /// The bounds check sits here rather than beside the header read so that @@ -4607,12 +4635,25 @@ impl MiniMarkGC { obj_addr: usize, type_id: u32, visitor: &mut dyn FnMut(GcRef), + ) { + self.visit_referents_with_type_id_filtered(obj_addr, type_id, None, visitor); + } + + fn visit_referents_with_type_id_filtered( + &self, + obj_addr: usize, + type_id: u32, + ignored_offset: Option, + visitor: &mut dyn FnMut(GcRef), ) { self.validate_type_id(type_id, obj_addr, "visit_referents"); let type_info = self.types.get(type_id); if let Some(trace_fn) = type_info.custom_trace { unsafe { trace_fn(obj_addr, &mut |slot_ptr: *mut GcRef| { + if ignored_offset.is_some_and(|offset| slot_ptr as usize == obj_addr + offset) { + return; + } let field_ref = *slot_ptr; if !field_ref.is_null() { visitor(field_ref); @@ -4622,6 +4663,9 @@ impl MiniMarkGC { return; } for &offset in &type_info.gc_ptr_offsets { + if ignored_offset == Some(offset) { + continue; + } let field_ref = unsafe { *((obj_addr + offset) as *const GcRef) }; if !field_ref.is_null() { visitor(field_ref); @@ -4936,7 +4980,7 @@ impl MiniMarkGC { /// list reports its items rather than its item array. GCFLAG_EXTRA keeps /// each node out of the walk twice and is restored before returning. pub fn do_get_referents(&mut self, obj: GcRef, visitor: &mut dyn FnMut(GcRef)) { - if obj.is_null() || !self.is_managed_heap_object(obj.0) { + if obj.is_null() || self.get_actual_typeid(obj).is_none() { return; } // A host-side raw local can still hold a just-forwarded nursery @@ -4959,16 +5003,23 @@ impl MiniMarkGC { let mut parent = obj; loop { let mut children: Vec = Vec::new(); - self.visit_referents(parent.0, &mut |child| children.push(child)); + self.visit_actual_inspector_referents(parent, &mut |child| children.push(child)); for child in children { - if child.is_null() || !self.is_managed_heap_object(child.0) { + if child.is_null() || self.get_actual_typeid(child).is_none() { continue; } - let hdr = unsafe { header_of(child.0) }; - if unsafe { (*hdr).has_flag(flags::EXTRA) } { + if self.is_managed_heap_object(child.0) { + let hdr = unsafe { header_of(child.0) }; + if unsafe { (*hdr).has_flag(flags::EXTRA) } { + continue; + } + unsafe { (*hdr).set_flag(flags::EXTRA) }; + } else if pending.contains(&child) { + // RPython toggles GCFLAG_EXTRA on prebuilt objects too. + // Pyre's foreign wrappers have no GC header, so the same + // per-call identity set lives in the existing pending Vec. continue; } - unsafe { (*hdr).set_flag(flags::EXTRA) }; pending.push(child); } // Walk the queue until a non-app-level node needs expanding; on @@ -4989,7 +5040,9 @@ impl MiniMarkGC { } } for gcref in &pending { - unsafe { (*header_of(gcref.0)).clear_flag(flags::EXTRA) }; + if self.is_managed_heap_object(gcref.0) { + unsafe { (*header_of(gcref.0)).clear_flag(flags::EXTRA) }; + } } for gcref in result { visitor(gcref); @@ -8031,6 +8084,87 @@ mod tests { gc.roots.clear(); } + #[test] + fn get_referents_hides_inherited_frontend_type_edge_by_slot() { + unsafe fn trace_pair(obj_addr: usize, visit: &mut dyn FnMut(*mut GcRef)) { + visit(obj_addr as *mut GcRef); + visit((obj_addr + std::mem::size_of::()) as *mut GcRef); + } + + let ptr_size = std::mem::size_of::(); + let mut gc = test_gc(4096); + let root_tid = gc.register_type( + TypeInfo::object_with_gc_ptrs(2 * ptr_size, vec![0]) + .with_app_level_inspector_hidden_edge(0), + ); + let holder_tid = gc.register_type(TypeInfo::object_subclass_with_custom_trace( + 2 * ptr_size, + root_tid, + trace_pair, + )); + let class = gc.alloc_with_type(root_tid, 2 * ptr_size); + let item = gc.alloc_with_type(root_tid, 2 * ptr_size); + let mut holder = gc.alloc_with_type(holder_tid, 2 * ptr_size); + unsafe { + *(holder.0 as *mut GcRef) = class; + *((holder.0 + ptr_size) as *mut GcRef) = item; + gc.roots.add(&mut holder); + } + + let mut referents = Vec::new(); + gc.do_get_referents(holder, &mut |gcref| referents.push(gcref)); + assert_eq!(referents, vec![item]); + + // Filtering is by the header slot, not by pointer identity: an actual + // payload entry equal to the class object remains visible. + unsafe { *((holder.0 + ptr_size) as *mut GcRef) = class }; + let mut referents = Vec::new(); + gc.do_get_referents(holder, &mut |gcref| referents.push(gcref)); + assert_eq!(referents, vec![class]); + for object in [holder, class, item] { + assert!(!unsafe { (*header_of(object.0)).has_flag(flags::EXTRA) }); + } + gc.roots.clear(); + } + + #[test] + fn get_referents_reports_prebuilt_foreign_objects() { + #[repr(C)] + struct ForeignLeaf { + vtable: usize, + } + + #[repr(C)] + struct ForeignHolder { + vtable: usize, + child: GcRef, + } + + let ptr_size = std::mem::size_of::(); + let mut gc = test_gc(4096); + let leaf_vtable = 0x1000usize; + let holder_vtable = 0x2000usize; + let leaf_tid = gc.register_type(TypeInfo::object(ptr_size)); + let holder_tid = + gc.register_type(TypeInfo::object_with_gc_ptrs(2 * ptr_size, vec![ptr_size])); + crate::GcAllocator::register_vtable_for_type(&mut gc, leaf_vtable, leaf_tid); + crate::GcAllocator::register_vtable_for_type(&mut gc, holder_vtable, holder_tid); + + let leaf = Box::new(ForeignLeaf { + vtable: leaf_vtable, + }); + let holder = Box::new(ForeignHolder { + vtable: holder_vtable, + child: GcRef((&*leaf as *const ForeignLeaf) as usize), + }); + let holder_ref = GcRef((&*holder as *const ForeignHolder) as usize); + let leaf_ref = GcRef((&*leaf as *const ForeignLeaf) as usize); + + let mut referents = Vec::new(); + gc.do_get_referents(holder_ref, &mut |gcref| referents.push(gcref)); + assert_eq!(referents, vec![leaf_ref]); + } + #[test] fn rpy_inspector_keeps_raw_roots_and_referents_unexpanded() { let ptr_size = std::mem::size_of::(); diff --git a/majit/majit-gc/src/trace.rs b/majit/majit-gc/src/trace.rs index 5bf8b4670f7..fa6c3b2f4d6 100644 --- a/majit/majit-gc/src/trace.rs +++ b/majit/majit-gc/src/trace.rs @@ -386,6 +386,14 @@ pub struct TypeInfo { /// their relevant edges, while an unmaterialized execution frame is not /// itself reported as an app-level referrer. pub hide_from_app_level_inspector: bool, + /// One frontend-owned GC edge in the common OBJECT header that keeps + /// runtime class metadata alive but is not an app-level referent. PyPy's + /// RPython objects carry their class in the non-GC `typeptr`; pyre's + /// augmented `PyObject` header additionally stores the corresponding + /// Python class in a managed `w_class` slot. The collector must trace + /// that slot, while `gc.get_referents()` must omit it. Set this on the + /// OBJECT root; subclasses inherit it through [`Self::parent`]. + pub app_level_inspector_hidden_edge_offset: Option, /// Parent class typeid in the `rclass.OBJECT` hierarchy, or /// `None` for `rclass.OBJECT` itself / non-OBJECT types. /// Mirrors what `classdef.getmro()` traverses in @@ -443,6 +451,7 @@ impl TypeInfo { is_object: false, has_subclass_range: false, hide_from_app_level_inspector: false, + app_level_inspector_hidden_edge_offset: None, parent: None, subclassrange_min: 0, subclassrange_max: 0, @@ -468,6 +477,7 @@ impl TypeInfo { is_object: false, has_subclass_range: false, hide_from_app_level_inspector: false, + app_level_inspector_hidden_edge_offset: None, parent: None, subclassrange_min: 0, subclassrange_max: 0, @@ -513,6 +523,20 @@ impl TypeInfo { self } + /// Mark the frontend's managed mirror of RPython's non-GC type pointer. + /// This changes app-level inspection only; ordinary GC tracing continues + /// to visit the edge. + pub fn with_app_level_inspector_hidden_edge(mut self, offset: usize) -> Self { + assert!( + offset + .checked_add(std::mem::size_of::()) + .is_some_and(|end| end <= self.size), + "app-level inspector hidden edge exceeds the object payload" + ); + self.app_level_inspector_hidden_edge_offset = Some(offset); + self + } + /// `gctypelayout.is_weakref_type(WEAKREF)` parity — TypeInfo for /// the singleton WEAKREF GcStruct itself (gctypelayout.py). /// The struct payload is one `weakptr: GcRef` slot; the collector @@ -530,6 +554,7 @@ impl TypeInfo { is_object: false, has_subclass_range: false, hide_from_app_level_inspector: false, + app_level_inspector_hidden_edge_offset: None, parent: None, subclassrange_min: 0, subclassrange_max: 0, @@ -561,6 +586,7 @@ impl TypeInfo { is_object: true, has_subclass_range: true, hide_from_app_level_inspector: false, + app_level_inspector_hidden_edge_offset: None, parent: None, subclassrange_min: 0, subclassrange_max: 0, @@ -591,6 +617,7 @@ impl TypeInfo { is_object: true, has_subclass_range: true, hide_from_app_level_inspector: false, + app_level_inspector_hidden_edge_offset: None, parent: None, subclassrange_min: 0, subclassrange_max: 0, @@ -619,6 +646,7 @@ impl TypeInfo { is_object: true, has_subclass_range: true, hide_from_app_level_inspector: false, + app_level_inspector_hidden_edge_offset: None, parent: Some(parent_typeid), subclassrange_min: 0, subclassrange_max: 0, @@ -656,6 +684,7 @@ impl TypeInfo { is_object: true, has_subclass_range: true, hide_from_app_level_inspector: false, + app_level_inspector_hidden_edge_offset: None, parent: Some(parent_typeid), subclassrange_min: 0, subclassrange_max: 0, @@ -683,6 +712,7 @@ impl TypeInfo { is_object: true, has_subclass_range: true, hide_from_app_level_inspector: false, + app_level_inspector_hidden_edge_offset: None, parent: Some(parent_typeid), subclassrange_min: 0, subclassrange_max: 0, @@ -707,6 +737,7 @@ impl TypeInfo { is_object: false, has_subclass_range: false, hide_from_app_level_inspector: false, + app_level_inspector_hidden_edge_offset: None, parent: None, subclassrange_min: 0, subclassrange_max: 0, @@ -735,6 +766,7 @@ impl TypeInfo { is_object: false, has_subclass_range: false, hide_from_app_level_inspector: false, + app_level_inspector_hidden_edge_offset: None, parent: None, subclassrange_min: 0, subclassrange_max: 0, @@ -759,6 +791,7 @@ impl TypeInfo { is_object: false, has_subclass_range: false, hide_from_app_level_inspector: false, + app_level_inspector_hidden_edge_offset: None, parent: None, subclassrange_min: 0, subclassrange_max: 0, @@ -799,6 +832,7 @@ impl TypeInfo { is_object: false, has_subclass_range: false, hide_from_app_level_inspector: false, + app_level_inspector_hidden_edge_offset: None, parent: None, subclassrange_min: 0, subclassrange_max: 0, diff --git a/majit/majit-metainterp/src/jitdriver.rs b/majit/majit-metainterp/src/jitdriver.rs index 0a000b44823..9dd713002bd 100644 --- a/majit/majit-metainterp/src/jitdriver.rs +++ b/majit/majit-metainterp/src/jitdriver.rs @@ -75,6 +75,11 @@ pub struct SingleFrameBlackholeResult { pub registers_f: Vec, pub position: usize, pub last_opcode_position: usize, + /// The drive's virtualizable, forwarded across every collection the run + /// performed. The register banks above cannot answer this: a `-live-` + /// marker clears each Ref the live set omits, so the frame red reads 0 + /// whenever the run stopped past that register's last use. + pub virtualizable_ptr: i64, } fn bh_jitdrivers_sd( @@ -146,7 +151,7 @@ pub fn drive_single_frame_blackhole( miframe: &mut crate::pyjitpl::MIFrame, state_field_layout: crate::blackhole::StateFieldLayout, virtualizable_info: *const crate::virtualizable::VirtualizableInfo, - virtualizable_ptr: i64, + mut virtualizable_ptr: i64, virtualizable_stack_base: usize, metainterp_sd: &crate::pyjitpl::MetaInterpStaticData, mut last_exc_value: i64, @@ -174,6 +179,17 @@ pub fn drive_single_frame_blackhole( packed_ref_roots.push(last_exc_value); index }); + // The virtualizable is reachable from the Ref bank only while a `-live-` + // marker still names its register. The marker hook clears every Ref the + // live set omits, so a run that stops past the frame red's last use leaves + // that slot at 0 and the bank can no longer answer where the frame moved. + // Root the address beside the bank, the same way the exception value above + // is rooted, so the identity survives independently of the liveness set. + let virtualizable_root = (virtualizable_ptr != 0).then(|| { + let index = packed_ref_roots.len(); + packed_ref_roots.push(virtualizable_ptr); + index + }); unsafe { majit_gc::shadow_stack::push_resume_ref_roots(packed_ref_roots.as_mut_slice()); } @@ -191,6 +207,9 @@ pub fn drive_single_frame_blackhole( if let Some(index) = exception_root { last_exc_value = packed_ref_roots[index]; } + if let Some(index) = virtualizable_root { + virtualizable_ptr = packed_ref_roots[index]; + } builder.setup_jitdrivers_sd(bh_jitdrivers_sd(metainterp_sd)); @@ -224,6 +243,10 @@ pub fn drive_single_frame_blackhole( )) } }; + // Read the forwarded frame address out of the root area before dropping it. + if let Some(index) = virtualizable_root { + virtualizable_ptr = packed_ref_roots[index]; + } majit_gc::shadow_stack::pop_resume_ref_roots_to(root_depth); let result = SingleFrameBlackholeResult { @@ -233,6 +256,7 @@ pub fn drive_single_frame_blackhole( registers_f: std::mem::take(&mut bh.registers_f), position: bh.position, last_opcode_position: bh.last_opcode_position, + virtualizable_ptr, }; builder.release_interp(bh); result diff --git a/pyre/bench/synth/gc_native_strings_collectable.py b/pyre/bench/synth/gc_native_strings_collectable.py index 669d0d8fce4..fe5292e936b 100644 --- a/pyre/bench/synth/gc_native_strings_collectable.py +++ b/pyre/bench/synth/gc_native_strings_collectable.py @@ -53,8 +53,16 @@ def managed(label, value): managed("mmap closed repr", mmap.mmap.__repr__(mapping)) -objects = gc.get_objects() +# The census names only tracked types and a string is never one, so it can no +# longer report a result directly. It does name `results`, and the collector's +# own edge walk goes the rest of the way: one hop reaches the entry tuples and +# a second reaches the strings. A value the native path handed back without +# giving it a managed identity would be missing from that walk. +assert any(obj is results for obj in gc.get_objects()) +reached = [] +for entry in gc.get_referents(results): + reached.extend(gc.get_referents(entry)) for label, value in results: - assert any(obj is value for obj in objects), label + assert any(ref is value for ref in reached), label print("native runtime string results are collectable") diff --git a/pyre/bench/synth/gc_runtime_strings_collectable.py b/pyre/bench/synth/gc_runtime_strings_collectable.py index de645087034..4b26f4a6455 100644 --- a/pyre/bench/synth/gc_runtime_strings_collectable.py +++ b/pyre/bench/synth/gc_runtime_strings_collectable.py @@ -318,9 +318,17 @@ def __call__(self): managed(label, value) -# Take one heap census after every result is rooted in `results`. -objects = gc.get_objects() +# Take one heap census after every result is rooted in `results`. The census +# names only tracked types and a string is never one, so it can no longer +# report a result directly. It does name `results`, and the collector's own +# edge walk goes the rest of the way: one hop reaches the entry tuples and a +# second reaches the strings. A value the runtime handed back without giving +# it a managed identity would be missing from that walk. +assert any(obj is results for obj in gc.get_objects()) +reached = [] +for entry in gc.get_referents(results): + reached.extend(gc.get_referents(entry)) for label, value in results: - assert any(obj is value for obj in objects), label + assert any(ref is value for ref in reached), label print("runtime string results are collectable") diff --git a/pyre/bench/synth/inline_freevar_after_mayforce.cranelift.jitstats b/pyre/bench/synth/inline_freevar_after_mayforce.cranelift.jitstats index 45c515acad9..60dda91e67f 100644 --- a/pyre/bench/synth/inline_freevar_after_mayforce.cranelift.jitstats +++ b/pyre/bench/synth/inline_freevar_after_mayforce.cranelift.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=1008 +guard_failures=1009 internal_compile_panics=0 loops_aborted=0 loops_compiled=7 diff --git a/pyre/bench/synth/inline_freevar_after_mayforce.dynasm.jitstats b/pyre/bench/synth/inline_freevar_after_mayforce.dynasm.jitstats index 0bda1382c1c..635b86646ae 100644 --- a/pyre/bench/synth/inline_freevar_after_mayforce.dynasm.jitstats +++ b/pyre/bench/synth/inline_freevar_after_mayforce.dynasm.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=1004 +guard_failures=1005 internal_compile_panics=0 loops_aborted=0 loops_compiled=7 diff --git a/pyre/bench/synth/inline_freevar_after_mayforce.py b/pyre/bench/synth/inline_freevar_after_mayforce.py index afd841de8d2..0e734281486 100644 --- a/pyre/bench/synth/inline_freevar_after_mayforce.py +++ b/pyre/bench/synth/inline_freevar_after_mayforce.py @@ -1,4 +1,11 @@ # pyre-check: max-pypy-ratio=86 +# pyre-check: jitstats-band=guard_failures=1 +# One tree, three runners, one CI run (`1d212895c6b`): macOS and ubuntu read +# 1003 dynasm / 1008 cranelift, windows 1004 / 1009. The loop and bridge counts +# agreed at six and five everywhere, so the split is carried entirely by this +# counter and is not a function of the tree. The baseline holds the pair the two +# agreeing runners read; the band is exactly the measured width, so anything +# wider than the split still gates. # The ceiling is a function of N, so raising N refits it. pypy's execution here # is almost all fixed cost -- doubling N moved it 0.035s to 0.039s -- while this # backend pays roughly 27us per iteration, so the ratio tracks N nearly one for @@ -29,10 +36,25 @@ # and moved between two runs of one binary -- 923/6 loops against 925/7 here, # 923/6 against 938/8 on ubuntu and 922 against 923 on windows. Convergence # completes by 48000 on both native backends, and past it every gated counter is -# independent of N: dynasm holds 1004 guard failures and cranelift 1010, with +# independent of N: dynasm holds 1003 guard failures and cranelift 1008, with # six loops and five bridges, unchanged from 48000 through 96000. This sits far # enough above that point to keep the fixed point on a host that needs a few # more iterations to reach it. +# +# The fixed point was seven loops and 1007/1012 guard failures for as long as +# the blackhole recognized `catch_exception/L`: once jd0's staticdata carries +# the assembler's real opcode ids instead of the 255 `op_live` sentinel, an +# exception it used to let escape is caught, and the extra loop plus three +# guard failures are that arm being compiled. `driver_finish_setup` still +# installs those ids, but the arm is no longer reached, so the counters sit +# below that pair on both native backends. Whatever stopped reaching it is +# unattributed; the wasm backend never reached the arm at all, which is why its +# baseline never moved off six. +# +# At six loops the guard counts have alternated between 1004/1009 and 1003/1008 +# across runs while the loop and bridge counts held. Only the loop count answers +# whether the arm compiles, so treat a one-count move here as the unattributed +# remainder rather than as this fixture's subject. N = 64000 diff --git a/pyre/bench/synth/pickle_terminal_raise_resume.cranelift.jitstats b/pyre/bench/synth/pickle_terminal_raise_resume.cranelift.jitstats index c54b11a422a..fdb5ee1f631 100644 --- a/pyre/bench/synth/pickle_terminal_raise_resume.cranelift.jitstats +++ b/pyre/bench/synth/pickle_terminal_raise_resume.cranelift.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=298 +guard_failures=317 internal_compile_panics=0 loops_aborted=1 loops_compiled=30 diff --git a/pyre/bench/synth/pickle_terminal_raise_resume.dynasm.jitstats b/pyre/bench/synth/pickle_terminal_raise_resume.dynasm.jitstats index c54b11a422a..fdb5ee1f631 100644 --- a/pyre/bench/synth/pickle_terminal_raise_resume.dynasm.jitstats +++ b/pyre/bench/synth/pickle_terminal_raise_resume.dynasm.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=298 +guard_failures=317 internal_compile_panics=0 loops_aborted=1 loops_compiled=30 diff --git a/pyre/bench/synth/pickle_terminal_raise_resume.wasm.jitstats b/pyre/bench/synth/pickle_terminal_raise_resume.wasm.jitstats index 549f07ddf58..1173c2d3072 100644 --- a/pyre/bench/synth/pickle_terminal_raise_resume.wasm.jitstats +++ b/pyre/bench/synth/pickle_terminal_raise_resume.wasm.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=299 +guard_failures=318 internal_compile_panics=0 loops_aborted=8 loops_compiled=69 diff --git a/pyre/bench/synth/pypy_type_surface.cranelift.jitstats b/pyre/bench/synth/pypy_type_surface.cranelift.jitstats index ebea74eff3b..0d5b33feb8a 100644 --- a/pyre/bench/synth/pypy_type_surface.cranelift.jitstats +++ b/pyre/bench/synth/pypy_type_surface.cranelift.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=3 +bridges_compiled=2 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,8 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=604 +guard_failures=401 internal_compile_panics=0 loops_aborted=0 -loops_compiled=7 +loops_compiled=6 retraces_compiled=0 diff --git a/pyre/bench/synth/pypy_type_surface.dynasm.jitstats b/pyre/bench/synth/pypy_type_surface.dynasm.jitstats index ebea74eff3b..0d5b33feb8a 100644 --- a/pyre/bench/synth/pypy_type_surface.dynasm.jitstats +++ b/pyre/bench/synth/pypy_type_surface.dynasm.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=3 +bridges_compiled=2 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,8 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=604 +guard_failures=401 internal_compile_panics=0 loops_aborted=0 -loops_compiled=7 +loops_compiled=6 retraces_compiled=0 diff --git a/pyre/bench/synth/pypy_type_surface.py b/pyre/bench/synth/pypy_type_surface.py index 8c364961ce2..b2644619a7f 100644 --- a/pyre/bench/synth/pypy_type_surface.py +++ b/pyre/bench/synth/pypy_type_surface.py @@ -12,20 +12,34 @@ class Derived(Heap): pass -# Py_TPFLAGS_IMMUTABLETYPE. pyre publishes it on every type that is not a -# heap type and `get_flags` publishes no such bit, so it is the one bit of this -# word that follows 3.14 rather than PyPy — which makes it the one bit this -# fixture cannot compare against its oracle. Masked out below rather than -# dropped: what it names is asserted directly in -# `check_exception_group_immutable`, by making the store and failing if it -# succeeds. -PYPY_FLAGS = ~(1 << 8) +# The bits `W_TypeObject.get_flags` publishes, and only those: `_CPYTYPE` (1, +# cpyext-defined static types, which have no owner here), PATMA_SEQUENCE +# (1 << 5), PATMA_MAPPING (1 << 6), `_HEAPTYPE` (1 << 9), +# Py_TPFLAGS_METHOD_DESCRIPTOR (1 << 17) and `_ABSTRACT` (1 << 20). +# +# Comparing against a positive mask rather than the whole word is what keeps +# this an oracle comparison. pyre also publishes a 3.14 `tp_flags` surface — +# IMMUTABLETYPE, BASETYPE, HAVE_GC, READY, DISALLOW_INSTANTIATION, the managed +# dict/weakref pair, the fast-subclass family — and every one of those bits +# falls outside this mask precisely because `get_flags` has no opinion to +# compare it to. Each carries its own justification where it is published; a +# bit with no oracle does not belong in a fixture whose oracle is PyPy. +# +# What the masked-out bits name is still asserted where it is observable: +# IMMUTABLETYPE by `check_exception_group_immutable`, which makes the store and +# fails if it succeeds. +PYPY_FLAGS = 1 | (1 << 5) | (1 << 6) | (1 << 9) | (1 << 17) | (1 << 20) def check_flags(): - # `get_flags`: _HEAPTYPE 1<<9, PATMA_SEQUENCE 1<<5, PATMA_MAPPING 1<<6, - # _ABSTRACT 1<<20, Py_TPFLAGS_METHOD_DESCRIPTOR 1<<17. _CPYTYPE marks - # cpyext-defined static types and has no owner here. + # 0x20 is PATMA_SEQUENCE, 0x40 PATMA_MAPPING, 0x200 `_HEAPTYPE`. + # + # str, bytes and bytearray carry no row. `get_flags` answers 0x20 for + # them, but a sequence pattern must not match a string, so pyre publishes + # 0 and keeps the `S` marker only where `issequence_w` reads it. That + # makes them a place where PyPy is not the reference, which is what this + # fixture's rows are for; `bench/synth/match_sequence_str.py` is where the + # observable half is pinned. expected = { object: 0x0, type: 0x0, @@ -34,9 +48,6 @@ def check_flags(): frozenset: 0x0, BaseException: 0x0, type(None): 0x0, - str: 0x20, - bytes: 0x20, - bytearray: 0x20, list: 0x20, tuple: 0x20, range: 0x20, diff --git a/pyre/bench/synth/pypy_type_surface.wasm.jitstats b/pyre/bench/synth/pypy_type_surface.wasm.jitstats index ebea74eff3b..0d5b33feb8a 100644 --- a/pyre/bench/synth/pypy_type_surface.wasm.jitstats +++ b/pyre/bench/synth/pypy_type_surface.wasm.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=3 +bridges_compiled=2 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,8 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=604 +guard_failures=401 internal_compile_panics=0 loops_aborted=0 -loops_compiled=7 +loops_compiled=6 retraces_compiled=0 diff --git a/pyre/check.py b/pyre/check.py index 9f1eed58f46..514b635b122 100644 --- a/pyre/check.py +++ b/pyre/check.py @@ -902,15 +902,29 @@ def _dump_output_mismatch(actual, expected, limit=80): def _first_stderr_line(stderr): """Return `" "`, or `""` when there is none. + A traceback additionally carries its final line, which is the one that + names the exception. + A crash reason is otherwise just an exit code, which on a binary that dies before it writes any stdout says nothing about why. The empty answer is itself informative: it names a process that produced no diagnostic at all. + + An app-level crash is the one case where the first line carries nothing: + every Python traceback opens with the same banner, so a fixture that dies + on an assertion reports only `Traceback (most recent call last):` and the + CI log holds no other copy of the frames. The line that names the failure + is the last one, so append it — the same reason `_jit_panic_reason` + appends the message line that follows a Rust panic's location. """ - for line in (stderr or "").splitlines(): - line = line.strip() - if line: - return f" {line[:160]}" - return "" + lines = [line.strip() for line in (stderr or "").splitlines()] + lines = [line for line in lines if line] + if not lines: + return "" + reason = lines[0][:160] + banner = "Traceback (most recent call last):" + if len(lines) > 1 and any(line.startswith(banner) for line in lines): + reason += f" | {lines[-1][:400]}" + return f" {reason}" def _jit_panic_reason(stderr): @@ -936,10 +950,16 @@ def _jit_panic_reason(stderr): # actionable detail (e.g. a GC 'invalid type_id ... site=...' # diagnostic); the location line alone is not enough to triage # a flaky crash from CI logs, so append the first message line. + # + # The width has to reach `site=`, which the GC prints last. At + # 200 the varsize-length diagnostic was cut mid-`nursery_start`, + # dropping both `forwarded=` and `site=` — the two fields that + # name which path reached the object — and leaving a CI-only + # crash with no way to attribute it. for follow in lines[idx + 1 :]: follow = follow.strip() if follow: - reason += f" | {follow[:200]}" + reason += f" | {follow[:400]}" break return reason return "rust panic" diff --git a/pyre/extra_tests/parity_tests/text_signatures_python314.py b/pyre/extra_tests/parity_tests/text_signatures_python314.py index 0351fc2b56f..89e7a50c567 100644 --- a/pyre/extra_tests/parity_tests/text_signatures_python314.py +++ b/pyre/extra_tests/parity_tests/text_signatures_python314.py @@ -7,9 +7,11 @@ type does not duplicate the test machinery or interpreter startup. """ +import array import builtins import inspect import sys +import types def check_descriptors(owner, expected): @@ -99,6 +101,31 @@ def check_descriptors(owner, expected): "(*args, sep=' ', end='\\n', file=None, flush=False)" ) +# Constructor signatures are metadata on the type object, separate from the +# signatures on each type's ``__new__`` descriptor. +TYPE_EXPECTED = { + bool: "(object=False, /)", + float: "(x=0, /)", + complex: "(real=0, imag=0)", + memoryview: "(object)", + list: "(iterable=(), /)", + tuple: "(iterable=(), /)", + set: "(iterable=(), /)", + frozenset: "(iterable=(), /)", + property: "(fget=None, fset=None, fdel=None, doc=None)", + enumerate: "(iterable, start=0)", + reversed: "(sequence, /)", + map: "(function, iterable, /, *iterables, strict=False)", + filter: "(function, iterable, /)", + zip: "(*iterables, strict=False)", +} + +for cls, signature in TYPE_EXPECTED.items(): + assert cls.__text_signature__ == signature, cls + +for cls in (int, str, bytes, bytearray, dict, range, slice, super, type): + assert cls.__text_signature__ is None, cls + # bytearray_text_signatures_python314 @@ -429,6 +456,94 @@ def check_descriptors(owner, expected): assert str(inspect.signature(enumerate.__class_getitem__)) == "(object, /)" +class Sequence: + def __getitem__(self, index): + raise IndexError + + +ITERATOR_METHODS = [ + ( + type(iter(Sequence())), + ("__iter__", "__next__", "__reduce__", "__length_hint__", "__setstate__"), + ), + ( + type(iter([])), + ("__iter__", "__next__", "__length_hint__", "__reduce__", "__setstate__"), + ), + ( + type(reversed([])), + ("__iter__", "__next__", "__length_hint__", "__reduce__", "__setstate__"), + ), + ( + type(iter(())), + ("__iter__", "__next__", "__length_hint__", "__reduce__", "__setstate__"), + ), + ( + type(iter(b"")), + ("__iter__", "__next__", "__reduce__", "__length_hint__", "__setstate__"), + ), + ( + type(iter(bytearray())), + ("__iter__", "__next__", "__reduce__", "__length_hint__", "__setstate__"), + ), + ( + type(iter("")), + ("__iter__", "__next__", "__reduce__", "__length_hint__", "__setstate__"), + ), + ( + type(iter("é")), + ("__iter__", "__next__", "__reduce__", "__length_hint__", "__setstate__"), + ), + ( + type(iter(range(1))), + ("__iter__", "__next__", "__length_hint__", "__reduce__", "__setstate__"), + ), + ( + type(iter(range(2**100))), + ("__iter__", "__next__", "__length_hint__", "__reduce__", "__setstate__"), + ), + (type(iter(set())), ("__iter__", "__next__", "__length_hint__", "__reduce__")), + (type(iter({})), ("__iter__", "__next__", "__length_hint__", "__reduce__")), + ( + type(iter({}.values())), + ("__iter__", "__next__", "__length_hint__", "__reduce__"), + ), + (type(iter({}.items())), ("__iter__", "__next__", "__length_hint__", "__reduce__")), + (type(reversed({})), ("__iter__", "__next__", "__length_hint__", "__reduce__")), + ( + type(reversed({}.values())), + ("__iter__", "__next__", "__length_hint__", "__reduce__"), + ), + ( + type(reversed({}.items())), + ("__iter__", "__next__", "__length_hint__", "__reduce__"), + ), + (type(iter(lambda: None, None)), ("__iter__", "__next__", "__reduce__")), + (type(iter(memoryview(b""))), ("__iter__", "__next__")), + ( + type(iter(array.array("i"))), + ("__iter__", "__next__", "__reduce__", "__setstate__"), + ), +] + +for typ, names in ITERATOR_METHODS: + for name in names: + parameter = "state" if typ.__name__ == "arrayiterator" else "object" + expected = ( + f"($self, {parameter}, /)" if name == "__setstate__" else "($self, /)" + ) + assert typ.__dict__[name].__text_signature__ == expected, (typ, name) + +callable_iterator = type(iter(lambda: None, None)) +assert "__doc__" in callable_iterator.__dict__ +assert callable_iterator.__dict__["__doc__"] is None +assert str(inspect.signature(type(iter([])).__setstate__)) == "(self, object, /)" +assert ( + str(inspect.signature(type(iter(array.array("i"))).__setstate__)) + == "(self, state, /)" +) + + # int_text_signatures_python314 VALUE_BINARY = { @@ -908,4 +1023,50 @@ def check_descriptors(owner, expected): assert str(inspect.signature(type.__prepare__)) == "(name, bases, /, **kwds)" assert str(inspect.signature(type.__instancecheck__)) == "(self, instance, /)" + +# internal_type_text_signatures_python314 +# +# The signature sits on the type object rather than on a `__new__` descriptor, +# so `inspect.signature` resolves against it instead of falling back to +# `(*args, **kwargs)`. + +EXPECTED_TYPES = { + types.EllipsisType: "()", + types.GenericAlias: "(origin, args, /)", + types.NotImplementedType: "()", + types.ModuleType: "(name, doc=None)", + types.MappingProxyType: "(mapping)", + types.CellType: "([contents])", + types.MethodType: "(function, instance, /)", + types.FunctionType: ( + "(code, globals, name=None, argdefs=None, closure=None,\n" + " kwdefaults=None)" + ), + types.CodeType: ( + "(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize,\n" + " flags, codestring, constants, names, varnames, filename, name,\n" + " qualname, firstlineno, linetable, exceptiontable, freevars=(),\n" + " cellvars=(), /)" + ), +} + +for owner, signature in EXPECTED_TYPES.items(): + assert owner.__text_signature__ == signature, (owner, owner.__text_signature__) + +assert str(inspect.signature(types.MethodType)) == "(function, instance, /)" +assert str(inspect.signature(types.ModuleType)) == "(name, doc=None)" + + +# module_doc_python314 +# +# Every module publishes `__doc__` in its own dict, so the key is listed by +# `dir()` and the read never falls through to the `module` type's docstring. + +assert "__doc__" in builtins.__dict__ +assert "__doc__" in dir(builtins) +assert isinstance(builtins.__doc__, str) + +for name in ("sys", "gc", "_ast", "_io"): + assert "__doc__" in __import__(name).__dict__, name + print("OK") diff --git a/pyre/extra_tests/snippets/attribute_error_lookup_context.py b/pyre/extra_tests/snippets/attribute_error_lookup_context.py new file mode 100644 index 00000000000..1095ee8c484 --- /dev/null +++ b/pyre/extra_tests/snippets/attribute_error_lookup_context.py @@ -0,0 +1,48 @@ +# PyPy 9883bb2a9d `enrich_attribute_error`, with CPython 3.14's distinction +# between an omitted slot and an explicitly supplied None. An inner lookup's +# more specific context wins over the outer `__getattr__` receiver, and getattr +# keeps the caller's name object. + + +class Empty: + def __getattr__(self, name): + raise AttributeError() + + +empty = Empty() +name = "".join(["mis", "sing"]) +try: + getattr(empty, name) +except AttributeError as exc: + assert exc.name is name + assert exc.obj is empty +else: + raise AssertionError("getattr on a raising __getattr__ must fail") + + +class ExplicitNone: + def __getattr__(self, name): + raise AttributeError(name=None, obj=None) + + +try: + ExplicitNone().missing +except AttributeError as exc: + assert exc.name is None + assert exc.obj is None +else: + raise AssertionError("an explicit AttributeError must propagate") + + +class InnerLookup: + def __getattr__(self, name): + return getattr(list, name + "nd") + + +try: + InnerLookup().app +except AttributeError as exc: + assert exc.name == "appnd" + assert exc.obj is list +else: + raise AssertionError("the inner lookup must fail and win the context") diff --git a/pyre/extra_tests/snippets/builtin_memoryview.py b/pyre/extra_tests/snippets/builtin_memoryview.py index 2d81b981608..7af25471e63 100644 --- a/pyre/extra_tests/snippets/builtin_memoryview.py +++ b/pyre/extra_tests/snippets/builtin_memoryview.py @@ -15,6 +15,15 @@ a = memoryview(obj) assert a.obj == obj +# A view is weak-referenceable without a published `__weakref__` descriptor: +# `PyMemoryView_Type` sets `tp_weaklistoffset` and leaves the getset table +# without an entry for it. +assert "__weakref__" not in memoryview.__dict__ +with assert_raises(AttributeError): + a.__weakref__ +a_ref = weakref.ref(a) +assert a_ref() is a + assert a[2:3] == b"c" assert hash(obj) == hash(a) @@ -120,7 +129,17 @@ def test_bytesio_readinto(): assert writable.tobytes() == b"hello" stream.seek(0) - assert_raises(TypeError, lambda: stream.readinto(memoryview(b"hello"))) + with assert_raises(TypeError) as error: + stream.readinto(memoryview(b"hello")) + assert str(error.exception) == ( + "readinto() argument must be read-write bytes-like object, not memoryview" + ) + + with assert_raises(TypeError) as error: + stream.readinto("hello") + assert str(error.exception) == ( + "readinto() argument must be read-write bytes-like object, not str" + ) test_bytesio_readinto() diff --git a/pyre/extra_tests/snippets/builtin_range.py b/pyre/extra_tests/snippets/builtin_range.py index 6bfb99f453d..2be417b55e3 100644 --- a/pyre/extra_tests/snippets/builtin_range.py +++ b/pyre/extra_tests/snippets/builtin_range.py @@ -7,6 +7,21 @@ assert len(range(10, 5, -2)) == 3, "Expected length 3, for elements: 10, 8, 6" assert len(range(5, 10, 2)) == 3, "Expected length 3, for elements: 5, 7, 9" +# Range iterators expose their remaining count as __length_hint__, not as a +# sequence length. Both the machine-word and bigint iterator variants must +# therefore reject len(), and the hint has to fall as items are consumed +# rather than repeat the length of the range it came from. +small_iter = iter(range(3)) +assert small_iter.__length_hint__() == 3 +assert_raises(TypeError, len, small_iter) +assert next(small_iter) == 0 +assert small_iter.__length_hint__() == 2 +big_iter = iter(range(2**100)) +assert big_iter.__length_hint__() == 2**100 +assert_raises(TypeError, len, big_iter) +assert next(big_iter) == 0 +assert big_iter.__length_hint__() == 2**100 - 1 + # index tests assert range(10).index(6) == 6 assert range(4, 10).index(6) == 2 diff --git a/pyre/extra_tests/snippets/builtin_type.py b/pyre/extra_tests/snippets/builtin_type.py index 2af8f3c45e3..2333ae0ef90 100644 --- a/pyre/extra_tests/snippets/builtin_type.py +++ b/pyre/extra_tests/snippets/builtin_type.py @@ -1,4 +1,5 @@ import types +import weakref from testutils import assert_raises @@ -69,6 +70,22 @@ assert type.__qualname__ == "type" assert type.__name__ == "type" assert isinstance(type.__doc__, str) +# `type` is weak-referenceable without publishing a `__weakref__` descriptor. +# Publishing one would put a data descriptor on the metatype, which wins over +# the class's own entry and would answer every `SomeClass.__weakref__` with the +# class's weak reference instead of that class's slot descriptor. +assert "__weakref__" not in type.__dict__ +with assert_raises(AttributeError): + type.__weakref__ +assert weakref.ref(type)() is type + + +class WeakrefSlotted: + __slots__ = ("__weakref__",) + + +assert WeakrefSlotted.__weakref__.__name__ == "__weakref__" +assert WeakrefSlotted.__weakref__.__objclass__ is WeakrefSlotted assert object.__qualname__ == "object" assert int.__qualname__ == "int" diff --git a/pyre/extra_tests/snippets/match_sequence_excludes_strings.py b/pyre/extra_tests/snippets/match_sequence_excludes_strings.py new file mode 100644 index 00000000000..ea21367b5cb --- /dev/null +++ b/pyre/extra_tests/snippets/match_sequence_excludes_strings.py @@ -0,0 +1,45 @@ +# pyre-check: gate=1 +# A sequence pattern must not match a string. All three implementations agree +# on the behaviour -- `pyopcode.py:MATCH_SEQUENCE` rejects unicode, bytes and +# bytearray after the collection flag has already said "sequence" -- but they +# disagree on what `__flags__` publishes: `W_TypeObject.get_flags` reports the +# bit the `_abc` registration set, while the 3.14 surface leaves it clear on +# exactly the three types the opcode excludes. `__flags__` is +# caller-observable, so the bit follows the spec and only the internal marker +# keeps PyPy's value. +# +# `bench/synth/pypy_type_surface.py` carries no row for these three types for +# that reason, and points here instead. + +SEQUENCE = 1 << 5 + + +def is_sequence_pattern(value): + match value: + case [*_]: + return True + case _: + return False + + +for value in ("ab", b"ab", bytearray(b"ab")): + assert not is_sequence_pattern(value), type(value).__name__ + assert type(value).__flags__ & SEQUENCE == 0, type(value).__name__ + +for value in ([1, 2], (1, 2), range(2), memoryview(b"ab")): + assert is_sequence_pattern(value), type(value).__name__ + +# list and tuple are the two the collection flag is set on directly rather +# than through an `_abc` registration, so they are the pair whose published +# bit is the same everywhere. +assert list.__flags__ & SEQUENCE == SEQUENCE +assert tuple.__flags__ & SEQUENCE == SEQUENCE + + +# A str subclass inherits the exclusion; the pattern still must not match. +class S(str): + pass + + +assert not is_sequence_pattern(S("ab")) +assert S.__flags__ & SEQUENCE == 0 diff --git a/pyre/extra_tests/snippets/stdlib_typing.py b/pyre/extra_tests/snippets/stdlib_typing.py index b521cb66d59..ad2f7ca6e75 100644 --- a/pyre/extra_tests/snippets/stdlib_typing.py +++ b/pyre/extra_tests/snippets/stdlib_typing.py @@ -1,6 +1,6 @@ from collections.abc import Awaitable, Callable from types import GenericAlias -from typing import ClassVar, Protocol, TypeVar +from typing import ClassVar, ParamSpec, Protocol, TypeVar T = TypeVar("T") @@ -299,3 +299,44 @@ def __index__(self): # A refused write leaves every constant evaluator in the process callable. assert const_evaluator(annotationlib.Format.STRING) == "int" + +# `bound=None` means two different things across the two parameter kinds: +# `typevar_new_impl` drops a None bound before `type_check`, so it stays None, +# while `paramspec_new_impl` has no such shortcut and hands every bound — +# including the one its signature defaults to — to `typing._type_check`, which +# maps None to NoneType. +assert TypeVar("T").__bound__ is None +assert TypeVar("T", bound=None).__bound__ is None +assert ParamSpec("P").__bound__ is type(None) +assert ParamSpec("P", bound=None).__bound__ is type(None) + +# `paramspecargs` / `paramspeckwargs` keep the origin in a `Py_READONLY` +# member, so neither the ordinary write nor the one that steps around a +# `__setattr__` guard reaches it, and the private storage stays out of `dir()`. +_P = ParamSpec("P") +for _view in (_P.args, _P.kwargs): + assert _view.__origin__ is _P + for _write in ( + lambda: setattr(_view, "__origin__", 1), + lambda: object.__setattr__(_view, "__origin__", 1), + lambda: delattr(_view, "__origin__"), + lambda: object.__delattr__(_view, "__origin__"), + ): + try: + _write() + except AttributeError as exc: + assert str(exc) == "readonly attribute", str(exc) + else: + raise AssertionError("__origin__ accepted a write") + assert _view.__origin__ is _P + assert "__origin__" in dir(_view) + assert not [name for name in dir(_view) if name == "_origin"] + +# A compiler-created parameter is allocated with no module of its own, so it +# reports the class attribute rather than the module that declares it. +def _identity[T](x: T) -> T: + return x + + +assert _identity.__type_params__[0].__module__ == "typing" +assert TypeVar("T").__module__ == __name__ diff --git a/pyre/pyre-interpreter/src/async_operation.rs b/pyre/pyre-interpreter/src/async_operation.rs index ecc60be4842..f0951b20c92 100644 --- a/pyre/pyre-interpreter/src/async_operation.rs +++ b/pyre/pyre-interpreter/src/async_operation.rs @@ -12,10 +12,12 @@ use pyre_object::PyObjectRef; use crate::PyResult; -/// `aiter` / `anext` ported verbatim from app_operation.py. The +/// `aiter` / `anext` ported from app_operation.py. The /// `_NOT_PROVIDED` sentinel stays in the intermediate namespace that both /// functions retain as their `__globals__`, so only the two public names -/// need to be surfaced. +/// need to be surfaced. `anext` calls `__anext__` before constructing its +/// default wrapper, matching CPython 3.14's `builtin_anext_impl` synchronous +/// exception behavior. const ASYNC_OP_SRC: &str = r#" _NOT_PROVIDED = object() @@ -47,12 +49,14 @@ def anext(iterator, default=_NOT_PROVIDED): except AttributeError: raise TypeError(f"'{type(iterator).__name__}' object is not an async iterator") + awaitable = __anext__(iterator) + if default is _NOT_PROVIDED: - return __anext__(iterator) + return awaitable async def anext_impl(): try: - return await __anext__(iterator) + return await awaitable except StopAsyncIteration: return default diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 19c245c36fd..7a6b22df671 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -421,6 +421,24 @@ pub(crate) unsafe fn issubtype_w(w_type: PyObjectRef, cls: PyObjectRef) -> bool issubtype_slow_and_wrong(w_type, cls) } +/// JIT walker accessor for `issubtype_w` without exposing the internal helper +/// as a general cross-crate API. +/// # Safety +/// The caller must uphold every validity, runtime-type, aliasing, and lifetime +/// invariant required by the object and pointer arguments for the entire call. +pub unsafe fn jit_issubtype_w(w_type: PyObjectRef, cls: PyObjectRef) -> bool { + unsafe { issubtype_w(w_type, cls) } +} + +/// JIT walker accessor for abstractinst.py's `space.isinstance_w(obj, type)` +/// class test. +/// # Safety +/// The caller must uphold every validity, runtime-type, aliasing, and lifetime +/// invariant required by the object and pointer arguments for the entire call. +pub unsafe fn jit_is_type_like_w(obj: PyObjectRef) -> bool { + unsafe { is_type_like_w(obj) } +} + /// `typeobject.py _issubtype_slow_and_wrong` — the subtype test /// for a partially-initialised `w_type` whose MRO is not yet installed /// (reached only from a custom `MetaCls.mro()`). Walks the best-base chain @@ -4853,18 +4871,12 @@ pub(crate) fn len_slot(obj: PyObjectRef) -> PyResult { )), }; } - if pyre_object::is_long_range_iter(obj) { - // `functional.py W_LongRangeIterator.descr_len → w_len - w_index`. - return Ok(pyre_object::range_bigint_to_obj( - pyre_object::w_long_range_iter_len(obj), - )); - } - if is_range_iter(obj) { - // `functional.py W_IntRangeIterator.descr_len` reports the - // stored `remaining` count directly. - let r = &*(obj as *const pyre_object::functional::W_IntRangeIterator); - return Ok(w_int_new(r.remaining.max(0))); - } + // A range iterator deliberately has `__length_hint__`, not `__len__`. + // `functional.py W_AbstractRangeIterator.typedef` exposes `descr_len` + // only under the former name, just like CPython 3.14's + // `range_iterator` and `longrange_iterator` type dictionaries. Do + // not turn the concrete payload's remaining count into a length slot: + // `len(iter(range(...)))` must fall through and raise TypeError. // descroperation.py `_len` — `space.lookup(w_obj, '__len__')` // then `space.get_and_call_function(w_descr, w_obj)`. Routed through // `r#type` so a true user instance, a W_Root type (e.g. `deque`), and @@ -5413,6 +5425,10 @@ fn getdictvalue(obj: PyObjectRef, name: &str) -> Result, PyE /// /// MapdictWeakrefSupport.getweakref overrides it. pub fn getweakref(obj: PyObjectRef) -> Option { + if unsafe { crate::pycode::is_code(obj) } { + let lifeline = unsafe { crate::pycode::w_code_getweakref(obj) }; + return (!lifeline.is_null()).then_some(lifeline); + } if unsafe { pyre_object::memoryview::is_w_memoryview(obj) } { let lifeline = unsafe { pyre_object::memoryview::w_memoryview_getweakref(obj) }; return (!lifeline.is_null()).then_some(lifeline); @@ -5471,6 +5487,10 @@ pub fn getweakref(obj: PyObjectRef) -> Option { /// /// MapdictWeakrefSupport.setweakref overrides it. pub fn setweakref(obj: PyObjectRef, weakreflifeline: PyObjectRef) -> Result<(), PyError> { + if unsafe { crate::pycode::is_code(obj) } { + unsafe { crate::pycode::w_code_setweakref(obj, weakreflifeline) }; + return Ok(()); + } if unsafe { pyre_object::memoryview::is_w_memoryview(obj) } { unsafe { pyre_object::memoryview::w_memoryview_setweakref(obj, weakreflifeline) }; return Ok(()); @@ -5530,6 +5550,10 @@ pub fn setweakref(obj: PyObjectRef, weakreflifeline: PyObjectRef) -> Result<(), /// pass /// ``` pub fn delweakref(obj: PyObjectRef) { + if unsafe { crate::pycode::is_code(obj) } { + unsafe { crate::pycode::w_code_setweakref(obj, PY_NULL) }; + return; + } if unsafe { pyre_object::memoryview::is_w_memoryview(obj) } { unsafe { pyre_object::memoryview::w_memoryview_setweakref(obj, PY_NULL) }; return; @@ -5579,7 +5603,13 @@ pub fn delweakref(obj: PyObjectRef) { pub fn getattr_str(obj: PyObjectRef, name: &str) -> PyResult { // `space.getattr` — the full path, including the `__getattr__` fallback. - getattr_str_impl(obj, name, true, false) + getattr_str_impl(obj, name, true, false).map_err(|mut err| { + // PyPy 9883bb2a9d `DescrOperation.getattr`: enrich an AttributeError + // only after the complete `__getattribute__` / `__getattr__` chain has + // failed, preserving a more specific inner lookup's existing context. + err.enrich_attribute_error_str(obj, name); + err + }) } /// Shared body of `space.getattr` and the bare `object.__getattribute__` slot. @@ -6396,14 +6426,36 @@ pub fn getattr(obj: PyObjectRef, w_name: PyObjectRef) -> PyResult { crate::type_methods::arg_type_name(w_name) ))); } + // A user-defined `__getattribute__` / `__getattr__` runs below and can + // allocate enough to move both operands. The collector rewrites roots, + // not this frame's Rust locals, and the enrichment stores the operands on + // the exception itself, so hold them on the shadow stack across the lookup + // and read them back from their slots rather than from the addresses + // captured here. + let _roots = pyre_object::gc_roots::push_roots(); + let operands = pyre_object::gc_roots::pin_roots(&[obj, w_name]); let name = unsafe { pyre_object::w_str_get_wtf8(w_name) }; - if unsafe { pyre_object::dictmultiobject::wtf8_key_is_utf8(name) } { - getattr_str(obj, unsafe { - pyre_object::dictmultiobject::wtf8_key_as_str_unchecked(name) - }) + let result = if unsafe { pyre_object::dictmultiobject::wtf8_key_is_utf8(name) } { + // `getattr_str_impl` rather than `getattr_str`: this entry point already + // holds the wrapped name, so the enrichment below can hand it over + // directly instead of letting `enrich_attribute_error_str` allocate a + // fresh one, matching the `w_name`-taking `DescrOperation.getattr`. + getattr_str_impl( + obj, + unsafe { pyre_object::dictmultiobject::wtf8_key_as_str_unchecked(name) }, + true, + false, + ) } else { unsafe { getattr_surrogate(obj, w_name, name) } - } + }; + result.map_err(|mut err| { + err.enrich_attribute_error( + pyre_object::gc_roots::shadow_stack_get(operands), + pyre_object::gc_roots::shadow_stack_get(operands + 1), + ); + err + }) } /// `_PyObject_LookupAttr` — the full `space.getattr` protocol for a caller @@ -6707,7 +6759,7 @@ pub(crate) unsafe fn object_setattr_surrogate( // (correct: a surrogate can never name `__eq__`/`__hash__`). if is_type(obj) { // typeobject.py — only heap types may have their dict mutated. - if !pyre_object::w_type_is_heaptype(obj) { + if pyre_object::w_type_is_cpython_immutabletype(obj) { return Err(PyError::type_error(format!( "cannot set {} attribute of immutable type '{}'", crate::display::format_wtf8_repr(name), @@ -6796,7 +6848,7 @@ pub(crate) unsafe fn object_delattr_surrogate( } if is_type(obj) { // typeobject.py:437 — only heap types may have attributes deleted. - if !pyre_object::w_type_is_heaptype(obj) { + if pyre_object::w_type_is_cpython_immutabletype(obj) { return Err(PyError::type_error(format!( "cannot delete attributes on immutable type object '{}'", w_type_get_name(obj) @@ -7185,7 +7237,7 @@ pub(crate) fn type_get_annotations(obj: PyObjectRef) -> PyResult { // Static builtin types cannot acquire annotations, so reject them before // consulting their namespace. Heap types below may have an explicit // class-body entry with this name. - if !unsafe { pyre_object::w_type_is_heaptype(obj) } { + if !unsafe { pyre_object::w_type_is_cpython_heaptype(obj) } { return Err(PyError::attribute_error(format!( "type object '{}' has no attribute '__annotations__'", unsafe { w_type_get_name(obj) }, @@ -7224,7 +7276,7 @@ pub(crate) fn type_get_annotations(obj: PyObjectRef) -> PyResult { } pub(crate) fn type_set_annotations(obj: PyObjectRef, value: PyObjectRef) -> PyResult { - if !unsafe { pyre_object::w_type_is_heaptype(obj) } { + if unsafe { pyre_object::w_type_is_cpython_immutabletype(obj) } { return Err(PyError::type_error(format!( "cannot set '__annotations__' attribute of immutable type '{}'", unsafe { w_type_get_name(obj) }, @@ -7249,7 +7301,7 @@ pub(crate) fn type_set_annotations(obj: PyObjectRef, value: PyObjectRef) -> PyRe /// own-type slot, never inherited, and replacing it invalidates the result /// cached by `type_get_annotations`. pub(crate) fn type_set_annotate(obj: PyObjectRef, value: PyObjectRef) -> PyResult { - if !unsafe { pyre_object::w_type_is_heaptype(obj) } { + if unsafe { pyre_object::w_type_is_cpython_immutabletype(obj) } { return Err(PyError::type_error(format!( "cannot set '__annotate__' attribute of immutable type '{}'", unsafe { w_type_get_name(obj) }, @@ -7278,7 +7330,7 @@ pub(crate) fn type_set_annotate(obj: PyObjectRef, value: PyObjectRef) -> PyResul /// None. A class-body `__annotate__` entry is an ordinary own-class value /// and takes precedence over the compiler-facing slot. pub(crate) fn type_get_annotate(obj: PyObjectRef) -> PyResult { - if !unsafe { pyre_object::w_type_is_heaptype(obj) } { + if !unsafe { pyre_object::w_type_is_cpython_heaptype(obj) } { return Err(PyError::attribute_error(format!( "type object '{}' has no attribute '__annotate__'", unsafe { w_type_get_name(obj) }, @@ -7295,7 +7347,7 @@ pub(crate) fn type_get_annotate(obj: PyObjectRef) -> PyResult { /// the empty tuple. Unlike function type parameters, the class slot accepts /// any object; the compiler normally stores a tuple. pub(crate) fn type_get_type_params(obj: PyObjectRef) -> PyResult { - if unsafe { pyre_object::w_type_is_heaptype(obj) } + if unsafe { pyre_object::w_type_is_cpython_heaptype(obj) } && let Some(value) = crate::type_dict_lookup(obj, "__type_params__") { return Ok(value); @@ -7304,7 +7356,7 @@ pub(crate) fn type_get_type_params(obj: PyObjectRef) -> PyResult { } pub(crate) fn type_set_type_params(obj: PyObjectRef, value: PyObjectRef) -> PyResult { - if !unsafe { pyre_object::w_type_is_heaptype(obj) } { + if unsafe { pyre_object::w_type_is_cpython_immutabletype(obj) } { return Err(PyError::type_error(format!( "cannot set '__type_params__' attribute of immutable type '{}'", unsafe { w_type_get_name(obj) }, @@ -7317,7 +7369,7 @@ pub(crate) fn type_set_type_params(obj: PyObjectRef, value: PyObjectRef) -> PyRe } pub(crate) fn type_del_annotations(obj: PyObjectRef) -> PyResult { - if !unsafe { pyre_object::w_type_is_heaptype(obj) } { + if unsafe { pyre_object::w_type_is_cpython_immutabletype(obj) } { return Err(PyError::type_error(format!( "cannot delete '__annotations__' attribute of immutable type '{}'", unsafe { w_type_get_name(obj) }, @@ -7365,7 +7417,7 @@ pub(crate) fn type_get_doc(obj: PyObjectRef) -> PyResult { let Some(value) = crate::type_dict_lookup(obj, "__doc__") else { return Ok(w_none()); }; - if w_type_is_heaptype(obj) { + if w_type_is_cpython_heaptype(obj) { return match get(value, PY_NULL, obj)? { Some(result) => Ok(result), None => Ok(value), @@ -7377,7 +7429,7 @@ pub(crate) fn type_get_doc(obj: PyObjectRef) -> PyResult { /// PyPy `typeobject.py descr_set__doc`. pub(crate) fn type_set_doc(obj: PyObjectRef, value: PyObjectRef) -> PyResult { - if !unsafe { w_type_is_heaptype(obj) } { + if unsafe { w_type_is_cpython_immutabletype(obj) } { return Err(PyError::type_error(format!( "cannot set '__doc__' attribute of immutable type '{}'", unsafe { w_type_get_name(obj) }, @@ -9659,7 +9711,7 @@ pub unsafe fn _pure_version_tag(w_type: *mut PyObject) -> u64 { #[inline] pub(crate) unsafe fn w_type_version_tag(w_type: PyObjectRef) -> u64 { if majit_metainterp::jit::we_are_jitted() { - if pyre_object::typeobject::w_type_is_heaptype(w_type) { + if !pyre_object::w_type_is_cpython_immutabletype(w_type) { // Heap types can still be mutated; read the live field (the // caller promotes the result). return pyre_object::typeobject::w_type_get_version_tag(w_type); @@ -9933,7 +9985,7 @@ pub unsafe fn getfulltypename(w_obj: PyObjectRef) -> String { /// # Safety /// `w_type` must be a valid `W_TypeObject`. pub unsafe fn getfulltypename_of_type(w_type: PyObjectRef) -> String { - if !pyre_object::w_type_is_heaptype(w_type) { + if !pyre_object::w_type_is_cpython_heaptype(w_type) { return w_type_get_name(w_type).to_string(); } let qualname = pyre_object::w_type_get_qualname(w_type).to_string(); @@ -9954,7 +10006,7 @@ pub unsafe fn getfulltypename_of_type(w_type: PyObjectRef) -> String { /// `w_type` must be a valid `W_TypeObject`. pub unsafe fn type_repr_qualified_name(w_type: PyObjectRef) -> String { let name = w_type_get_name(w_type).to_string(); - if !pyre_object::w_type_is_heaptype(w_type) { + if !pyre_object::w_type_is_cpython_heaptype(w_type) { return name; } let module = lookup_in_type_where(w_type, "__module__") @@ -10025,7 +10077,7 @@ pub fn load_special_resolve(obj: PyObjectRef, name: &str) -> Result String { - if !pyre_object::w_type_is_heaptype(w_type) { + if !pyre_object::w_type_is_cpython_heaptype(w_type) { return w_type_get_name(w_type).to_string(); } let qualname = pyre_object::w_type_get_qualname(w_type).to_string(); @@ -10607,7 +10659,7 @@ pub(crate) unsafe fn compute_and_set_mro(w_self: PyObjectRef) -> PyResult { } let default_mro = |index: usize| pyre_object::gc_roots::shadow_stack_get(default_mro_start + index); - if pyre_object::w_type_is_heaptype(w_self) { + if pyre_object::w_type_is_cpython_heaptype(w_self) { let w_metaclass = (*w_self).w_class; if !w_metaclass.is_null() && let Some((w_where, w_mro_func)) = lookup_where_with_method_cache(w_metaclass, "mro") @@ -11476,12 +11528,21 @@ pub(crate) fn descr_set___class__(w_obj: PyObjectRef, w_newcls: PyObjectRef) -> let w_module_type = crate::typedef::gettypefor(&pyre_object::MODULE_TYPE as *const pyre_object::PyType) .map_or(PY_NULL, |p| p.as_ptr()); - let old_supported = - w_type_is_heaptype(w_oldcls.as_ptr()) || std::ptr::eq(w_oldcls.as_ptr(), w_module_type); - let new_supported = w_type_is_heaptype(w_newcls) || std::ptr::eq(w_newcls, w_module_type); + // A heap projection is not by itself a mutable one: the extension + // families marked through `mark_cpython_heap_type(tp, true)` — array, + // the sre types, the _io stack, the posix result types — carry + // IMMUTABLETYPE beside HEAPTYPE, and an exact instance of one must not + // be retagged to a layout-compatible no-slot subclass. Both ends need + // the mutable half. + let old_mutable_heap = w_type_is_cpython_heaptype(w_oldcls.as_ptr()) + && !w_type_is_cpython_immutabletype(w_oldcls.as_ptr()); + let new_mutable_heap = + w_type_is_cpython_heaptype(w_newcls) && !w_type_is_cpython_immutabletype(w_newcls); + let old_supported = old_mutable_heap || std::ptr::eq(w_oldcls.as_ptr(), w_module_type); + let new_supported = new_mutable_heap || std::ptr::eq(w_newcls, w_module_type); if !old_supported || !new_supported { return Err(crate::PyError::type_error( - "__class__ assignment only supported for heap types or ModuleType subclasses" + "__class__ assignment only supported for mutable types or ModuleType subclasses" .to_string(), )); } @@ -11582,7 +11643,7 @@ pub fn type_immutable_attr_raise_is_stable(obj: PyObjectRef, name: &str, is_dele unsafe { if obj.is_null() || !pyre_object::typeobject::is_type(obj) - || pyre_object::w_type_is_heaptype(obj) + || pyre_object::w_type_is_cpython_heaptype(obj) { return false; } @@ -12071,7 +12132,7 @@ pub fn object_setattr(obj: PyObjectRef, name: &str, value: PyObjectRef) -> PyRes unsafe { if is_type(obj) { // typeobject.py — only heap types may have their dict mutated. - if !pyre_object::w_type_is_heaptype(obj) { + if pyre_object::w_type_is_cpython_immutabletype(obj) { return Err(PyError::type_error(format!( "cannot set '{}' attribute of immutable type '{}'", name, @@ -12906,7 +12967,7 @@ pub fn object_delattr(obj: PyObjectRef, name: &str) -> PyResult { unsafe { if is_type(obj) { // typeobject.py:437 — only heap types may have attributes deleted. - if !pyre_object::w_type_is_heaptype(obj) { + if pyre_object::w_type_is_cpython_immutabletype(obj) { return Err(PyError::type_error(format!( "cannot delete attributes on immutable type object '{}'", w_type_get_name(obj) @@ -12985,36 +13046,72 @@ pub fn call( w_args: PyObjectRef, w_kwds: Option, ) -> PyObjectRef { - if let Some(w_kwargs) = w_kwds - && !w_kwargs.is_null() - && !unsafe { is_none(w_kwargs) } - { - panic!("call with kwargs is not yet implemented in pyre"); + // baseobjspace.py:1213-1215 — the packed objects remain live while + // `Arguments.frompacked` expands `*args` / `**kwargs`, either of which can + // execute Python and collect. RPython's GC transform roots these locals; + // publish and reload the corresponding raw pointers explicitly. + let roots = pyre_object::gc_roots::push_roots(); + let root_base = roots.base(); + roots.pin_root(callable); + roots.pin_root(w_args); + roots.pin_root(w_kwds.unwrap_or(PY_NULL)); + let callable = || roots.get(root_base); + let w_args = || roots.get(root_base + 1); + let w_kwds = || roots.get(root_base + 2); + + let args = crate::argument::Arguments::frompacked( + (!w_args().is_null()).then_some(w_args()), + (!w_kwds().is_null()).then_some(w_kwds()), + ); + let args = match args { + Ok(args) => args, + Err(error) => { + crate::call::set_call_error(error); + return PY_NULL; + } + }; + match call_args(callable(), &args) { + Ok(result) => result, + Err(error) => { + crate::call::set_call_error(error); + PY_NULL + } } +} - let mut args = Vec::new(); - unsafe { - if is_tuple(w_args) { - let len = w_tuple_len(w_args); - args.reserve(len); - for i in 0..len { - if let Some(arg) = w_tuple_getitem(w_args, i as i64) { - args.push(arg); - } - } - } else if is_list(w_args) { - let len = w_list_len(w_args); - args.reserve(len); - for i in 0..len { - if let Some(arg) = w_list_getitem(w_args, i as i64) { - args.push(arg); - } - } - } else if !w_args.is_null() { - panic!("call() expects tuple or list positional arguments"); - } +/// PyPy `descroperation.py:189 call_args` — dispatch one structured +/// `Arguments` instance without flattening its keyword half. +pub fn call_args( + callable: PyObjectRef, + args: &crate::argument::Arguments, +) -> Result { + if let (Some(keyword_names_w), Some(keywords_w)) = + (args.keyword_names_w.as_ref(), args.keywords_w.as_ref()) + && !keyword_names_w.is_empty() + { + debug_assert_eq!(keyword_names_w.len(), keywords_w.len()); + let mut kwargs = Vec::with_capacity(keyword_names_w.len()); + for (&name, &value) in keyword_names_w.iter().zip(keywords_w.iter()) { + // `do_combine_starstarargs_wrapped` already rejects a non-string + // key while `**` is expanded, so this only guards the WTF-8 read + // below for `Arguments` values built natively instead. + if unsafe { !is_str(name) } { + return Err(PyError::type_error("keywords must be strings")); + } + kwargs.push((unsafe { w_str_get_wtf8(name) }.to_owned(), value)); + } + return crate::call::call_with_kwargs_in_ctx( + crate::call::getexecutioncontext(), + callable, + &args.arguments_w, + &kwargs, + ); } - call_function(callable, &args) + crate::call::call_callable_in_ctx( + crate::call::getexecutioncontext(), + callable, + &args.arguments_w, + ) } /// PyPy: baseobjspace.py `call_obj_args` — add a leading object before args. @@ -18494,19 +18591,18 @@ fn async_generator_init_hooks(async_gen: PyObjectRef) -> PyResult { pub(crate) fn async_generator_anext_method(args: &[PyObjectRef]) -> PyResult { let async_gen = args.first().copied().unwrap_or(PY_NULL); async_generator_init_hooks(async_gen)?; - Ok(pyre_object::generator::w_async_gen_asend_new( - async_gen, - w_none(), - )) + let awaitable = pyre_object::generator::w_async_gen_asend_new(async_gen, w_none()); + crate::executioncontext::register_finalizer(awaitable); + Ok(awaitable) } pub(crate) fn async_generator_asend_method(args: &[PyObjectRef]) -> PyResult { let async_gen = args.first().copied().unwrap_or(PY_NULL); let value = crate::type_methods::arg_or_none(args, 1); async_generator_init_hooks(async_gen)?; - Ok(pyre_object::generator::w_async_gen_asend_new( - async_gen, value, - )) + let awaitable = pyre_object::generator::w_async_gen_asend_new(async_gen, value); + crate::executioncontext::register_finalizer(awaitable); + Ok(awaitable) } pub(crate) fn async_generator_athrow_method(args: &[PyObjectRef]) -> PyResult { @@ -18532,23 +18628,23 @@ pub(crate) fn async_generator_athrow_method(args: &[PyObjectRef]) -> PyResult { } let async_gen = args[0]; async_generator_init_hooks(async_gen)?; - Ok(pyre_object::generator::w_async_gen_athrow_new( + let awaitable = pyre_object::generator::w_async_gen_athrow_new( async_gen, args[1], crate::type_methods::arg_or_none(args, 2), crate::type_methods::arg_or_none(args, 3), - )) + ); + crate::executioncontext::register_finalizer(awaitable); + Ok(awaitable) } pub(crate) fn async_generator_aclose_method(args: &[PyObjectRef]) -> PyResult { let async_gen = args.first().copied().unwrap_or(PY_NULL); async_generator_init_hooks(async_gen)?; - Ok(pyre_object::generator::w_async_gen_athrow_new( - async_gen, - PY_NULL, - w_none(), - w_none(), - )) + let awaitable = + pyre_object::generator::w_async_gen_athrow_new(async_gen, PY_NULL, w_none(), w_none()); + crate::executioncontext::register_finalizer(awaitable); + Ok(awaitable) } fn async_gen_unwrap_value(async_gen: PyObjectRef, value: PyObjectRef) -> PyResult { @@ -18897,6 +18993,95 @@ pub(crate) fn async_gen_athrow_throw_method(args: &[PyObjectRef]) -> PyResult { result } +/// CPython 3.14 `async_gen_asend_finalize` / +/// `async_gen_athrow_finalize`: an async-generator method awaitable that never +/// left its initial state reports a `RuntimeWarning` from finalizer context. +/// PyPy's tracing collector reaches the same lifetime through +/// `W_Root.register_finalizer`; unlike CPython's refcount-triggered +/// `tp_finalize`, the queue is drained at a GC safe point. +pub(crate) fn async_gen_awaitable_finalize(awaitable: PyObjectRef) { + use pyre_object::generator::{ + ASYNC_GEN_STATE_INIT, AsyncGenASend, AsyncGenAThrow, w_generator_get_qualname, + }; + + let (async_gen, method, state) = if let Some(payload) = AsyncGenASend::from_obj(awaitable) { + (payload.async_gen, "asend", payload.state) + } else if let Some(payload) = AsyncGenAThrow::from_obj(awaitable) { + ( + payload.async_gen, + if payload.w_exc_type.is_null() { + "aclose" + } else { + "athrow" + }, + payload.state, + ) + } else { + return; + }; + if state != ASYNC_GEN_STATE_INIT { + return; + } + + // `py_repr_wtf8` and `warn_category_w` both run Python and are therefore + // collection points. `async_gen` and its qualname are plain Rust locals, + // which no root walker scans, so pin them on the shadow stack and read + // them back at every use past this point. + let _roots = pyre_object::gc_roots::push_roots(); + let async_gen_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(async_gen); + + let qualname = unsafe { + w_generator_get_qualname(pyre_object::gc_roots::shadow_stack_get(async_gen_slot)) + }; + let qualname_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(qualname); + let method_repr = unsafe { crate::display::py_repr_wtf8(w_str_new(method)) } + .unwrap_or_else(|_| Wtf8Buf::from_string(format!("'{method}'"))); + let qualname_repr = unsafe { + crate::display::py_repr_wtf8(pyre_object::gc_roots::shadow_stack_get(qualname_slot)) + } + .unwrap_or_else(|_| Wtf8Buf::from_string("''".to_owned())); + let message = crate::display::wtf8_format!( + "coroutine method ", + method_repr, + " of ", + qualname_repr, + " was never awaited" + ); + let w_message = w_str_from_wtf8(message); + if let Err(mut err) = crate::warn::warn_category_w(w_message, "RuntimeWarning", 1) { + // A filter turned into `error` hands back the only reference to the + // materialised exception, and it lives in this Rust `PyError`, which + // the collector does not scan. `py_repr_wtf8` below runs app-level + // `__repr__` and allocates, so hold the exception on the shadow stack + // across the formatting and read it back before it is reported. + let exc_slot = if err.exc_object.is_null() { + None + } else { + let slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(err.exc_object); + Some(slot) + }; + let repr = unsafe { + crate::display::py_repr_wtf8(pyre_object::gc_roots::shadow_stack_get(async_gen_slot)) + } + .unwrap_or_else(|_| Wtf8Buf::from_string("".to_owned())); + let where_desc = crate::display::wtf8_format!( + "Exception ignored while finalizing async generator ", + repr + ); + if let Some(slot) = exc_slot { + err.exc_object = pyre_object::gc_roots::shadow_stack_get(slot); + } + err.write_unraisable( + w_none(), + &where_desc, + pyre_object::gc_roots::shadow_stack_get(async_gen_slot), + ); + } +} + /// generator.py `_finalize_` — called by the GC finalizer when a suspended generator /// is collected. If the suspended frame is still live and its current instruction is /// covered by an exception-table handler (a `finally`/`except`/`with` cleanup), raise @@ -19703,6 +19888,62 @@ pub fn dict_move_to_end(obj: PyObjectRef, key: PyObjectRef, last: bool) -> Resul mod tests { use super::*; + #[test] + fn call_expands_packed_arguments_and_keywords() { + crate::typedef::init_typeobjects(); + crate::test_hooks::install_hash_hook(); + crate::call::clear_call_error(); + + let kwargs = w_dict_new(); + setitem(kwargs, w_str_new("answer"), w_int_new(42)).unwrap(); + let dict_type = crate::typedef::gettypeobject(&pyre_object::DICT_TYPE); + let result = call(dict_type, w_tuple_new(Vec::new()), Some(kwargs)); + assert!(!result.is_null(), "dict(**kwargs) must return a dict"); + assert!(crate::call::take_call_error().is_none()); + let value = unsafe { w_dict_getitem_str(result, "answer") }.expect("keyword stored"); + assert_eq!(unsafe { w_int_get_value(value) }, 42); + } + + #[test] + fn call_reports_packed_shape_errors_instead_of_panicking() { + crate::typedef::init_typeobjects(); + crate::test_hooks::install_hash_hook(); + let dict_type = crate::typedef::gettypeobject(&pyre_object::DICT_TYPE); + + crate::call::clear_call_error(); + let result = call(dict_type, w_int_new(1), None); + assert!(result.is_null()); + let error = crate::call::take_call_error().expect("non-iterable *args error"); + assert_eq!(error.kind, PyErrorKind::TypeError); + // Not the bare `argument after *`, which is a prefix of the `**` + // message and would pass on either error. + assert!( + error + .message_text() + .contains("argument after * must be an iterable") + ); + + crate::call::clear_call_error(); + let result = call(dict_type, w_tuple_new(Vec::new()), Some(w_none())); + assert!(result.is_null()); + let error = crate::call::take_call_error().expect("non-mapping **kwargs error"); + assert_eq!(error.kind, PyErrorKind::TypeError); + assert!(error.message_text().contains("argument after **")); + + // A mapping whose keys are not strings is rejected while `**` is + // expanded, before any of them reaches `call_args`; the identical + // check there only guards the unsafe text read for `Arguments` + // instances built natively rather than from a packed mapping. + crate::call::clear_call_error(); + let int_keyed = w_dict_new(); + setitem(int_keyed, w_int_new(1), w_int_new(2)).unwrap(); + let result = call(dict_type, w_tuple_new(Vec::new()), Some(int_keyed)); + assert!(result.is_null()); + let error = crate::call::take_call_error().expect("non-string keyword error"); + assert_eq!(error.kind, PyErrorKind::TypeError); + assert!(error.message_text().contains("keywords must be strings")); + } + #[test] fn uint_and_truncatedint_use_rbigint_word_conversions() { let unsigned_max = pyre_object::longobject::w_long_new(BigInt::from_u128(u64::MAX as u128)); diff --git a/pyre/pyre-interpreter/src/builtins.rs b/pyre/pyre-interpreter/src/builtins.rs index 4d9172c45f6..159d3642652 100644 --- a/pyre/pyre-interpreter/src/builtins.rs +++ b/pyre/pyre-interpreter/src/builtins.rs @@ -2593,6 +2593,10 @@ pub(crate) fn init_memoryview_type(ns: PyObjectRef) { "__new__", crate::typedef::make_new_descr(memoryview_descr_new), ); + // No `__weakref__` descriptor: `PyMemoryView_Type` sets + // `tp_weaklistoffset` and leaves its getset table without the entry, so a + // view is weak-referenceable while `view.__weakref__` raises + // AttributeError and `memoryview.__dict__` has no such key. for (name, f, arity) in [ ("__getitem__", memoryview_getitem as MvFn, 2u16), ("__setitem__", memoryview_setitem, 3), @@ -2835,6 +2839,14 @@ fn install_builtin_text_signatures(ns: PyObjectRef) { } pub fn install_default_builtins(ns: PyObjectRef) { + // The `Module` class docstring at `pypy/module/__builtin__/moduledef.py:9`, + // which `MixedModule.get__doc__` publishes as this module's `__doc__`. + // Seeded here rather than in the module def because the execution + // context builds this namespace directly, and the module `import + // builtins` returns wraps that same dict. + crate::module_ns_get_or_insert_with(ns, "__doc__", || { + pyre_object::w_str_new("Built-in functions, exceptions, and other objects.") + }); crate::module_ns_get_or_insert_with(ns, "print", || { make_module_builtin_function("print", builtin_print) }); @@ -4404,6 +4416,16 @@ pub fn is_builtin_getattr_function(callable: PyObjectRef) -> bool { is_builtin_code_function(callable, builtin_getattr) } +/// True iff `callable` is the builtin `hasattr` function object. +/// +/// The walker uses this to recognize the `hasattr(obj, name)` residual whose +/// answer an instance-shape guard already settles: [`builtin_hasattr`] reports +/// False only for the `AttributeError` its lookup raises, so an attribute the +/// shape carries makes the call a constant True. +pub fn is_builtin_hasattr_function(callable: PyObjectRef) -> bool { + is_builtin_code_function(callable, builtin_hasattr) +} + /// True iff `callable` is the builtin `locals` function object. /// /// The JIT walker uses this to recognize the `locals()` residual it can @@ -4512,7 +4534,28 @@ pub fn is_builtin_ord_function(callable: PyObjectRef) -> bool { /// paths through `abstractinst.py`; a rebound global must not inherit that /// classification merely because it is still named `isinstance`. pub fn is_builtin_isinstance_function(callable: PyObjectRef) -> bool { - is_builtin_code_function(callable, __pyre_wrap_builtin_isinstance) + unsafe { + if callable.is_null() || !crate::is_function(callable) { + return false; + } + let code = crate::function_get_code(callable) as PyObjectRef; + if code.is_null() || !crate::gateway::is_builtin_code(code) { + return false; + } + crate::gateway::builtin_code_fn_eq( + crate::gateway::builtin_code_get(code), + __pyre_wrap_builtin_isinstance as crate::gateway::BuiltinCodeFn, + ) + } +} + +/// True iff `callable` is the canonical builtin `issubclass` function object. +/// +/// The JIT walker uses this together with abstractinst.py's non-overridable +/// `type.__subclasscheck__` path; a rebound global named `issubclass` must not +/// inherit that fold. +pub fn is_builtin_issubclass_function(callable: PyObjectRef) -> bool { + is_builtin_code_function(callable, builtin_issubclass) } /// `len(obj)` — return the length of an object. @@ -9767,7 +9810,10 @@ pub(crate) fn builtin_str(args: &[PyObjectRef]) -> Result Result { @@ -12654,6 +12700,7 @@ fn builtin_compile(args: &[PyObjectRef]) -> Result // bits the codegen honours, and the optimisation level. let opts = crate::compile::CompileOpts { optimize: optimize as u8, + debug_ranges: crate::importing::code_debug_ranges_flag(), allow_top_level_await: flags & PYCF_ALLOW_TOP_LEVEL_AWAIT != 0, dont_imply_dedent: flags & PYCF_DONT_IMPLY_DEDENT != 0, future_features: crate::CodeFlags::from_bits_truncate((flags & COMPILER_FLAGS) as u32), @@ -15170,6 +15217,10 @@ pub fn file_wrapper_type() -> PyObjectRef { static FILE_WRAPPER_TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); *FILE_WRAPPER_TYPE.get_or_init(|| { let tp = crate::typedef::make_builtin_type("_io.TextIOWrapper", init_file_wrapper_type); + // CPython 3.14 Modules/_io/_iomodule.c:ADD_TYPE creates the immutable + // TextIOWrapper heap spec. This legacy wrapper accessor must publish + // the same owner as `_io::textio::type_object`. + crate::typedef::mark_cpython_heap_type(tp, true); unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; tp as usize }) as PyObjectRef @@ -15951,6 +16002,16 @@ impl Drop for WritableBuffer { unsafe fn fileio_writebuf( obj: PyObjectRef, ) -> Result<(&'static mut [u8], PyObjectRef), crate::PyError> { + fn type_error(obj: PyObjectRef) -> crate::PyError { + // PyPy `ObjSpace.acquire_writebuf` reports the rejected exporter's + // type. CPython 3.14's readinto gateways keep the same information + // but prefix it with the argument name owned by the builtin method. + let type_name = unsafe { pyre_object::type_name_of(obj) }; + crate::PyError::type_error(format!( + "readinto() argument must be read-write bytes-like object, not {type_name}" + )) + } + unsafe { if pyre_object::bytearrayobject::is_bytearray(obj) { return Ok((pyre_object::bytearrayobject::w_bytearray_data_mut(obj), obj)); @@ -15984,15 +16045,11 @@ unsafe fn fileio_writebuf( memoryview_check_released(obj)?; if pyre_object::memoryview::w_memoryview_readonly(obj) || !memoryview_contiguity(obj).0 { - return Err(crate::PyError::type_error( - "readinto() argument must be read-write bytes-like object", - )); + return Err(type_error(obj)); } let view = pyre_object::memoryview::w_memoryview_view(obj); let Some(full) = view.backing().as_bytes_mut() else { - return Err(crate::PyError::type_error( - "readinto() argument must be read-write bytes-like object", - )); + return Err(type_error(obj)); }; let offset = view.offset() as usize; let length = pyre_object::memoryview::w_memoryview_length(obj) as usize; @@ -16003,9 +16060,7 @@ unsafe fn fileio_writebuf( } return Ok((&mut full[offset..offset + length], obj)); } - Err(crate::PyError::type_error( - "readinto() argument must be read-write bytes-like object", - )) + Err(type_error(obj)) } } @@ -19232,6 +19287,19 @@ mod tests { assert!(!is_builtin_isinstance_function(std::ptr::null_mut())); } + #[test] + fn builtin_issubclass_identity_uses_wrapped_code() { + crate::typedef::init_typeobjects(); + let issubclass = + make_module_builtin_function_with_arity("issubclass", builtin_issubclass, 2); + let renamed_issubclass = + make_module_builtin_function_with_arity("issubclass", builtin_repr, 2); + + assert!(is_builtin_issubclass_function(issubclass)); + assert!(!is_builtin_issubclass_function(renamed_issubclass)); + assert!(!is_builtin_issubclass_function(std::ptr::null_mut())); + } + #[test] fn long_abs_reuses_nonnegative_rbigint_payload() { let value = BigInt::one().lshift(80).unwrap(); diff --git a/pyre/pyre-interpreter/src/compile.rs b/pyre/pyre-interpreter/src/compile.rs index 2140f8afbad..75ba43950ca 100644 --- a/pyre/pyre-interpreter/src/compile.rs +++ b/pyre/pyre-interpreter/src/compile.rs @@ -59,6 +59,7 @@ pub fn compile_source(source: &str, mode: Mode) -> Result CompileOpts { CompileOpts { optimize: crate::importing::optimize_flag(), + debug_ranges: crate::importing::code_debug_ranges_flag(), ..Default::default() } } diff --git a/pyre/pyre-interpreter/src/cpyext/capsule.rs b/pyre/pyre-interpreter/src/cpyext/capsule.rs index 6c84b907e28..553f938ec2b 100644 --- a/pyre/pyre-interpreter/src/cpyext/capsule.rs +++ b/pyre/pyre-interpreter/src/cpyext/capsule.rs @@ -42,10 +42,13 @@ pub(crate) fn capsule_type() -> PyObjectRef { }), ); }); - unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; - // No `Py_TPFLAGS_BASETYPE` either -- a subclass would carry the name - // without the payload `is_capsule` reads. - unsafe { pyre_object::w_type_set_acceptable_as_base_class(tp, false) }; + unsafe { + pyre_object::typeobject::w_type_set_hasdict(tp, true); + pyre_object::w_type_set_disallow_instantiation(tp); + // No `Py_TPFLAGS_BASETYPE` either -- a subclass would carry the name + // without the payload `is_capsule` reads. + pyre_object::w_type_set_acceptable_as_base_class(tp, false); + } tp as usize }) as PyObjectRef } diff --git a/pyre/pyre-interpreter/src/cpyext/typeobject.rs b/pyre/pyre-interpreter/src/cpyext/typeobject.rs index a57fb0f4bcc..82a1bf606d6 100644 --- a/pyre/pyre-interpreter/src/cpyext/typeobject.rs +++ b/pyre/pyre-interpreter/src/cpyext/typeobject.rs @@ -181,6 +181,7 @@ pub struct CPyTypeObject { } pub const PY_TPFLAGS_DEFAULT: std::ffi::c_ulong = 0; +pub const PY_TPFLAGS_STATIC_BUILTIN: std::ffi::c_ulong = 1 << 1; pub const PY_TPFLAGS_HEAPTYPE: std::ffi::c_ulong = 1 << 9; pub const PY_TPFLAGS_BASETYPE: std::ffi::c_ulong = 1 << 10; pub const PY_TPFLAGS_READY: std::ffi::c_ulong = 1 << 12; @@ -444,13 +445,26 @@ pub(super) fn describe_interpreter_type(mirror: *mut CPyTypeObject, w_type: PyOb // The bytes are boxed, so moving the `CString` into the table below leaves // this pointer valid. let pointer = name.as_ptr(); - let heaptype = match unsafe { pyre_object::typeobject::w_type_is_heaptype(w_type) } { + let heaptype = match unsafe { pyre_object::w_type_is_cpython_heaptype(w_type) } { true => PY_TPFLAGS_HEAPTYPE, false => 0, }; + let static_builtin = match unsafe { pyre_object::w_type_is_cpython_static_builtin(w_type) } { + true => PY_TPFLAGS_STATIC_BUILTIN, + false => 0, + }; + let immutabletype = match unsafe { pyre_object::w_type_is_cpython_immutabletype(w_type) } { + true => PY_TPFLAGS_IMMUTABLETYPE, + false => 0, + }; unsafe { (*mirror).tp_name = pointer; - (*mirror).tp_flags = PY_TPFLAGS_DEFAULT | PY_TPFLAGS_READY | PY_TPFLAGS_BASETYPE | heaptype; + (*mirror).tp_flags = PY_TPFLAGS_DEFAULT + | PY_TPFLAGS_READY + | PY_TPFLAGS_BASETYPE + | heaptype + | static_builtin + | immutabletype; } TYPE_NAMES.lock().insert(mirror as usize, name); } @@ -2528,6 +2542,12 @@ fn ready(tp: *mut CPyTypeObject) -> Result<(), crate::PyError> { )); } unsafe { (*tp).tp_flags |= PY_TPFLAGS_READYING }; + // CPython 3.14 `PyType_Ready`: legacy non-heap extension statics are + // immutable, but do not receive the private STATIC_BUILTIN bit reserved + // for interpreter-core `_PyStaticType_InitBuiltin` owners. + if unsafe { (*tp).tp_flags } & PY_TPFLAGS_HEAPTYPE == 0 { + unsafe { (*tp).tp_flags |= PY_TPFLAGS_IMMUTABLETYPE }; + } let base = unsafe { (*tp).tp_base }; if !base.is_null() { @@ -2583,6 +2603,14 @@ fn ready(tp: *mut CPyTypeObject) -> Result<(), crate::PyError> { |ns| install_namespace(ns, tp), pyre_object::gc_roots::shadow_stack_get(base_slot), ); + unsafe { + pyre_object::w_type_set_cpython_type_flags( + w_type, + (*tp).tp_flags & PY_TPFLAGS_HEAPTYPE != 0, + false, + (*tp).tp_flags & PY_TPFLAGS_IMMUTABLETYPE != 0, + ); + } let type_slot = pyre_object::gc_roots::shadow_stack_len(); roots.pin_root(w_type); unsafe { @@ -3537,7 +3565,7 @@ pub unsafe extern "C" fn PyType_Freeze(tp: *mut CPyTypeObject) -> c_int { // The type itself is the head of its own MRO and is the one being frozen, // so only what it inherits from has to be immutable already. for &w_base in unsafe { (*mro).as_slice() }.iter().skip(1) { - if unsafe { pyre_object::w_type_is_heaptype(w_base) } { + if !unsafe { pyre_object::w_type_is_cpython_immutabletype(w_base) } { super::pyerrors::set_pending_error(crate::PyError::type_error(format!( "Creating immutable type {} from mutable base {}", type_name_of(tp), @@ -3548,6 +3576,8 @@ pub unsafe extern "C" fn PyType_Freeze(tp: *mut CPyTypeObject) -> c_int { } unsafe { pyre_object::typeobject::w_type_set_heaptype(w_type, false); + // Freezing changes CPython's immutable axis but not its HEAPTYPE owner. + pyre_object::w_type_set_cpython_immutabletype(w_type, true); if !tp.is_null() { (*tp).tp_flags |= PY_TPFLAGS_IMMUTABLETYPE; } diff --git a/pyre/pyre-interpreter/src/error.rs b/pyre/pyre-interpreter/src/error.rs index d722af82439..17b25dfb75b 100644 --- a/pyre/pyre-interpreter/src/error.rs +++ b/pyre/pyre-interpreter/src/error.rs @@ -623,6 +623,75 @@ impl PyError { err } + /// PyPy `error.py:725-737 enrich_attribute_error` / CPython 3.14 + /// `_PyObject_SetAttributeErrorContext`: an `AttributeError` escaping a + /// public attribute lookup gets the receiver and requested name when (and + /// only when) neither slot was already supplied by an inner lookup or by + /// the exception constructor. In particular, an explicit `name=None` or + /// `obj=None` is a populated slot in CPython and must not be overwritten. + pub fn enrich_attribute_error(&mut self, w_obj: PyObjectRef, w_name: PyObjectRef) { + if self.kind != PyErrorKind::AttributeError || w_obj.is_null() || w_name.is_null() { + return; + } + if self.exc_object.is_null() { + if self.w_name_context.is_null() && self.w_obj_context.is_null() { + self.w_name_context = w_name; + self.w_obj_context = w_obj; + } + return; + } + unsafe { + let current_name = + pyre_object::interp_exceptions::w_exception_get_name(self.exc_object); + let current_obj = + pyre_object::interp_exceptions::w_exception_get_attr_obj(self.exc_object); + if current_name.is_null() && current_obj.is_null() { + pyre_object::interp_exceptions::w_exception_set_name(self.exc_object, w_name); + pyre_object::interp_exceptions::w_exception_set_attr_obj(self.exc_object, w_obj); + } + } + } + + /// String-taking companion for pyre's optimized `getattr_str` path. PyPy + /// already carries a wrapped `w_name`; pyre delays that allocation until + /// an un-enriched AttributeError actually escapes. Root the receiver and + /// error payload while constructing it because the precise collector does + /// not scan this Rust `PyError` local. + pub fn enrich_attribute_error_str(&mut self, w_obj: PyObjectRef, name: &str) { + if self.kind != PyErrorKind::AttributeError || w_obj.is_null() { + return; + } + let missing = if self.exc_object.is_null() { + self.w_name_context.is_null() && self.w_obj_context.is_null() + } else { + unsafe { + pyre_object::interp_exceptions::w_exception_get_name(self.exc_object).is_null() + && pyre_object::interp_exceptions::w_exception_get_attr_obj(self.exc_object) + .is_null() + } + }; + if !missing { + return; + } + + let _roots = pyre_object::gc_roots::push_roots(); + let exc_slot = if self.exc_object.is_null() { + None + } else { + let slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(self.exc_object); + Some(slot) + }; + let obj_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(w_obj); + let w_name = pyre_object::w_str_new(name); + if let Some(slot) = exc_slot { + self.exc_object = pyre_object::gc_roots::shadow_stack_get(slot); + } + let w_obj = pyre_object::gc_roots::shadow_stack_get(obj_slot); + self.enrich_attribute_error(w_obj, w_name); + } + pub fn value_error(msg: impl Into) -> Self { Self::new(PyErrorKind::ValueError, msg) } @@ -1885,7 +1954,7 @@ fn exc_object_class_name(exc: PyObjectRef) -> Option { } crate::typedef::r#type(exc).map(|tp| unsafe { let w_type = tp.as_ptr(); - if !pyre_object::w_type_is_heaptype(w_type) { + if !pyre_object::w_type_is_cpython_heaptype(w_type) { return pyre_object::w_type_get_name(w_type).to_string(); } let qualname = pyre_object::w_type_get_qualname(w_type).to_string(); diff --git a/pyre/pyre-interpreter/src/eval.rs b/pyre/pyre-interpreter/src/eval.rs index 52214acc6b9..a971f2bddc6 100644 --- a/pyre/pyre-interpreter/src/eval.rs +++ b/pyre/pyre-interpreter/src/eval.rs @@ -330,6 +330,11 @@ pub unsafe fn walk_raw_code_roots( // PREBUILT_CODE_ROOTS, so the same direct-edge shape covers them too. let code = &mut *(value as *mut crate::pycode::PyCode); visitor(&mut *(&mut code.w_globals as *mut PyObjectRef as *mut majit_ir::GcRef)); + // typedef.py:724 `make_weakref_descr(PyCode)` adds the strong + // `_lifeline_` field to PyCode. Its own rweakrefs point weakly + // back to this code object; tracing this edge keeps callbacks + // alive exactly until their owner becomes unreachable. + visitor(&mut *(&mut code.w_weakreflifeline as *mut PyObjectRef as *mut majit_ir::GcRef)); // The realized `co_qualname` is an ordinary movable string object // shared by every function built from this code. visitor(&mut *(&mut code.w_qualname as *mut PyObjectRef as *mut majit_ir::GcRef)); diff --git a/pyre/pyre-interpreter/src/executioncontext.rs b/pyre/pyre-interpreter/src/executioncontext.rs index 58f6fe58e6f..78146c633a3 100644 --- a/pyre/pyre-interpreter/src/executioncontext.rs +++ b/pyre/pyre-interpreter/src/executioncontext.rs @@ -2637,17 +2637,46 @@ impl UserDelAction { } return; } + if pyre_object::generator::AsyncGenASend::from_obj(current()).is_some() + || pyre_object::generator::AsyncGenAThrow::from_obj(current()).is_some() + { + // executioncontext.py:651-661 — a finalizer that would call a + // user-defined app-level function defers under `gc.disable()`. + // This one emits a RuntimeWarning, so it reaches the overridable + // `warnings` machinery, exactly like the generator branch below. + if self.gc_disabled(current()) { + return; + } + crate::baseobjspace::async_gen_awaitable_finalize(current()); + return; + } if unsafe { pyre_object::generator::is_generator_or_coroutine(current()) } { if self.gc_disabled(current()) { return; } - if let Err(error) = crate::baseobjspace::generator_finalize(current()) { - report_error( - self.base.space, - &error, - rustpython_wtf8::Wtf8::new(""), - current(), + if let Err(mut error) = crate::baseobjspace::generator_finalize(current()) { + // CPython 3.14 `_PyGen_Finalize` reports close failures with + // `PyErr_FormatUnraisable`, whose hook object is None and + // whose message names the generator itself. + let w_exc = error.to_exc_object(); + let frame = crate::eval::current_frame(); + if !frame.is_null() { + unsafe { + crate::pytraceback::record_application_traceback( + w_exc, + frame, + (*frame).last_instr as i64, + ) + }; + } + let repr = unsafe { crate::display::py_repr_wtf8(current()) } + .unwrap_or_else(|_| rustpython_wtf8::Wtf8Buf::from_string("".to_owned())); + let where_desc = crate::display::wtf8_format!( + "Exception ignored while closing generator ", + repr ); + report_error(self.base.space, &error, &where_desc, pyre_object::w_none()); + crate::eval::set_in_flight_exception(pyre_object::PY_NULL); } // pyframe.py:75-76/276-279 stores this back-reference as // `f_generator_wref` in translated PyPy. The collector has diff --git a/pyre/pyre-interpreter/src/importing.rs b/pyre/pyre-interpreter/src/importing.rs index c4a93888219..720cc12e82a 100644 --- a/pyre/pyre-interpreter/src/importing.rs +++ b/pyre/pyre-interpreter/src/importing.rs @@ -1524,6 +1524,14 @@ pub(crate) fn load_builtin_module(name: &str) -> Option { crate::gateway::with_module(static_name, value); } } + // `mixedmodule.py:192-193` — a module def that names no `__doc__` still + // publishes one, through `get__doc__`'s `newtext_or_none(cls.__doc__)`. + // A Rust module def carries no class docstring, so the None arm is what + // every def that stays silent resolves to; the key itself is not optional, + // and `dir()` lists it. + if unsafe { pyre_object::dictmultiobject::w_dict_getitem_str(w_dict, "__doc__") }.is_none() { + crate::module_ns_store(w_dict, "__doc__", pyre_object::w_none()); + } // Before `sys.modules` exists the native bootstrap registry is the only // owner and retains the legacy immortal module shape. Afterwards an // audited collectible module follows the PyPy `space.sys.modules` object @@ -3113,6 +3121,7 @@ static SYS_ORIG_ARGV: LazyLock>> = LazyLock::new(|| Mutex::new(Vec::new())); static SYS_STDIO_ENCODING: LazyLock>> = LazyLock::new(|| Mutex::new(None)); static SYS_WARN_DEFAULT_ENCODING: AtomicBool = AtomicBool::new(false); +static SYS_CODE_DEBUG_RANGES: AtomicBool = AtomicBool::new(true); /// Record whether the launcher was given `-S` (no `site` import), so the /// `sys.flags.no_site` field built during sys module init reflects it. Set @@ -3144,6 +3153,7 @@ pub fn set_runtime_flags(flags: &crate::launch_env::LaunchFlags) { #[cfg(windows)] crate::typedef::LEGACY_WINDOWS_FS_ENCODING .store(flags.legacy_windows_fs_encoding, Ordering::Relaxed); + SYS_CODE_DEBUG_RANGES.store(!flags.no_debug_ranges, Ordering::Relaxed); SYS_UTF8_MODE.store(flags.utf8_mode.unwrap_or(0), Ordering::Relaxed); SYS_SAFE_PATH.store(flags.safe_path, Ordering::Relaxed); SYS_OPTIMIZE.store(flags.optimize, Ordering::Relaxed); @@ -3164,6 +3174,12 @@ pub fn bytes_warning_flag() -> i64 { SYS_BYTES_WARNING.load(Ordering::Relaxed) } +/// CPython 3.14 `PyConfig.code_debug_ranges`, consumed by every compiler +/// entry point so command-line, import and builtin `compile()` agree. +pub fn code_debug_ranges_flag() -> bool { + SYS_CODE_DEBUG_RANGES.load(Ordering::Relaxed) +} + pub fn unbuffered_flag() -> bool { SYS_UNBUFFERED.load(Ordering::Relaxed) } @@ -3855,8 +3871,8 @@ fn load_source_module( ) } }; - // The whole unit was named by this path, so the nested constants still - // held unrealized take the same spelling when they are boxed. + // The whole unit was named by this path, so recurse through the eager + // nested PyCode constants like PyPy `update_code_filenames`. unsafe { crate::pycode::set_compilation_unit_filename_bytes(w_code, filename_bytes) }; // Root before any allocation (fresh_module_globals, the cache write) can // collect the freshly boxed code out from under us. diff --git a/pyre/pyre-interpreter/src/launch_env.rs b/pyre/pyre-interpreter/src/launch_env.rs index 76e933f42d6..e85511d3efe 100644 --- a/pyre/pyre-interpreter/src/launch_env.rs +++ b/pyre/pyre-interpreter/src/launch_env.rs @@ -29,6 +29,9 @@ pub struct LaunchFlags { /// Read only on Windows, the only platform whose `PyPreConfig` carries it, /// and left false everywhere else. pub legacy_windows_fs_encoding: bool, + /// CPython 3.14 `code_debug_ranges == 0`, selected by either + /// `-X no_debug_ranges` or the PYTHONNODEBUGRANGES presence flag. + pub no_debug_ranges: bool, /// `None` until [`finalize`] resolves it; a command line that named /// `-X utf8` carries that value through instead. pub utf8_mode: Option, @@ -90,6 +93,7 @@ pub const LAUNCH_ENV_NAMES: &[&str] = &[ "PYTHONLEGACYWINDOWSFSENCODING", "PYTHONWARNINGS", "PYTHONIOENCODING", + "PYTHONNODEBUGRANGES", "LC_ALL", "LC_CTYPE", "LANG", @@ -278,6 +282,8 @@ pub fn finalize(mut flags: LaunchFlags) -> Result { flags.warn_default_encoding, "PYTHONWARNDEFAULTENCODING", ); + flags.no_debug_ranges = + fold_presence_flag(&flags, flags.no_debug_ranges, "PYTHONNODEBUGRANGES"); flags.stdio_encoding = if flags.ignore_environment { None } else { diff --git a/pyre/pyre-interpreter/src/module/_bz2/mod.rs b/pyre/pyre-interpreter/src/module/_bz2/mod.rs index a4a4ee87771..bfec96ecbe5 100644 --- a/pyre/pyre-interpreter/src/module/_bz2/mod.rs +++ b/pyre/pyre-interpreter/src/module/_bz2/mod.rs @@ -14,14 +14,17 @@ use std::sync::Mutex; /// `interp_bz2.py W_BZ2Compressor`: the stream and its lock belong to /// the wrapper object; there is no process-global side table. -#[crate::pyre_class("_bz2.BZ2Compressor")] +// CPython 3.14 Modules/_bz2module.c:bz2_exec uses +// PyType_FromModuleAndSpec with IMMUTABLETYPE. +#[crate::pyre_class("_bz2.BZ2Compressor", cpython_heaptype)] #[derive(Default)] pub struct W_BZ2Compressor { backend: *mut Mutex, } /// `interp_bz2.py W_BZ2Decompressor`, object-owned the same way. -#[crate::pyre_class("_bz2.BZ2Decompressor")] +// Same `bz2_exec` owner and flags as BZ2Compressor. +#[crate::pyre_class("_bz2.BZ2Decompressor", cpython_heaptype)] #[derive(Default)] pub struct W_BZ2Decompressor { backend: *mut Mutex, diff --git a/pyre/pyre-interpreter/src/module/_collections/mod.rs b/pyre/pyre-interpreter/src/module/_collections/mod.rs index c9f6a5f16d7..869d471769f 100644 --- a/pyre/pyre-interpreter/src/module/_collections/mod.rs +++ b/pyre/pyre-interpreter/src/module/_collections/mod.rs @@ -52,7 +52,9 @@ use deque_block::W_DequeBlock; /// (append / appendleft / pop / popleft / clear / extend / extendleft / /// rotate / count / remove / reverse / index / copy + container and repr /// protocols, with `maxlen` bounding). -#[crate::pyre_class("collections.deque")] +// CPython 3.14 Modules/_collectionsmodule.c:collections_exec ADD_TYPEs +// deque_spec through a module heap type; the spec is immutable. +#[crate::pyre_class("collections.deque", cpython_heaptype)] pub struct W_Deque { leftblock: PyObjectRef, rightblock: PyObjectRef, @@ -282,7 +284,8 @@ pub mod deque_iter { use super::{BLOCKLEN, W_Deque, block, block_get, checklock, getlock}; use pyre_object::*; - #[crate::pyre_class("_collections._deque_iterator")] + // `collections_exec` ADD_TYPEs dequeiter_spec as an immutable heap type. + #[crate::pyre_class("_collections._deque_iterator", cpython_heaptype)] pub struct W_DequeIter { deque: PyObjectRef, block: PyObjectRef, @@ -409,7 +412,8 @@ pub mod deque_rev_iter { use super::{BLOCKLEN, W_Deque, block, block_get, checklock, getlock}; use pyre_object::*; - #[crate::pyre_class("_collections._deque_reverse_iterator")] + // `collections_exec` ADD_TYPEs dequereviter_spec as an immutable heap type. + #[crate::pyre_class("_collections._deque_reverse_iterator", cpython_heaptype)] pub struct W_DequeRevIter { deque: PyObjectRef, block: PyObjectRef, diff --git a/pyre/pyre-interpreter/src/module/_contextvars/mod.rs b/pyre/pyre-interpreter/src/module/_contextvars/mod.rs index b6ab183d03a..7b2948a8ece 100644 --- a/pyre/pyre-interpreter/src/module/_contextvars/mod.rs +++ b/pyre/pyre-interpreter/src/module/_contextvars/mod.rs @@ -483,6 +483,17 @@ crate::py_module! { ); let context = crate::module_ns_get(ns, "Context") .expect("_contextvars.Context must be installed by appleveldefs"); + // [3.14-spec] PyPy keeps Context as the ordinary app-level class in + // lib_pypy/_contextvars.py (with Unsubclassable as its metaclass), and + // pyre keeps that owner and control-flow shape. CPython 3.14 exposes + // PyContext_Type as a static immutable type instead + // (Python/context.c:750-770). No @jit.*, _immutable_fields_, or + // runtime reader in PyPy's class definition depends on the public + // owner flags, so project only CPython's observable axes here. + unsafe { + pyre_object::w_type_set_cpython_type_flags(context, false, true, true); + pyre_object::w_type_suppress_cpython_basetype(context); + } let context_var_dict = unsafe { pyre_object::w_type_get_dict_ptr(context_var) as PyObjectRef }; unsafe { diff --git a/pyre/pyre-interpreter/src/module/_csv/mod.rs b/pyre/pyre-interpreter/src/module/_csv/mod.rs index 1a68508f95f..95143a62137 100644 --- a/pyre/pyre-interpreter/src/module/_csv/mod.rs +++ b/pyre/pyre-interpreter/src/module/_csv/mod.rs @@ -569,6 +569,9 @@ mod dialect_class { }; } }); + // CPython 3.14 Modules/_csv.c:csv_exec uses + // PyType_FromModuleAndSpec; Dialect_Type_spec is immutable. + crate::typedef::mark_cpython_heap_type(tp, true); unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; tp as usize }) as PyObjectRef diff --git a/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs b/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs index be36280ce5a..3fdb36a05db 100644 --- a/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs +++ b/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs @@ -42,7 +42,7 @@ pub(super) fn cdata_type() -> PyObjectRef { *CDATA_TYPE_OBJ.get_or_init(|| { let tp = crate::typedef::make_builtin_type("_CData", init_cdata_type); unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; - tp as usize + super::finish_cpython_type(tp, "_ctypes", true) as usize }) as PyObjectRef } @@ -253,8 +253,13 @@ pub(super) fn simplecdata_type() -> PyObjectRef { init_simplecdata_type, cdata_type(), ); - unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; - tp as usize + unsafe { + pyre_object::typeobject::w_type_set_hasdict(tp, true); + // PyPy `primitive.py:SimpleType(_CDataMeta)` / CPython 3.14 + // `CREATE_TYPE(... PyCSimpleType_Type ...)`. + (*tp).w_class = super::metaclass::pycsimpletype_type(); + } + super::finish_cpython_type(tp, "_ctypes", true) as usize }) as PyObjectRef } diff --git a/pyre/pyre-interpreter/src/module/_ctypes/funcptr.rs b/pyre/pyre-interpreter/src/module/_ctypes/funcptr.rs index 49a448780c4..20234827053 100644 --- a/pyre/pyre-interpreter/src/module/_ctypes/funcptr.rs +++ b/pyre/pyre-interpreter/src/module/_ctypes/funcptr.rs @@ -68,8 +68,12 @@ pub(super) fn cfuncptr_type() -> PyObjectRef { init_cfuncptr_type, cdata::cdata_type(), ); - unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; - tp as usize + unsafe { + pyre_object::typeobject::w_type_set_hasdict(tp, true); + // PyPy `function.py:CFuncPtr.__metaclass__ = CFuncPtrType`. + (*tp).w_class = super::metaclass::pycfuncptrtype_type(); + } + super::finish_cpython_type(tp, "_ctypes", true) as usize }) as PyObjectRef } diff --git a/pyre/pyre-interpreter/src/module/_ctypes/interp_ctypes.rs b/pyre/pyre-interpreter/src/module/_ctypes/interp_ctypes.rs index 9ac82a215f1..07b1ff9c75c 100644 --- a/pyre/pyre-interpreter/src/module/_ctypes/interp_ctypes.rs +++ b/pyre/pyre-interpreter/src/module/_ctypes/interp_ctypes.rs @@ -356,15 +356,15 @@ fn register_host_ctypes(ns: pyre_object::PyObjectRef) { // ── ArgumentError — a real Exception subclass ── let w_exception = crate::builtins::lookup_exc_class("Exception") .expect("Exception must be installed before _ctypes init"); - crate::module_ns_store( - ns, + let argument_error = crate::builtins::make_exc_type( "ArgumentError", - crate::builtins::make_exc_type( - "ArgumentError", - crate::builtins::exc_exception_new, - w_exception, - ), + crate::builtins::exc_exception_new, + w_exception, ); + // Both CPython's module exception and PyPy's app-level ArgumentError are + // mutable heap classes, unlike the immutable native `_ctypes` types. + let argument_error = super::finish_cpython_type(argument_error, "ctypes", false); + crate::module_ns_store(ns, "ArgumentError", argument_error); // ── aggregate + array/pointer types: real `Structure`/`Union`/`Array`/ // `_Pointer`/`CField` ── @@ -381,9 +381,6 @@ fn register_host_ctypes(ns: pyre_object::PyObjectRef) { // ── the functional scalar + foreign-function types ── let simplecdata_tp = super::cdata::simplecdata_type(); - // `_SimpleCData`'s metaclass routes `class c_int(_SimpleCData): _type_="i"` - // through `PyCSimpleType.__new__` (validation + StgInfo). - unsafe { (*simplecdata_tp).w_class = metaclass::pycsimpletype_type() }; crate::module_ns_store(ns, "_SimpleCData", simplecdata_tp); let cfuncptr_tp = super::funcptr::cfuncptr_type(); crate::module_ns_store(ns, "CFuncPtr", cfuncptr_tp); @@ -975,7 +972,7 @@ static CARG_TYPE_OBJ: std::sync::OnceLock = std::sync::OnceLock::new(); /// (the referenced instance, kept alive). Foreign-call consumption of the /// carrier (the CArgObject P-tag path) is a later slice. #[cfg(all(any(unix, windows), feature = "host_env"))] -fn carg_type() -> pyre_object::PyObjectRef { +pub(super) fn carg_type() -> pyre_object::PyObjectRef { let raw = *CARG_TYPE_OBJ.get_or_init(|| { let tp = crate::typedef::make_builtin_type("CArgObject", |ns| { super::type_ns_store( @@ -991,7 +988,7 @@ fn carg_type() -> pyre_object::PyObjectRef { ); }); unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; - tp as usize + super::finish_cpython_type(tp, "_ctypes", true) as usize }); raw as pyre_object::PyObjectRef } diff --git a/pyre/pyre-interpreter/src/module/_ctypes/metaclass.rs b/pyre/pyre-interpreter/src/module/_ctypes/metaclass.rs index cf06210309b..5629c823b4f 100644 --- a/pyre/pyre-interpreter/src/module/_ctypes/metaclass.rs +++ b/pyre/pyre-interpreter/src/module/_ctypes/metaclass.rs @@ -34,12 +34,13 @@ type PyResult = Result; /// its ctypes metaclasses are app-level (`class _CDataMeta(type)`, /// basics.py:48) and inherit `type`'s instance layout from their base. fn make_ctypes_metatype(name: &str, init: impl FnOnce(PyObjectRef)) -> PyObjectRef { - crate::typedef::make_builtin_type_with_layout( + let tp = crate::typedef::make_builtin_type_with_layout( name, init, - crate::typedef::w_type(), + ctype_type(), &pyre_object::pyobject::TYPE_TYPE as *const pyre_object::PyType, - ) + ); + super::finish_cpython_type(tp, "_ctypes", true) } // ── cached type objects ──────────────────────────────────────────────── @@ -53,11 +54,23 @@ macro_rules! cached_type { }; } +// CPython 3.14 `_ctypes.CType_Type` and PyPy `_CDataMeta` are the common +// metaclass owner for every concrete ctypes metaclass. Keeping the shared +// methods here avoids the former pyre-only duplication on every child. +cached_type!(CTYPE_TYPE, ctype_type, || { + let tp = crate::typedef::make_builtin_type_with_layout( + "CType_Type", + install_shared_meta, + crate::typedef::w_type(), + &pyre_object::pyobject::TYPE_TYPE as *const pyre_object::PyType, + ); + super::finish_cpython_type(tp, "_ctypes", true) +}); + cached_type!(PYCSIMPLETYPE, pycsimpletype_type, || { make_ctypes_metatype("PyCSimpleType", |ns| { install_new(ns, csimpletype_new); install_init(ns, csimpletype_init); - install_shared_meta(ns); }) }); @@ -65,7 +78,6 @@ cached_type!(PYCSTRUCTTYPE, pycstructtype_type, || { make_ctypes_metatype("PyCStructType", |ns| { install_new(ns, cstructtype_new); install_init(ns, cstructtype_init); - install_shared_meta(ns); install_fields_getset(ns); }) }); @@ -76,11 +88,14 @@ cached_type!(PYCUNIONTYPE, pycuniontype_type, || { make_ctypes_metatype("UnionType", |ns| { install_new(ns, cuniontype_new); install_init(ns, cuniontype_init); - install_shared_meta(ns); install_fields_getset(ns); }) }); +cached_type!(PYCFUNCPTRTYPE, pycfuncptrtype_type, || { + make_ctypes_metatype("PyCFuncPtrType", |_| {}) +}); + cached_type!(STRUCTURE, structure_type, || { let tp = crate::typedef::make_builtin_type_with_base( "Structure", @@ -88,7 +103,7 @@ cached_type!(STRUCTURE, structure_type, || { cdata::cdata_type(), ); finish_aggregate_base(tp, pycstructtype_type(), "struct"); - tp + super::finish_cpython_type(tp, "_ctypes", true) }); cached_type!(UNION, union_type, || { @@ -98,7 +113,7 @@ cached_type!(UNION, union_type, || { cdata::cdata_type(), ); finish_aggregate_base(tp, pycuniontype_type(), "union"); - tp + super::finish_cpython_type(tp, "_ctypes", true) }); cached_type!(CFIELD, cfield_type, || { @@ -141,14 +156,13 @@ cached_type!(CFIELD, cfield_type, || { ); }); unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; - tp + super::finish_cpython_type(tp, "ctypes", true) }); cached_type!(PYCARRAYTYPE, pycarraytype_type, || { make_ctypes_metatype("PyCArrayType", |ns| { install_new(ns, carraytype_new); install_init(ns, carraytype_init); - install_shared_meta(ns); }) }); @@ -156,7 +170,6 @@ cached_type!(PYCPOINTERTYPE, pycpointertype_type, || { make_ctypes_metatype("PyCPointerType", |ns| { install_new(ns, cpointertype_new); install_init(ns, cpointertype_init); - install_shared_meta(ns); }) }); @@ -164,7 +177,7 @@ cached_type!(ARRAY, array_type, || { let tp = crate::typedef::make_builtin_type_with_base("Array", init_array_base, cdata::cdata_type()); finish_element_base(tp, pycarraytype_type()); - tp + super::finish_cpython_type(tp, "_ctypes", true) }); cached_type!(POINTER_BASE, pointer_base_type, || { @@ -174,7 +187,7 @@ cached_type!(POINTER_BASE, pointer_base_type, || { cdata::cdata_type(), ); finish_element_base(tp, pycpointertype_type()); - tp + super::finish_cpython_type(tp, "_ctypes", true) }); fn install_new(ns: PyObjectRef, f: crate::gateway::BuiltinCodeFn) { diff --git a/pyre/pyre-interpreter/src/module/_ctypes/mod.rs b/pyre/pyre-interpreter/src/module/_ctypes/mod.rs index 6233a7ddfda..73827ccc522 100644 --- a/pyre/pyre-interpreter/src/module/_ctypes/mod.rs +++ b/pyre/pyre-interpreter/src/module/_ctypes/mod.rs @@ -17,6 +17,27 @@ fn type_ns_store(ns: pyre_object::PyObjectRef, name: &str, value: pyre_object::P unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy(ns, name, value) } } +/// CPython 3.14 constructs the native `_ctypes` type family from immutable +/// `PyType_Spec`s (`Modules/_ctypes/_ctypes.c:6270-6318` and +/// `callproc.c:carg_spec`). PyPy implements the same family as ordinary +/// classes rooted at `_CDataMeta`; pyre preserves that common-metaclass owner +/// while projecting CPython's observable heap owner, immutability, and module. +#[cfg(all(any(unix, windows), feature = "host_env"))] +fn finish_cpython_type( + tp: pyre_object::PyObjectRef, + module: &str, + immutable: bool, +) -> pyre_object::PyObjectRef { + let roots = pyre_object::gc_roots::push_roots(); + roots.pin_root(tp); + let slot = roots.base(); + let w_module = pyre_object::w_str_new(module); + let tp = roots.get(slot); + crate::type_dict_store(tp, "__module__", w_module); + crate::typedef::mark_cpython_heap_type(tp, immutable); + tp +} + #[cfg(all(any(unix, windows), feature = "host_env"))] pub mod callbacks; #[cfg(all(any(unix, windows), feature = "host_env"))] @@ -31,3 +52,73 @@ pub mod metaclass; mod seh; #[cfg(all(any(unix, windows), feature = "host_env"))] pub mod stginfo; + +#[cfg(all(test, any(unix, windows), feature = "host_env"))] +mod tests { + use super::*; + + #[test] + fn native_type_family_has_common_meta_and_cpython_314_owner() { + crate::typedef::init_typeobjects(); + const IMMUTABLETYPE: i64 = 1 << 8; + const HEAPTYPE: i64 = 1 << 9; + const MASK: i64 = IMMUTABLETYPE | HEAPTYPE; + + let ctype = metaclass::ctype_type(); + let metas = [ + metaclass::pycsimpletype_type(), + metaclass::pycstructtype_type(), + metaclass::pycuniontype_type(), + metaclass::pycarraytype_type(), + metaclass::pycpointertype_type(), + metaclass::pycfuncptrtype_type(), + ]; + for meta in metas { + assert_eq!( + crate::baseobjspace::getattr_str(meta, "__base__").unwrap(), + ctype + ); + assert!(crate::baseobjspace::getattr_str(meta, "from_address").is_ok()); + } + + let simple = cdata::simplecdata_type(); + let funcptr = funcptr::cfuncptr_type(); + assert_eq!(crate::typedef::r#type(simple).unwrap().as_ptr(), metas[0]); + assert_eq!(crate::typedef::r#type(funcptr).unwrap().as_ptr(), metas[5]); + + for (name, ty, module) in [ + ("CType_Type", ctype, "_ctypes"), + ("PyCSimpleType", metas[0], "_ctypes"), + ("PyCStructType", metas[1], "_ctypes"), + ("UnionType", metas[2], "_ctypes"), + ("PyCArrayType", metas[3], "_ctypes"), + ("PyCPointerType", metas[4], "_ctypes"), + ("PyCFuncPtrType", metas[5], "_ctypes"), + ("_CData", cdata::cdata_type(), "_ctypes"), + ("_SimpleCData", simple, "_ctypes"), + ("CFuncPtr", funcptr, "_ctypes"), + ("Structure", metaclass::structure_type(), "_ctypes"), + ("Union", metaclass::union_type(), "_ctypes"), + ("Array", metaclass::array_type(), "_ctypes"), + ("_Pointer", metaclass::pointer_base_type(), "_ctypes"), + ("CField", metaclass::cfield_type(), "ctypes"), + ("CArgObject", interp_ctypes::carg_type(), "_ctypes"), + ] { + assert_eq!( + unsafe { pyre_object::w_type_get_flags(ty) } & MASK, + MASK, + "{name}.__flags__" + ); + let w_module = crate::baseobjspace::getattr_str(ty, "__module__").unwrap(); + assert_eq!( + unsafe { pyre_object::w_str_get_value(w_module) }, + module, + "{name}.__module__" + ); + assert!( + !unsafe { pyre_object::w_type_is_heaptype(ty) }, + "{name} must keep the PyPy builtin storage owner" + ); + } + } +} diff --git a/pyre/pyre-interpreter/src/module/_functools/mod.rs b/pyre/pyre-interpreter/src/module/_functools/mod.rs index ad291dca9a1..52dd5803512 100644 --- a/pyre/pyre-interpreter/src/module/_functools/mod.rs +++ b/pyre/pyre-interpreter/src/module/_functools/mod.rs @@ -18,7 +18,9 @@ use pyre_object::*; /// mirrors `keyobject.cmp` / `keyobject.object` and, unlike an app-level /// function, leaves the module callable descriptor-neutral when a user stores /// it on a class. -#[crate::pyre_class("functools.KeyWrapper")] +// CPython 3.14 Modules/_functoolsmodule.c:functools_exec creates +// keyobject_type_spec as an immutable module heap type. +#[crate::pyre_class("functools.KeyWrapper", cpython_heaptype)] pub struct W_KeyWrapper { cmp: PyObjectRef, object: PyObjectRef, diff --git a/pyre/pyre-interpreter/src/module/_hashlib/mod.rs b/pyre/pyre-interpreter/src/module/_hashlib/mod.rs index 9c8c3c5c5c2..b319505e7e1 100644 --- a/pyre/pyre-interpreter/src/module/_hashlib/mod.rs +++ b/pyre/pyre-interpreter/src/module/_hashlib/mod.rs @@ -78,7 +78,9 @@ impl HashStateStorage { /// The fixed-size native digest context embedded directly in its Python /// owner. CPython stores an `EVP_MD_CTX *` on `EVPobject`; this inline opaque /// buffer is pyre-native's allocation-free equivalent. -#[crate::pyre_class("_hashlib.HASH")] +// CPython 3.14 Modules/_hashopenssl.c creates HASH from a heap type spec; +// HASH_spec carries IMMUTABLETYPE. +#[crate::pyre_class("_hashlib.HASH", cpython_heaptype)] pub struct W_HashState { name: PyObjectRef, digest_size: i64, @@ -324,7 +326,7 @@ mod hash_state_class { fn hash_xof_type() -> PyObjectRef { static TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); *TYPE.get_or_init(|| { - crate::typedef::make_builtin_type_with_layout( + let tp = crate::typedef::make_builtin_type_with_layout( "_hashlib.HASHXOF", |ns| unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( @@ -339,7 +341,11 @@ fn hash_xof_type() -> PyObjectRef { }, hash_state_class::type_object(), ::PYTYPE, - ) as usize + ); + // CPython 3.14 Modules/_hashopenssl.c creates HASHXOF from the same + // immutable module heap family as HASH. + crate::typedef::mark_cpython_heap_type(tp, true); + tp as usize }) as PyObjectRef } @@ -409,7 +415,8 @@ impl HmacStateStorage { } } -#[crate::pyre_class("_hashlib.HMAC")] +// CPython 3.14 `_hashlib.HMAC` is the same immutable heap-spec family. +#[crate::pyre_class("_hashlib.HMAC", cpython_heaptype)] pub struct W_Hmac { name: PyObjectRef, digest_size: i64, diff --git a/pyre/pyre-interpreter/src/module/_io/buffered.rs b/pyre/pyre-interpreter/src/module/_io/buffered.rs index d730825fd68..3ec115f8030 100644 --- a/pyre/pyre-interpreter/src/module/_io/buffered.rs +++ b/pyre/pyre-interpreter/src/module/_io/buffered.rs @@ -69,7 +69,9 @@ pub(super) fn raw_readinto_size( Ok(size as usize) } -#[crate::pyre_class("_io.BufferedReader")] +// CPython 3.14 Modules/_io/_iomodule.c:ADD_TYPE creates the immutable +// BufferedReader heap spec. +#[crate::pyre_class("_io.BufferedReader", cpython_heaptype)] pub struct W_BufferedReader { state: i64, w_raw: PyObjectRef, diff --git a/pyre/pyre-interpreter/src/module/_io/buffered_random.rs b/pyre/pyre-interpreter/src/module/_io/buffered_random.rs index 8c08e9ff231..7b1b324b63d 100644 --- a/pyre/pyre-interpreter/src/module/_io/buffered_random.rs +++ b/pyre/pyre-interpreter/src/module/_io/buffered_random.rs @@ -8,7 +8,9 @@ const STATE_ZERO: i64 = 0; const STATE_OK: i64 = 1; const STATE_DETACHED: i64 = 2; -#[crate::pyre_class("_io.BufferedRandom")] +// CPython 3.14 Modules/_io/_iomodule.c:ADD_TYPE creates the immutable +// BufferedRandom heap spec. +#[crate::pyre_class("_io.BufferedRandom", cpython_heaptype)] pub struct W_BufferedRandom { state: i64, w_raw: PyObjectRef, diff --git a/pyre/pyre-interpreter/src/module/_io/buffered_rwpair.rs b/pyre/pyre-interpreter/src/module/_io/buffered_rwpair.rs index f08787575b8..9ff948f96da 100644 --- a/pyre/pyre-interpreter/src/module/_io/buffered_rwpair.rs +++ b/pyre/pyre-interpreter/src/module/_io/buffered_rwpair.rs @@ -4,7 +4,9 @@ use pyre_object::*; use super::DEFAULT_BUFFER_SIZE; -#[crate::pyre_class("_io.BufferedRWPair")] +// CPython 3.14 Modules/_io/_iomodule.c:ADD_TYPE creates the immutable +// BufferedRWPair heap spec. +#[crate::pyre_class("_io.BufferedRWPair", cpython_heaptype)] pub struct W_BufferedRWPair { w_reader: PyObjectRef, w_writer: PyObjectRef, diff --git a/pyre/pyre-interpreter/src/module/_io/buffered_writer.rs b/pyre/pyre-interpreter/src/module/_io/buffered_writer.rs index 26a06146c91..ebdb912d3f3 100644 --- a/pyre/pyre-interpreter/src/module/_io/buffered_writer.rs +++ b/pyre/pyre-interpreter/src/module/_io/buffered_writer.rs @@ -47,7 +47,9 @@ pub(super) fn input_bytes(obj: PyObjectRef) -> Result, crate::PyError> { } } -#[crate::pyre_class("_io.BufferedWriter")] +// CPython 3.14 Modules/_io/_iomodule.c:ADD_TYPE uses +// PyType_FromModuleAndSpec; the BufferedWriter spec is immutable. +#[crate::pyre_class("_io.BufferedWriter", cpython_heaptype)] pub struct W_BufferedWriter { state: i64, w_raw: PyObjectRef, diff --git a/pyre/pyre-interpreter/src/module/_io/bytesio.rs b/pyre/pyre-interpreter/src/module/_io/bytesio.rs index 497658d5bda..32b923185a2 100644 --- a/pyre/pyre-interpreter/src/module/_io/bytesio.rs +++ b/pyre/pyre-interpreter/src/module/_io/bytesio.rs @@ -4,7 +4,9 @@ use pyre_object::*; const AT_END: i64 = -1; -#[crate::pyre_class("_io.BytesIO")] +// CPython 3.14 Modules/_io/_iomodule.c:ADD_TYPE creates the immutable +// BytesIO heap spec. +#[crate::pyre_class("_io.BytesIO", cpython_heaptype)] pub struct W_BytesIO { // rpython/rlib/rStringIO.py:16-23 splits immutable strings between an // append-optimized builder and a mutable character list. A bytearray is diff --git a/pyre/pyre-interpreter/src/module/_io/mod.rs b/pyre/pyre-interpreter/src/module/_io/mod.rs index 3d13e5927b2..b9335382877 100644 --- a/pyre/pyre-interpreter/src/module/_io/mod.rs +++ b/pyre/pyre-interpreter/src/module/_io/mod.rs @@ -1219,6 +1219,9 @@ fn io_base_type() -> PyObjectRef { init_iobase_type, crate::typedef::w_object(), ); + // CPython 3.14 Modules/_io/_iomodule.c:ADD_TYPE creates the immutable + // iobase_spec through PyType_FromModuleAndSpec. + crate::typedef::mark_cpython_heap_type(tp, true); unsafe { pyre_object::w_type_set_acceptable_as_base_class(tp, true); pyre_object::w_type_set_weakrefable(tp, true); @@ -1236,6 +1239,8 @@ pub(super) fn raw_iobase_type() -> PyObjectRef { init_rawiobase_type, io_base_type(), ); + // `_iomodule.c:ADD_TYPE` creates rawiobase_spec as immutable heap. + crate::typedef::mark_cpython_heap_type(tp, true); unsafe { pyre_object::w_type_set_acceptable_as_base_class(tp, true); pyre_object::w_type_set_weakrefable(tp, true); @@ -1265,6 +1270,8 @@ pub(crate) fn fileio_type() -> PyObjectRef { }, raw_iobase_type(), ); + // `_iomodule.c:ADD_TYPE` creates fileio_spec as immutable heap. + crate::typedef::mark_cpython_heap_type(tp, true); unsafe { pyre_object::w_type_set_acceptable_as_base_class(tp, true); pyre_object::w_type_set_weakrefable(tp, true); @@ -1299,6 +1306,8 @@ pub(super) fn buffered_iobase_type() -> PyObjectRef { init_buffered_iobase_type, io_base_type(), ); + // `_iomodule.c:ADD_TYPE` creates bufferediobase_spec as immutable heap. + crate::typedef::mark_cpython_heap_type(tp, true); unsafe { pyre_object::w_type_set_acceptable_as_base_class(tp, true); pyre_object::w_type_set_weakrefable(tp, true); @@ -1316,6 +1325,8 @@ fn text_iobase_type() -> PyObjectRef { init_text_iobase_type, io_base_type(), ); + // `_iomodule.c:ADD_TYPE` creates textiobase_spec as immutable heap. + crate::typedef::mark_cpython_heap_type(tp, true); unsafe { pyre_object::w_type_set_acceptable_as_base_class(tp, true); pyre_object::w_type_set_weakrefable(tp, true); diff --git a/pyre/pyre-interpreter/src/module/_io/stringio.rs b/pyre/pyre-interpreter/src/module/_io/stringio.rs index d163c4cd818..c694a3be9b1 100644 --- a/pyre/pyre-interpreter/src/module/_io/stringio.rs +++ b/pyre/pyre-interpreter/src/module/_io/stringio.rs @@ -3,7 +3,9 @@ use pyre_object::*; use rustpython_wtf8::{CodePoint, Wtf8Buf}; -#[crate::pyre_class("_io.StringIO")] +// CPython 3.14 Modules/_io/_iomodule.c:ADD_TYPE creates the immutable +// StringIO heap spec. +#[crate::pyre_class("_io.StringIO", cpython_heaptype)] pub struct W_StringIO { // interp_stringio.py stores UnicodeIO.data as a list of r_int32. // `array('w')` is the existing GC object whose raw payload is a mutable diff --git a/pyre/pyre-interpreter/src/module/_io/textio.rs b/pyre/pyre-interpreter/src/module/_io/textio.rs index 46f57426741..92eb96c4d86 100644 --- a/pyre/pyre-interpreter/src/module/_io/textio.rs +++ b/pyre/pyre-interpreter/src/module/_io/textio.rs @@ -270,7 +270,9 @@ impl DecodeBuffer { } } -#[crate::pyre_class("_io.TextIOWrapper")] +// CPython 3.14 Modules/_io/_iomodule.c:ADD_TYPE creates the immutable +// TextIOWrapper heap spec. +#[crate::pyre_class("_io.TextIOWrapper", cpython_heaptype)] pub struct W_TextIOWrapper { state: i64, w_buffer: PyObjectRef, diff --git a/pyre/pyre-interpreter/src/module/_json/mod.rs b/pyre/pyre-interpreter/src/module/_json/mod.rs index b6e5d92b77e..33bc361711c 100644 --- a/pyre/pyre-interpreter/src/module/_json/mod.rs +++ b/pyre/pyre-interpreter/src/module/_json/mod.rs @@ -616,7 +616,9 @@ fn scanner_call_impl(self_obj: PyObjectRef, doc: PyObjectRef, index: i64) -> PyR ])) } -#[crate::pyre_class("_json.Scanner")] +// CPython 3.14 Modules/_json.c:PyInit__json uses PyType_FromSpec; +// PyScannerType_spec is mutable. +#[crate::pyre_class("_json.Scanner", cpython_mutable)] pub struct W_Scanner { strict: bool, parse_float: PyObjectRef, @@ -662,7 +664,8 @@ mod scanner_class { } } -#[crate::pyre_class("_json.Encoder")] +// CPython 3.14 Modules/_json.c:PyInit__json creates the mutable encoder spec. +#[crate::pyre_class("_json.Encoder", cpython_mutable)] pub struct W_Encoder { markers: PyObjectRef, default: PyObjectRef, diff --git a/pyre/pyre-interpreter/src/module/_lsprof/mod.rs b/pyre/pyre-interpreter/src/module/_lsprof/mod.rs index db82a1dd0ba..f6380337147 100644 --- a/pyre/pyre-interpreter/src/module/_lsprof/mod.rs +++ b/pyre/pyre-interpreter/src/module/_lsprof/mod.rs @@ -12,7 +12,9 @@ use std::time::Instant; /// `interp_lsprof.py W_Profiler`. The entry trees and the context stack /// belong to the wrapper object. The mapdict prefix is required because /// `cProfile.Profile` subclasses the type. -#[crate::pyre_class("_lsprof.Profiler")] +// CPython 3.14 Modules/_lsprof.c:_lsprof_exec uses +// PyType_FromModuleAndSpec with IMMUTABLETYPE. +#[crate::pyre_class("_lsprof.Profiler", cpython_heaptype)] #[derive(Default)] pub struct W_Profiler { pub map: usize, @@ -43,7 +45,9 @@ const _: () = assert!( /// `interp_lsprof.py W_StatsEntry`. Its typedef publishes no `__new__`, so /// no subclass of it can be instantiated and it carries no mapdict prefix; the /// two references below are ordinary inline `gc_ptr_offsets` edges. -#[crate::pyre_class("_lsprof.profiler_entry")] +// CPython 3.14 Modules/_lsprof.c creates this with PyStructSequence_NewType; +// the resulting struct-sequence type is a mutable heap type. +#[crate::pyre_class("_lsprof.profiler_entry", cpython_mutable)] pub struct W_StatsEntry { frame: PyObjectRef, callcount: i64, @@ -55,7 +59,8 @@ pub struct W_StatsEntry { /// `interp_lsprof.py W_StatsSubEntry`, likewise not instantiable and /// therefore prefix-free. -#[crate::pyre_class("_lsprof.profiler_subentry")] +// Same PyStructSequence_NewType mutable heap owner as profiler_entry. +#[crate::pyre_class("_lsprof.profiler_subentry", cpython_mutable)] pub struct W_StatsSubEntry { frame: PyObjectRef, callcount: i64, diff --git a/pyre/pyre-interpreter/src/module/_lzma/mod.rs b/pyre/pyre-interpreter/src/module/_lzma/mod.rs index 753835edec1..d1e53a4d243 100644 --- a/pyre/pyre-interpreter/src/module/_lzma/mod.rs +++ b/pyre/pyre-interpreter/src/module/_lzma/mod.rs @@ -17,7 +17,9 @@ use std::sync::Mutex; /// `Compressor`: the liblzma stream and its lock belong to the object, and /// there is no process-global side table. -#[crate::pyre_class("_lzma.LZMACompressor")] +// CPython 3.14 Modules/_lzmamodule.c:lzma_exec creates this spec with +// PyType_FromModuleAndSpec; its spec includes IMMUTABLETYPE. +#[crate::pyre_class("_lzma.LZMACompressor", cpython_heaptype)] #[derive(Default)] pub struct W_LZMACompressor { backend: *mut Mutex, @@ -25,7 +27,8 @@ pub struct W_LZMACompressor { /// `Decompressor`, object-owned the same way. The unconsumed input and the /// `unused_data` tail live with the stream rather than as Python attributes. -#[crate::pyre_class("_lzma.LZMADecompressor")] +// Same `lzma_exec` owner and flags as LZMACompressor. +#[crate::pyre_class("_lzma.LZMADecompressor", cpython_heaptype)] #[derive(Default)] pub struct W_LZMADecompressor { backend: *mut Mutex, diff --git a/pyre/pyre-interpreter/src/module/_overlapped/mod.rs b/pyre/pyre-interpreter/src/module/_overlapped/mod.rs index cef3f006367..e216de89be2 100644 --- a/pyre/pyre-interpreter/src/module/_overlapped/mod.rs +++ b/pyre/pyre-interpreter/src/module/_overlapped/mod.rs @@ -46,7 +46,9 @@ struct NativeOverlapped { // handed to Windows all name fields/buffers owned by the stable Box. unsafe impl Send for NativeOverlapped {} -#[crate::pyre_class("_overlapped.Overlapped")] +// CPython 3.14 Modules/overlapped.c creates overlapped_type_spec through +// PyType_FromModuleAndSpec; its spec is immutable. +#[crate::pyre_class("_overlapped.Overlapped", cpython_heaptype)] #[derive(Default)] pub struct W_Overlapped { backend: *mut Mutex, @@ -903,6 +905,7 @@ pub fn overlapped_type() -> PyObjectRef { crate::typedef::w_object(), ::PYTYPE, ); + crate::typedef::mark_cpython_heap_type(tp, true); pyre_object::pyobject::set_instantiate( unsafe { &*::PYTYPE }, tp, diff --git a/pyre/pyre-interpreter/src/module/_pickle/pickler.rs b/pyre/pyre-interpreter/src/module/_pickle/pickler.rs index dcb7af82718..914869b7292 100644 --- a/pyre/pyre-interpreter/src/module/_pickle/pickler.rs +++ b/pyre/pyre-interpreter/src/module/_pickle/pickler.rs @@ -71,7 +71,9 @@ fn pickler_write_barrier(obj: PyObjectRef) { pyre_object::gc_hook::try_gc_write_barrier(obj as *mut u8); } -#[crate::pyre_class("_pickle.Pickler")] +// CPython 3.14 Modules/_pickle.c:pickle_exec CREATE_TYPEs pickler_type_spec; +// the spec is an immutable heap type. +#[crate::pyre_class("_pickle.Pickler", cpython_heaptype)] pub struct W_Pickler { /// Output file (has a `write` method). w_file: PyObjectRef, @@ -877,7 +879,8 @@ pub use memo_proxy::PicklerMemoProxy; mod memo_proxy { use super::*; - #[crate::pyre_class("_pickle.PicklerMemoProxy")] + // Modules/_pickle.c:pickle_exec CREATE_TYPEs memoproxy_spec too. + #[crate::pyre_class("_pickle.PicklerMemoProxy", cpython_heaptype)] pub struct PicklerMemoProxy { pub(super) w_pickler: PyObjectRef, } diff --git a/pyre/pyre-interpreter/src/module/_pickle/unpickler.rs b/pyre/pyre-interpreter/src/module/_pickle/unpickler.rs index a4149f30f0a..b352bd91d77 100644 --- a/pyre/pyre-interpreter/src/module/_pickle/unpickler.rs +++ b/pyre/pyre-interpreter/src/module/_pickle/unpickler.rs @@ -56,7 +56,9 @@ impl Drop for RunningGuard { } } -#[crate::pyre_class("_pickle.Unpickler")] +// CPython 3.14 Modules/_pickle.c:pickle_exec CREATE_TYPEs +// unpickler_type_spec; the spec is an immutable heap type. +#[crate::pyre_class("_pickle.Unpickler", cpython_heaptype)] pub struct W_Unpickler { w_file_read: PyObjectRef, w_file_readline: PyObjectRef, @@ -588,7 +590,8 @@ pub use memo_proxy::UnpicklerMemoProxy; mod memo_proxy { use super::*; - #[crate::pyre_class("_pickle.UnpicklerMemoProxy")] + // Modules/_pickle.c:pickle_exec CREATE_TYPEs unpickler_memoproxy_spec too. + #[crate::pyre_class("_pickle.UnpicklerMemoProxy", cpython_heaptype)] pub struct UnpicklerMemoProxy { pub(super) w_unpickler: PyObjectRef, } diff --git a/pyre/pyre-interpreter/src/module/_queue/mod.rs b/pyre/pyre-interpreter/src/module/_queue/mod.rs index bf371c64303..fb00cf9a16c 100644 --- a/pyre/pyre-interpreter/src/module/_queue/mod.rs +++ b/pyre/pyre-interpreter/src/module/_queue/mod.rs @@ -5,7 +5,9 @@ use std::collections::VecDeque; use std::sync::{Condvar, Mutex, MutexGuard}; use std::time::{Duration, Instant}; -#[crate::pyre_class("_queue.SimpleQueue")] +// CPython 3.14 Modules/_queuemodule.c:_queue_exec uses +// PyType_FromModuleAndSpec with IMMUTABLETYPE. +#[crate::pyre_class("_queue.SimpleQueue", cpython_heaptype)] #[derive(Default)] pub struct W_SimpleQueue { pub map: *const u8, diff --git a/pyre/pyre-interpreter/src/module/_random/mod.rs b/pyre/pyre-interpreter/src/module/_random/mod.rs index c76bc9dccdb..ea2b09e75ea 100644 --- a/pyre/pyre-interpreter/src/module/_random/mod.rs +++ b/pyre/pyre-interpreter/src/module/_random/mod.rs @@ -131,7 +131,9 @@ impl Random { } } -#[crate::pyre_class("_random.Random")] +// CPython 3.14 Modules/_randommodule.c:_random_exec uses +// PyType_FromModuleAndSpec and does not request IMMUTABLETYPE. +#[crate::pyre_class("_random.Random", cpython_mutable)] #[derive(Default)] pub struct W_Random { /// PyPy composes `MapdictStorageMixin` into a native-layout object when diff --git a/pyre/pyre-interpreter/src/module/_socket/interp_socket.rs b/pyre/pyre-interpreter/src/module/_socket/interp_socket.rs index e3f806a6e21..8cf9b826ff7 100644 --- a/pyre/pyre-interpreter/src/module/_socket/interp_socket.rs +++ b/pyre/pyre-interpreter/src/module/_socket/interp_socket.rs @@ -2322,6 +2322,9 @@ fn socket_type() -> pyre_object::PyObjectRef { static SOCKET_TYPE_OBJ: std::sync::OnceLock = std::sync::OnceLock::new(); *SOCKET_TYPE_OBJ.get_or_init(|| { let tp = crate::typedef::make_builtin_type("socket", init_socket_type); + // CPython 3.14 Modules/socketmodule.c:sock_exec creates sock_type from + // a mutable module heap spec. + crate::typedef::mark_cpython_heap_type(tp, false); unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; unsafe { pyre_object::w_type_set_hasuserdel(tp, true) }; tp as usize diff --git a/pyre/pyre-interpreter/src/module/_ssl/mod.rs b/pyre/pyre-interpreter/src/module/_ssl/mod.rs index e5f8ede6845..51ed0ccc408 100644 --- a/pyre/pyre-interpreter/src/module/_ssl/mod.rs +++ b/pyre/pyre-interpreter/src/module/_ssl/mod.rs @@ -17,7 +17,9 @@ const CERT_REQUIRED: i32 = 2; /// The mapdict prefix is required because `ssl.SSLContext` is an app-level /// subclass of this native type. PyPy composes `MapdictStorageMixin` into /// that allocation; pyre preserves the same native prefix. -#[crate::pyre_class("_ssl._SSLContext")] +// CPython 3.14 Modules/_ssl.c:_ssl_exec uses PyType_FromModuleAndSpec; +// the SSLContext spec is immutable. +#[crate::pyre_class("_ssl._SSLContext", cpython_heaptype)] #[derive(Default)] pub struct W_SSLContext { pub map: usize, @@ -33,7 +35,8 @@ pub struct W_SSLContext { /// `ssl.MemoryBIO` is subclassable in CPython, so it uses the same mapdict /// prefix rather than relying on a side table for subclass attributes. -#[crate::pyre_class("_ssl.MemoryBIO")] +// `_ssl_exec` creates the immutable MemoryBIO module heap type. +#[crate::pyre_class("_ssl.MemoryBIO", cpython_heaptype)] #[derive(Default)] pub struct W_MemoryBIO { pub map: usize, @@ -62,7 +65,8 @@ const _: () = assert!( "W_MemoryBIO must keep W_ObjectObject's storage offset" ); -#[crate::pyre_class("_ssl.SSLSession")] +// `_ssl_exec` creates the immutable SSLSession module heap type. +#[crate::pyre_class("_ssl.SSLSession", cpython_heaptype)] #[derive(Default)] pub struct W_SSLSession { pub backend: *mut pyre_native::ssl::NativeSession, @@ -71,7 +75,8 @@ pub struct W_SSLSession { /// One TLS state machine plus its Python-owned transport endpoints. Rustls /// remains opaque in `pyre-native`; these references preserve the same /// per-object ownership shape as PyPy's `W_SSLObject`. -#[crate::pyre_class("_ssl._SSLSocket")] +// `_ssl_exec` creates the immutable SSLSocket module heap type. +#[crate::pyre_class("_ssl._SSLSocket", cpython_heaptype)] #[derive(Default)] pub struct W_SSLSocket { pub backend: *mut pyre_native::ssl::TlsConnection, @@ -88,7 +93,8 @@ pub struct W_SSLSocket { pub requested_session: *mut pyre_native::ssl::NativeSession, } -#[crate::pyre_class("_ssl.Certificate")] +// `_ssl_exec` creates the immutable Certificate module heap type. +#[crate::pyre_class("_ssl.Certificate", cpython_heaptype)] #[derive(Default)] pub struct W_Certificate { pub der: PyObjectRef, diff --git a/pyre/pyre-interpreter/src/module/_tokenize/mod.rs b/pyre/pyre-interpreter/src/module/_tokenize/mod.rs index ec35fc5b84f..55a31950930 100644 --- a/pyre/pyre-interpreter/src/module/_tokenize/mod.rs +++ b/pyre/pyre-interpreter/src/module/_tokenize/mod.rs @@ -72,7 +72,9 @@ enum TokenizerPhase { Done, } -#[crate::pyre_class("_tokenize.TokenizerIter")] +// CPython 3.14 Python/Python-tokenize.c:tokenizemodule_exec uses +// PyType_FromModuleAndSpec; tokenizeriter_spec is immutable. +#[crate::pyre_class("_tokenize.TokenizerIter", cpython_heaptype)] pub struct W_TokenizerIter { readline: PyObjectRef, extra_tokens: bool, diff --git a/pyre/pyre-interpreter/src/module/_types/mod.rs b/pyre/pyre-interpreter/src/module/_types/mod.rs index f1f66cdd4b2..deebfcda128 100644 --- a/pyre/pyre-interpreter/src/module/_types/mod.rs +++ b/pyre/pyre-interpreter/src/module/_types/mod.rs @@ -43,7 +43,10 @@ fn capsule_type() -> PyObjectRef { }), ); }); - unsafe { pyre_object::w_type_set_acceptable_as_base_class(tp, false) }; + unsafe { + pyre_object::w_type_set_disallow_instantiation(tp); + pyre_object::w_type_set_acceptable_as_base_class(tp, false); + } tp as usize }) as PyObjectRef } @@ -174,3 +177,17 @@ pub fn init(ns: PyObjectRef) { crate::typedef::gettypeobject(&crate::function::SLOT_WRAPPER_TYPE), ); } + +#[cfg(test)] +mod tests { + #[test] + fn capsule_type_publishes_null_tp_new() { + crate::typedef::init_typeobjects(); + let capsule_type = super::capsule_type(); + assert!(unsafe { pyre_object::w_type_disallows_instantiation(capsule_type) }); + + let flags = crate::baseobjspace::getattr_str(capsule_type, "__flags__") + .expect("PyCapsule.__flags__ lookup failed"); + assert_ne!(unsafe { pyre_object::w_int_get_value(flags) } & (1 << 7), 0); + } +} diff --git a/pyre/pyre-interpreter/src/module/_typing/_typing_app.py b/pyre/pyre-interpreter/src/module/_typing/_typing_app.py index b19a228f1db..c179ca945ae 100644 --- a/pyre/pyre-interpreter/src/module/_typing/_typing_app.py +++ b/pyre/pyre-interpreter/src/module/_typing/_typing_app.py @@ -27,6 +27,134 @@ def _caller_module(): return None +class _PickleUsingNameMixin: + """PyPy's name-based reducer for runtime typing objects.""" + + __slots__ = () + + def __reduce__(self): + return self.__name__ + + +def _immutable_type_error(cls, name): + return TypeError( + f"cannot set {name!r} attribute of immutable type " + f"'{cls.__module__}.{cls.__name__}'" + ) + + +class _ImmutableTypeMeta(type): + """``Py_TPFLAGS_IMMUTABLETYPE`` for app-level typing types. + + The guard is the metaclass hook, so it answers ``setattr``/``delattr`` but + not an explicit ``type.__setattr__(TypeVar, ...)``, which reaches the base + implementation and still mutates the class. Closing that would need an + immutability bit separate from heaptype, and ``W_TypeObject`` declares + ``flag_heaptype`` in ``_immutable_fields_`` unqualified -- a JIT hint + governs the value, so the split stays PyPy's to make. + """ + + def __new__(mcls, name, bases, namespace): + for base in bases: + if isinstance(base, _ImmutableTypeMeta): + raise TypeError( + f"type '{base.__module__}.{base.__name__}' " + "is not an acceptable base type" + ) + return super().__new__(mcls, name, bases, namespace) + + def __setattr__(cls, name, value): + raise _immutable_type_error(cls, name) + + def __delattr__(cls, name): + # `type_setattro` uses the same immutable-type error for deletion. + raise _immutable_type_error(cls, name) + + +def _readonly_member(slot): + """Expose a private slot as a `Py_READONLY` struct member. + + A slot named for the public attribute installs a writable member + descriptor, which `object.__setattr__` reaches past the `__setattr__` + guard below. A property is a data descriptor, so it answers every path, + and `member_set` phrases both the write and the delete the same way. + """ + + def get(self): + return object.__getattribute__(self, slot) + + def refuse(self, *_value): + raise AttributeError("readonly attribute") + + return property(get, refuse, refuse) + + +class _Immutable: + """PyPy's per-instance readonly-field mixin. + + CPython 3.14 keeps a managed attribute dictionary on TypeVar, ParamSpec, + and TypeVarTuple, but does not expose it as ``__dict__``. Their native + struct members and getsets stay read-only while unrelated user attributes + remain writable. ``None`` means the instance has no user dictionary. + + The readonly guard is ``__setattr__``/``__getattribute__``, so + ``object.__setattr__(t, '__name__', ...)`` and + ``object.__getattribute__(t, '__dict__')`` still reach the real instance + dict. Native per-field storage would close both, but it moves the storage + owner and clears ``hasdict``, which ``W_TypeObject`` also declares in + ``_immutable_fields_``. + """ + + __slots__ = () + + _readonly_attrs = None + _readonly_members = frozenset() + # Slots that carry a member's storage under a private name because the + # public one is a read-only descriptor. The native types have no such + # attribute, so `dir()` must not report it either. + _hidden_slots = frozenset() + + def __getattribute__(self, name): + if name == "__dict__": + qualname = f"{type(self).__module__}.{type(self).__name__}" + raise AttributeError( + f"{qualname!r} object has no attribute '__dict__'" + ) + return object.__getattribute__(self, name) + + def __dir__(self): + hidden = {"__dict__", "__weakref__"} | set(type(self)._hidden_slots) + return [name for name in object.__dir__(self) if name not in hidden] + + def __setattr__(self, name, value): + readonly = type(self)._readonly_attrs + if name in type(self)._readonly_members: + raise AttributeError("readonly attribute") + if readonly is not None and name in readonly: + qualname = f"{type(self).__module__}.{type(self).__name__}" + raise AttributeError( + f"attribute {name!r} of {qualname!r} objects is not writable" + ) + object.__setattr__(self, name, value) + + def __delattr__(self, name): + readonly = type(self)._readonly_attrs + if name in type(self)._readonly_members: + raise AttributeError("readonly attribute") + if readonly is not None and name in readonly: + qualname = f"{type(self).__module__}.{type(self).__name__}" + raise AttributeError( + f"attribute {name!r} of {qualname!r} objects is not writable" + ) + object.__delattr__(self, name) + + def __copy__(self): + return self + + def __deepcopy__(self, memo): + return self + + def _evaluate_typeparam(thunk): # Call a PEP 695 bound / constraints thunk emitted by the compiler. A 3.14 # thunk takes an annotation `format` argument (annotationlib.Format.VALUE @@ -172,41 +300,61 @@ def _variance_prefix(infer_variance, covariant, contravariant): return '~' -class TypeVar: +class TypeVar( + _Immutable, _PickleUsingNameMixin, metaclass=_ImmutableTypeMeta +): """Type variable — PEP 484 / PEP 695.""" + _readonly_members = frozenset(( + "__name__", "__covariant__", "__contravariant__", + "__infer_variance__", + )) + _readonly_attrs = frozenset(( + "__bound__", "__constraints__", "__default__", "evaluate_bound", + "evaluate_constraints", "evaluate_default", "_bound", + "_constraints", "_default_value", "_evaluate_bound", + "_evaluate_constraints", "_evaluate_default", + )) + def __init__(self, name, *constraints, bound=None, default=NoDefault, covariant=False, contravariant=False, infer_variance=False): - self.__name__ = name + if not isinstance(name, str): + raise TypeError( + f"typevar() argument 'name' must be str, not " + f"{type(name).__name__}" + ) + object.__setattr__(self, "__name__", name) if covariant and contravariant: raise ValueError("Bivariant types are not supported.") if infer_variance and (covariant or contravariant): raise ValueError("Variance cannot be specified with infer_variance.") - self.__covariant__ = bool(covariant) - self.__contravariant__ = bool(contravariant) - self.__infer_variance__ = bool(infer_variance) - self._default_value = default - self._evaluate_default = None + object.__setattr__(self, "__covariant__", bool(covariant)) + object.__setattr__(self, "__contravariant__", bool(contravariant)) + object.__setattr__(self, "__infer_variance__", bool(infer_variance)) + object.__setattr__(self, "_default_value", default) + object.__setattr__(self, "_evaluate_default", None) if constraints and bound is not None: raise TypeError("Constraints cannot be combined with bound=...") if len(constraints) == 1: raise TypeError("A single constraint is not allowed") import typing - self._constraints = tuple( + constraints = tuple( typing._type_check( constraint, f"TypeVar(name, constraint, ...). Constraints must be types. Got {constraint!r}.", ) for constraint in constraints ) - self._evaluate_constraints = None - self._bound = ( + object.__setattr__(self, "_constraints", constraints) + object.__setattr__(self, "_evaluate_constraints", None) + bound = ( None if bound is None else typing._type_check(bound, "Bound must be a type.") ) - self._evaluate_bound = None - self.__module__ = _caller_module() + object.__setattr__(self, "_bound", bound) + object.__setattr__(self, "_evaluate_bound", None) + object.__setattr__(self, "__module__", _caller_module()) @classmethod def _make(cls, name, *, evaluate_bound=None, evaluate_constraints=None): @@ -216,36 +364,48 @@ def _make(cls, name, *, evaluate_bound=None, evaluate_constraints=None): # the enclosing scope; they are evaluated on first `__bound__` / # `__constraints__` access and cached (Objects/typevarobject.c). self = cls.__new__(cls) - self.__name__ = name - self.__covariant__ = False - self.__contravariant__ = False + object.__setattr__(self, "__name__", name) + object.__setattr__(self, "__covariant__", False) + object.__setattr__(self, "__contravariant__", False) # CPython 3.14 `_Py_make_typevar`: type parameters created by the # compiler infer variance, unlike an ordinary `TypeVar(...)` call. - self.__infer_variance__ = True + object.__setattr__(self, "__infer_variance__", True) # `_Py_make_typevar` leaves `default_value` NULL, which `_MISSING` # stands for. A `TypeVar(...)` call instead stores the `NoDefault` # sentinel its signature defaults to, and the two states differ: # `evaluate_default` answers `None` for the first and a constant # evaluator over `NoDefault` for the second. - self._default_value = _MISSING - self._evaluate_default = None - self._constraints = _MISSING if evaluate_constraints is not None else () - self._evaluate_constraints = evaluate_constraints - self._bound = _MISSING if evaluate_bound is not None else None - self._evaluate_bound = evaluate_bound - self.__module__ = _caller_module() + object.__setattr__(self, "_default_value", _MISSING) + object.__setattr__(self, "_evaluate_default", None) + object.__setattr__( + self, "_constraints", + _MISSING if evaluate_constraints is not None else (), + ) + object.__setattr__(self, "_evaluate_constraints", evaluate_constraints) + object.__setattr__( + self, "_bound", _MISSING if evaluate_bound is not None else None + ) + object.__setattr__(self, "_evaluate_bound", evaluate_bound) + # `_Py_make_typevar` passes a NULL module, so the compiler-created + # parameter never gets an instance `__module__` and reports the class + # attribute instead. return self @property def __bound__(self): if self._bound is _MISSING: - self._bound = _evaluate_typeparam(self._evaluate_bound) + object.__setattr__( + self, "_bound", _evaluate_typeparam(self._evaluate_bound) + ) return self._bound @property def __constraints__(self): if self._constraints is _MISSING: - self._constraints = tuple(_evaluate_typeparam(self._evaluate_constraints)) + object.__setattr__( + self, "_constraints", + tuple(_evaluate_typeparam(self._evaluate_constraints)), + ) return self._constraints @property @@ -253,7 +413,10 @@ def __default__(self): if self._default_value is _MISSING: if self._evaluate_default is None: return NoDefault - self._default_value = _evaluate_typeparam(self._evaluate_default) + object.__setattr__( + self, "_default_value", + _evaluate_typeparam(self._evaluate_default), + ) return self._default_value @property @@ -302,9 +465,6 @@ def has_default(self): and self._default_value is not NoDefault ) - def __reduce__(self): - return self.__name__ - def __mro_entries__(self, bases): raise TypeError("Cannot subclass an instance of TypeVar") @@ -324,37 +484,59 @@ def __init_subclass__(cls, **kwargs): raise TypeError("type 'typing.TypeVar' is not an acceptable base type") -class ParamSpec: +class ParamSpec( + _Immutable, _PickleUsingNameMixin, metaclass=_ImmutableTypeMeta +): """Parameter specification variable — PEP 612.""" + _readonly_members = frozenset(( + "__name__", "__bound__", "__covariant__", "__contravariant__", + "__infer_variance__", + )) + _readonly_attrs = frozenset(( + "args", "kwargs", "__default__", "evaluate_default", + "_default_value", "_evaluate_default", + )) + def __init__(self, name, *, bound=None, default=NoDefault, covariant=False, contravariant=False, infer_variance=False): - self.__name__ = name + if not isinstance(name, str): + raise TypeError( + f"paramspec() argument 'name' must be str, not " + f"{type(name).__name__}" + ) + object.__setattr__(self, "__name__", name) if covariant and contravariant: raise ValueError("Bivariant types are not supported.") if infer_variance and (covariant or contravariant): raise ValueError("Variance cannot be specified with infer_variance.") - self.__covariant__ = bool(covariant) - self.__contravariant__ = bool(contravariant) - self.__infer_variance__ = bool(infer_variance) - self._default_value = default - self._evaluate_default = None - self.__bound__ = bound - self.__module__ = _caller_module() + object.__setattr__(self, "__covariant__", bool(covariant)) + object.__setattr__(self, "__contravariant__", bool(contravariant)) + object.__setattr__(self, "__infer_variance__", bool(infer_variance)) + object.__setattr__(self, "_default_value", default) + object.__setattr__(self, "_evaluate_default", None) + # `paramspec_new_impl` has no `None` shortcut: `bound` defaults to + # `None` and reaches `type_check` either way, so an omitted bound is + # `type(None)` here, unlike `TypeVar`, which drops `None` first. + import typing + bound = typing._type_check(bound, "Bound must be a type.") + object.__setattr__(self, "__bound__", bound) + object.__setattr__(self, "__module__", _caller_module()) @classmethod def _make(cls, name): # `_Py_make_paramspec`: the compiler-created parameter infers variance # and leaves `default_value` NULL, which `_MISSING` stands for. self = cls.__new__(cls) - self.__name__ = name - self.__covariant__ = False - self.__contravariant__ = False - self.__infer_variance__ = True - self._default_value = _MISSING - self._evaluate_default = None - self.__bound__ = None - self.__module__ = _caller_module() + object.__setattr__(self, "__name__", name) + object.__setattr__(self, "__covariant__", False) + object.__setattr__(self, "__contravariant__", False) + object.__setattr__(self, "__infer_variance__", True) + object.__setattr__(self, "_default_value", _MISSING) + object.__setattr__(self, "_evaluate_default", None) + object.__setattr__(self, "__bound__", None) + # `paramspec_alloc` is handed a NULL module here, so `__module__` stays + # the class attribute. return self @property @@ -384,7 +566,10 @@ def __default__(self): if self._default_value is _MISSING: if self._evaluate_default is None: return NoDefault - self._default_value = _evaluate_typeparam(self._evaluate_default) + object.__setattr__( + self, "_default_value", + _evaluate_typeparam(self._evaluate_default), + ) return self._default_value @property @@ -395,9 +580,6 @@ def evaluate_default(self): return None return _const_evaluator(self._default_value) - def __reduce__(self): - return self.__name__ - def __mro_entries__(self, bases): raise TypeError("Cannot subclass an instance of ParamSpec") @@ -415,22 +597,37 @@ def __repr__(self): self.__contravariant__) + self.__name__ -class ParamSpecArgs: +class ParamSpecArgs(_Immutable, metaclass=_ImmutableTypeMeta): """The args of a ParamSpec, e.g. P.args.""" + # `paramspecargs` stores the origin in a `Py_READONLY` member, so a slot + # named `__origin__` would be writable through `object.__setattr__` where + # the native view is not. The storage takes a private name and the public + # one is a read-only property, which is a data descriptor and so refuses + # the write on both paths. + __slots__ = ("_origin", "__weakref__") + _readonly_members = frozenset(("__origin__",)) + _hidden_slots = frozenset(("_origin",)) + def __init__(self, origin): - self.__origin__ = origin + object.__setattr__(self, "_origin", origin) + + __origin__ = _readonly_member("_origin") def __repr__(self): - return f"{self.__origin__.__name__}.args" + if type(self.__origin__) is ParamSpec: + return f"{self.__origin__.__name__}.args" + return f"{self.__origin__!r}.args" def __eq__(self, other): - if not isinstance(other, ParamSpecArgs): + if type(other) is not type(self): return NotImplemented return self.__origin__ == other.__origin__ - def __hash__(self): - return hash((self.__origin__, "args")) + # `P.args` builds a fresh object per access while `__eq__` compares by + # origin, so an identity hash would give equal objects different hashes. + # The native `paramspecargs` supplies `tp_richcompare` and no `tp_hash`, + # which is the unhashable that leaving `__hash__` alone reproduces here. def __mro_entries__(self, bases): raise TypeError("Cannot subclass an instance of ParamSpecArgs") @@ -439,22 +636,31 @@ def __init_subclass__(cls, **kwargs): raise TypeError("type 'typing.ParamSpecArgs' is not an acceptable base type") -class ParamSpecKwargs: +class ParamSpecKwargs(_Immutable, metaclass=_ImmutableTypeMeta): """The kwargs of a ParamSpec, e.g. P.kwargs.""" + # Read-only through a private slot, for the reason given on + # `ParamSpecArgs`. + __slots__ = ("_origin", "__weakref__") + _readonly_members = frozenset(("__origin__",)) + _hidden_slots = frozenset(("_origin",)) + def __init__(self, origin): - self.__origin__ = origin + object.__setattr__(self, "_origin", origin) + + __origin__ = _readonly_member("_origin") def __repr__(self): - return f"{self.__origin__.__name__}.kwargs" + if type(self.__origin__) is ParamSpec: + return f"{self.__origin__.__name__}.kwargs" + return f"{self.__origin__!r}.kwargs" def __eq__(self, other): - if not isinstance(other, ParamSpecKwargs): + if type(other) is not type(self): return NotImplemented return self.__origin__ == other.__origin__ - def __hash__(self): - return hash((self.__origin__, "kwargs")) + # Unhashable for the reason given on `ParamSpecArgs`. def __mro_entries__(self, bases): raise TypeError("Cannot subclass an instance of ParamSpecKwargs") @@ -463,24 +669,38 @@ def __init_subclass__(cls, **kwargs): raise TypeError("type 'typing.ParamSpecKwargs' is not an acceptable base type") -class TypeVarTuple: +class TypeVarTuple( + _Immutable, _PickleUsingNameMixin, metaclass=_ImmutableTypeMeta +): """Type variable tuple — PEP 646.""" + _readonly_members = frozenset(("__name__",)) + _readonly_attrs = frozenset(( + "__default__", "evaluate_default", "_default_value", + "_evaluate_default", + )) + def __init__(self, name, *, default=NoDefault): - self.__name__ = name - self._default_value = default - self._evaluate_default = None - self.__module__ = _caller_module() + if not isinstance(name, str): + raise TypeError( + f"typevartuple() argument 'name' must be str, not " + f"{type(name).__name__}" + ) + object.__setattr__(self, "__name__", name) + object.__setattr__(self, "_default_value", default) + object.__setattr__(self, "_evaluate_default", None) + object.__setattr__(self, "__module__", _caller_module()) @classmethod def _make(cls, name): # `_Py_make_typevartuple` leaves `default_value` NULL, which `_MISSING` # stands for. self = cls.__new__(cls) - self.__name__ = name - self._default_value = _MISSING - self._evaluate_default = None - self.__module__ = _caller_module() + object.__setattr__(self, "__name__", name) + object.__setattr__(self, "_default_value", _MISSING) + object.__setattr__(self, "_evaluate_default", None) + # `typevartuple_alloc` takes no module here, so `__module__` stays the + # class attribute. return self def __iter__(self): @@ -505,7 +725,10 @@ def __default__(self): if self._default_value is _MISSING: if self._evaluate_default is None: return NoDefault - self._default_value = _evaluate_typeparam(self._evaluate_default) + object.__setattr__( + self, "_default_value", + _evaluate_typeparam(self._evaluate_default), + ) return self._default_value @property @@ -516,9 +739,6 @@ def evaluate_default(self): return None return _const_evaluator(self._default_value) - def __reduce__(self): - return self.__name__ - def __mro_entries__(self, bases): raise TypeError("Cannot subclass an instance of TypeVarTuple") @@ -529,20 +749,108 @@ def __init_subclass__(cls, **kwargs): raise TypeError("type 'typing.TypeVarTuple' is not an acceptable base type") -class TypeAliasType: +class TypeAliasType(_PickleUsingNameMixin, metaclass=_ImmutableTypeMeta): """A PEP 695 ``type X = ...`` alias.""" - def __init__(self, name, value, *, type_params=(), _evaluate_value=None): - self.__name__ = name - self._value = value - self._evaluate_value = _evaluate_value - self.__type_params__ = tuple(type_params) - self.__module__ = _caller_module() + __slots__ = ("_name", "_type_params", "_value", "_evaluate_value", "_module") + + def __init__(self, name, value, *, type_params=()): + if not isinstance(name, str): + raise TypeError( + f"typealias() argument 'name' must be str, not " + f"{type(name).__name__}" + ) + if not isinstance(type_params, tuple): + raise TypeError("type_params must be a tuple") + self._check_type_params(type_params) + object.__setattr__(self, "_name", name) + object.__setattr__(self, "_type_params", type_params) + object.__setattr__(self, "_value", value) + object.__setattr__(self, "_evaluate_value", None) + object.__setattr__(self, "_module", _caller_module()) + + @staticmethod + def _check_type_params(type_params): + default_seen = False + for param in type_params: + if not ( + type(param) is TypeVar + or type(param) is ParamSpec + or type(param) is TypeVarTuple + ): + raise TypeError(f"Expected a type param, got {param!r}") + default = param.__default__ + if default is NoDefault: + if default_seen: + raise TypeError( + f"non-default type parameter '{param!r}' " + "follows default type parameter" + ) + else: + default_seen = True + + @classmethod + def _from_evaluator(cls, name, type_params, evaluate_value): + # PyPy's `_make_typealiastype` allocates first and installs the lazy + # evaluator with `object.__setattr__`. Keep that shape so the public + # constructor does not grow a CPython-incompatible private keyword. + self = object.__new__(cls) + # CPython `_Py_make_typealias` trusts the compiler-provided tuple and + # deliberately skips `typealias_check_type_params`: checking here + # would force lazy defaults before the alias is even constructed. + object.__setattr__(self, "_name", name) + object.__setattr__(self, "_type_params", type_params) + object.__setattr__(self, "_value", _MISSING) + object.__setattr__(self, "_evaluate_value", evaluate_value) + # `_Py_make_typealias` stores no module. The `__module__` getter + # derives it from the evaluator function when first observed. + object.__setattr__(self, "_module", _MISSING) + return self + + def __getattribute__(self, name): + if name == "__module__": + module = object.__getattribute__(self, "_module") + if module is not _MISSING: + return module + evaluate_value = object.__getattribute__(self, "_evaluate_value") + return getattr(evaluate_value, "__module__", None) + return object.__getattribute__(self, name) + + def __setattr__(self, name, value): + if name == "__name__": + raise AttributeError("readonly attribute") + if name in { + "__module__", "__parameters__", "__type_params__", "__value__", + "evaluate_value", + }: + raise AttributeError( + f"attribute {name!r} of 'typing.TypeAliasType' objects " + "is not writable" + ) + raise AttributeError( + f"'typing.TypeAliasType' object has no attribute {name!r} " + "and no __dict__ for setting new attributes" + ) + + def __delattr__(self, name): + # The native object uses the same read-only/no-dict paths for writes + # and deletions. + self.__setattr__(name, None) + + @property + def __name__(self): + return self._name + + @property + def __type_params__(self): + return self._type_params @property def __value__(self): if self._value is _MISSING: - self._value = _evaluate_typeparam(self._evaluate_value) + object.__setattr__( + self, "_value", _evaluate_typeparam(self._evaluate_value) + ) return self._value @property @@ -553,7 +861,25 @@ def evaluate_value(self): @property def __parameters__(self): - return self.__type_params__ + if not self._type_params: + return () + if not any(type(param) is TypeVarTuple for param in self._type_params): + # CPython's `unpack_typevartuples` returns the original tuple when + # there is nothing to unpack. + return self._type_params + result = [] + for param in self._type_params: + if type(param) is TypeVarTuple: + # PyPy spells this `result.extend(param)`: TypeVarTuple's + # one-item iterator produces typing.Unpack[param]. + result.extend(param) + else: + result.append(param) + return tuple(result) + + def __iter__(self): + import typing + yield typing.Unpack[self] def __getitem__(self, args): if not self.__type_params__: @@ -615,8 +941,8 @@ def _intrinsic_typevar_with_constraints(name, evaluate_constraints): def _intrinsic_set_typeparam_default(typeparam, default): # CPython 3.14 `_Py_set_typeparam_default` stores the evaluator, not its # result. `__default__` evaluates and caches it on first access. - typeparam._default_value = _MISSING - typeparam._evaluate_default = default + object.__setattr__(typeparam, "_default_value", _MISSING) + object.__setattr__(typeparam, "_evaluate_default", default) return typeparam @@ -633,14 +959,4 @@ def _intrinsic_typealias(args): name, type_params, value = args if type_params is None: type_params = () - alias = TypeAliasType( - name, _MISSING, type_params=type_params, _evaluate_value=value - ) - # The constructor is called through this intrinsic helper, so its ordinary - # two-frame caller lookup sees the helper's private ``typing`` globals. - # CPython records the namespace executing the TYPEALIAS intrinsic instead. - try: - alias.__module__ = sys._getframe(1).f_globals.get('__name__') - except (AttributeError, ValueError): - alias.__module__ = None - return alias + return TypeAliasType._from_evaluator(name, type_params, value) diff --git a/pyre/pyre-interpreter/src/module/_winapi/overlapped.rs b/pyre/pyre-interpreter/src/module/_winapi/overlapped.rs index 9a288082e23..ede7112b4d2 100644 --- a/pyre/pyre-interpreter/src/module/_winapi/overlapped.rs +++ b/pyre/pyre-interpreter/src/module/_winapi/overlapped.rs @@ -68,7 +68,9 @@ impl Drop for NativeOverlapped { } } -#[crate::pyre_class("_winapi.Overlapped")] +// CPython 3.14 Modules/_winapi.c creates winapi_overlapped_type_spec through +// PyType_FromModuleAndSpec; its spec is immutable. +#[crate::pyre_class("_winapi.Overlapped", cpython_heaptype)] #[derive(Default)] pub struct W_Overlapped { backend: *mut Mutex, @@ -284,6 +286,7 @@ pub fn overlapped_type() -> PyObjectRef { crate::typedef::w_object(), ::PYTYPE, ); + crate::typedef::mark_cpython_heap_type(tp, true); pyre_object::pyobject::set_instantiate( unsafe { &*::PYTYPE }, tp, diff --git a/pyre/pyre-interpreter/src/module/binascii/mod.rs b/pyre/pyre-interpreter/src/module/binascii/mod.rs index 9816256e1e3..dc0a50f111c 100644 --- a/pyre/pyre-interpreter/src/module/binascii/mod.rs +++ b/pyre/pyre-interpreter/src/module/binascii/mod.rs @@ -66,8 +66,15 @@ fn as_bytes(obj: PyObjectRef) -> Result, crate::PyError> { } Ok(s.as_bytes().to_vec()) } else { - crate::typedef::require_contiguous_buffer(obj)?; - match crate::typedef::buffer_as_bytes_like(obj)? { + // `ascii_buffer_converter` replaces every buffer-acquisition + // failure with the type error below, so a released or + // non-contiguous memoryview names its own type rather than + // surfacing the ValueError or BufferError the acquisition + // raised. Only the `b2a_*` converter keeps those. + let acquired = crate::typedef::require_contiguous_buffer(obj) + .and_then(|()| crate::typedef::buffer_as_bytes_like(obj)) + .unwrap_or(None); + match acquired { Some(src) => Ok(bytesobject::bytes_like_data(src).to_vec()), None => Err(crate::PyError::type_error(format!( "argument should be bytes, buffer or ASCII string, not '{}'", diff --git a/pyre/pyre-interpreter/src/module/gc/mod.rs b/pyre/pyre-interpreter/src/module/gc/mod.rs index e105ee61fb4..2796d232ecf 100644 --- a/pyre/pyre-interpreter/src/module/gc/mod.rs +++ b/pyre/pyre-interpreter/src/module/gc/mod.rs @@ -664,10 +664,108 @@ fn pin_object(object: majit_ir::GcRef) { pyre_object::gc_roots::pin_root(object.0 as PyObjectRef); } +/// `[3.14-spec]` PyPy's `referents.py:115-122` exposes every app-level +/// object reached by `rgc.do_get_objects`, while CPython 3.14 +/// `Modules/gcmodule.c:319-342` exposes only objects tracked by its cyclic +/// collector. Keep the PyPy/RPython traversal in `majit_gc`; filter only at +/// the public CPython-compatible boundary, using the same tracked-state model +/// as `gc.is_tracked` below. +fn pin_cpython_tracked_object(object: majit_ir::GcRef) { + let w_obj = object.0 as PyObjectRef; + if crate::typedef::cpython_object_is_gc(w_obj) { + pyre_object::gc_roots::pin_root(w_obj); + } +} + +/// CPython's container traversal sees logical entries even where a PyPy list +/// strategy, dict strategy, or specialised tuple stores an unboxed scalar. The +/// collector walk correctly has no GC edge to report for those fields, so +/// materialise only the missing logical half at the public API boundary. +/// Object-strategy entries remain the collector's responsibility: rebuilding +/// all of them here would both duplicate results and lose the identity of +/// direct referents. +fn pin_unboxed_container_referents(source_slot: usize) { + let w_obj = pyre_object::gc_roots::shadow_stack_get(source_slot); + if w_obj.is_null() + || (pyre_object::tagged_int::CAN_BE_TAGGED && tagged_int::is_tagged_int(w_obj)) + { + return; + } + unsafe { + if std::ptr::eq((*w_obj).ob_type, &LIST_TYPE) { + let list = &*(w_obj as *const listobject::W_ListObject); + if matches!( + list.strategy, + listobject::ListStrategy::Empty | listobject::ListStrategy::Object + ) { + return; + } + let len = listobject::w_list_len(w_obj); + for index in 0..len { + let list = pyre_object::gc_roots::shadow_stack_get(source_slot); + if let Some(item) = listobject::w_list_getitem(list, index as i64) { + pyre_object::gc_roots::pin_root(item); + } + } + } else if std::ptr::eq((*w_obj).ob_type, &DICT_TYPE) { + let kind = dictmultiobject::w_dict_get_strategy(w_obj).strategy_kind(); + if !matches!( + kind, + dictmultiobject::StrategyKind::Int | dictmultiobject::StrategyKind::Bytes + ) { + return; + } + let len = dictmultiobject::w_dict_len(w_obj); + for index in 0..len { + let dict = pyre_object::gc_roots::shadow_stack_get(source_slot); + if let Some((key, _)) = dictmultiobject::w_dict_nth_item(dict, index) { + // The typed strategy's GC walker already reported the + // boxed value; only its native i64/Vec key was absent. + pyre_object::gc_roots::pin_root(key); + } + } + } else if is_specialised_tuple_ii(w_obj) || is_specialised_tuple_ff(w_obj) { + // Both items live in inline i64/f64 fields, so these two variants + // carry no GC-pointer slot at all and the walker reports an empty + // tuple. `w_tuple_getitem` re-wraps them the same way + // `specialisedtupleobject.py:138-141 wraps[i](self.space, value)` + // does. `Cls_oo` stores both items as GC pointers and stays the + // collector's. + for index in 0..2 { + let tuple = pyre_object::gc_roots::shadow_stack_get(source_slot); + if let Some(item) = tupleobject::w_tuple_getitem(tuple, index) { + pyre_object::gc_roots::pin_root(item); + } + } + } + } +} + +/// Remove one temporary root while retaining every result appended after it. +/// Shadow-stack slots, rather than copied addresses, are moved so a collection +/// during scalar materialisation cannot leave a stale result behind. +fn remove_root_slot_preserving_tail(slot: usize) { + let end = pyre_object::gc_roots::shadow_stack_len(); + // Every caller pushes the root it names, so `slot < end` holds. Keep it a + // runtime check anyway: a release build would otherwise underflow `end - 1` + // below and truncate the stack to a nonsense length. + if slot >= end { + return; + } + for index in slot + 1..end { + let value = pyre_object::gc_roots::shadow_stack_get(index); + pyre_object::gc_roots::shadow_stack_set(index - 1, value); + } + pyre_object::gc_roots::shadow_stack_cell_truncate( + pyre_object::gc_roots::shadow_stack_cell(), + end - 1, + ); +} + /// `referents.py _list_w_obj_referents`: push the app-level objects -/// `w_obj` refers to directly onto the shadow stack. The walk looks through -/// the interpreter-internal structs in between, so a list reports its items -/// and not the array holding them. +/// `w_obj` refers to directly onto the shadow stack. The collector walk looks +/// through interpreter-internal structs; the CPython-facing supplement then +/// restores logical entries hidden by PyPy's unboxed strategies. /// /// Only managed-heap referents are reported, the same boundary `gc.get_objects` /// and `gc.is_tracked` draw. An immortal referent carries a GC header but sits @@ -675,7 +773,12 @@ fn pin_object(object: majit_ir::GcRef) { /// static that has no header at all, so there is no address the walk could /// safely widen to. fn pin_referents(w_obj: PyObjectRef) { - majit_gc::get_referents(majit_ir::GcRef(w_obj as usize), pin_object); + let source_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(w_obj); + let source = pyre_object::gc_roots::shadow_stack_get(source_slot); + majit_gc::get_referents(majit_ir::GcRef(source as usize), pin_object); + pin_unboxed_container_referents(source_slot); + remove_root_slot_preserving_tail(source_slot); } /// Wrap every raw collector node rooted in `[first, last)` as @@ -1209,10 +1312,10 @@ crate::py_module! { fn collect( #[default(w_int_new(0))] generation: PyObjectRef, ) -> Result { - // PyPy `interp_gc.py collect` unwraps the optional generation + // `interp_gc.py collect` unwraps the optional generation // as an int, but deliberately ignores its value. In particular, - // unlike CPython's three-generation frontend, every integer is - // accepted and the default is 0. + // unlike a three-generation frontend, every integer is accepted + // and the default is 0. let _generation = crate::baseobjspace::int_w( crate::baseobjspace::space_index(generation)?, )?; @@ -1220,14 +1323,10 @@ crate::py_module! { crate::objspace::std::mapdict::clear_map_attr_cache(); pyre_object::gc_hook::try_gc_collect(); run_finalizers_now(); - // The generation is bound and ignored, but the return value is a - // spec axis, and there the caller-observable answer is an int - // engineered the way upstream would. `interp_gc.py:48` still - // carries the `return space.newint(0)` that closed `collect` until - // `_run_finalizers` was split out of it (`b5632df5b6e0`) and - // neither caller of the helper reads its result: the constant is - // upstream's own answer for a collector that never counts - // unreachable objects. + // The return value is the caller-observable axis and is an int. + // A collector that never counts unreachable objects has no count + // to report, so the constant `interp_gc.py:48` carries is what it + // answers. `extra_tests/snippets/stdlib_gc.py` pins the type. Ok(w_int_new(0)) } @@ -1252,8 +1351,11 @@ crate::py_module! { fn get_objects( #[default(w_none())] generation: PyObjectRef, ) -> Result { - // PyPy `referents.py:112-123`: the audit event precedes argument - // validation and always carries -1, even for a rejected argument. + // `referents.py:116-126 get_objects`: "Return a list of all + // app-level objects." The audit event always reports -1 and fires + // before the argument is examined, so a non-None value is still + // audited; only None is then accepted, because the collector has + // no generations to select between. let _generation_root = pyre_object::gc_roots::push_roots(); let generation_slot = pyre_object::gc_roots::shadow_stack_len(); pyre_object::gc_roots::pin_root(generation); @@ -1266,7 +1368,7 @@ crate::py_module! { } let _roots = pyre_object::gc_roots::push_roots(); let first = pyre_object::gc_roots::shadow_stack_len(); - majit_gc::get_objects(-1, pin_object); + majit_gc::get_objects(-1, pin_cpython_tracked_object); Ok(list_from_roots(first)) } @@ -1352,7 +1454,7 @@ crate::py_module! { pyre_object::gc_roots::shadow_stack_copy_range(args_base, &mut rooted_args); crate::module::sys::vm::audit("gc.get_referrers", &rooted_args)?; let all_first = pyre_object::gc_roots::shadow_stack_len(); - majit_gc::get_objects(-1, pin_object); + majit_gc::get_objects(-1, pin_cpython_tracked_object); let all_last = pyre_object::gc_roots::shadow_stack_len(); // Accumulate the matches as slot indices, not as addresses: the // entries stay pinned in `all_first..all_last`, but a copy of one @@ -1470,17 +1572,14 @@ crate::py_module! { Ok(w_none()) }, "is_tracked" / 1 = |args| { - // CPython 3.14 `gc.is_tracked(obj)`: whether the collector - // traverses references out of the object. Asked of the registered - // type rather than of the heap the instance landed in, so an int - // answers the same under `PYRE_GC_INTERP`, under the JIT and on - // wasm as it does on the immortal path. - // - // `bytes` and `int` answer True where CPython answers False: their - // pyre structs carry `w_dict` and `w_weakreflifeline` slots that - // the collector really does follow, which the CPython objects have - // no equivalent of. - Ok(w_bool_from(majit_gc::is_tracked(majit_ir::GcRef(args[0] as usize)))) + // CPython 3.14 `PyObject_GC_IsTracked` first requires + // `_PyObject_IS_GC`. Pyre's collector does not dynamically + // untrack eligible tuples/dicts, so that type-level eligibility + // is also its tracked state. Some eligible objects (notably + // modules) are rooted outside the moving arena, while scalar Rust + // structs may live inside it; arena membership therefore cannot + // be used as the app-level answer. + Ok(w_bool_from(crate::typedef::cpython_object_is_gc(args[0]))) }, "get_rpy_memory_usage" / 1 = |args| { // referents.py:97-104 / inspector.py:76-77. The size is just the diff --git a/pyre/pyre-interpreter/src/module/marshal/mod.rs b/pyre/pyre-interpreter/src/module/marshal/mod.rs index 73a42fb21e4..a3284f9ba7e 100644 --- a/pyre/pyre-interpreter/src/module/marshal/mod.rs +++ b/pyre/pyre-interpreter/src/module/marshal/mod.rs @@ -240,7 +240,7 @@ unsafe fn write_code( out.write_u32(code.max_stackdepth); out.write_u32(code.flags.bits()); - write_bytes(out, &code.instructions.original_bytes())?; + write_bytes(out, &unsafe { crate::pycode::code_bytes(code_root.get()) })?; out.write_u8(b'('); write_len(out, code.constants.len())?; @@ -647,6 +647,37 @@ impl PyreMarshalBag { self.errors.remember(error); wire::MarshalError::BadType } + + /// Room for a container the decoder publishes before it reads what goes + /// in it. RustPython's runtime bag asks for this room explicitly because + /// the length comes from untrusted marshal input; mirror that shape so an + /// impossible reservation becomes MemoryError rather than an allocator + /// abort. + fn placeholder_elements(&self, len: usize) -> Result, wire::MarshalError> { + let mut elements = Vec::new(); + elements + .try_reserve_exact(len) + .map_err(|_| self.remember_python_error(PyError::memory_error("")))?; + elements.resize(len, PY_NULL); + Ok(elements) + } + + fn make_runtime_code( + &self, + code: CodeObject, + constants: Vec, + raw_code_bytes: Option>, + ) -> Result { + let code = Rooted::new(crate::pycode::box_code_constant(&code)); + // `box_code_constant` allocates, so read each constant out of its + // shadow-stack slot only now. PyPy gives the complete decoded wrapped + // list to `PyCode.__init__`; replace the compiler-boundary eager values + // with those exact marshal objects. + let constants: Vec<_> = constants.into_iter().map(Rooted::get).collect(); + unsafe { crate::pycode::w_code_fill_wrapped_consts(code.get(), &constants) }; + unsafe { crate::pycode::set_co_code_bytes(code.get(), raw_code_bytes) }; + Ok(code) + } } impl wire::MarshalBag for PyreMarshalBag { @@ -706,8 +737,9 @@ impl wire::MarshalBag for PyreMarshalBag { } fn make_tuple_placeholder(&self, len: usize) -> Result, wire::MarshalError> { + let elements = self.placeholder_elements(len)?; Ok(Some(Rooted::new(tupleobject::w_tuple_new_array_backed( - vec![PY_NULL; len], + elements, )))) } @@ -723,7 +755,7 @@ impl wire::MarshalBag for PyreMarshalBag { } fn make_code(&self, code: CodeObject) -> Result { - Ok(Rooted::new(crate::pycode::box_code_object(code))) + Ok(Rooted::new(crate::pycode::box_code_constant(&code))) } fn make_stop_iter(&self) -> Result { @@ -740,10 +772,8 @@ impl wire::MarshalBag for PyreMarshalBag { } fn make_list_placeholder(&self, len: usize) -> Result, wire::MarshalError> { - Ok(Some(Rooted::new(listobject::w_list_new_object(vec![ - PY_NULL; - len - ])))) + let elements = self.placeholder_elements(len)?; + Ok(Some(Rooted::new(listobject::w_list_new_object(elements)))) } fn set_list_item( @@ -878,14 +908,34 @@ impl wire::MarshalBag for PyreMarshalBag { code: CodeObject, constants: Vec, ) -> Result { - let code = Rooted::new(crate::pycode::box_code_object(code)); - // `box_code_object` allocates, so read each constant out of its - // shadow-stack slot only now. Only the slots the compiler - // representation cannot reproduce are stored; the rest realize - // lazily like a fresh compile's. - let constants: Vec<_> = constants.into_iter().map(Rooted::get).collect(); - unsafe { crate::pycode::w_code_fill_wrapped_consts(code.get(), &constants) }; - Ok(code) + self.make_runtime_code(code, constants, None) + } + + fn code_units_from_bytes( + &self, + code_bytes: &[u8], + ) -> Result { + crate::pycode::decode_code_units(code_bytes) + .map(|(instructions, _)| instructions) + .map_err(|()| wire::MarshalError::InvalidBytecode) + } + + fn make_code_with_constants_and_bytes( + &self, + code: CodeObject, + constants: Vec, + code_bytes: Vec, + ) -> Result { + // `code_units_from_bytes` already substituted the `Reserved` + // placeholder for every opcode byte `CodeUnit` cannot spell, so the + // decoded stream itself says whether the exact public bytes still have + // to be carried; decoding them a second time would only rediscover it. + let raw_code_bytes = code + .instructions + .iter() + .any(|unit| matches!(unit.op, crate::bytecode::Instruction::Reserved)) + .then_some(code_bytes); + self.make_runtime_code(code, constants, raw_code_bytes) } // `deserialize_code_value_inner` reads a code object's fields as bag diff --git a/pyre/pyre-interpreter/src/module/mmap/interp_mmap.rs b/pyre/pyre-interpreter/src/module/mmap/interp_mmap.rs index 1fe94052f9c..19511645ae3 100644 --- a/pyre/pyre-interpreter/src/module/mmap/interp_mmap.rs +++ b/pyre/pyre-interpreter/src/module/mmap/interp_mmap.rs @@ -114,7 +114,7 @@ impl Drop for NativeMMap { /// on the object and the low-level `rmmap.MMap` is owned by that same object. /// The mapdict prefix is required because mmap is an acceptable base class. #[cfg(any(unix, windows))] -#[crate::pyre_class("mmap.mmap")] +#[crate::pyre_class("mmap.mmap", cpython_heaptype)] #[derive(Default)] pub struct W_MMap { pub map: usize, @@ -246,6 +246,9 @@ pub(crate) fn mmap_type() -> pyre_object::PyObjectRef { crate::typedef::w_object(), ::PYTYPE, ); + // CPython 3.14 Modules/mmapmodule.c:mmap_exec uses + // PyType_FromModuleAndSpec; mmap_object_type_spec is immutable. + crate::typedef::mark_cpython_heap_type(tp, true); pyre_object::pyobject::set_instantiate( unsafe { &*::PYTYPE }, tp, diff --git a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs index bf7488ebcbe..c392dadf861 100644 --- a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs +++ b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs @@ -42,7 +42,7 @@ struct ApplevelForkCallbacks { /// type — which falls through to the stat. /// The layout carries no instance dict; `name`/`path` are read-only getset /// descriptors, so the type is not instantiable and not acceptable as a base. -#[crate::pyre_class("posix.DirEntry")] +#[crate::pyre_class("posix.DirEntry", cpython_heaptype)] #[derive(Default)] pub struct W_DirEntry { pub w_name: PyObjectRef, @@ -57,7 +57,7 @@ pub struct W_DirEntry { /// Native owner for `posix.ScandirIterator` entries and enumeration state. /// PyPy's `interp_scandir.W_ScandirIterator` keeps the equivalent state on /// `dirp`; its typedef exposes operations rather than these fields. -#[crate::pyre_class("posix.ScandirIterator")] +#[crate::pyre_class("posix.ScandirIterator", cpython_heaptype)] #[derive(Default)] pub struct W_ScandirIterator { pub entries: PyObjectRef, @@ -5077,6 +5077,9 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { crate::typedef::w_object(), ::PYTYPE, ); + // CPython 3.14 Modules/posixmodule.c creates DirEntryType from an + // immutable module type spec. + crate::typedef::mark_cpython_heap_type(tp, true); pyre_object::pyobject::set_instantiate( unsafe { &*::PYTYPE }, tp, @@ -5276,6 +5279,9 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { crate::typedef::w_object(), ::PYTYPE, ); + // CPython 3.14 Modules/posixmodule.c creates ScandirIteratorType + // from an immutable module type spec. + crate::typedef::mark_cpython_heap_type(tp, true); pyre_object::pyobject::set_instantiate( unsafe { &*::PYTYPE diff --git a/pyre/pyre-interpreter/src/module/select/interp_kevent.rs b/pyre/pyre-interpreter/src/module/select/interp_kevent.rs index a30ab3652c1..163153fa3f9 100644 --- a/pyre/pyre-interpreter/src/module/select/interp_kevent.rs +++ b/pyre/pyre-interpreter/src/module/select/interp_kevent.rs @@ -16,7 +16,9 @@ use pyre_object::PyObjectRef; /// 16-bit `filter`, 16-bit `flags`, 32-bit `fflags`, signed 64-bit /// `data`, and an opaque pointer-sized `udata`. #[cfg(all(target_os = "macos", feature = "host_env"))] -#[crate::pyre_class("select.kevent")] +// CPython 3.14 Modules/selectmodule.c:select_exec creates +// kqueue_event_Type_spec as a mutable module heap type. +#[crate::pyre_class("select.kevent", cpython_mutable)] #[derive(Default)] pub struct W_Kevent { pub ident: u64, diff --git a/pyre/pyre-interpreter/src/module/select/interp_kqueue.rs b/pyre/pyre-interpreter/src/module/select/interp_kqueue.rs index 22f3960bdad..b10ab811189 100644 --- a/pyre/pyre-interpreter/src/module/select/interp_kqueue.rs +++ b/pyre/pyre-interpreter/src/module/select/interp_kqueue.rs @@ -16,7 +16,9 @@ use pyre_object::PyObjectRef; /// marshals a changelist of `kevent`s into the syscall and returns the /// triggered events as fresh `kevent` instances. #[cfg(all(target_os = "macos", feature = "host_env"))] -#[crate::pyre_class("select.kqueue")] +// CPython 3.14 Modules/selectmodule.c:select_exec creates +// kqueue_queue_Type_spec as a mutable module heap type. +#[crate::pyre_class("select.kqueue", cpython_mutable)] pub struct W_Kqueue { kqfd: i32, } diff --git a/pyre/pyre-interpreter/src/module/select/interp_select.rs b/pyre/pyre-interpreter/src/module/select/interp_select.rs index a8fc7e966c2..7a92536e02d 100644 --- a/pyre/pyre-interpreter/src/module/select/interp_select.rs +++ b/pyre/pyre-interpreter/src/module/select/interp_select.rs @@ -11,7 +11,9 @@ use pyre_object::PyObjectRef; /// Instances are created only through the module-level `select.poll()` /// factory (`interp_select.py:18`); the type has no public constructor. #[cfg(all(unix, feature = "host_env"))] -#[crate::pyre_class("select.poll")] +// CPython 3.14 Modules/selectmodule.c:select_exec uses +// PyType_FromModuleAndSpec; poll_Type_spec is a mutable heap type. +#[crate::pyre_class("select.poll", cpython_mutable)] #[derive(Default)] pub struct Poll { fddict: std::collections::HashMap, diff --git a/pyre/pyre-interpreter/src/module/struct/mod.rs b/pyre/pyre-interpreter/src/module/struct/mod.rs index e83de9d57bd..410e6e07689 100644 --- a/pyre/pyre-interpreter/src/module/struct/mod.rs +++ b/pyre/pyre-interpreter/src/module/struct/mod.rs @@ -1096,7 +1096,9 @@ pub(crate) fn unpack_half(bits: u16) -> f64 { /// `interp_struct.py W_Struct` — a compiled struct object holding its /// format string and precomputed size. -#[crate::pyre_class("_struct.Struct")] +// CPython 3.14 Modules/_struct.c:_struct_exec uses +// PyType_FromModuleAndSpec; PyStructType carries IMMUTABLETYPE. +#[crate::pyre_class("_struct.Struct", cpython_heaptype)] pub struct W_Struct { /// Format string object (`text_or_bytes_w` of the constructor arg), /// promoted by value before each pack/unpack. `_immutable_fields_ = @@ -1359,7 +1361,8 @@ pub mod unpack_iter { crate::baseobjspace::object_getattribute(args[0], name) } - #[crate::pyre_class("_struct.unpack_iterator")] + // `_struct_exec` creates unpackiter_type from its immutable heap spec. + #[crate::pyre_class("_struct.unpack_iterator", cpython_heaptype)] pub struct W_UnpackIter { format: PyObjectRef, buffer: PyObjectRef, diff --git a/pyre/pyre-interpreter/src/module/sys/vm.rs b/pyre/pyre-interpreter/src/module/sys/vm.rs index 7327b5dc575..d53c09943d7 100644 --- a/pyre/pyre-interpreter/src/module/sys/vm.rs +++ b/pyre/pyre-interpreter/src/module/sys/vm.rs @@ -302,8 +302,71 @@ fn make_sys_namespace_instance() -> PyObjectRef { /// Fresh `types.SimpleNamespace` instance — `type(sys.implementation)`, the /// attribute bag `time.get_clock_info` fills in and returns. -pub fn new_simple_namespace_instance() -> PyObjectRef { - w_instance_new(simple_namespace_type()) +pub fn new_simple_namespace_instance() -> crate::PyResult { + simple_namespace_new(&[simple_namespace_type()]) +} + +/// CPython 3.14 `namespace_new`: allocate the instance and give it an +/// independent insertion-ordered dictionary before `namespace_init` sees any +/// arguments. +/// +/// PyPy 3.11's app-level SimpleNamespace leaves its `__dict__` on +/// MapDictStrategy. A delete on one instance can then reuse a reordered map +/// transition for a later instance, which is why PyPy sorts namespace reprs. +/// CPython 3.14 instead stores a fresh exact dict in `ns_dict`, and its repr +/// follows insertion order. Use mapdict.py's own +/// `MapDictStrategy.switch_to_text_strategy` devolution path here: the +/// dictionary remains the instance's one authoritative `__dict__`, while its +/// storage becomes private and ordered like CPython's `ns_dict`. +/// +/// The devolution is load-bearing, and an ordinary attribute-assigning class +/// does not show why — its instances keep insertion order across the same +/// delete. The case that needs it is the keyword constructor: +/// +/// ```text +/// del SimpleNamespace(x=1, y=2, z=3).y # reaches a shortened map +/// list(SimpleNamespace(x=1, y=2, z=3).__dict__) +/// # ['z', 'x', 'y'] on the map strategy, ['x', 'y', 'z'] devolved +/// ``` +fn simple_namespace_new(args: &[PyObjectRef]) -> crate::PyResult { + let Some(&subtype) = args.first() else { + return Err(crate::PyError::type_error( + "types.SimpleNamespace.__new__(): not enough arguments", + )); + }; + if !unsafe { pyre_object::is_type(subtype) } { + return Err(crate::PyError::type_error(format!( + "types.SimpleNamespace.__new__(X): X is not a type object ({})", + crate::type_methods::arg_type_name(subtype), + ))); + } + // `tp_new_wrapper` checks the declared owner before namespace_new reaches + // `type->tp_alloc`: calling SimpleNamespace.__new__(int) must not fall + // through to object.__new__'s unrelated layout diagnostic. + crate::typedef::check_user_subclass(simple_namespace_type(), subtype)?; + // namespace_new ignores constructor arguments after the subtype; they + // belong to namespace_init. Passing only the subtype also mirrors + // `type->tp_alloc(type, 0)` rather than object.__new__'s excess-args + // validation. + let object = crate::typedef::object_descr_new(&args[..1])?; + let _roots = pyre_object::gc_roots::push_roots(); + let object_slot = pyre_object::gc_roots::pin_roots(&[object]); + // `W_Root.getdict`, not the public attribute lookup: `namespace_new` + // reaches the instance mapping directly, so a subclass that overrides + // `__getattribute__` or shadows `__dict__` neither runs during + // construction nor can make `S()` raise. + let dict = crate::baseobjspace::getdict(pyre_object::gc_roots::shadow_stack_get(object_slot))?; + pyre_object::gc_roots::pin_root(dict); + let dict = pyre_object::gc_roots::shadow_stack_get(object_slot + 1); + unsafe { + use pyre_object::dictmultiobject::DictStrategy; + if pyre_object::dictmultiobject::w_dict_get_strategy(dict).strategy_kind() + == pyre_object::dictmultiobject::StrategyKind::Map + { + crate::objspace::std::mapdict::mapdict_switch_to_text_strategy(dict); + } + } + Ok(pyre_object::gc_roots::shadow_stack_get(object_slot)) } /// CPython 3.14 `namespace_init`: accept at most one positional mapping or @@ -381,6 +444,11 @@ pub(crate) fn simple_namespace_type() -> PyObjectRef { let raw = *TYPE.get_or_init(|| { let tp = crate::typedef::make_builtin_type("types.SimpleNamespace", |ns| { unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__new__", + crate::typedef::make_new_descr(simple_namespace_new), + ); pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, "__init__", @@ -3510,7 +3578,7 @@ fn sys_clear_type_descriptors(args: &[PyObjectRef]) -> crate::PyResult { "_clear_type_descriptors() argument must be a type", )); } - if !unsafe { pyre_object::w_type_is_heaptype(w_type) } { + if unsafe { pyre_object::w_type_is_cpython_immutabletype(w_type) } { return Err(crate::PyError::type_error("argument is immutable")); } diff --git a/pyre/pyre-interpreter/src/module/thread/mod.rs b/pyre/pyre-interpreter/src/module/thread/mod.rs index 6bc07a599a3..39be67ddd1d 100644 --- a/pyre/pyre-interpreter/src/module/thread/mod.rs +++ b/pyre/pyre-interpreter/src/module/thread/mod.rs @@ -759,7 +759,9 @@ mod lock_class { // would swallow exactly the wakeup that carries the interrupt. use std::sync::{Condvar, Mutex}; - #[crate::pyre_class("_thread.lock")] + // CPython 3.14 Modules/_threadmodule.c builds lock_type_spec as an + // immutable module heap type. + #[crate::pyre_class("_thread.lock", cpython_heaptype)] #[derive(Default)] pub struct W_Lock { locked: Mutex, @@ -904,7 +906,8 @@ mod rlock_class { owner: i64, } - #[crate::pyre_class("_thread.RLock")] + // Modules/_threadmodule.c builds rlock_type_spec as immutable heap type. + #[crate::pyre_class("_thread.RLock", cpython_heaptype)] #[derive(Default)] pub struct W_RLock { state: Mutex, @@ -1134,7 +1137,8 @@ mod handle_class { pub(super) daemon: bool, } - #[crate::pyre_class("_thread._ThreadHandle")] + // Modules/_threadmodule.c builds ThreadHandle_Type_spec as immutable heap. + #[crate::pyre_class("_thread._ThreadHandle", cpython_heaptype)] #[derive(Default)] pub struct W_ThreadHandle { pub(super) state: Mutex, @@ -1278,7 +1282,8 @@ mod local_class { /// takes the positional and keyword halves separately, so they are stored as /// the positional tuple and the construction call's keyword mapping (null /// when it had none), and `create_new_dict` replays the call from both. - #[crate::pyre_class("_thread._local")] + // Modules/_threadmodule.c builds local_type_spec as immutable module heap. + #[crate::pyre_class("_thread._local", cpython_heaptype)] pub struct W_Local { dicts: PyObjectRef, initargs: PyObjectRef, diff --git a/pyre/pyre-interpreter/src/module/time/interp_time.rs b/pyre/pyre-interpreter/src/module/time/interp_time.rs index f97f51f90df..fb3e9e9f064 100644 --- a/pyre/pyre-interpreter/src/module/time/interp_time.rs +++ b/pyre/pyre-interpreter/src/module/time/interp_time.rs @@ -1793,7 +1793,7 @@ pub fn get_clock_info(args: &[PyObjectRef]) -> Result PyResult { /// PyPy `interp_ucd.py:UCD` — a Unicode database view whose methods are /// descriptors on the type, rather than entries in an instance dictionary. -#[crate::pyre_class("unicodedata.UCD")] +// CPython 3.14 Modules/unicodedata.c:PyInit_unicodedata uses PyType_FromSpec; +// ucd_type_spec carries IMMUTABLETYPE. +#[crate::pyre_class("unicodedata.UCD", cpython_heaptype)] pub struct W_UCD { legacy: bool, } diff --git a/pyre/pyre-interpreter/src/module/zlib/mod.rs b/pyre/pyre-interpreter/src/module/zlib/mod.rs index 8ad4da119f6..0bb0a504087 100644 --- a/pyre/pyre-interpreter/src/module/zlib/mod.rs +++ b/pyre/pyre-interpreter/src/module/zlib/mod.rs @@ -17,7 +17,9 @@ use std::sync::Mutex; /// PyPy `interp_zlib.py Compress`: the stream and lock belong to the /// wrapper object. The mapdict prefix preserves PyPy's ability to subclass /// the type without moving its native payload into a side table. -#[crate::pyre_class("zlib.Compress")] +// CPython 3.14 Modules/zlibmodule.c:zlib_exec creates this heap spec without +// IMMUTABLETYPE, so its type namespace remains mutable. +#[crate::pyre_class("zlib.Compress", cpython_mutable)] #[derive(Default)] pub struct W_Compress { pub map: usize, @@ -26,7 +28,8 @@ pub struct W_Compress { } /// PyPy `interp_zlib.py Decompress` object-owned stream state. -#[crate::pyre_class("zlib.Decompress")] +// Same `zlib_exec` mutable heap owner as Compress. +#[crate::pyre_class("zlib.Decompress", cpython_mutable)] #[derive(Default)] pub struct W_Decompress { pub map: usize, @@ -35,7 +38,9 @@ pub struct W_Decompress { } /// PyPy `interp_zlib.py:419 ZlibDecompressor` object-owned buffered stream. -#[crate::pyre_class("zlib._ZlibDecompressor")] +// CPython 3.14's ZlibDecompressorType spec adds IMMUTABLETYPE, unlike the two +// public stream types above. +#[crate::pyre_class("zlib._ZlibDecompressor", cpython_heaptype)] #[derive(Default)] pub struct W_ZlibDecompressor { pub map: usize, @@ -191,6 +196,23 @@ static COMPRESS_RUNTIME_TYPE: std::sync::OnceLock = std::sync::OnceLock:: static DECOMPRESS_RUNTIME_TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); static ZDECOMPRESS_RUNTIME_TYPE: std::sync::OnceLock = std::sync::OnceLock::new(); +/// [3.14-spec] CPython's zlib specs publish `__module__ = "zlib"`; PyPy's +/// unqualified `TypeDef('Compress')` / `TypeDef('Decompress')` do not. Keep +/// those PyPy type names and add only the observable namespace entry. +fn publish_cpython_module(ns: PyObjectRef) { + let roots = pyre_object::gc_roots::push_roots(); + roots.pin_root(ns); + let slot = roots.base(); + let module = pyre_object::w_str_new("zlib"); + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + roots.get(slot), + "__module__", + module, + ); + } +} + fn compress_type() -> PyObjectRef { *COMPRESS_RUNTIME_TYPE.get_or_init(|| { let tp = crate::typedef::make_builtin_type_with_layout( @@ -199,6 +221,9 @@ fn compress_type() -> PyObjectRef { crate::typedef::w_object(), ::PYTYPE, ); + // CPython 3.14 Modules/zlibmodule.c:zlib_exec creates Comptype from a + // mutable heap spec. + crate::typedef::mark_cpython_heap_type(tp, false); pyre_object::pyobject::set_instantiate( unsafe { &*::PYTYPE }, tp, @@ -208,6 +233,7 @@ fn compress_type() -> PyObjectRef { } fn init_compress_type(ns: PyObjectRef) { + publish_cpython_module(ns); let new_sig = { let mut b = crate::SignatureBuilder::default(); b.append("cls"); @@ -403,6 +429,8 @@ fn decompress_type() -> PyObjectRef { crate::typedef::w_object(), ::PYTYPE, ); + // `zlib_exec` creates Decomptype from a mutable heap spec. + crate::typedef::mark_cpython_heap_type(tp, false); pyre_object::pyobject::set_instantiate( unsafe { &*::PYTYPE }, tp, @@ -460,6 +488,7 @@ fn decompress_decompress(args: &[PyObjectRef]) -> Result PyObjectRef { crate::typedef::w_object(), ::PYTYPE, ); + // `zlib_exec` creates ZlibDecompressorType from an immutable heap + // spec, unlike Compress and Decompress. + crate::typedef::mark_cpython_heap_type(tp, true); pyre_object::pyobject::set_instantiate( unsafe { &*::PYTYPE }, tp, @@ -747,6 +779,7 @@ fn zdecompress_decompress(args: &[PyObjectRef]) -> Result Option<(PyObjectRef, u64, MapRef, PyObjectRef)> { + let (w_type, version_tag, map) = unsafe { getattr_resolves_nowhere(w_obj, name) }?; + let w_getattr = unsafe { crate::baseobjspace::lookup_in_type_where(w_type, "__getattr__") }?; + Some((w_type, version_tag, map, w_getattr)) +} + +/// The `__getattr__`-less twin of [`getattr_hook_fast_path`]: `name` resolves +/// nowhere *and* the type has no hook to run afterwards, so the access ends in +/// the `AttributeError` `object_getattr_miss` raises. +/// +/// That makes the whole miss a compile-time answer for the callers that only +/// need to know the access fails — `hasattr(obj, name)` is then a constant +/// False and `getattr(obj, name, default)` is `default`. The two pins the +/// caller owes are the same ones [`getattr_hook_fast_path`] documents. +/// +/// # Safety +/// `w_obj` must be a live object. +pub unsafe fn getattr_absent_fast_path( + w_obj: PyObjectRef, + name: &str, +) -> Option<(PyObjectRef, u64, MapRef)> { + let pins = unsafe { getattr_resolves_nowhere(w_obj, name) }?; + // A hook would run application code whose answer neither pin describes, + // and which can return a value or raise something other than + // AttributeError. + if unsafe { crate::baseobjspace::lookup_in_type_where(pins.0, "__getattr__") }.is_some() { + return None; + } + Some(pins) +} + +/// The shared miss proof behind [`getattr_hook_fast_path`] and +/// [`getattr_absent_fast_path`]: `name` is on neither the type nor the +/// receiver's own storage, and the returned `version_tag` / `map` pins are what +/// keep that true. +/// +/// # Safety +/// `w_obj` must be a live object. +unsafe fn getattr_resolves_nowhere( + w_obj: PyObjectRef, + name: &str, +) -> Option<(PyObjectRef, u64, MapRef)> { // mapdict.py:1495 `if map is not None:` — also filters non-instances. let map = unsafe { mapdict_map_or_null(w_obj) }; if map.is_null() { @@ -1841,8 +1882,7 @@ pub unsafe fn getattr_hook_fast_path( if unsafe { find_map_attr(map, Wtf8::new(name), DICT) }.is_some() { return None; } - let w_getattr = unsafe { crate::baseobjspace::lookup_in_type_where(w_type, "__getattr__") }?; - Some((w_type, version_tag, map, w_getattr)) + Some((w_type, version_tag, map)) } /// The [`load_attr_fast_path`] twin for a receiver that keeps its attributes in diff --git a/pyre/pyre-interpreter/src/opcode_ops.rs b/pyre/pyre-interpreter/src/opcode_ops.rs index d9fbe873131..c266f9481d6 100644 --- a/pyre/pyre-interpreter/src/opcode_ops.rs +++ b/pyre/pyre-interpreter/src/opcode_ops.rs @@ -441,30 +441,7 @@ pub fn match_class_value( // No `__match_args__`: the builtin "atomic" types (int, str, // bytes, ...) match the subject itself as their single // positional sub-pattern (Py_TPFLAGS_MATCH_SELF). - let is_self = { - use pyre_object::pyobject::get_instantiate; - let atomics: [PyObjectRef; 11] = [ - get_instantiate(&pyre_object::pyobject::INT_TYPE), - get_instantiate(&pyre_object::pyobject::BOOL_TYPE), - get_instantiate(&pyre_object::pyobject::FLOAT_TYPE), - get_instantiate(&pyre_object::pyobject::STR_TYPE), - get_instantiate(&pyre_object::pyobject::LIST_TYPE), - get_instantiate(&pyre_object::pyobject::TUPLE_TYPE), - get_instantiate(&pyre_object::pyobject::DICT_TYPE), - get_instantiate(&pyre_object::bytesobject::BYTES_TYPE), - get_instantiate(&pyre_object::bytearrayobject::BYTEARRAY_TYPE), - get_instantiate(&pyre_object::setobject::SET_TYPE), - get_instantiate(&pyre_object::setobject::FROZENSET_TYPE), - ]; - let mut found = false; - for ty_obj in atomics { - if crate::baseobjspace::issubclass(cls, ty_obj)? { - found = true; - break; - } - } - found - }; + let is_self = unsafe { pyre_object::typeobject::w_type_has_match_self(cls) }; if is_self { if count == 1 { extracted.push(subject_slot); diff --git a/pyre/pyre-interpreter/src/pycode.rs b/pyre/pyre-interpreter/src/pycode.rs index e6728761e7c..3bf04e4ba55 100644 --- a/pyre/pyre-interpreter/src/pycode.rs +++ b/pyre/pyre-interpreter/src/pycode.rs @@ -9,6 +9,7 @@ use pyre_object::{ w_bool_from, w_bool_get_value, w_int_new, w_list_new, w_seq_iter_new, w_str_new, w_tuple_new, }; use rustpython_compiler_core::SourceLocation; +use rustpython_compiler_core::bytecode::PyCodeLocationInfoKind; const YIELDS_INSIDE_TRY_BIT: u16 = 0x8000; @@ -25,12 +26,166 @@ impl From for crate::PyError { } } +/// CPython 3.14 `_PyCodeAddressRange`, ported through RustPython's code +/// object implementation. This reads the authoritative `co_linetable`; +/// `CodeObject.locations` is an execution-oriented expansion which cannot +/// represent a missing line or column. +struct PyCodeAddressRange<'a> { + ar_start: i32, + ar_end: i32, + ar_line: i32, + computed_line: i32, + reader: LineTableReader<'a>, +} + +impl<'a> PyCodeAddressRange<'a> { + fn new(linetable: &'a [u8], first_line: i32) -> Self { + Self { + ar_start: 0, + ar_end: 0, + ar_line: -1, + computed_line: first_line, + reader: LineTableReader::new(linetable), + } + } + + fn advance(&mut self) -> bool { + let Some(first_byte) = self.reader.read_byte() else { + return false; + }; + // A byte in header position is decoded as one, bit 7 or not: the + // marker separates a header from the payload bytes the skip below + // consumes, and is not consulted here. `code.replace()` stores an + // arbitrary `co_linetable`, and stopping on the marker made + // `co_linetable=b"\0"` report no ranges where one entry is decoded. + let code = (first_byte >> 3) & 0x0f; + let length = ((first_byte & 0x07) + 1) as i32; + self.computed_line += self.get_line_delta(code); + self.ar_line = if first_byte >> 3 == 0x1f { + -1 + } else { + self.computed_line + }; + self.ar_start = self.ar_end; + self.ar_end += length * 2; + + // Every payload byte has bit 7 clear; the next header has it set. + while self.reader.peek_byte().is_some_and(|byte| byte & 0x80 == 0) { + self.reader.read_byte(); + } + true + } + + fn get_line_delta(&mut self, code: u8) -> i32 { + let Some(kind) = PyCodeLocationInfoKind::from_code(code) else { + return 0; + }; + match kind { + PyCodeLocationInfoKind::None => 0, + PyCodeLocationInfoKind::Long => { + let delta = self.reader.read_signed_varint(); + self.reader.read_varint(); + self.reader.read_varint(); + self.reader.read_varint(); + delta + } + PyCodeLocationInfoKind::NoColumns => self.reader.read_signed_varint(), + PyCodeLocationInfoKind::OneLine0 + | PyCodeLocationInfoKind::OneLine1 + | PyCodeLocationInfoKind::OneLine2 => { + self.reader.read_byte(); + self.reader.read_byte(); + kind.one_line_delta().unwrap_or(0) + } + _ if kind.is_short() => { + self.reader.read_byte(); + 0 + } + _ => 0, + } + } +} + +/// RustPython `LineTableReader`, matching CPython's 6-bit little-endian +/// location-table varints. +struct LineTableReader<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> LineTableReader<'a> { + fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + fn read_byte(&mut self) -> Option { + let byte = self.data.get(self.pos).copied()?; + self.pos += 1; + Some(byte) + } + + fn peek_byte(&self) -> Option { + self.data.get(self.pos).copied() + } + + /// Read one location-table varint: 6 bits per byte, **least + /// significant group first**, bit 6 (0x40) marking continuation. + /// + /// This inverts `write_varint`, and deliberately differs from + /// `decode_varint` below, which reads the exception table most + /// significant group first to invert `parse_varint`. Both helpers sit + /// a few lines apart in `pycore_code.h`: the byte order is a property + /// of the table, not of the file. `_decode_varint` in PyPy's pycode.py + /// is the exception-table reader despite its generic name, so reusing + /// it here would decode every multi-byte line delta wrong. + fn read_varint(&mut self) -> u32 { + let Some(first) = self.read_byte() else { + return 0; + }; + let mut value = (first & 0x3f) as u32; + let mut shift = 0; + let mut byte = first; + while byte & 0x40 != 0 { + let Some(next) = self.read_byte() else { + break; + }; + shift += 6; + // `code.replace(co_linetable=...)` stores arbitrary bytes, so the + // continuation chain can run past what a u32 holds. Those groups + // are unrepresentable either way; drop them rather than shift by + // the full width, which panics in a debug build. + if shift < u32::BITS { + value |= ((next & 0x3f) as u32) << shift; + } + byte = next; + } + value + } + + fn read_signed_varint(&mut self) -> i32 { + let value = self.read_varint(); + if value & 1 != 0 { + -((value >> 1) as i32) + } else { + (value >> 1) as i32 + } + } + + fn at_end(&self) -> bool { + self.pos >= self.data.len() + } +} + /// pycode.py:683-695 — decode one CPython-3.11 varint at `i`. /// /// Returns `(value, new_i)`. Reads 6 bits per byte, MSB first. Bit 6 /// (0x40) is the continuation flag; bit 7 (0x80) is the start-of-entry /// marker, ignored here and masked off along with the continuation bit /// via `& 63`. +/// +/// Mirrors `parse_varint`. The location table uses the opposite byte +/// order — see `LineTableReader::read_varint` — so this decoder must not +/// be reused for `co_linetable`. #[inline] pub fn decode_varint(table: &[u8], mut i: usize) -> (u32, usize) { let mut b = table[i] as u32; @@ -204,11 +359,22 @@ pub struct PyCode { /// for the bootstrap family; without both it holds a pre-move address after /// the first collection that moves the string. pub filename_bytes: *mut Vec, - /// Whether an unrealized nested compiler constant selected by + /// PyPy `pycode.py:132 self.co_code = co_code`: byte-exact public + /// bytecode supplied to `CodeType` / `code.replace` when it contains an + /// opcode that RustPython's enum cannot represent (or its explicit + /// `Reserved` hole). Ordinary compiler-produced code leaves this null + /// and derives `co_code` from `CodeUnits`. + /// + /// The execution stream uses `Instruction::Reserved` as a placeholder, + /// but getters, equality, hashing and marshal must retain the actual byte. + /// Keeping the owner-local field mirrors PyPy's `PyCode.co_code`; an + /// address-keyed side table would lose both lifetime and structural parity. + pub co_code_bytes: *mut Vec, + /// Whether a nested compiler constant selected by /// `importing.py update_code_filenames`' `oldname` guard inherits - /// `filename_bytes` when pyre crosses its lazy wrapping boundary. False - /// for `pycode.py` constructor/replace filenames, which affect only - /// the code object being constructed. + /// `filename_bytes` if a fallback slot ever has to be rebuilt. False for + /// `pycode.py` constructor/replace filenames, which affect only the + /// code object being constructed. pub filename_inherits_to_nested: bool, /// PyPy: `PyCode.w_globals` — the globals dict OBJECT (`W_DictMultiObject`, /// `pycode.py "w_globals?"`). Module globals are `malloc_typed`- @@ -218,6 +384,12 @@ pub struct PyCode { /// [`W_GLOBALS_STAMPED_CODES`] and forwarded from there. Null until first /// stamped by `frame_stores_global`. pub w_globals: PyObjectRef, + /// `typedef.py make_weakref_descr(PyCode)` installs `_lifeline_` on + /// the interpreter-level class. Keep that owner-local field here rather + /// than routing code objects through the fallback address-keyed table. + /// The lifeline owns the cached weakrefs and their callbacks and is traced + /// with the rest of this code object's managed children. + pub w_weakreflifeline: PyObjectRef, /// PyPy: `PyCode.hidden_applevel` (`pycode.py, 147`). Set by /// `pycompiler.compile(hidden_applevel=True)` for PyPy gateway/ /// app_main bridge code. Pyre has no such call site yet, so this @@ -276,13 +448,16 @@ pub struct PyCode { /// same virtualizable `pycode`) preserve object identity. /// /// PyPy receives this list already wrapped from the compiler. Pyre's - /// compiler keeps `ConstantData` unwrapped, so slots are realized lazily at - /// the equivalent boundary. `eval::walk_raw_code_roots` traces every filled - /// slot because value constants (notably `W_LongObject`) are GC-managed. + /// compiler keeps `ConstantData` unwrapped, so the `PyCode` constructor + /// wraps every slot before publishing the code object. This is observable: + /// `gc.get_objects()` must not gain a permanent object the first time a + /// `LOAD_CONST` executes. `eval::walk_raw_code_roots` traces every slot + /// because value constants (notably `W_LongObject`) are GC-managed. /// /// Owned via `Box::into_raw`, sized to `code.constants.len()` at construction, - /// never resized; a `null` slot is unrealized. The whole pointer is `null` - /// when `code_ptr` is null or unaligned (test fixtures, gateway builtins). + /// never resized. A `null` slot is reserved for unreadable test stubs or a + /// defensive fallback after construction. The whole pointer is `null` when + /// `code_ptr` is null or unaligned (test fixtures, gateway builtins). pub co_consts_w: *mut Vec>, /// `pycode.py:127-129 self.co_names_w = [space.new_interned_str(aname) for /// aname in names]` (`_immutable_fields_ co_names_w[*]`, pycode.py:100). @@ -343,6 +518,8 @@ pub struct PyCode { pub const CODE_PTR_OFFSET: usize = std::mem::offset_of!(PyCode, code_ptr); /// Field offset of `w_globals` within `PyCode`. pub const CODE_W_GLOBALS_OFFSET: usize = std::mem::offset_of!(PyCode, w_globals); +/// Field offset of `w_weakreflifeline` within `PyCode`. +pub const CODE_W_WEAKREFLIFELINE_OFFSET: usize = std::mem::offset_of!(PyCode, w_weakreflifeline); /// Field offset of `w_qualname` within `PyCode`. pub const CODE_W_QUALNAME_OFFSET: usize = std::mem::offset_of!(PyCode, w_qualname); /// Field offset of `w_name` within `PyCode`. @@ -360,6 +537,26 @@ pub unsafe fn w_code_firstlineno_raw(w_code: PyObjectRef) -> i32 { unsafe { (*(w_code as *const PyCode)).co_firstlineno_raw } } +/// `make_weakref_descr(PyCode).getweakref` — read the owner-local lifeline. +/// +/// # Safety +/// `w_code` must point to a live [`PyCode`]. +pub unsafe fn w_code_getweakref(w_code: PyObjectRef) -> PyObjectRef { + unsafe { (*(w_code as *const PyCode)).w_weakreflifeline } +} + +/// `make_weakref_descr(PyCode).setweakref` — publish the lifeline and retain +/// the old-to-young edge when a stable-oldgen code object receives it. +/// +/// # Safety +/// `w_code` must point to a live [`PyCode`]. +pub unsafe fn w_code_setweakref(w_code: PyObjectRef, lifeline: PyObjectRef) { + unsafe { + (*(w_code as *mut PyCode)).w_weakreflifeline = lifeline; + pyre_object::gc_hook::try_gc_write_barrier(w_code as *mut u8); + } +} + /// `pycode.py self.co_qualname = qualname` — the shared wrapped qualified /// name, realized on first demand and retained on the code object. /// @@ -655,9 +852,9 @@ pub fn w_code_new_with_hidden_applevel(code_ptr: *const (), hidden_applevel: boo v.resize_with(names_len, || None); Box::into_raw(Box::new(v)) }; - // `pycode.py self.co_consts_w = consts` — the realized-constant table - // sized to the constant count, with slots filled lazily by `w_code_const` - // at pyre's wrapped/unwrapped compiler boundary. + // `pycode.py self.co_consts_w = consts` — allocate the wrapped-constant + // table at the compiler/interpreter boundary. It is filled immediately + // after the stable `PyCode` allocation below. let co_consts_w = if !code_ptr_aligned { std::ptr::null_mut() } else { @@ -703,8 +900,10 @@ pub fn w_code_new_with_hidden_applevel(code_ptr: *const (), hidden_applevel: boo code_ptr, co_firstlineno_raw, filename_bytes: std::ptr::null_mut(), + co_code_bytes: std::ptr::null_mut(), filename_inherits_to_nested: false, w_globals: pyre_object::PY_NULL, + w_weakreflifeline: pyre_object::PY_NULL, hidden_applevel, fast_natural_arity, npure_cellvars, @@ -722,10 +921,30 @@ pub fn w_code_new_with_hidden_applevel(code_ptr: *const (), hidden_applevel: boo // `malloc_typed_stable` falls back to the prebuilt family and the explicit // root registry remains necessary. let obj = pyre_object::lltype::malloc_typed_stable(obj) as PyObjectRef; + // PyPy's ast compiler has already wrapped every entry before PyCode.__init__ + // (`assemble.py:479-492`). Pin the freshly allocated stable wrapper while + // recursive code constants and managed scalar constants allocate, then + // publish one object in every co_consts_w slot before returning PyCode. + let _roots = pyre_object::gc_roots::push_roots(); + let obj_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(obj); + // The shadow-stack root forwards the wrapper itself; only the raw walker + // driven from this registry reaches its `co_consts_w` slots. Enrol an + // off-GC wrapper before the fill loop, or a collection triggered by a + // later constant reclaims the constants already published into it. A + // managed wrapper is traced from its own allocation and stays out. if !pyre_object::gc_hook::try_gc_owns_object(obj as *mut u8) { register_prebuilt_code_root(obj); } - obj + if code_ptr_aligned { + let consts_len = unsafe { &*(code_ptr as *const crate::CodeObject) } + .constants + .len(); + for index in 0..consts_len { + unsafe { w_code_const(pyre_object::gc_roots::shadow_stack_get(obj_slot), index) }; + } + } + pyre_object::gc_roots::shadow_stack_get(obj_slot) } /// pypy/interpreter/pycode.py `PyCode.__init__` shorthand — @@ -826,14 +1045,29 @@ unsafe fn box_code_constant_inheriting_filename( /// Attach the filesystem bytes a whole compilation unit was named with. /// /// `compiling.py filename='fsencode'` names the unit, not one object, so -/// the nested constants this code object still holds unrealized take the same -/// spelling when they are boxed. That is the difference from `pycode.py:431`, -/// whose constructor and `replace` filenames rename only the object being -/// built and leave every nested constant on the name it compiled under. +/// recurse through the already-wrapped constants exactly like +/// `importing.py update_code_filenames`. That is the difference from +/// the code constructor and `replace`, whose filename changes only the object +/// being built and leaves nested constants on the name they compiled under. pub(crate) unsafe fn set_compilation_unit_filename_bytes( w_code: PyObjectRef, bytes: Option>, ) { + let old_filename = unsafe { code_filename_bytes(w_code) }; + if let Some(bytes) = bytes.as_ref() { + let pycode = unsafe { &*(w_code as *const PyCode) }; + if !pycode.co_consts_w.is_null() { + for slot in unsafe { &*pycode.co_consts_w } { + let nested = slot.load(std::sync::atomic::Ordering::Acquire); + if !nested.is_null() + && unsafe { is_code(nested) } + && unsafe { code_filename_bytes(nested) } == old_filename + { + unsafe { set_compilation_unit_filename_bytes(nested, Some(bytes.clone())) }; + } + } + } + } let inherits = bytes.is_some(); unsafe { set_filename_bytes(w_code, bytes) }; unsafe { (*(w_code as *mut PyCode)).filename_inherits_to_nested = inherits }; @@ -856,6 +1090,38 @@ unsafe fn set_filename_bytes(obj: PyObjectRef, bytes: Option>) { } } +/// Replace the byte-exact `PyCode.co_code` fallback owned by `obj`. +/// +/// A null slot means every opcode is representable by compiler-core and its +/// canonical `original_bytes()` is authoritative. +pub(crate) unsafe fn set_co_code_bytes(obj: PyObjectRef, bytes: Option>) { + let slot = unsafe { &mut (*(obj as *mut PyCode)).co_code_bytes }; + if let Some(bytes) = bytes { + if slot.is_null() { + *slot = Box::into_raw(Box::new(bytes)); + } else { + unsafe { **slot = bytes }; + } + } else if !slot.is_null() { + unsafe { drop(Box::from_raw(*slot)) }; + *slot = std::ptr::null_mut(); + } +} + +/// Return the public, byte-exact `co_code` spelling for a live `PyCode`. +/// +/// # Safety +/// `w_code` must point to a live [`PyCode`]. +pub(crate) unsafe fn code_bytes(w_code: PyObjectRef) -> Vec { + let pycode = unsafe { &*(w_code as *const PyCode) }; + if pycode.co_code_bytes.is_null() { + let code = unsafe { &*(pycode.code_ptr as *const crate::CodeObject) }; + code.instructions.original_bytes() + } else { + unsafe { (&*pycode.co_code_bytes).clone() } + } +} + pub(crate) unsafe fn code_filename_bytes(w_code: PyObjectRef) -> Vec { let pycode = unsafe { &*(w_code as *const PyCode) }; if pycode.filename_bytes.is_null() { @@ -904,101 +1170,53 @@ unsafe fn w_code_fill_consts_from_tuple(obj: PyObjectRef, constants: PyObjectRef } } if filled { - pyre_object::gc_roots::mark_prebuilt_roots_dirty(); + publish_code_slot_store(obj); } } -/// Whether any `co_consts_w` slot is realized. On a code object the marshal -/// reader just decoded, a realized slot is a wrapped constant the compiler -/// representation cannot reproduce, so a parent decode must store this object -/// itself rather than realize a fresh one from the compiler clone. -unsafe fn w_code_has_wrapped_consts(obj: PyObjectRef) -> bool { - let code = unsafe { &*(obj as *const PyCode) }; - if code.co_consts_w.is_null() { - return false; - } - unsafe { &*code.co_consts_w } - .iter() - .any(|slot| !slot.load(std::sync::atomic::Ordering::Acquire).is_null()) -} - -/// Whether realizing `data` would lose what `value` carries. +/// Record a store into one of the Rust-side tables a `PyCode` owns — +/// `co_consts_w`, `w_globals`, the mapdict method cache. /// -/// `ConstantData::None` is the placeholder `code_constant_from_value` falls -/// back to for a value it cannot serialize at all — a list, a dict, a set — so -/// it stands for a wrapped constant whenever the decoded value is not itself -/// `None`. A nested code object serializes fine but its clone realizes its own -/// constants from the same lossy representation, so it is reproducible only -/// while it carries no wrapped slot; a tuple and a slice, only while every -/// element is. The remaining variants describe their value exactly. A -/// frozenset needs no case either: every wrapped constant is unhashable, so no -/// frozenset can hold one. -unsafe fn is_wrapped_constant(data: &crate::bytecode::ConstantData, value: PyObjectRef) -> bool { - use crate::bytecode::ConstantData; - unsafe { - match data { - ConstantData::None => !is_none(value), - ConstantData::Code { .. } => is_code(value) && w_code_has_wrapped_consts(value), - ConstantData::Tuple { elements } => { - is_tuple(value) - && elements.iter().enumerate().any(|(index, element)| { - pyre_object::w_tuple_getitem(value, index as i64) - .is_some_and(|item| is_wrapped_constant(element, item)) - }) - } - ConstantData::Slice { elements } => { - pyre_object::sliceobject::is_slice(value) - && elements - .iter() - .zip([ - pyre_object::sliceobject::w_slice_get_start(value), - pyre_object::sliceobject::w_slice_get_stop(value), - pyre_object::sliceobject::w_slice_get_step(value), - ]) - .any(|(element, item)| is_wrapped_constant(element, item)) - } - _ => false, - } +/// Those tables are not collector objects: only the wrapper's custom trace +/// reaches them, so a store there is a store into the wrapper. A prebuilt +/// wrapper is covered by the root walk, which clean minor collections skip, +/// hence `mark_prebuilt_roots_dirty`. A managed wrapper is not in that +/// registry at all — `w_code_new` registers a code object only when the +/// collector does not already own it — so a tenured wrapper that now points at +/// a nursery constant needs its remembered-set entry back, which is what the +/// write barrier restores. Without it the next minor collection never traces +/// the slot and leaves the wrapper holding a stale pointer. +#[inline] +fn publish_code_slot_store(obj: PyObjectRef) { + if obj.is_null() { + return; } + pyre_object::gc_hook::try_gc_write_barrier(obj as *mut u8); + pyre_object::gc_roots::mark_prebuilt_roots_dirty(); } /// `w_code_fill_consts_from_tuple` for the marshal reader, whose `co_consts` -/// arrive as decoded objects rather than as a tuple — but selective: only the -/// slots the compiler representation cannot stand in for are stored, as -/// `is_wrapped_constant` decides. Every other slot stays unrealized: code -/// wrappers are immortal, so storing every decoded constant would retain the -/// whole constant graph of every unmarshalled code object for the process -/// lifetime, where a compiled equivalent realizes on demand. +/// arrive as decoded objects rather than as a tuple. PyPy's marshal reader +/// passes the complete wrapped list to `PyCode.__init__`; replace every eager +/// compiler-boundary placeholder with that authoritative decoded object. pub(crate) unsafe fn w_code_fill_wrapped_consts(obj: PyObjectRef, constants: &[PyObjectRef]) { let code = unsafe { &*(obj as *const PyCode) }; if code.co_consts_w.is_null() { return; } - let align_mask = std::mem::align_of::() as i64 - 1; - if code.code_ptr.is_null() || (code.code_ptr as i64) & align_mask != 0 { - return; - } - let consts = - crate::pyframe::code_constants(unsafe { &*(code.code_ptr as *const crate::CodeObject) }); let slots = unsafe { &*code.co_consts_w }; - let count = slots.len().min(constants.len()).min(consts.len()); - let mut filled = false; + let count = slots.len().min(constants.len()); for index in 0..count { - let value = constants[index]; - if unsafe { is_wrapped_constant(&consts[index], value) } { - slots[index].store(value, std::sync::atomic::Ordering::Release); - filled = true; - } + slots[index].store(constants[index], std::sync::atomic::Ordering::Release); } - if filled { - pyre_object::gc_roots::mark_prebuilt_roots_dirty(); + if count != 0 { + publish_code_slot_store(obj); } } /// Preserve the existing wrapped constant array when `code.replace()` changes -/// fields other than `co_consts`. PyPy copies an already-wrapped list; pyre -/// must therefore copy only source slots that have already been realized, -/// without turning `code.replace()` into an eager realization boundary. +/// fields other than `co_consts`. PyPy copies its already-wrapped list; pyre +/// copies those same eager slot identities into the newly built wrapper. unsafe fn w_code_copy_const_slots(dst: PyObjectRef, src: PyObjectRef) { let dst_code = unsafe { &*(dst as *const PyCode) }; let src_code = unsafe { &*(src as *const PyCode) }; @@ -1016,7 +1234,7 @@ unsafe fn w_code_copy_const_slots(dst: PyObjectRef, src: PyObjectRef) { } } if copied { - pyre_object::gc_roots::mark_prebuilt_roots_dirty(); + publish_code_slot_store(dst); } } @@ -1109,10 +1327,16 @@ fn legacy_lnotab(code: &crate::CodeObject, firstlineno: i64) -> Vec { let mut out = Vec::new(); let mut line = firstlineno; let mut start_offset = 0usize; - for (index, (start, _)) in code_locations(code).iter().enumerate() { - let next_line = start.line.get() as i64; + let mut range = PyCodeAddressRange::new( + &code.linetable, + firstlineno.clamp(i32::MIN as i64, i32::MAX as i64) as i32, + ); + while range.advance() { + // CPython's `decode_linetable` follows the computed line rather than + // `ar_line`, so a NO_LOCATION range does not manufacture a -1 delta. + let next_line = range.computed_line as i64; if next_line != line { - let offset = index * 2; + let offset = range.ar_start as usize; encode_pair(offset - start_offset, next_line - line, &mut out); line = next_line; start_offset = offset; @@ -1128,6 +1352,27 @@ fn legacy_lnotab(code: &crate::CodeObject, firstlineno: i64) -> Vec { /// The caller must uphold every validity, runtime-type, aliasing, and lifetime /// invariant required by the object and pointer arguments for the entire call. pub unsafe fn code_get_field(obj: PyObjectRef, name: &str) -> Result { + if name == "co_lnotab" { + // CPython 3.14 `code_getlnotab`: issue the warning before decoding or + // allocating the bytes result, and propagate warnings-as-errors. + unsafe { require_code(obj, name)? }; + // Warning dispatch can run arbitrary Python, so it is a collection + // point. `obj` arrives as a plain Rust local, which no root walker + // scans: pin it on the shadow stack and read it back afterwards, then + // reacquire the immutable CodeObject view rather than retaining a Rust + // borrow across the re-entrant boundary. + let _roots = pyre_object::gc_roots::push_roots(); + let obj_slot = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(obj); + crate::warn::warn_deprecation("co_lnotab is deprecated, use co_lines instead.")?; + let obj = pyre_object::gc_roots::shadow_stack_get(obj_slot); + let code = unsafe { require_code(obj, name)? }; + return Ok(pyre_object::bytesobject::w_bytes_from_bytes( + &legacy_lnotab(code, unsafe { + (*(obj as *const PyCode)).co_firstlineno_raw as i64 + }), + )); + } let code = unsafe { require_code(obj, name)? }; Ok(match name { "co_argcount" => w_int_new(code.arg_count as i64), @@ -1137,7 +1382,7 @@ pub unsafe fn code_get_field(obj: PyObjectRef, name: &str) -> Result w_int_new(code.max_stackdepth as i64), "co_flags" => w_int_new(code.flags.bits() as i64), "co_code" | "_co_code_adaptive" => { - pyre_object::bytesobject::w_bytes_from_bytes(&code.instructions.original_bytes()) + pyre_object::bytesobject::w_bytes_from_bytes(&unsafe { code_bytes(obj) }) } "co_consts" => constants_tuple(obj, code), "co_names" => names_tuple(&code.names), @@ -1154,12 +1399,6 @@ pub unsafe fn code_get_field(obj: PyObjectRef, name: &str) -> Result w_int_new((*(obj as *const PyCode)).co_firstlineno_raw as i64), "co_linetable" => pyre_object::bytesobject::w_bytes_from_bytes(&code.linetable), "co_exceptiontable" => pyre_object::bytesobject::w_bytes_from_bytes(&code.exceptiontable), - // location.py `linetable2lnotab`, reconstructed from the - // canonical decoded positions kept on CodeObject. - "co_lnotab" => pyre_object::bytesobject::w_bytes_from_bytes(&legacy_lnotab( - code, - (*(obj as *const PyCode)).co_firstlineno_raw as i64, - )), _ => { return Err(crate::PyError::attribute_error(format!( "'code' object has no attribute '{name}'" @@ -1195,7 +1434,7 @@ pub unsafe fn code_new(args: &[PyObjectRef]) -> Result Result Result { } add_obj( &mut result, - pyre_object::bytesobject::w_bytes_from_bytes(&code.instructions.original_bytes()), + pyre_object::bytesobject::w_bytes_from_bytes(&unsafe { code_bytes(obj) }), )?; add_obj( &mut result, @@ -1493,17 +1736,77 @@ pub unsafe fn code_varname_from_oparg( /// invariant required by the object and pointer arguments for the entire call. pub unsafe fn code_positions(obj: PyObjectRef) -> Result { let code = unsafe { require_code(obj, "co_positions")? }; - let rows = code_locations(code) - .iter() - .map(|(start, end)| { - w_tuple_new(vec![ - w_int_new(start.line.get() as i64), - w_int_new(end.line.get() as i64), - w_int_new(start.character_offset.get().saturating_sub(1) as i64), - w_int_new(end.character_offset.get().saturating_sub(1) as i64), - ]) - }) - .collect::>(); + let mut rows = Vec::new(); + let mut reader = LineTableReader::new(&code.linetable); + let mut line = unsafe { (*(obj as *const PyCode)).co_firstlineno_raw }; + + while !reader.at_end() { + let Some(first_byte) = reader.read_byte() else { + break; + }; + // Decoded as a header regardless of bit 7, for the reason given on + // `PyCodeAddressRange::advance`. + let code = (first_byte >> 3) & 0x0f; + let length = ((first_byte & 0x07) + 1) as usize; + let Some(kind) = PyCodeLocationInfoKind::from_code(code) else { + break; + }; + let (line_delta, end_line_delta, column, end_column) = match kind { + PyCodeLocationInfoKind::None => (0, 0, None, None), + PyCodeLocationInfoKind::Long => { + let delta = reader.read_signed_varint(); + let end_line_delta = reader.read_varint() as i32; + let column = match reader.read_varint() { + 0 => None, + value => Some((value - 1) as i32), + }; + let end_column = match reader.read_varint() { + 0 => None, + value => Some((value - 1) as i32), + }; + (delta, end_line_delta, column, end_column) + } + PyCodeLocationInfoKind::NoColumns => (reader.read_signed_varint(), 0, None, None), + PyCodeLocationInfoKind::OneLine0 + | PyCodeLocationInfoKind::OneLine1 + | PyCodeLocationInfoKind::OneLine2 => { + let column = reader.read_byte().unwrap_or(0) as i32; + let end_column = reader.read_byte().unwrap_or(0) as i32; + ( + kind.one_line_delta().unwrap_or(0), + 0, + Some(column), + Some(end_column), + ) + } + _ if kind.is_short() => { + let column_data = reader.read_byte().unwrap_or(0); + let column_group = kind.short_column_group().unwrap_or(0); + let column = ((column_group as i32) << 3) | ((column_data >> 4) as i32); + let end_column = column + (column_data & 0x0f) as i32; + (0, 0, Some(column), Some(end_column)) + } + _ => (0, 0, None, None), + }; + line += line_delta; + + for _ in 0..length { + let (line_obj, end_line_obj) = if kind == PyCodeLocationInfoKind::None { + (pyre_object::w_none(), pyre_object::w_none()) + } else { + ( + w_int_new(line as i64), + w_int_new((line + end_line_delta) as i64), + ) + }; + rows.push(w_tuple_new(vec![ + line_obj, + end_line_obj, + column.map_or_else(pyre_object::w_none, |value| w_int_new(value as i64)), + end_column.map_or_else(pyre_object::w_none, |value| w_int_new(value as i64)), + ])); + } + } let n = rows.len(); Ok(w_seq_iter_new(w_list_new(rows), n)) } @@ -1513,21 +1816,44 @@ pub unsafe fn code_positions(obj: PyObjectRef) -> Result Result { let code = unsafe { require_code(obj, "co_lines")? }; - let locations = code_locations(code); let mut rows = Vec::new(); - let mut start = 0usize; - while start < locations.len() { - let line = locations[start].0.line.get(); - let mut end = start + 1; - while end < locations.len() && locations[end].0.line.get() == line { - end += 1; + let first_line = unsafe { (*(obj as *const PyCode)).co_firstlineno_raw }; + let mut range = PyCodeAddressRange::new(&code.linetable, first_line); + let mut pending: Option<(i32, i32, i32)> = None; + + while range.advance() { + let start = range.ar_start; + let end = range.ar_end; + let line = range.ar_line; + if let Some((previous_start, _, previous_line)) = pending { + if previous_line == line { + pending = Some((previous_start, end, previous_line)); + } else { + rows.push(w_tuple_new(vec![ + w_int_new(previous_start as i64), + w_int_new(start as i64), + if previous_line == -1 { + pyre_object::w_none() + } else { + w_int_new(previous_line as i64) + }, + ])); + pending = Some((start, end, line)); + } + } else { + pending = Some((start, end, line)); } + } + if let Some((start, end, line)) = pending { rows.push(w_tuple_new(vec![ - w_int_new((start * 2) as i64), - w_int_new((end * 2) as i64), - w_int_new(line as i64), + w_int_new(start as i64), + w_int_new(end as i64), + if line == -1 { + pyre_object::w_none() + } else { + w_int_new(line as i64) + }, ])); - start = end; } let n = rows.len(); Ok(w_seq_iter_new(w_list_new(rows), n)) @@ -1655,6 +1981,14 @@ pub unsafe fn code_replace(args: &[PyObjectRef]) -> Result Result Result Result, crat /// `co_code` bytes → the decoded `CodeUnits` instruction stream. The byte /// form is the `original_bytes` layout: one `(opcode, arg)` pair per unit. -unsafe fn read_code_units(v: PyObjectRef) -> Result { +unsafe fn read_code_units( + v: PyObjectRef, +) -> Result<(crate::bytecode::CodeUnits, Option>), crate::PyError> { if !unsafe { pyre_object::bytesobject::is_bytes_like(v) } { return Err(crate::PyError::type_error("co_code must be a bytes object")); } let bytes = unsafe { pyre_object::bytesobject::bytes_like_data(v) }; + decode_code_units(bytes) + .map_err(|()| crate::PyError::value_error("co_code length must be a multiple of 2")) +} + +/// Decode public `co_code` bytes into compiler-core's execution storage while +/// retaining an exact fallback for opcode values its enum cannot represent. +/// +/// The marshal runtime bag calls this same boundary, so `CodeType`, +/// `code.replace` and marshal loading cannot disagree about which bytes are +/// deferred to dispatch as `Instruction::Reserved`. +pub(crate) fn decode_code_units( + bytes: &[u8], +) -> Result<(crate::bytecode::CodeUnits, Option>), ()> { if bytes.len() % 2 != 0 { - return Err(crate::PyError::value_error( - "co_code length must be a multiple of 2", - )); + return Err(()); } let mut units = Vec::with_capacity(bytes.len() / 2); + let mut preserve_raw = false; for pair in bytes.chunks_exact(2) { - let op = crate::bytecode::Instruction::try_from(pair[0]).map_err(|_| { - crate::PyError::value_error(format!("co_code contains unknown opcode {}", pair[0])) - })?; - units.push(crate::bytecode::CodeUnit::new( - op, - crate::bytecode::OpArgByte::from(pair[1]), - )); + let (op, arg) = match crate::bytecode::Instruction::try_from(pair[0]) { + Ok(crate::bytecode::Instruction::Reserved) | Err(_) => { + preserve_raw = true; + // `CodeUnit` cannot carry an arbitrary opcode byte. Keep the + // exact public stream in `PyCode.co_code_bytes`, and carry the + // invalid opcode in the Reserved placeholder's otherwise + // meaningless arg so shared interpreter/JIT dispatch can + // report CPython's `unknown opcode N` at execution time. + ( + crate::bytecode::Instruction::Reserved, + crate::bytecode::OpArgByte::from(pair[0]), + ) + } + Ok(op) => (op, crate::bytecode::OpArgByte::from(pair[1])), + }; + units.push(crate::bytecode::CodeUnit::new(op, arg)); } - Ok(crate::bytecode::CodeUnits::from(units)) + Ok(( + crate::bytecode::CodeUnits::from(units), + preserve_raw.then(|| bytes.to_vec()), + )) } /// A `tuple` `co_consts` field → the compiler `Constants` backing table. @@ -2043,9 +2404,9 @@ pub(crate) unsafe fn obj_to_constant_data( } /// `pyopcode.py getconstant_w(index) -> co_consts_w[index]`: return the -/// one shared constant object the enclosing code holds at `index`, realizing -/// it into the slot on first access (`pycode.py:126` builds `co_consts_w` -/// eagerly; pyre's compiler representation is unwrapped). +/// one shared constant object the enclosing code holds at `index`. Normal +/// constructors filled the slot eagerly, matching `pycode.py`; realization +/// here is only a defensive fallback for a readable empty slot. /// /// `w_code_obj` is the enclosing `PyCode` (`frame.pycode` for the interpreter, /// the virtualizable `pycode` field for the blackhole), and `idx` is the @@ -2081,9 +2442,8 @@ pub unsafe fn w_code_const(w_code_obj: PyObjectRef, idx: usize) -> PyObjectRef { let Some(slot) = slot_table.get(idx) else { return pyre_object::pyobject::PY_NULL; }; - // PyPy's GIL serializes first access to its already-wrapped list. Pyre is - // free-threaded and realizes this compiler-boundary slot lazily, so every - // reader and writer uses the AtomicPtr element stored in co_consts_w. + // Normal slots are already filled. Keep the fallback free-thread safe for + // test stubs and alternate construction paths by retaining the AtomicPtr. let existing = slot.load(std::sync::atomic::Ordering::Acquire); if !existing.is_null() { return existing; @@ -2106,7 +2466,7 @@ pub unsafe fn w_code_const(w_code_obj: PyObjectRef, idx: usize) -> PyObjectRef { std::sync::atomic::Ordering::Acquire, ) { Ok(_) => { - pyre_object::gc_roots::mark_prebuilt_roots_dirty(); + publish_code_slot_store(w_code_obj); realized } Err(winner) => winner, @@ -2262,8 +2622,7 @@ pub unsafe fn fix_co_filename(w_code: PyObjectRef, newname: &[u8]) { let old_filename = unsafe { code_filename_bytes(w_code) }; // `importing.py update_code_filenames` mutates already-wrapped - // nested `PyCode` constants. Pyre realizes that list lazily, so update the - // filled slots now; unrealized children inherit when they are boxed. + // nested `PyCode` constants, selected by the root's original filename. let pycode = unsafe { &*(w_code as *const PyCode) }; if !pycode.co_consts_w.is_null() { for slot in unsafe { &*pycode.co_consts_w } { @@ -2353,7 +2712,7 @@ pub unsafe fn w_code_set_w_globals(obj: PyObjectRef, w_globals: PyObjectRef) { } // A bootstrap code slot is reached only by the prebuilt root walk, which // clean minor collections may skip; record the store. - pyre_object::gc_roots::mark_prebuilt_roots_dirty(); + publish_code_slot_store(obj); if !w_globals.is_null() { let code_ptr = unsafe { (*(obj as *const PyCode)).code_ptr }; register_live_code_wrapper(code_ptr, obj); @@ -2374,7 +2733,7 @@ pub unsafe fn w_code_frame_stores_global(obj: PyObjectRef, w_globals: PyObjectRe if code.w_globals.is_null() { code.w_globals = w_globals; // Prebuilt-family store (see `w_code_set_w_globals`). - pyre_object::gc_roots::mark_prebuilt_roots_dirty(); + publish_code_slot_store(obj); register_live_code_wrapper(code.code_ptr, obj); register_w_globals_stamped_code(obj); return false; @@ -2610,6 +2969,14 @@ pub unsafe fn pycode_destructor(obj_addr: usize) { drop(unsafe { Box::from_raw(code.co_names_w) }); code.co_names_w = std::ptr::null_mut(); } + if !code.filename_bytes.is_null() { + drop(unsafe { Box::from_raw(code.filename_bytes) }); + code.filename_bytes = std::ptr::null_mut(); + } + if !code.co_code_bytes.is_null() { + drop(unsafe { Box::from_raw(code.co_code_bytes) }); + code.co_code_bytes = std::ptr::null_mut(); + } } /// pycode.py `_compute_flatcall`. @@ -2840,14 +3207,16 @@ pub unsafe fn w_code_mapdict_caches_set( // The LOAD_METHOD fill (mapdict.py:1474) stores a movable // `w_method` reference; register this code object so // `walk_mapdict_method_cache_gc` forwards the slot. - if !entry.w_method.is_null() && !pyre_object::gc_hook::try_gc_owns_object(obj as *mut u8) { - mapdict_method_cache_codes() - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .insert(obj as usize); - // Prebuilt-family store: the slot is reached only by - // `walk_mapdict_method_cache_gc`, skipped on clean minors. - pyre_object::gc_roots::mark_prebuilt_roots_dirty(); + if !entry.w_method.is_null() { + if !pyre_object::gc_hook::try_gc_owns_object(obj as *mut u8) { + mapdict_method_cache_codes() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .insert(obj as usize); + } + // The slot is reached only by `walk_mapdict_method_cache_gc`, + // skipped on clean minors. + publish_code_slot_store(obj); } } } @@ -3028,6 +3397,37 @@ mod tests { } } + /// The two tables disagree on byte order: `co_exceptiontable` puts the + /// most significant 6-bit group first (`parse_varint`), `co_linetable` + /// the least significant one (`write_varint`). 70 therefore encodes + /// differently under each, and handing either reader the other's bytes + /// yields 385 — the failure a shared decoder would produce on every + /// multi-byte delta. + #[test] + fn the_two_varint_tables_use_opposite_byte_order() { + let exception_bytes = [0x41u8, 0x06]; + let location_bytes = [0x46u8, 0x01]; + + assert_eq!(decode_varint(&exception_bytes, 0), (70, 2)); + assert_eq!(LineTableReader::new(&location_bytes).read_varint(), 70); + + assert_eq!(decode_varint(&location_bytes, 0), (385, 2)); + assert_eq!(LineTableReader::new(&exception_bytes).read_varint(), 385); + } + + /// `code.replace(co_linetable=...)` accepts arbitrary bytes, so the + /// continuation chain can be longer than a u32 holds. Shifting by the full + /// width panics in a debug build, so the groups past it are dropped. + #[test] + fn a_location_varint_chain_past_the_word_width_does_not_panic() { + let overlong = [0x7fu8, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x00]; + let mut expected = 0x3fu32; + for shift in [6u32, 12, 18, 24, 30] { + expected |= 0x3fu32 << shift; + } + assert_eq!(LineTableReader::new(&overlong).read_varint(), expected); + } + #[test] fn empty_table_returns_none() { assert_eq!(lookup_exceptiontable(&[], 0), None); @@ -3141,8 +3541,18 @@ mod tests { .expect("large integer constant"); let w_code = box_code_constant(&code); + let eager = unsafe { + (&*(*(w_code as *const PyCode)).co_consts_w)[idx] + .load(std::sync::atomic::Ordering::Acquire) + }; + assert_ne!( + eager, + pyre_object::pyobject::PY_NULL, + "PyCode construction must eagerly wrap every compiler constant" + ); let first = unsafe { w_code_const(w_code, idx) }; let second = unsafe { w_code_const(w_code, idx) }; + assert_eq!(first, eager); assert_eq!( first, second, "getconstant_w must return the co_consts_w slot identity" @@ -3181,7 +3591,7 @@ mod tests { } #[test] - fn copy_const_slots_preserves_unrealized_source_slots() { + fn copy_const_slots_preserves_eager_source_identities() { let code = compile_exec( "x = 12345678901234567890123456789012345678901234567890\n\ y = 98765432109876543210987654321098765432109876543210\n", @@ -3201,24 +3611,24 @@ mod tests { }) .collect(); assert!(integer_indices.len() >= 2); - let realized_idx = integer_indices[0]; - let unrealized_idx = integer_indices[1]; + let first_idx = integer_indices[0]; + let second_idx = integer_indices[1]; let src = box_code_constant(&code); let dst = box_code_constant(&code); - let realized = unsafe { w_code_const(src, realized_idx) }; + let first = unsafe { w_code_const(src, first_idx) }; + let second = unsafe { w_code_const(src, second_idx) }; unsafe { w_code_copy_const_slots(dst, src); let dst_slots = &*(*(dst as *const PyCode)).co_consts_w; assert_eq!( - dst_slots[realized_idx].load(std::sync::atomic::Ordering::Acquire), - realized + dst_slots[first_idx].load(std::sync::atomic::Ordering::Acquire), + first ); - assert!( - dst_slots[unrealized_idx] - .load(std::sync::atomic::Ordering::Acquire) - .is_null(), - "code.replace must not eagerly realize a missing source constant" + assert_eq!( + dst_slots[second_idx].load(std::sync::atomic::Ordering::Acquire), + second, + "code.replace must preserve every eager co_consts_w identity" ); } } @@ -3301,7 +3711,7 @@ mod tests { } #[test] - fn w_code_const_first_publication_is_free_threaded_identity_safe() { + fn w_code_const_reads_are_free_threaded_identity_safe() { let code = compile_exec("x = 314159265358979323846264338327950288419716939937510582097494\n") .expect("compile failed"); @@ -3317,6 +3727,16 @@ mod tests { }) .expect("large integer constant"); let w_code = box_code_constant(&code) as usize; + // The constructor publishes every slot eagerly, so readers would all + // observe that one store and never reach the path the identity + // guarantee actually rests on. Clear the slot under test first: each + // worker then realizes its own candidate and races to install it, and + // the assertion below becomes a statement about the CAS rather than + // about the constructor. + let pycode = unsafe { &*(w_code as *const PyCode) }; + assert!(!pycode.co_consts_w.is_null(), "wrapped constant array"); + let slots = unsafe { &*pycode.co_consts_w }; + slots[idx].store(std::ptr::null_mut(), std::sync::atomic::Ordering::Release); let barrier = std::sync::Arc::new(std::sync::Barrier::new(8)); let mut workers = Vec::new(); for _ in 0..8 { @@ -3333,7 +3753,7 @@ mod tests { assert!(values[0] != 0); assert!( values.iter().all(|value| *value == values[0]), - "the co_consts_w CAS must select one canonical wrapper" + "all readers must observe one canonical co_consts_w wrapper" ); } } diff --git a/pyre/pyre-interpreter/src/pyopcode.rs b/pyre/pyre-interpreter/src/pyopcode.rs index be86546d95a..1f3e00efde1 100644 --- a/pyre/pyre-interpreter/src/pyopcode.rs +++ b/pyre/pyre-interpreter/src/pyopcode.rs @@ -216,7 +216,13 @@ pub fn decode_instruction_for_dispatch( decode_instruction_at(code, opcode_pc).ok_or(crate::pycode::BytecodeCorruption)?; instruction = decoded.0; op_arg = decoded.1; - if !matches!(instruction, Instruction::ExtendedArg) && u8::from(instruction) < 44 { + // `Reserved` is excluded for the same reason the forward decoder + // excludes it: an unknown opcode byte is preserved here so dispatch + // reaches it and reports `SystemError: unknown opcode N`. + if !matches!(instruction, Instruction::ExtendedArg) + && u8::from(instruction) < 44 + && !matches!(instruction, Instruction::Reserved) + { return Err(crate::pycode::BytecodeCorruption); } } @@ -277,7 +283,10 @@ pub fn decode_instruction_forward( opcode_pc += 1; continue; } - if opcode_pc != start && u8::from(instruction) < 44 { + if opcode_pc != start + && u8::from(instruction) < 44 + && !matches!(instruction, Instruction::Reserved) + { return Err(crate::pycode::BytecodeCorruption); } return Ok((opcode_pc, instruction, op_arg)); @@ -3511,6 +3520,14 @@ where + ArithmeticOpcodeHandler, { match instruction { + // CPython 3.14 accepts unknown bytes in `CodeType` / `code.replace` + // and reports them only when dispatch reaches the instruction. The + // constructor stores the raw opcode byte in this placeholder's low + // arg byte because compiler-core's enum cannot represent it. + Instruction::Reserved => Err(PyError::new( + crate::PyErrorKind::SystemError, + format!("unknown opcode {}", u32::from(op_arg) & 0xff), + )), Instruction::ExtendedArg | Instruction::Resume { .. } | Instruction::Nop @@ -3934,6 +3951,27 @@ mod tests { assert!(decode_instruction_for_dispatch(&code, 0).is_err()); } + /// An opcode byte the enum cannot spell decodes to `Reserved`, and the + /// `unknown opcode N` report is raised by dispatch rather than by the + /// decoder, so an `ExtendedArg` prefix in front of one must not be + /// rejected as a malformed chain. + #[test] + fn decode_instruction_for_dispatch_keeps_reserved_after_extended_arg() { + let code = compile_exec("x = 1").expect("compile failed"); + assert!( + code.instructions.len() >= 2, + "expected at least two instructions" + ); + unsafe { + code.instructions.replace_op(0, Instruction::ExtendedArg); + code.instructions.replace_op(1, Instruction::Reserved); + } + let (opcode_pc, instruction, _) = + decode_instruction_for_dispatch(&code, 0).expect("Reserved stays decodable"); + assert_eq!(opcode_pc, 1); + assert!(matches!(instruction, Instruction::Reserved)); + } + #[test] fn forward_decode_matches_full_scan_and_dispatch() { let source = (0..400) diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index 3758590e8bf..eca667e50e7 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -369,14 +369,18 @@ pub fn init_typeobjects() { ); // array.array — interp_array.py, bases=(object,) + let array_type = new_typeobject_with_base_and_layout( + "array.array", + crate::module::array::init_array_type, + object_type, + &pyre_object::interp_array::ARRAY_TYPE as *const PyType, + ); + // CPython 3.14 Modules/arraymodule.c:array_modexec uses + // PyType_FromModuleAndSpec; array_spec carries IMMUTABLETYPE. + mark_cpython_heap_type(array_type, true); reg.insert( &pyre_object::interp_array::ARRAY_TYPE as *const PyType as usize, - new_typeobject_with_base_and_layout( - "array.array", - crate::module::array::init_array_type, - object_type, - &pyre_object::interp_array::ARRAY_TYPE as *const PyType, - ) as usize, + array_type as usize, ); // bool — boolobject.py, bases=(int,) @@ -853,6 +857,9 @@ pub fn init_typeobjects() { crate::module::_sre::interp_sre::init_sre_pattern_type, object_type, ); + // CPython 3.14 Modules/_sre/sre.c:sre_exec creates the Pattern spec + // with PyType_FromModuleAndSpec and IMMUTABLETYPE. + mark_cpython_heap_type(sre_pattern_type, true); unsafe { pyre_object::w_type_set_acceptable_as_base_class(sre_pattern_type, false) }; reg.insert( &pyre_object::interp_sre::SRE_PATTERN_TYPE as *const PyType as usize, @@ -863,6 +870,8 @@ pub fn init_typeobjects() { crate::module::_sre::interp_sre::init_sre_match_type, object_type, ); + // Same `sre_exec` immutable heap owner as Pattern. + mark_cpython_heap_type(sre_match_type, true); unsafe { pyre_object::w_type_set_acceptable_as_base_class(sre_match_type, false) }; reg.insert( &pyre_object::interp_sre::SRE_MATCH_TYPE as *const PyType as usize, @@ -877,6 +886,8 @@ pub fn init_typeobjects() { crate::module::_sre::interp_sre::init_sre_scanner_type, object_type, ); + // Same `sre_exec` immutable heap owner as Pattern. + mark_cpython_heap_type(sre_scanner_type, true); unsafe { pyre_object::w_type_set_acceptable_as_base_class(sre_scanner_type, false) }; reg.insert( &pyre_object::interp_sre::SRE_SCANNER_TYPE as *const PyType as usize, @@ -1161,6 +1172,14 @@ pub fn init_typeobjects() { ), ] { let iterator_type = new_typeobject_with_base(name, init, object_type); + if std::ptr::eq( + pytype, + &pyre_object::iterobject::ARRAY_ITER_TYPE as *const PyType, + ) { + // CPython 3.14 Modules/arraymodule.c creates arrayiter_spec + // through PyType_FromModuleAndSpec with IMMUTABLETYPE. + mark_cpython_heap_type(iterator_type, true); + } unsafe { pyre_object::w_type_set_disallow_instantiation(iterator_type); pyre_object::w_type_set_acceptable_as_base_class(iterator_type, false); @@ -1504,6 +1523,40 @@ pub fn init_typeobjects() { &pyre_object::interp_itertools::CHAIN_TYPE as *const PyType, ) as usize, ); + // [3.14-spec] CPython 3.14 Modules/itertoolsmodule.c:itertools_exec + // creates every spec in `typelist` through PyType_FromModuleAndSpec. + // Each spec includes IMMUTABLETYPE. PyPy keeps the same classes as + // builtin TypeDefs; publish the CPython owner on those exact registry + // rows without changing their internal `flag_heaptype`. + for pytype in [ + &pyre_object::interp_itertools::COUNT_TYPE as *const PyType, + &pyre_object::interp_itertools::REPEAT_TYPE as *const PyType, + &pyre_object::interp_itertools::TAKEWHILE_TYPE as *const PyType, + &pyre_object::interp_itertools::DROPWHILE_TYPE as *const PyType, + &pyre_object::interp_itertools::FILTERFALSE_TYPE as *const PyType, + &pyre_object::interp_itertools::ISLICE_TYPE as *const PyType, + &pyre_object::interp_itertools::BATCHED_TYPE as *const PyType, + &pyre_object::interp_itertools::PRODUCT_TYPE as *const PyType, + &pyre_object::interp_itertools::COMBINATIONS_TYPE as *const PyType, + &pyre_object::interp_itertools::COMBINATIONS_WITH_REPLACEMENT_TYPE + as *const PyType, + &pyre_object::interp_itertools::PERMUTATIONS_TYPE as *const PyType, + &pyre_object::interp_itertools::GROUPBY_TYPE as *const PyType, + &pyre_object::interp_itertools::GROUPBY_ITERATOR_TYPE as *const PyType, + &pyre_object::interp_itertools::TEE_DATAOBJECT_TYPE as *const PyType, + &pyre_object::interp_itertools::TEE_ITERABLE_TYPE as *const PyType, + &pyre_object::interp_itertools::COMPRESS_TYPE as *const PyType, + &pyre_object::interp_itertools::STARMAP_TYPE as *const PyType, + &pyre_object::interp_itertools::ACCUMULATE_TYPE as *const PyType, + &pyre_object::interp_itertools::ZIP_LONGEST_TYPE as *const PyType, + &pyre_object::interp_itertools::PAIRWISE_TYPE as *const PyType, + &pyre_object::interp_itertools::CYCLE_TYPE as *const PyType, + &pyre_object::interp_itertools::CHAIN_TYPE as *const PyType, + ] { + if let Some(&type_obj) = reg.get(&(pytype as usize)) { + mark_cpython_heap_type(type_obj as PyObjectRef, true); + } + } // `pypy/objspace/std/specialisedtupleobject.py` — three SpecialisedTuple // variants share the public `tuple` PyType name, so all three // foreign statics map to a "tuple" typedef. `gettypefor` keys @@ -1527,6 +1580,106 @@ pub fn init_typeobjects() { new_typeobject_with_base("tuple", |_| {}, object_type) as usize, ); + // TypeDef constructor signatures live on W_TypeObject itself, not on + // the `__new__` descriptor. PyPy supplies this field explicitly for + // list (`listobject.py:2459`) and the two functional types + // (`functional.py:341,434`). Python 3.14 publishes the additional + // signatures below; pyre targets that newer public surface when it + // differs from the vendored PyPy 3.11 sources. Types omitted here + // intentionally expose None (for example int, str, bytes and dict). + for (pytype, signature) in [ + ( + &pyre_object::BOOL_TYPE as *const PyType, + "(object=False, /)", + ), + (&pyre_object::FLOAT_TYPE as *const PyType, "(x=0, /)"), + ( + &pyre_object::COMPLEX_TYPE as *const PyType, + "(real=0, imag=0)", + ), + ( + &pyre_object::memoryview::MEMORYVIEW_TYPE as *const PyType, + "(object)", + ), + (&pyre_object::LIST_TYPE as *const PyType, "(iterable=(), /)"), + ( + &pyre_object::TUPLE_TYPE as *const PyType, + "(iterable=(), /)", + ), + ( + &pyre_object::setobject::SET_TYPE as *const PyType, + "(iterable=(), /)", + ), + ( + &pyre_object::setobject::FROZENSET_TYPE as *const PyType, + "(iterable=(), /)", + ), + ( + &pyre_object::descriptor::PROPERTY_TYPE as *const PyType, + "(fget=None, fset=None, fdel=None, doc=None)", + ), + ( + &pyre_object::functional::ENUMERATE_TYPE as *const PyType, + "(iterable, start=0)", + ), + ( + &pyre_object::functional::REVERSED_TYPE as *const PyType, + "(sequence, /)", + ), + ( + &pyre_object::functional::MAP_TYPE as *const PyType, + "(function, iterable, /, *iterables, strict=False)", + ), + ( + &pyre_object::functional::FILTER_TYPE as *const PyType, + "(function, iterable, /)", + ), + ( + &pyre_object::functional::ZIP_TYPE as *const PyType, + "(*iterables, strict=False)", + ), + // The internal object families 3.14 also publishes a signature + // for. Each is constructible from Python, so `inspect.signature` + // resolves against these rather than falling back to `(*args, + // **kwargs)`. + (&pyre_object::ELLIPSIS_TYPE as *const PyType, "()"), + ( + &pyre_object::GENERIC_ALIAS_TYPE as *const PyType, + "(origin, args, /)", + ), + ( + &pyre_object::pyobject::NOTIMPLEMENTED_TYPE as *const PyType, + "()", + ), + (&pyre_object::MODULE_TYPE as *const PyType, "(name, doc=None)"), + ( + &pyre_object::MAPPING_PROXY_TYPE as *const PyType, + "(mapping)", + ), + ( + &pyre_object::nestedscope::CELL_TYPE as *const PyType, + "([contents])", + ), + ( + &pyre_object::function::METHOD_TYPE as *const PyType, + "(function, instance, /)", + ), + ( + &crate::function::FUNCTION_TYPE as *const PyType, + "(code, globals, name=None, argdefs=None, closure=None,\n kwdefaults=None)", + ), + ( + &crate::pycode::CODE_TYPE as *const PyType, + "(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize,\n flags, codestring, constants, names, varnames, filename, name,\n qualname, firstlineno, linetable, exceptiontable, freevars=(),\n cellvars=(), /)", + ), + ] { + let w_typeobject = *reg + .get(&(pytype as usize)) + .expect("text-signature type must already be registered") + as PyObjectRef; + unsafe { pyre_object::w_type_set_text_signature(w_typeobject, signature) }; + } + // rclass.py:739-743 parity — cache W_TypeObject on each PyType // so allocators can set w_class at allocation time (like RPython's // `self.setfield(vptr, '__class__', ctypeptr, llops)` in new_instance). @@ -1586,6 +1739,9 @@ pub fn init_typeobjects() { &pyre_object::pyobject::NOTIMPLEMENTED_TYPE, &pyre_object::ELLIPSIS_TYPE, &pyre_object::functional::RANGE_TYPE, + &crate::pycode::CODE_TYPE, + &pyre_object::functional::RANGE_ITER_TYPE, + &pyre_object::functional::LONG_RANGE_ITER_TYPE, ] { let w_typeobject = *reg .get(&(pytype as *const PyType as usize)) @@ -2522,7 +2678,7 @@ pub fn make_builtin_type_with_bases( newslotnames: vec![], base_layout: parent_layout, acceptable_as_base_class: has_new, - typedef_hasdict: false, + typedef_hasdict: has_dict, }) }; pyre_object::w_type_set_layout(type_obj, layout); @@ -2581,6 +2737,24 @@ pub fn make_builtin_type_with_layout( new_typeobject_with_base_and_layout(name, init, base, layout_pytype) } +/// [3.14-spec] Project a CPython `PyType_From*Spec` owner onto a PyPy builtin +/// `TypeDef`. The latter remains `flag_heaptype = False`; only the public +/// HEAPTYPE / STATIC_BUILTIN / IMMUTABLETYPE axes change. Each caller cites +/// the CPython 3.14 construction site that requires this adaptation. +pub fn mark_cpython_heap_type(type_obj: PyObjectRef, immutable: bool) { + unsafe { + pyre_object::w_type_set_cpython_type_flags(type_obj, true, false, immutable); + } +} + +/// The public `PyType_Ready` legacy-extension shape: neither a CPython core +/// STATIC_BUILTIN nor a heap allocation, but necessarily immutable in 3.14. +pub fn mark_cpython_static_extension_type(type_obj: PyObjectRef) { + unsafe { + pyre_object::w_type_set_cpython_type_flags(type_obj, false, false, true); + } +} + /// int.__new__(cls, *args) — PyPy: intobject.py descr__new__ /// /// If cls is the builtin int type, returns a plain W_IntObject. @@ -10375,6 +10549,18 @@ fn readonly_attribute(descr: pyre_object::PyObjectRef) -> crate::PyError { } } +/// CPython 3.14 `slotdefs[]`'s single `tp_descr_get` wrapper definition. +/// Every builtin descriptor type below publishes the same positional-only +/// surface; keeping it here mirrors the one `TPSLOT(__get__, ...)` row rather +/// than duplicating its signature at each PyPy TypeDef registration. +fn make_descr_get_builtin(func: crate::gateway::BuiltinCodeFn) -> PyObjectRef { + crate::gateway::make_builtin_function_with_text_signature( + "__get__", + func, + "($self, instance, owner=None, /)", + ) +} + /// typedef.py GetSetProperty.typedef = TypeDef("getset_descriptor", ...) fn init_getset_descriptor_type(ns: PyObjectRef) { // typedef.py GetSetProperty.descr_property_get @@ -10404,7 +10590,7 @@ fn init_getset_descriptor_type(ns: PyObjectRef) { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, "__get__", - make_builtin_function("__get__", |args| { + make_descr_get_builtin(|args| { crate::type_methods::arity_at_least(args, "__get__", 1)?; crate::type_methods::arity_at_most(args, "__get__", 2)?; let w_self = args[0]; @@ -10937,13 +11123,8 @@ fn make_getset_property_full( /// GC ownership instead answers a different question — pyre's nursery owns /// `object()` and `b""`, which `object` and `bytes` never declare. /// -/// Nothing consumes it at present. `sys.getsizeof` used to, for the -/// `PyGC_Head` that `_PyType_PreHeaderSize` charges a tracked instance, but a -/// build without a global interpreter lock keeps the collector's bits in the -/// object header and so charges none. The flag itself remains the answer -/// `type.__flags__` owes for `Py_TPFLAGS_HAVE_GC`, which it does not yet -/// report. -#[allow(dead_code)] +/// `gc.is_tracked` consumes it here, and `type.__flags__` publishes the same +/// canonical field as `Py_TPFLAGS_HAVE_GC`. pub(crate) fn cpython_object_is_gc(w_obj: PyObjectRef) -> bool { let Some(tp) = r#type(w_obj) else { return false; @@ -10955,57 +11136,16 @@ pub(crate) fn cpython_object_is_gc(w_obj: PyObjectRef) -> bool { // with `Py_TPFLAGS_HEAPTYPE`: a statically allocated type object is never // collected, while one built by `type_new` is. if unsafe { pyre_object::is_type(w_obj) } { - return unsafe { pyre_object::w_type_is_heaptype(w_obj) }; + return unsafe { pyre_object::w_type_is_cpython_heaptype(w_obj) }; } true } -/// Whether `w_type` declares `Py_TPFLAGS_HAVE_GC`. -/// -/// A type carries the flag when its instances can hold a reference that joins -/// a cycle, which is nearly all of them, so what is enumerated here is the -/// complement: the scalars, the buffers and the singletons. `type_new` sets -/// the flag on every heap type. -/// -/// Two matches are needed because a builtin only gets its own Layout when it -/// has a payload of its own. `int` and `bytes` do, so their layout identifies -/// them; `NoneType` and `code` do not, and share the base instance layout with -/// `function`, `generator`, the iterators and the dict views — all of which do -/// declare the flag. Those are matched on the type object instead. -#[allow(dead_code)] +/// Whether `w_type` declares `Py_TPFLAGS_HAVE_GC`. Registration initializes +/// the canonical field once; all readers, including `type.__flags__`, must +/// consult that field rather than rebuilding a parallel type census. fn cpython_type_has_gc_flag(w_type: PyObjectRef) -> bool { - if w_type.is_null() || !unsafe { pyre_object::is_type(w_type) } { - return false; - } - if unsafe { pyre_object::w_type_is_heaptype(w_type) } { - return true; - } - let same_type = |tp: &PyType| std::ptr::eq(w_type, gettypeobject(tp)); - // `object` holds nothing; the singletons are one instance each; a code - // object's references are all reachable from the function that owns it; - // and both range iterators count, one in machine words and one in ints - // that cannot themselves reference anything. - if std::ptr::eq(w_type, w_object()) - || same_type(&pyre_object::NONE_TYPE) - || same_type(&pyre_object::NOTIMPLEMENTED_TYPE) - || same_type(&pyre_object::ELLIPSIS_TYPE) - || same_type(&crate::pycode::CODE_TYPE) - || same_type(&pyre_object::functional::RANGE_ITER_TYPE) - || same_type(&pyre_object::functional::LONG_RANGE_ITER_TYPE) - { - return false; - } - let layout = unsafe { pyre_object::w_type_get_layout(w_type) }; - let is = |candidate: *const PyType| std::ptr::eq(layout, candidate); - !(is(&pyre_object::INT_TYPE) - || is(&pyre_object::LONG_TYPE) - || is(&pyre_object::BOOL_TYPE) - || is(&pyre_object::FLOAT_TYPE) - || is(&pyre_object::COMPLEX_TYPE) - || is(&pyre_object::STR_TYPE) - || is(&pyre_object::bytesobject::BYTES_TYPE) - || is(&pyre_object::bytearrayobject::BYTEARRAY_TYPE) - || is(&pyre_object::functional::RANGE_TYPE)) + unsafe { pyre_object::w_type_get_have_gc(w_type) } } /// Logical CPython 3.14 `tp_basicsize` / `tp_itemsize` values ported so far. @@ -11130,7 +11270,7 @@ fn cpython_type_offsets(w_type: PyObjectRef) -> Option<(i64, i64)> { // Without the lock that pre-header is the managed dict pair alone — the // collector keeps its bits in the object header instead of a `PyGC_Head` // ahead of it — so the weakref word sits two words back, not four. - if unsafe { pyre_object::w_type_is_heaptype(w_type) } { + if unsafe { pyre_object::w_type_is_cpython_heaptype(w_type) } { if dict == 0 && unsafe { pyre_object::w_type_get_hasdict(w_type) } { dict = -1; } @@ -11170,6 +11310,14 @@ fn type_weakrefoffset_getter(args: &[PyObjectRef]) -> Result() as i64; - let size = if pyre_object::w_type_is_heaptype(args[0]) { + let size = if pyre_object::w_type_is_cpython_heaptype(args[0]) { // CPython 3.14 typeobject.c:type___sizeof___impl: // PyHeapTypeObject plus the cached-keys table carried // by a managed instance dictionary. The struct sizes @@ -11581,7 +11729,7 @@ fn init_type_type(ns: PyObjectRef) { "descriptor '__abstractmethods__' for 'type' objects doesn't apply", )); } - if !unsafe { pyre_object::w_type_is_heaptype(w_type) } { + if unsafe { pyre_object::w_type_is_cpython_immutabletype(w_type) } { return Err(crate::PyError::type_error(format!( "cannot set '__abstractmethods__' attribute of immutable type '{}'", unsafe { pyre_object::w_type_get_name(w_type) }, @@ -11619,7 +11767,7 @@ fn init_type_type(ns: PyObjectRef) { "descriptor '__abstractmethods__' for 'type' objects doesn't apply", )); } - if !unsafe { pyre_object::w_type_is_heaptype(w_type) } { + if unsafe { pyre_object::w_type_is_cpython_immutabletype(w_type) } { return Err(crate::PyError::type_error(format!( "cannot delete '__abstractmethods__' attribute of immutable type '{}'", unsafe { pyre_object::w_type_get_name(w_type) }, @@ -11983,7 +12131,7 @@ fn init_type_type(ns: PyObjectRef) { // type derives it from the qualified name. `lookup_in_type` // filters out null entries but preserves `w_none()`, matching // PyPy's "value present even if it's None" semantic. - if unsafe { pyre_object::w_type_is_heaptype(cls) } + if unsafe { pyre_object::w_type_is_cpython_heaptype(cls) } && let Some(v) = crate::type_dict_lookup(cls, "__module__") && !v.is_null() { @@ -12012,7 +12160,7 @@ fn init_type_type(ns: PyObjectRef) { check_set_special_type_attr(cls, value, "__module__")?; unsafe { if pyre_object::is_type(cls) { - if !pyre_object::w_type_is_heaptype(cls) { + if pyre_object::w_type_is_cpython_immutabletype(cls) { let name = pyre_object::w_type_get_name(cls); return Err(crate::PyError::type_error(format!( "cannot set '__module__' attribute of immutable type '{name}'" @@ -12314,7 +12462,7 @@ fn check_set_special_type_attr( w_value: PyObjectRef, name: &str, ) -> Result<(), crate::PyError> { - let immutable = !unsafe { pyre_object::w_type_is_heaptype(w_type) }; + let immutable = unsafe { pyre_object::w_type_is_cpython_immutabletype(w_type) }; if !immutable && !w_value.is_null() { return Ok(()); } @@ -14785,7 +14933,7 @@ fn init_slot_wrapper_type(ns: PyObjectRef) { pyre_object::w_dict_setitem_str_no_proxy( ns, "__get__", - make_builtin_function("__get__", |args| wrap_descr_get(args, bind_slot_wrapper)), + make_descr_get_builtin(|args| wrap_descr_get(args, bind_slot_wrapper)), ); pyre_object::w_dict_setitem_str_no_proxy( ns, @@ -15185,9 +15333,7 @@ fn init_method_descriptor_type(ns: PyObjectRef) { pyre_object::w_dict_setitem_str_no_proxy( ns, "__get__", - make_builtin_function("__get__", |args| { - wrap_descr_get(args, bind_method_descriptor) - }), + make_descr_get_builtin(|args| wrap_descr_get(args, bind_method_descriptor)), ); pyre_object::w_dict_setitem_str_no_proxy( ns, @@ -15350,9 +15496,7 @@ fn init_classmethod_descriptor_type(ns: PyObjectRef) { pyre_object::w_dict_setitem_str_no_proxy( ns, "__get__", - make_builtin_function("__get__", |args| { - wrap_descr_get(args, bind_classmethod_descriptor) - }), + make_descr_get_builtin(|args| wrap_descr_get(args, bind_classmethod_descriptor)), ); pyre_object::w_dict_setitem_str_no_proxy( ns, @@ -15800,7 +15944,7 @@ fn init_member_descriptor_type(ns: PyObjectRef) { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, "__get__", - make_builtin_function("__get__", |args| { + make_descr_get_builtin(|args| { let descr = args.first().copied().unwrap_or(pyre_object::PY_NULL); if descr.is_null() || !unsafe { pyre_object::typedef::is_member(descr) } { return Ok(pyre_object::w_none()); @@ -18111,9 +18255,11 @@ fn init_int_type(ns: PyObjectRef) { // intobject.py descr_repr. CPython 3.14 inherits object.__str__, whose // implementation delegates virtually to this repr slot. let int_to_text = |args: &[PyObjectRef]| { - Ok(pyre_object::w_str_new(&unsafe { - crate::builtins::int_to_decimal_string(args[0])? - })) + let text = unsafe { crate::builtins::int_to_decimal_string(args[0])? }; + // `args[0]` is fully consumed above. This terminal `space.newtext` + // allocation can therefore use the ordinary movable header PyPy gets + // from `StdObjSpace.newutf8`, with no unrooted operand live across it. + Ok(unsafe { pyre_object::w_str_new_managed_collecting(&text) }) }; unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( @@ -27275,6 +27421,15 @@ fn init_set_iterator_type(ns: PyObjectRef) { for (name, value) in entries { unsafe { pyre_object::w_dict_setitem_str_no_proxy(ns, name, value) }; } + set_iterator_text_signatures( + ns, + &[ + ("__iter__", "($self, /)"), + ("__next__", "($self, /)"), + ("__length_hint__", "($self, /)"), + ("__reduce__", "($self, /)"), + ], + ); } fn generator_frame(obj: PyObjectRef) -> *mut crate::pyframe::PyFrame { @@ -27964,6 +28119,20 @@ fn init_coroutine_wrapper_type(ns: PyObjectRef) { } } +/// Attach `__text_signature__` to callables an iterator namespace already holds. +/// +/// The TypeDefs remain the owner of the concrete methods; 3.14's slot wrappers +/// and Argument Clinic descriptors additionally carry these introspection +/// strings, so this fills the corresponding Function field without replacing +/// the carriers. +fn set_iterator_text_signatures(ns: PyObjectRef, signatures: &[(&'static str, &'static str)]) { + for &(name, text_signature) in signatures { + let function = unsafe { pyre_object::w_dict_getitem_str(ns, name) } + .expect("iterator TypeDef callable was just installed"); + unsafe { crate::function::fset_func_text_signature(function, w_str_new(text_signature)) }; + } +} + /// PyPy `iterobject.py W_AbstractSeqIterObject.typedef`. fn init_sequence_iterator_type(ns: PyObjectRef) { // PyPy carries the `iter()` builtin documentation on the abstract typedef; @@ -28006,6 +28175,16 @@ fn init_sequence_iterator_type(ns: PyObjectRef) { for (name, value) in entries { unsafe { pyre_object::w_dict_setitem_str_no_proxy(ns, name, value) }; } + set_iterator_text_signatures( + ns, + &[ + ("__iter__", "($self, /)"), + ("__next__", "($self, /)"), + ("__reduce__", "($self, /)"), + ("__length_hint__", "($self, /)"), + ("__setstate__", "($self, object, /)"), + ], + ); } /// Python 3.14 `PySeqIter_Type` restricted to `memory_iterator`'s surface: @@ -28028,6 +28207,10 @@ fn init_memory_iterator_type(ns: PyObjectRef) { for (name, value) in entries { unsafe { pyre_object::w_dict_setitem_str_no_proxy(ns, name, value) }; } + set_iterator_text_signatures( + ns, + &[("__iter__", "($self, /)"), ("__next__", "($self, /)")], + ); } /// `arrayiterator`'s Python 3.14 surface: the iteration protocol plus the @@ -28065,12 +28248,22 @@ fn init_array_iterator_type(ns: PyObjectRef) { for (name, value) in entries { unsafe { pyre_object::w_dict_setitem_str_no_proxy(ns, name, value) }; } + set_iterator_text_signatures( + ns, + &[ + ("__iter__", "($self, /)"), + ("__next__", "($self, /)"), + ("__reduce__", "($self, /)"), + ("__setstate__", "($self, state, /)"), + ], + ); } /// Python 3.14 `PyCallIter_Type` (`callable_iterator`) surface. PyPy 3.11's /// `_CallableIterator` is app-level and has only the iteration methods; 3.14 /// additionally exposes the native pickle reduction hook. fn init_callable_iterator_type(ns: PyObjectRef) { + unsafe { pyre_object::w_dict_setitem_str(ns, "__doc__", pyre_object::w_none()) }; for (name, function) in [ ( "__iter__", @@ -28090,6 +28283,14 @@ fn init_callable_iterator_type(ns: PyObjectRef) { ) }; } + set_iterator_text_signatures( + ns, + &[ + ("__iter__", "($self, /)"), + ("__next__", "($self, /)"), + ("__reduce__", "($self, /)"), + ], + ); } fn dict_iterator_receiver( @@ -28177,6 +28378,15 @@ macro_rules! define_dict_iterator_type { for (name, value) in entries { unsafe { pyre_object::w_dict_setitem_str_no_proxy(ns, name, value) }; } + set_iterator_text_signatures( + ns, + &[ + ("__iter__", "($self, /)"), + ("__next__", "($self, /)"), + ("__length_hint__", "($self, /)"), + ("__reduce__", "($self, /)"), + ], + ); } }; } @@ -28312,6 +28522,16 @@ fn init_range_iterator_type(ns: PyObjectRef) { for (name, value) in entries { unsafe { pyre_object::w_dict_setitem_str_no_proxy(ns, name, value) }; } + set_iterator_text_signatures( + ns, + &[ + ("__iter__", "($self, /)"), + ("__length_hint__", "($self, /)"), + ("__next__", "($self, /)"), + ("__reduce__", "($self, /)"), + ("__setstate__", "($self, object, /)"), + ], + ); } /// Python 3.14 exposes the arbitrary-precision implementation as the distinct @@ -28343,6 +28563,16 @@ fn init_long_range_iterator_type(ns: PyObjectRef) { for (name, value) in entries { unsafe { pyre_object::w_dict_setitem_str_no_proxy(ns, name, value) }; } + set_iterator_text_signatures( + ns, + &[ + ("__iter__", "($self, /)"), + ("__length_hint__", "($self, /)"), + ("__next__", "($self, /)"), + ("__reduce__", "($self, /)"), + ("__setstate__", "($self, object, /)"), + ], + ); } fn init_list_iterator_type(ns: PyObjectRef) { @@ -28384,6 +28614,16 @@ fn init_list_iterator_type(ns: PyObjectRef) { for (name, value) in entries { unsafe { pyre_object::w_dict_setitem_str_no_proxy(ns, name, value) }; } + set_iterator_text_signatures( + ns, + &[ + ("__iter__", "($self, /)"), + ("__next__", "($self, /)"), + ("__length_hint__", "($self, /)"), + ("__reduce__", "($self, /)"), + ("__setstate__", "($self, object, /)"), + ], + ); } fn init_list_reverse_iterator_type(ns: PyObjectRef) { @@ -28425,6 +28665,16 @@ fn init_list_reverse_iterator_type(ns: PyObjectRef) { for (name, value) in entries { unsafe { pyre_object::w_dict_setitem_str_no_proxy(ns, name, value) }; } + set_iterator_text_signatures( + ns, + &[ + ("__iter__", "($self, /)"), + ("__next__", "($self, /)"), + ("__length_hint__", "($self, /)"), + ("__reduce__", "($self, /)"), + ("__setstate__", "($self, object, /)"), + ], + ); } fn init_tuple_iterator_type(ns: PyObjectRef) { @@ -28466,6 +28716,16 @@ fn init_tuple_iterator_type(ns: PyObjectRef) { for (name, value) in entries { unsafe { pyre_object::w_dict_setitem_str_no_proxy(ns, name, value) }; } + set_iterator_text_signatures( + ns, + &[ + ("__iter__", "($self, /)"), + ("__next__", "($self, /)"), + ("__length_hint__", "($self, /)"), + ("__reduce__", "($self, /)"), + ("__setstate__", "($self, object, /)"), + ], + ); } // ── itertools.count / itertools.repeat TypeDefs ───────────────────── @@ -30832,6 +31092,540 @@ mod tests { } } + #[test] + fn type_flags_publish_cpython_314_have_gc_from_the_canonical_field() { + crate::typedef::init_typeobjects(); + const HAVE_GC: i64 = 1 << 14; + let cases = [ + ("object", crate::typedef::w_object(), false), + ("type", crate::typedef::w_type(), true), + ( + "int", + crate::typedef::gettypeobject(&pyre_object::INT_TYPE), + false, + ), + ( + "bytearray", + crate::typedef::gettypeobject(&pyre_object::bytearrayobject::BYTEARRAY_TYPE), + false, + ), + ( + "tuple", + crate::typedef::gettypeobject(&pyre_object::TUPLE_TYPE), + true, + ), + ( + "dict", + crate::typedef::gettypeobject(&pyre_object::DICT_TYPE), + true, + ), + ( + "function", + crate::typedef::gettypeobject(&crate::function::FUNCTION_TYPE), + true, + ), + ( + "code", + crate::typedef::gettypeobject(&crate::pycode::CODE_TYPE), + false, + ), + ( + "range_iterator", + crate::typedef::gettypeobject(&pyre_object::functional::RANGE_ITER_TYPE), + false, + ), + ( + "longrange_iterator", + crate::typedef::gettypeobject(&pyre_object::functional::LONG_RANGE_ITER_TYPE), + false, + ), + ]; + for (name, w_type, expected) in cases { + let w_flags = crate::baseobjspace::getattr_str(w_type, "__flags__") + .unwrap_or_else(|err| panic!("{name}.__flags__ lookup failed: {err:?}")); + let flags = unsafe { pyre_object::w_int_get_value(w_flags) }; + assert_eq!(flags & HAVE_GC != 0, expected, "{name}.__flags__"); + assert_eq!( + unsafe { pyre_object::w_type_get_have_gc(w_type) }, + expected, + "{name}.flag_have_gc" + ); + } + } + + #[test] + fn type_flags_publish_cpython_314_disallow_instantiation() { + crate::typedef::init_typeobjects(); + const DISALLOW_INSTANTIATION: i64 = 1 << 7; + let cases = [ + ("object", crate::typedef::w_object(), false), + ("type", crate::typedef::w_type(), false), + ( + "int", + crate::typedef::gettypeobject(&pyre_object::INT_TYPE), + false, + ), + ( + "function", + crate::typedef::gettypeobject(&crate::function::FUNCTION_TYPE), + false, + ), + ( + "code", + crate::typedef::gettypeobject(&crate::pycode::CODE_TYPE), + false, + ), + ( + "mappingproxy", + crate::typedef::gettypeobject(&pyre_object::MAPPING_PROXY_TYPE), + false, + ), + ( + "range", + crate::typedef::gettypeobject(&pyre_object::functional::RANGE_TYPE), + false, + ), + ( + "memoryview", + crate::typedef::gettypeobject(&pyre_object::memoryview::MEMORYVIEW_TYPE), + false, + ), + ( + "builtin_function_or_method", + crate::typedef::gettypeobject(&crate::function::BUILTIN_FUNCTION_TYPE), + true, + ), + ( + "method_descriptor", + crate::typedef::gettypeobject(&crate::function::METHOD_DESCRIPTOR_TYPE), + true, + ), + ( + "wrapper_descriptor", + crate::typedef::gettypeobject(&crate::function::SLOT_WRAPPER_TYPE), + true, + ), + ( + "method-wrapper", + crate::typedef::gettypeobject(&crate::function::METHOD_WRAPPER_TYPE), + true, + ), + ( + "classmethod_descriptor", + crate::typedef::gettypeobject(&crate::function::CLASSMETHOD_DESCRIPTOR_TYPE), + true, + ), + ("getset_descriptor", super::getset_descriptor_type(), true), + ( + "member_descriptor", + crate::typedef::gettypeobject(&pyre_object::typedef::MEMBER_TYPE), + true, + ), + ( + "dict_keys", + crate::typedef::gettypeobject(&pyre_object::dictmultiobject::DICT_KEYS_TYPE), + true, + ), + ( + "frame", + crate::typedef::gettypeobject(&crate::pyframe::FRAME_TYPE), + true, + ), + ( + "generator", + crate::typedef::gettypeobject(&pyre_object::generator::GENERATOR_TYPE), + true, + ), + ( + "range_iterator", + crate::typedef::gettypeobject(&pyre_object::functional::RANGE_ITER_TYPE), + true, + ), + ( + "list_iterator", + crate::typedef::gettypeobject(&pyre_object::iterobject::LIST_ITER_TYPE), + true, + ), + ( + "set_iterator", + crate::typedef::gettypeobject(&pyre_object::setobject::SET_ITERATOR_TYPE), + true, + ), + ( + "callable_iterator", + crate::typedef::gettypeobject(&pyre_object::operation::CALLABLE_ITERATOR_TYPE), + true, + ), + ( + "typing.Union", + crate::typedef::gettypeobject(&pyre_object::UNION_TYPE), + true, + ), + ]; + for (name, w_type, expected) in cases { + let w_flags = crate::baseobjspace::getattr_str(w_type, "__flags__") + .unwrap_or_else(|err| panic!("{name}.__flags__ lookup failed: {err:?}")); + let flags = unsafe { pyre_object::w_int_get_value(w_flags) }; + assert_eq!( + flags & DISALLOW_INSTANTIATION != 0, + expected, + "{name}.__flags__" + ); + assert_eq!( + unsafe { pyre_object::w_type_disallows_instantiation(w_type) }, + expected, + "{name}.flag_disallow_instantiation" + ); + } + } + + #[test] + fn type_flags_publish_cpython_314_basetype_from_the_canonical_field() { + crate::typedef::init_typeobjects(); + const BASETYPE: i64 = 1 << 10; + let cases = [ + ("object", crate::typedef::w_object(), true), + ("type", crate::typedef::w_type(), true), + ( + "int", + crate::typedef::gettypeobject(&pyre_object::INT_TYPE), + true, + ), + ( + "str", + crate::typedef::gettypeobject(&pyre_object::STR_TYPE), + true, + ), + ( + "bytes", + crate::typedef::gettypeobject(&pyre_object::BYTES_TYPE), + true, + ), + ( + "bytearray", + crate::typedef::gettypeobject(&pyre_object::bytearrayobject::BYTEARRAY_TYPE), + true, + ), + ( + "tuple", + crate::typedef::gettypeobject(&pyre_object::TUPLE_TYPE), + true, + ), + ( + "list", + crate::typedef::gettypeobject(&pyre_object::LIST_TYPE), + true, + ), + ( + "dict", + crate::typedef::gettypeobject(&pyre_object::DICT_TYPE), + true, + ), + ( + "set", + crate::typedef::gettypeobject(&pyre_object::setobject::SET_TYPE), + true, + ), + ( + "property", + crate::typedef::gettypeobject(&pyre_object::descriptor::PROPERTY_TYPE), + true, + ), + ( + "classmethod", + crate::typedef::gettypeobject(&pyre_object::function::CLASSMETHOD_TYPE), + true, + ), + ( + "staticmethod", + crate::typedef::gettypeobject(&pyre_object::function::STATICMETHOD_TYPE), + true, + ), + ( + "super", + crate::typedef::gettypeobject(&pyre_object::descriptor::SUPER_TYPE), + true, + ), + ( + "types.GenericAlias", + crate::typedef::gettypeobject(&pyre_object::GENERIC_ALIAS_TYPE), + true, + ), + ( + "bool", + crate::typedef::gettypeobject(&pyre_object::BOOL_TYPE), + false, + ), + ( + "mappingproxy", + crate::typedef::gettypeobject(&pyre_object::MAPPING_PROXY_TYPE), + false, + ), + ( + "function", + crate::typedef::gettypeobject(&crate::function::FUNCTION_TYPE), + false, + ), + ( + "code", + crate::typedef::gettypeobject(&crate::pycode::CODE_TYPE), + false, + ), + ( + "dict_keys", + crate::typedef::gettypeobject(&pyre_object::dictmultiobject::DICT_KEYS_TYPE), + false, + ), + ( + "slice", + crate::typedef::gettypeobject(&pyre_object::SLICE_TYPE), + false, + ), + ( + "typing.Union", + crate::typedef::gettypeobject(&pyre_object::UNION_TYPE), + false, + ), + ( + "range", + crate::typedef::gettypeobject(&pyre_object::functional::RANGE_TYPE), + false, + ), + ( + "memoryview", + crate::typedef::gettypeobject(&pyre_object::memoryview::MEMORYVIEW_TYPE), + false, + ), + ]; + for (name, w_type, expected) in cases { + let w_flags = crate::baseobjspace::getattr_str(w_type, "__flags__") + .unwrap_or_else(|err| panic!("{name}.__flags__ lookup failed: {err:?}")); + let flags = unsafe { pyre_object::w_int_get_value(w_flags) }; + assert_eq!(flags & BASETYPE != 0, expected, "{name}.__flags__"); + assert_eq!( + unsafe { pyre_object::w_type_get_acceptable_as_base_class(w_type) }, + expected, + "{name}.acceptable_as_base_class" + ); + } + } + + #[test] + fn type_flags_publish_cpython_314_fast_subclass_family_from_the_mro() { + crate::typedef::init_typeobjects(); + let _builtins = crate::builtins::new_builtin_module_dict(); + const LONG_SUBCLASS: i64 = 1 << 24; + const LIST_SUBCLASS: i64 = 1 << 25; + const TUPLE_SUBCLASS: i64 = 1 << 26; + const BYTES_SUBCLASS: i64 = 1 << 27; + const UNICODE_SUBCLASS: i64 = 1 << 28; + const DICT_SUBCLASS: i64 = 1 << 29; + const BASE_EXC_SUBCLASS: i64 = 1 << 30; + const TYPE_SUBCLASS: i64 = 1 << 31; + const FAMILY_MASK: i64 = LONG_SUBCLASS + | LIST_SUBCLASS + | TUPLE_SUBCLASS + | BYTES_SUBCLASS + | UNICODE_SUBCLASS + | DICT_SUBCLASS + | BASE_EXC_SUBCLASS + | TYPE_SUBCLASS; + let cases = [ + ("object", crate::typedef::w_object(), 0), + ("type", crate::typedef::w_type(), TYPE_SUBCLASS), + ( + "int", + crate::typedef::gettypeobject(&pyre_object::INT_TYPE), + LONG_SUBCLASS, + ), + ( + "bool", + crate::typedef::gettypeobject(&pyre_object::BOOL_TYPE), + LONG_SUBCLASS, + ), + ( + "list", + crate::typedef::gettypeobject(&pyre_object::LIST_TYPE), + LIST_SUBCLASS, + ), + ( + "tuple", + crate::typedef::gettypeobject(&pyre_object::TUPLE_TYPE), + TUPLE_SUBCLASS, + ), + ( + "bytes", + crate::typedef::gettypeobject(&pyre_object::BYTES_TYPE), + BYTES_SUBCLASS, + ), + ( + "str", + crate::typedef::gettypeobject(&pyre_object::STR_TYPE), + UNICODE_SUBCLASS, + ), + ( + "dict", + crate::typedef::gettypeobject(&pyre_object::DICT_TYPE), + DICT_SUBCLASS, + ), + ( + "BaseException", + pyre_object::interp_exceptions::lookup_exc_class_for_kind( + pyre_object::interp_exceptions::ExcKind::BaseException, + ), + BASE_EXC_SUBCLASS, + ), + ( + "Exception", + pyre_object::interp_exceptions::lookup_exc_class_for_kind( + pyre_object::interp_exceptions::ExcKind::Exception, + ), + BASE_EXC_SUBCLASS, + ), + ( + "float", + crate::typedef::gettypeobject(&pyre_object::FLOAT_TYPE), + 0, + ), + ( + "set", + crate::typedef::gettypeobject(&pyre_object::setobject::SET_TYPE), + 0, + ), + ]; + for (name, w_type, expected) in cases { + let w_flags = crate::baseobjspace::getattr_str(w_type, "__flags__") + .unwrap_or_else(|err| panic!("{name}.__flags__ lookup failed: {err:?}")); + let flags = unsafe { pyre_object::w_int_get_value(w_flags) }; + assert_eq!(flags & FAMILY_MASK, expected, "{name}.__flags__"); + } + } + + #[test] + fn type_flags_publish_cpython_314_ready_match_self_and_collection_bits() { + crate::typedef::init_typeobjects(); + const SEQUENCE: i64 = 1 << 5; + const MAPPING: i64 = 1 << 6; + const READY: i64 = 1 << 12; + const MATCH_SELF: i64 = 1 << 22; + const MASK: i64 = SEQUENCE | MAPPING | READY | MATCH_SELF; + let cases = [ + ("object", crate::typedef::w_object(), READY), + ("type", crate::typedef::w_type(), READY), + ( + "int", + crate::typedef::gettypeobject(&pyre_object::INT_TYPE), + READY | MATCH_SELF, + ), + ( + "float", + crate::typedef::gettypeobject(&pyre_object::FLOAT_TYPE), + READY | MATCH_SELF, + ), + ( + "str", + crate::typedef::gettypeobject(&pyre_object::STR_TYPE), + READY | MATCH_SELF, + ), + ( + "bytes", + crate::typedef::gettypeobject(&pyre_object::BYTES_TYPE), + READY | MATCH_SELF, + ), + ( + "bytearray", + crate::typedef::gettypeobject(&pyre_object::bytearrayobject::BYTEARRAY_TYPE), + READY | MATCH_SELF, + ), + ( + "list", + crate::typedef::gettypeobject(&pyre_object::LIST_TYPE), + READY | MATCH_SELF | SEQUENCE, + ), + ( + "tuple", + crate::typedef::gettypeobject(&pyre_object::TUPLE_TYPE), + READY | MATCH_SELF | SEQUENCE, + ), + ( + "dict", + crate::typedef::gettypeobject(&pyre_object::DICT_TYPE), + READY | MATCH_SELF | MAPPING, + ), + ( + "set", + crate::typedef::gettypeobject(&pyre_object::setobject::SET_TYPE), + READY | MATCH_SELF, + ), + ( + "frozenset", + crate::typedef::gettypeobject(&pyre_object::setobject::FROZENSET_TYPE), + READY | MATCH_SELF, + ), + ( + "range", + crate::typedef::gettypeobject(&pyre_object::functional::RANGE_TYPE), + READY | SEQUENCE, + ), + ( + "memoryview", + crate::typedef::gettypeobject(&pyre_object::memoryview::MEMORYVIEW_TYPE), + READY | SEQUENCE, + ), + ( + "mappingproxy", + crate::typedef::gettypeobject(&pyre_object::MAPPING_PROXY_TYPE), + READY | MAPPING, + ), + ]; + for (name, w_type, expected) in cases { + let w_flags = crate::baseobjspace::getattr_str(w_type, "__flags__") + .unwrap_or_else(|err| panic!("{name}.__flags__ lookup failed: {err:?}")); + let flags = unsafe { pyre_object::w_int_get_value(w_flags) }; + assert_eq!(flags & MASK, expected, "{name}.__flags__"); + } + } + + #[test] + fn type_flags_keep_pypy_storage_and_cpython_owner_axes_orthogonal() { + crate::typedef::init_typeobjects(); + const STATIC_BUILTIN: i64 = 1 << 1; + const IMMUTABLETYPE: i64 = 1 << 8; + const HEAPTYPE: i64 = 1 << 9; + const OWNER_MASK: i64 = STATIC_BUILTIN | IMMUTABLETYPE | HEAPTYPE; + + let cases = [ + ( + "object", + crate::typedef::w_object(), + STATIC_BUILTIN | IMMUTABLETYPE, + ), + ( + "array.array", + crate::typedef::gettypeobject(&pyre_object::interp_array::ARRAY_TYPE), + HEAPTYPE | IMMUTABLETYPE, + ), + ( + "itertools.count", + crate::typedef::gettypeobject(&pyre_object::interp_itertools::COUNT_TYPE), + HEAPTYPE | IMMUTABLETYPE, + ), + ( + "_random.Random", + crate::module::_random::type_object(), + HEAPTYPE, + ), + ]; + for (name, w_type, expected) in cases { + let flags = unsafe { pyre_object::w_type_get_flags(w_type) }; + assert_eq!(flags & OWNER_MASK, expected, "{name}.__flags__"); + assert!( + !unsafe { pyre_object::w_type_is_heaptype(w_type) }, + "{name} must remain a PyPy builtin TypeDef internally" + ); + } + } + #[test] fn test_ellipsis_has_registered_typeobject() { crate::typedef::init_typeobjects(); diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/diag.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/diag.rs index 5147ba192d4..34a34d886ed 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/diag.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/diag.rs @@ -266,7 +266,7 @@ pub fn skip_python_trivia_forward(code: &pyre_interpreter::CodeObject, mut py_pc /// `parent` marks the second row as a split of the first so the reader does not /// sum them. #[rustfmt::skip] -pub const SPEC_FOLD_ROWS: [(&str, &str, &str); 56] = [ +pub const SPEC_FOLD_ROWS: [(&str, &str, &str); 60] = [ // (label, site, parent) ("truth_int", "residual_call", "-"), ("truth_bool", "residual_call", "-"), @@ -282,9 +282,13 @@ pub const SPEC_FOLD_ROWS: [(&str, &str, &str); 56] = [ ("newtuple_object", "residual_call", "-"), ("newlist", "residual_call", "-"), ("builtin_len", "residual_call", "-"), + ("builtin_isinstance", "residual_call", "-"), + ("builtin_issubclass", "residual_call", "-"), ("builtin_type", "residual_call", "-"), ("builtin_dict_get", "residual_call", "-"), ("builtin_type_getattr", "residual_call", "-"), + ("builtin_getattr", "residual_call", "-"), + ("builtin_hasattr", "residual_call", "-"), ("builtin_range", "residual_call", "-"), ("builtin_zip", "residual_call", "-"), ("builtin_locals", "residual_call", "-"), diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs index c7115d7d581..9ab171136fe 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs @@ -5341,6 +5341,33 @@ pub(crate) fn dispatch_residual_call_iRd_kind( return Ok((DispatchOutcome::Continue, op.next_pc)); } + // `isinstance(obj, cls)` exact-type hit: inline the first + // `abstractinst.py` `abstract_isinstance_w` check (`type(obj) is cls`) as + // callable/class guards plus a constant `True`. All non-quick cases fall + // through to the residual so tuple/union recursion and `__instancecheck__` + // lookup stay unchanged. + if ctx.is_authoritative_executor + && dst_bank == 'r' + && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn + && spec_gate("builtin_isinstance", || { + try_walker_specialize_builtin_isinstance(ctx, code, op, &r_args, dst) + })? + .is_some() + { + return Ok((DispatchOutcome::Continue, op.next_pc)); + } + + if ctx.is_authoritative_executor + && dst_bank == 'r' + && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn + && spec_gate("builtin_issubclass", || { + try_walker_specialize_builtin_issubclass(ctx, code, op, &r_args, dst) + })? + .is_some() + { + return Ok((DispatchOutcome::Continue, op.next_pc)); + } + // BuiltinCode.func is an indirect PBC target exactly like RPython's // gateway wrappers. Enter its generated JitCode before considering the // user-function-only full-body walk below. @@ -5926,6 +5953,34 @@ pub(crate) fn dispatch_residual_call_iRd_kind( return Ok((DispatchOutcome::Continue, op.next_pc)); } + // The same call on an ordinary instance: `getattr(obj, "name")` reaches the + // one `space.getattr` that `obj.name` does, so it folds to the instance + // read instead of an opaque MRO-walk residual. Runs after the type form, + // which answers the receiver the instance-shape read declines. + if ctx.is_authoritative_executor + && dst_bank == 'r' + && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn + && spec_gate("builtin_getattr", || { + try_walker_specialize_builtin_getattr(ctx, code, op, &r_args, dst) + })? + .is_some() + { + return Ok((DispatchOutcome::Continue, op.next_pc)); + } + + // `hasattr(obj, "name")` on the same receiver: the shape guards answer it + // outright, so it folds to a constant without reading the attribute. + if ctx.is_authoritative_executor + && dst_bank == 'r' + && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn + && spec_gate("builtin_hasattr", || { + try_walker_specialize_builtin_hasattr(ctx, code, op, &r_args, dst) + })? + .is_some() + { + return Ok((DispatchOutcome::Continue, op.next_pc)); + } + // An exact `range(...)` constructor call becomes a virtual W_Range whose // four wrapped-int fields can fold directly into GET_ITER virtualization. // Non-canonical callables and arguments fall through to the residual. @@ -6901,18 +6956,20 @@ pub(crate) fn dispatch_residual_call_iIRd_kind( ctx.trace_ctx.box_value(code_opref), ctx.trace_ctx.box_value(namei_opref), ) { - if spec_gate("load_attr", || { - try_walker_specialize_load_attr( - ctx, - op.pc, - obj_opref, - w_code_ptr, - namei as usize, - dst, - dst_bank, - ) - })? - .is_some() + // Resolve the name out of the jitcode's own `co_names` here + // rather than inside the fold (mirrors `bh_load_attr_fn`; the + // codewriter passes the raw index). The fold's body is shared + // with the `getattr(obj, "name")` builtin, which spells the + // same `space.getattr` but carries its name as a constant + // operand, so the lookup has to reach it already resolved. + let attr_name = walker_load_name_from_code(w_code_ptr, namei as usize); + if let Some(attr_name) = attr_name.as_deref() + && spec_gate("load_attr", || { + try_walker_specialize_load_attr( + ctx, op.pc, obj_opref, attr_name, dst, dst_bank, + ) + })? + .is_some() { return Ok((DispatchOutcome::Continue, op.next_pc)); } diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs index 5cbda0061b2..0f82f10e28f 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs @@ -522,6 +522,44 @@ pub(crate) fn try_walker_specialize_unary_invert_int( /// not both concrete `W_IntObject`, or an unsupported helper arm is reached — the caller /// then falls through to the generic `CallMayForce` record so the /// Python-level `__op__` semantics are preserved. +/// The raw machine int of an int/bool operand, for the specialized IR. +/// +/// The ordinary path guards the operand's exact class and loads `intval` out +/// of the box. A bool this same walk boxed still carries the truth Int it was +/// built from ([`bool_box_truth_lookup`]), so the arithmetic reads that +/// directly and the box, its class guard and its `intval` load all go dead — +/// the arithmetic twin of the `POP_JUMP_IF_*` fold, which is this walker's +/// runtime reconstruction of `jtransform.py optimize_goto_if_not`. +/// +/// The branch consumer may take the truth operand as it stands because it only +/// asks whether it is nonzero. Arithmetic may not: `jit_bool_value_from_truth` +/// maps *every* nonzero truth to `intval` 1, so the value the box would have +/// yielded is the normalized `int_is_true`, not the operand itself. +fn walker_int_operand_raw( + ctx: &mut WalkContext<'_, '_, Sym>, + op_pc: usize, + operand: OpRef, + operand_obj: pyre_object::PyObjectRef, + type_addr: i64, + intval_descr: majit_ir::DescrRef, + concrete: i64, +) -> Result { + if let Some(truth) = bool_box_truth_lookup(operand) { + let normalized = ctx.trace_ctx.record_op(OpCode::IntIsTrue, &[truth]); + ctx.trace_ctx + .set_opref_concrete(normalized, majit_ir::Value::Int(concrete)); + return Ok(normalized); + } + let raw = walker_unbox_int_typed(ctx, op_pc, operand, type_addr, intval_descr)?; + walker_guard_exact_w_class( + ctx, + op_pc, + operand, + walker_numeric_builtin_class(operand_obj), + )?; + Ok(raw) +} + pub(crate) fn try_walker_specialize_binary_op_int( ctx: &mut WalkContext<'_, '_, Sym>, op_pc: usize, @@ -697,10 +735,8 @@ pub(crate) fn try_walker_specialize_binary_op_int( // (BOOL_TYPE / INT_TYPE) so a bool unboxes through its own class. let (lhs_type, lhs_descr) = crate::state::int_or_bool_unbox_type_descr(lhs_obj); let (rhs_type, rhs_descr) = crate::state::int_or_bool_unbox_type_descr(rhs_obj); - let lhs_raw = walker_unbox_int_typed(ctx, op_pc, lhs, lhs_type, lhs_descr)?; - walker_guard_exact_w_class(ctx, op_pc, lhs, walker_numeric_builtin_class(lhs_obj))?; - let rhs_raw = walker_unbox_int_typed(ctx, op_pc, rhs, rhs_type, rhs_descr)?; - walker_guard_exact_w_class(ctx, op_pc, rhs, walker_numeric_builtin_class(rhs_obj))?; + let lhs_raw = walker_int_operand_raw(ctx, op_pc, lhs, lhs_obj, lhs_type, lhs_descr, la)?; + let rhs_raw = walker_int_operand_raw(ctx, op_pc, rhs, rhs_obj, rhs_type, rhs_descr, rb)?; if overflows { let concrete_value = match op_code { OpCode::IntAddOvf => la.wrapping_add(rb), @@ -2545,13 +2581,17 @@ fn walker_specialize_traceback_walk_field( /// this shape, so a successful fold provably cannot raise `AttributeError` — /// dropping the residual's exception guard is sound even in a handler-bearing /// body (same reasoning as the LoadGlobal fold). -#[allow(clippy::too_many_arguments)] +/// +/// `name` is the already-resolved attribute name, so the fold serves both the +/// `LOAD_ATTR` residual — whose caller reads it out of the jitcode's own +/// `co_names` — and the `getattr(obj, "name")` builtin, whose name arrives as a +/// constant string operand. Both spell one `space.getattr`, so they must reach +/// the same read. pub(crate) fn try_walker_specialize_load_attr( ctx: &mut WalkContext<'_, '_, Sym>, op_pc: usize, obj: OpRef, - w_code_ptr: usize, - name_idx: usize, + name: &str, dst: usize, dst_bank: char, ) -> Result, DispatchError> { @@ -2565,11 +2605,6 @@ pub(crate) fn try_walker_specialize_load_attr( let Some(concrete_obj) = walker_concrete_ref_object(ctx, obj) else { return Ok(None); }; - // Resolve the attribute name from the jitcode's own PyCode `co_names` - // (mirrors `bh_load_attr_fn`; the codewriter passes the raw co_names index). - let Some(name) = walker_load_name_from_code(w_code_ptr, name_idx) else { - return Ok(None); - }; // CPython 3.14 exposes an optimized frame's locals as a fresh // `FrameLocalsProxy`. Constructing that proxy does not read fast locals; // its operations synchronize through the frame when they are actually @@ -2618,9 +2653,9 @@ pub(crate) fn try_walker_specialize_load_attr( } // `mapdict.py` resolution, returning the fold ingredients (the // read is left to the caller so it can be folded to a guarded inline read). - if let Some((w_type, version_tag, map, storageindex)) = unsafe { - pyre_interpreter::objspace::std::mapdict::load_attr_fast_path(concrete_obj, &name) - } { + if let Some((w_type, version_tag, map, storageindex)) = + unsafe { pyre_interpreter::objspace::std::mapdict::load_attr_fast_path(concrete_obj, name) } + { walker_guard_mapdict_instance_shape( ctx, op_pc, @@ -2644,7 +2679,7 @@ pub(crate) fn try_walker_specialize_load_attr( return Ok(Some(())); } - if let Some(walk_field) = traceback_walk_field(concrete_obj, &name) { + if let Some(walk_field) = traceback_walk_field(concrete_obj, name) { if let Some(()) = walker_specialize_traceback_walk_field( ctx, op_pc, @@ -2680,7 +2715,7 @@ pub(crate) fn try_walker_specialize_load_attr( pyre_interpreter::objspace::std::mapdict::instance_dict_attr_fast_path( concrete_obj, dict, - &name, + name, ) } // An unboxed float slot keeps the `f64` bit pattern in the same @@ -2805,7 +2840,7 @@ pub(crate) fn try_walker_specialize_load_attr( } if let Some((slot, kind, w_type, version_tag, stored)) = unsafe { - pyre_interpreter::baseobjspace::exception_attr_slot_fold(concrete_obj, &name, false) + pyre_interpreter::baseobjspace::exception_attr_slot_fold(concrete_obj, name, false) } { if slot == pyre_interpreter::baseobjspace::ExceptionAttrSlot::Args && unsafe { (*(stored as *const pyre_object::listobject::W_ListObject)).strategy } @@ -3021,15 +3056,12 @@ pub(crate) fn try_walker_specialize_load_attr( pyre_object::pyobject::get_instantiate(&pyre_object::pyobject::MODULE_TYPE), ) && !unsafe { - pyre_interpreter::baseobjspace::type_lookup_is_data_descr( - (*concrete_obj).w_class, - &name, - ) + pyre_interpreter::baseobjspace::type_lookup_is_data_descr((*concrete_obj).w_class, name) } { let w_dict = unsafe { pyre_object::w_module_get_w_dict(concrete_obj) }; if !w_dict.is_null() && !majit_gc::can_move(majit_ir::GcRef(w_dict as usize)) { - if let Some(slot) = crate::state::module_dict_cell_slot_direct(w_dict, &name) { + if let Some(slot) = crate::state::module_dict_cell_slot_direct(w_dict, name) { if let Some(stored) = crate::state::module_dict_cell_value_direct(w_dict, slot) { if !stored.is_null() && !majit_gc::can_move(majit_ir::GcRef(stored as usize)) { // Pin the receiver to THIS module so the baked dict @@ -3055,7 +3087,7 @@ pub(crate) fn try_walker_specialize_load_attr( } let Some((w_type, version_tag, map, storageindex, listindex, unbox_type)) = (unsafe { - pyre_interpreter::objspace::std::mapdict::load_attr_unboxed_fast_path(concrete_obj, &name) + pyre_interpreter::objspace::std::mapdict::load_attr_unboxed_fast_path(concrete_obj, name) }) else { return Ok(None); }; @@ -7135,6 +7167,290 @@ pub(crate) fn try_walker_specialize_builtin_len( Ok(Some(())) } +fn walker_isinstance_exact_type_hit( + obj: pyre_object::PyObjectRef, + cls: pyre_object::PyObjectRef, +) -> Option<(i64, Option)> { + let obj_type = pyre_interpreter::typedef::r#type(obj)?; + if !std::ptr::eq(obj_type.as_ptr(), cls) { + return None; + } + walker_isinstance_obj_type_guard_info(obj, cls) +} + +fn walker_isinstance_obj_type_guard_info( + obj: pyre_object::PyObjectRef, + obj_type: pyre_object::PyObjectRef, +) -> Option<(i64, Option)> { + let physical_type = unsafe { (*obj).ob_type as i64 }; + let stored_w_class = unsafe { (*obj).w_class }; + if stored_w_class.is_null() { + Some((physical_type, None)) + } else if std::ptr::eq(stored_w_class, obj_type) { + Some((physical_type, Some(stored_w_class))) + } else { + None + } +} + +fn walker_isinstance_tuple_element_can_precede_hit(item: pyre_object::PyObjectRef) -> bool { + pyre_interpreter::typedef::r#type(item) + .is_some_and(|meta| std::ptr::eq(meta.as_ptr(), pyre_interpreter::typedef::w_type())) +} + +fn walker_guard_ref_value( + ctx: &mut WalkContext<'_, '_, Sym>, + op_pc: usize, + opref: OpRef, + expected_obj: pyre_object::PyObjectRef, +) -> Result<(), DispatchError> { + if !opref.is_constant() { + let expected = ctx.trace_ctx.const_ref(expected_obj as i64); + ctx.trace_ctx + .record_guard(OpCode::GuardValue, &[opref, expected], 0); + walker_capture_snapshot_for_last_guard(ctx, op_pc)?; + ctx.trace_ctx.heap_cache_mut().replace_box(opref, expected); + } + Ok(()) +} + +struct WalkerIsinstanceFold { + physical_type: i64, + exact_w_class: Option, + guarded_cls: pyre_object::PyObjectRef, + pinned_obj_type: Option, + result: bool, +} + +/// `isinstance(obj, cls)` in the quick exact-type cases from +/// `abstractinst.py` `abstract_isinstance_w`: before union recursion or +/// `__instancecheck__` lookup, `type(obj) is cls` returns `True`; for tuple +/// classinfo the interpreter loops the tuple and recurses into that same test. +/// +/// A scalar miss where `type(cls) is type` can also fold through +/// `p_recursive_isinstance_type_w`: a positive `issubtype_w(type(obj), cls)` +/// never reads `obj.__class__`; a negative answer is folded only when the +/// receiver type resolves `__class__` to the stock `object.__class__` +/// descriptor. +pub(crate) fn try_walker_specialize_builtin_isinstance( + ctx: &mut WalkContext<'_, '_, Sym>, + code: &[u8], + op: &DecodedOp, + r_args: &[OpRef], + dst: usize, +) -> Result, DispatchError> { + // Plain `bh_call_fn(callable, PY_NULL, obj, cls)` shape only. + if r_args.len() != 4 { + return Ok(None); + } + let arg_concretes = read_ref_var_list_concrete(code, op, 1, ctx); + let ( + ConcreteValue::Ref(concrete_callable), + ConcreteValue::Ref(null_or_self), + ConcreteValue::Ref(obj), + ConcreteValue::Ref(cls), + ) = ( + arg_concretes[0], + arg_concretes[1], + arg_concretes[2], + arg_concretes[3], + ) + else { + return Ok(None); + }; + // A non-null `null_or_self` is a bound receiver `bh_call_fn_impl` + // prepends as arg0, not a plain `isinstance(obj, cls)` call. + if concrete_callable.is_null() || !null_or_self.is_null() || obj.is_null() || cls.is_null() { + return Ok(None); + } + if !pyre_interpreter::builtins::is_builtin_isinstance_function(concrete_callable) { + return Ok(None); + } + if pyre_object::tagged_int::CAN_BE_TAGGED && pyre_object::tagged_int::is_tagged_int(obj) { + return Ok(None); + } + let fold = unsafe { + if pyre_object::tagged_int::CAN_BE_TAGGED && pyre_object::tagged_int::is_tagged_int(cls) { + return Ok(None); + } + if std::ptr::eq((*cls).ob_type, &pyre_object::pyobject::TUPLE_TYPE) { + let tuple_class = + pyre_object::pyobject::get_instantiate(&pyre_object::pyobject::TUPLE_TYPE); + if !std::ptr::eq((*cls).w_class, tuple_class) { + return Ok(None); + } + let n = pyre_object::w_tuple_len(cls); + for i in 0..n { + let Some(item) = pyre_object::w_tuple_getitem(cls, i as i64) else { + return Ok(None); + }; + if pyre_object::is_tuple(item) { + return Ok(None); + } + } + let mut hit = None; + for i in 0..n { + let Some(item) = pyre_object::w_tuple_getitem(cls, i as i64) else { + return Ok(None); + }; + if let Some(proof) = walker_isinstance_exact_type_hit(obj, item) { + hit = Some(proof); + break; + } + if !walker_isinstance_tuple_element_can_precede_hit(item) { + return Ok(None); + } + } + let Some((physical_type, exact_w_class)) = hit else { + return Ok(None); + }; + WalkerIsinstanceFold { + physical_type, + exact_w_class, + guarded_cls: cls, + pinned_obj_type: None, + result: true, + } + } else { + if let Some((physical_type, exact_w_class)) = walker_isinstance_exact_type_hit(obj, cls) + { + WalkerIsinstanceFold { + physical_type, + exact_w_class, + guarded_cls: cls, + pinned_obj_type: None, + result: true, + } + } else { + if !pyre_object::is_type(cls) { + return Ok(None); + } + let Some(cls_type) = pyre_interpreter::typedef::r#type(cls) else { + return Ok(None); + }; + if !std::ptr::eq(cls_type.as_ptr(), pyre_interpreter::typedef::w_type()) { + return Ok(None); + } + let Some(obj_type) = pyre_interpreter::typedef::r#type(obj) else { + return Ok(None); + }; + let obj_type = obj_type.as_ptr(); + if !pyre_object::is_type(obj_type) { + return Ok(None); + } + let version_tag = pyre_object::typeobject::w_type_get_version_tag(obj_type); + if version_tag == 0 { + return Ok(None); + } + let Some((physical_type, exact_w_class)) = + walker_isinstance_obj_type_guard_info(obj, obj_type) + else { + return Ok(None); + }; + let result = pyre_interpreter::baseobjspace::jit_issubtype_w(obj_type, cls); + if !result + && !pyre_interpreter::baseobjspace::isinstance_miss_class_lookup_is_pure( + obj_type, + ) + { + return Ok(None); + } + WalkerIsinstanceFold { + physical_type, + exact_w_class, + guarded_cls: cls, + pinned_obj_type: Some(obj_type), + result, + } + } + } + }; + + // --- emit the specialized IR (walker-native) --- + // Pin the callable identity (LOAD_GLOBAL `isinstance` is usually already + // constant via the namespace cell fold). + let callable_op = r_args[0]; + walker_guard_ref_value(ctx, op.pc, callable_op, concrete_callable)?; + let obj_op = r_args[2]; + walker_guard_class(ctx, op.pc, obj_op, fold.physical_type)?; + if let Some(exact_w_class) = fold.exact_w_class { + walker_guard_exact_w_class(ctx, op.pc, obj_op, exact_w_class)?; + } + let cls_op = r_args[3]; + walker_guard_ref_value(ctx, op.pc, cls_op, fold.guarded_cls)?; + if let Some(obj_type) = fold.pinned_obj_type { + let obj_type_const = ctx.trace_ctx.const_ref(obj_type as i64); + walker_pin_type_version_tag(ctx, op.pc, obj_type_const)?; + } + walker_write_const_bool_result(ctx, op.pc, fold.result, dst, 'r')?; + Ok(Some(())) +} + +pub(crate) fn try_walker_specialize_builtin_issubclass( + ctx: &mut WalkContext<'_, '_, Sym>, + code: &[u8], + op: &DecodedOp, + r_args: &[OpRef], + dst: usize, +) -> Result, DispatchError> { + // Plain `bh_call_fn(callable, PY_NULL, derived, cls)` shape only. + if r_args.len() != 4 { + return Ok(None); + } + let arg_concretes = read_ref_var_list_concrete(code, op, 1, ctx); + let ( + ConcreteValue::Ref(concrete_callable), + ConcreteValue::Ref(null_or_self), + ConcreteValue::Ref(derived), + ConcreteValue::Ref(cls), + ) = ( + arg_concretes[0], + arg_concretes[1], + arg_concretes[2], + arg_concretes[3], + ) + else { + return Ok(None); + }; + if concrete_callable.is_null() || !null_or_self.is_null() || derived.is_null() || cls.is_null() + { + return Ok(None); + } + if !pyre_interpreter::builtins::is_builtin_issubclass_function(concrete_callable) { + return Ok(None); + } + + let result = unsafe { + if pyre_object::is_generic_alias(derived) + || !pyre_object::is_type(derived) + || !pyre_object::is_type(cls) + || !pyre_interpreter::baseobjspace::jit_is_type_like_w(derived) + || !pyre_interpreter::baseobjspace::jit_is_type_like_w(cls) + { + return Ok(None); + } + let Some(cls_type) = pyre_interpreter::typedef::r#type(cls) else { + return Ok(None); + }; + if !std::ptr::eq(cls_type.as_ptr(), pyre_interpreter::typedef::w_type()) { + return Ok(None); + } + let version_tag = pyre_object::typeobject::w_type_get_version_tag(derived); + if version_tag == 0 { + return Ok(None); + } + pyre_interpreter::baseobjspace::jit_issubtype_w(derived, cls) + }; + + walker_guard_ref_value(ctx, op.pc, r_args[0], concrete_callable)?; + walker_guard_ref_value(ctx, op.pc, r_args[2], derived)?; + walker_guard_ref_value(ctx, op.pc, r_args[3], cls)?; + let derived_const = ctx.trace_ctx.const_ref(derived as i64); + walker_pin_type_version_tag(ctx, op.pc, derived_const)?; + walker_write_const_bool_result(ctx, op.pc, result, dst, 'r')?; + Ok(Some(())) +} + /// Fold plain `getattr(type, name)` when /// [`pyre_interpreter::type_attr_value_fast_path`] proves that /// `typeobject.py:811-828` returns the class-MRO value unchanged. The exact @@ -7251,6 +7567,242 @@ pub(crate) fn try_walker_specialize_builtin_type_getattr( Ok(Some(())) } +/// `getattr(obj, "name")` — the builtin spelling of the `LOAD_ATTR` fold. +/// +/// `space.getattr` is the one operation both `obj.name` and this builtin +/// reach, so a constant `str` name admits exactly the instance-shape read +/// [`try_walker_specialize_load_attr`] already emits, and the two forms stay on +/// one implementation rather than drifting the way a fast-path pair can +/// (`getattr(obj, 'm')` versus `obj.m` is the classic discriminator). +/// +/// The three-argument `getattr(obj, name, default)` stays on the residual: the +/// fold's map guard proves the attribute is *present*, which says nothing about +/// the branch that supplies the default. +pub(crate) fn try_walker_specialize_builtin_getattr( + ctx: &mut WalkContext<'_, '_, Sym>, + code: &[u8], + op: &DecodedOp, + r_args: &[OpRef], + dst: usize, +) -> Result, DispatchError> { + // Plain `bh_call_fn(callable, PY_NULL, obj, name)` shape only; the + // three-argument form arrives one operand longer and declines here. + if r_args.len() != 4 { + return Ok(None); + } + let arg_concretes = read_ref_var_list_concrete(code, op, 1, ctx); + let ( + ConcreteValue::Ref(concrete_callable), + ConcreteValue::Ref(null_or_self), + ConcreteValue::Ref(concrete_obj), + ConcreteValue::Ref(concrete_name), + ) = ( + arg_concretes[0], + arg_concretes[1], + arg_concretes[2], + arg_concretes[3], + ) + else { + return Ok(None); + }; + // A non-null `null_or_self` is a bound receiver `bh_call_fn_impl` prepends + // as arg0 — not a plain `getattr(obj, name)` call. + if concrete_callable.is_null() + || !null_or_self.is_null() + || concrete_obj.is_null() + || concrete_name.is_null() + { + return Ok(None); + } + if !pyre_interpreter::builtins::is_builtin_getattr_function(concrete_callable) { + return Ok(None); + } + // The name is rejected before any lookup unless it is a string, and the + // resolved bytes below stay valid only while this exact string is the + // operand. A name that is not valid UTF-8 cannot match an attribute the + // fold's `&str` lookups can find, so it declines with the rest. + if !unsafe { pyre_object::is_exact_type(concrete_name, &pyre_object::pyobject::STR_TYPE) } { + return Ok(None); + } + let Ok(name) = (unsafe { pyre_object::w_str_get_wtf8(concrete_name) }).as_str() else { + return Ok(None); + }; + + let pre_emit_pos = ctx.trace_ctx.get_trace_position(); + let callable_op = r_args[0]; + if !callable_op.is_constant() { + let expected = ctx.trace_ctx.const_ref(concrete_callable as i64); + walker_emit_fold_guard_with_snapshot( + ctx, + op.pc, + OpCode::GuardValue, + &[callable_op, expected], + )?; + ctx.trace_ctx + .heap_cache_mut() + .replace_box(callable_op, expected); + } + let name_ref = r_args[3]; + if !name_ref.is_constant() { + let name_const = ctx.trace_ctx.const_ref(concrete_name as i64); + walker_emit_fold_guard_with_snapshot( + ctx, + op.pc, + OpCode::GuardValue, + &[name_ref, name_const], + )?; + } + + // Every shape the read declines has to leave the trace as it found it: the + // two guards above are the premise of a fold that is no longer there, and + // the residual the caller falls through to recomputes the lookup from the + // unguarded operands. + if (try_walker_specialize_load_attr(ctx, op.pc, r_args[2], name, dst, 'r')?).is_none() { + ctx.trace_ctx.cut_trace_with_snapshots(pre_emit_pos); + ctx.trace_ctx.heap_cache_mut().reset(); + return Ok(None); + } + Ok(Some(())) +} + +/// `hasattr(obj, "name")` — settled by the instance shape, without a read. +/// +/// [`builtin_hasattr`] answers False only for the `AttributeError` its lookup +/// raises, and the map `guard_value` below proves the attribute is present on +/// this exact shape. So the guards alone decide the call: the result is a +/// constant True and the attribute's value is never loaded, which is strictly +/// less work than the `getattr` fold does for the same receiver. +/// +/// Absence is a different proof, and `getattr_absent_fast_path` is the one that +/// carries it: the two pins keep `name` off both the type and the receiver's +/// own storage, and with no `__getattr__` left to run the access ends in the +/// `AttributeError` this builtin reports as False. Neither hit resolver can +/// stand in for it — they decline a name they cannot *place*, which is not the +/// same claim as "not here". +pub(crate) fn try_walker_specialize_builtin_hasattr( + ctx: &mut WalkContext<'_, '_, Sym>, + code: &[u8], + op: &DecodedOp, + r_args: &[OpRef], + dst: usize, +) -> Result, DispatchError> { + // `hasattr` takes exactly two application arguments, so the residual is + // `bh_call_fn(callable, PY_NULL, obj, name)` and nothing else. + if r_args.len() != 4 { + return Ok(None); + } + let arg_concretes = read_ref_var_list_concrete(code, op, 1, ctx); + let ( + ConcreteValue::Ref(concrete_callable), + ConcreteValue::Ref(null_or_self), + ConcreteValue::Ref(concrete_obj), + ConcreteValue::Ref(concrete_name), + ) = ( + arg_concretes[0], + arg_concretes[1], + arg_concretes[2], + arg_concretes[3], + ) + else { + return Ok(None); + }; + // A non-null `null_or_self` is a bound receiver `bh_call_fn_impl` prepends + // as arg0 — not a plain `hasattr(obj, name)` call. + if concrete_callable.is_null() + || !null_or_self.is_null() + || concrete_obj.is_null() + || concrete_name.is_null() + { + return Ok(None); + } + if !pyre_interpreter::builtins::is_builtin_hasattr_function(concrete_callable) { + return Ok(None); + } + // `checkattrname` rejects a non-string name with TypeError, which is not an + // answer this fold may replace with a bool. + if !unsafe { pyre_object::is_exact_type(concrete_name, &pyre_object::pyobject::STR_TYPE) } { + return Ok(None); + } + let Ok(name) = (unsafe { pyre_object::w_str_get_wtf8(concrete_name) }).as_str() else { + return Ok(None); + }; + // Decide before emitting: every decline below this point would have to + // rewind guards for a fold that is no longer there. + // + // Presence is a property of the map, so either resolution answers it and + // neither storage kind changes the answer — an int attribute lands in an + // unboxed slot, which the boxed resolution alone declines. Both twins run + // the same refusals first (custom `__getattribute__`, an `INVALID` + // classify, an uncacheable `version_tag`), so a `Some` from either means + // the lookup this fold replaces cannot raise. + // + // Absence is its own proof, not the failure of these two: + // `getattr_absent_fast_path` reports it only when the same two pins keep + // `name` off both the type and the receiver's storage AND there is no + // `__getattr__` left to run, which is exactly when the access ends in the + // AttributeError this builtin reports as False. + let Some((w_type, version_tag, map, answer)) = (unsafe { + pyre_interpreter::objspace::std::mapdict::load_attr_fast_path(concrete_obj, name) + .map(|(w_type, version_tag, map, _storageindex)| (w_type, version_tag, map, true)) + .or_else(|| { + pyre_interpreter::objspace::std::mapdict::load_attr_unboxed_fast_path( + concrete_obj, + name, + ) + .map( + |(w_type, version_tag, map, _storageindex, _listindex, _unbox)| { + (w_type, version_tag, map, true) + }, + ) + }) + .or_else(|| { + pyre_interpreter::objspace::std::mapdict::getattr_absent_fast_path( + concrete_obj, + name, + ) + .map(|(w_type, version_tag, map)| (w_type, version_tag, map, false)) + }) + }) else { + return Ok(None); + }; + + let obj_ref = r_args[2]; + let callable_op = r_args[0]; + if !callable_op.is_constant() { + let expected = ctx.trace_ctx.const_ref(concrete_callable as i64); + walker_emit_fold_guard_with_snapshot( + ctx, + op.pc, + OpCode::GuardValue, + &[callable_op, expected], + )?; + ctx.trace_ctx + .heap_cache_mut() + .replace_box(callable_op, expected); + } + let name_ref = r_args[3]; + if !name_ref.is_constant() { + let name_const = ctx.trace_ctx.const_ref(concrete_name as i64); + walker_emit_fold_guard_with_snapshot( + ctx, + op.pc, + OpCode::GuardValue, + &[name_ref, name_const], + )?; + } + walker_guard_mapdict_instance_shape( + ctx, + op.pc, + obj_ref, + concrete_obj, + w_type, + version_tag, + map, + )?; + walker_write_const_bool_result(ctx, op.pc, answer, dst, 'r')?; + Ok(Some(())) +} + /// Record an overflow-checked machine-int operation and guard it. /// /// [`record_int_ovf`] folds a both-constant operand pair to a constant without diff --git a/pyre/pyre-jit-trace/src/trace.rs b/pyre/pyre-jit-trace/src/trace.rs index cee09712f2b..9235993df8c 100644 --- a/pyre/pyre-jit-trace/src/trace.rs +++ b/pyre/pyre-jit-trace/src/trace.rs @@ -2730,17 +2730,29 @@ fn try_adopt_single_frame_blackhole( latched.last_exc_value, latched.raising_exception, ); - // The blackhole roots and forwards its Ref bank in place. A collection - // during the drive can therefore move the live frame away from the - // pre-drive `committed_root_addr`; recover the authoritative post-drive - // identity from the same per-frame red register before dereferencing it. - let forwarded_root_addr = terminal - .registers_r - .get(frame_reg as usize) - .copied() - .filter(|&addr| addr != 0) - .expect("post-blackhole frame register lost the live frame identity") - as usize; + // The blackhole roots and forwards both its Ref bank and the virtualizable + // in place. A collection during the drive can therefore move the live frame + // away from the pre-drive `committed_root_addr`, so the post-drive identity + // has to come from one of those forwarded channels. + // + // The frame red is not that channel. The `-live-` marker hook clears every + // Ref register the marker's live set omits, so the register reads 0 as soon + // as the drive stops past that register's last use — which is the ordinary + // shape for a frame that ran to its return. The driver roots the frame + // address separately for exactly this reason; read it there, and keep the + // register only as the cross-check that the two channels name one frame. + let forwarded_root_addr = terminal.virtualizable_ptr as usize; + assert!( + forwarded_root_addr != 0, + "post-blackhole drive returned no virtualizable for a preflighted frame" + ); + debug_assert!( + matches!( + terminal.registers_r.get(frame_reg as usize).copied(), + None | Some(0) + ) || terminal.registers_r[frame_reg as usize] as usize == forwarded_root_addr, + "post-blackhole frame red and rooted virtualizable name different frames" + ); // Vable opcodes address the frame carried in the MIFrame's red register, // i.e. the live frame whose locals were published above. Fold every // blackhole write back into the tracing snapshot before the portal diff --git a/pyre/pyre-jit/src/eval.rs b/pyre/pyre-jit/src/eval.rs index 0848ae7a10d..d7fc53972e9 100644 --- a/pyre/pyre-jit/src/eval.rs +++ b/pyre/pyre-jit/src/eval.rs @@ -1617,10 +1617,16 @@ fn build_gc() -> Box { // on the root so every ordinary offset-traced subclass inherits it // through its embedded PyObject header, matching GcStruct `super` field // tracing. - let object_tid = gc.register_type(TypeInfo::object_with_gc_ptrs( - std::mem::size_of::(), - vec![std::mem::offset_of!(pyre_object::PyObject, w_class)], - )); + let object_tid = gc.register_type( + TypeInfo::object_with_gc_ptrs( + std::mem::size_of::(), + vec![std::mem::offset_of!(pyre_object::PyObject, w_class)], + ) + // RPython's OBJECT.typeptr is non-GC header metadata. Pyre's + // `w_class` is its managed frontend mirror: trace it for liveness but + // do not expose it as a Python-level referent. + .with_app_level_inspector_hidden_edge(std::mem::offset_of!(pyre_object::PyObject, w_class)), + ); debug_assert_eq!(object_tid, OBJECT_GC_TYPE_ID); // W_IntObject / W_FloatObject carry `PyObject.ob_type` at offset 0, // matching RPython `rclass.OBJECT` layout (T_IS_RPYTHON_INSTANCE, @@ -5071,24 +5077,20 @@ fn build_jit_driver_pair() -> JitDriverPair { // is traced in a later slice. let jd1 = pyre_jit_trace::unpack_state::UnpackJitState::unpackiterable_driver_descriptor(); d.meta_interp_mut().register_jitdriver_sd(jd1); - // jd1's merge-point walk (`drive_unpack_iterable_trace`) re-traces the - // build-time `_unpackiterable_unknown_length` jitcode, whose `BC_LIVE` ops - // carry offsets baked against the build-time assembler's `all_liveness` - // table. jd0 traces Python bytecode (PyFrame resume path) and never reads - // `metainterp_sd.op_live` / `liveness_info`, so pyre otherwise leaves them - // unset (`op_live = 255`). Install the build-time opcode-id table - // (`op_live = insns["live/"]`) + `all_liveness` byte stream into the shared - // `staticdata` so the jd1 walk resolves its guard snapshots - // (`get_list_of_active_snapshot_boxes`). Gated on the jd1 experiment so - // the default build leaves jd0's staticdata byte-identical; runs here, - // before any trace clones the `staticdata` Arc, to satisfy the single-owner - // `Arc::get_mut` invariant. - if jd1_experiment_enabled() { - d.meta_interp_mut().install_liveness_from_build_parts( - pyre_jit_trace::jitcode_runtime::insns_opname_to_byte(), - pyre_jit_trace::jitcode_runtime::all_liveness(), - ); - } + // `warmspot.py metainterp_sd.finish_setup(codewriter)` always installs + // the assembler's opcode ids and liveness stream before either tracing or + // blackhole execution. Every translated jitcode carries those ids: jd1's + // merge-point walk reads them while taking snapshots, and jd0's + // tracing-abort adoption copies them into each BlackholeInterpreter so a + // residual-call exception recognizes its following `live/` / + // `catch_exception/L`. Leaving the default jd0 staticdata at the sentinel + // value 255 made a caught exception escape whenever that adoption path ran. + // Install the one build-time table unconditionally, before any trace clones + // the staticdata Arc, exactly where upstream runs finish_setup. + d.meta_interp_mut().install_liveness_from_build_parts( + pyre_jit_trace::jitcode_runtime::insns_opname_to_byte(), + pyre_jit_trace::jitcode_runtime::all_liveness(), + ); // rlib/jit.py set_user_param — the translation-time `--jit STR` // option's analog. `PYRE_JIT="vec_all=1"` opts vectorization in the // PyPy way (parameter; the defaults stay off). `PYRE_JIT=0` keeps its @@ -13664,6 +13666,41 @@ mod tests { .unwrap_or_else(|| panic!("namespace should contain {name}")) } + #[test] + fn driver_finish_setup_installs_blackhole_control_opcodes() { + let (driver, _) = driver_pair(); + let staticdata = &driver.meta_interp().staticdata; + let insns = pyre_jit_trace::jitcode_runtime::insns_opname_to_byte(); + + assert_eq!(staticdata.op_live, i32::from(insns["live/"])); + assert_eq!( + staticdata.op_catch_exception, + i32::from(insns["catch_exception/L"]) + ); + assert_eq!( + staticdata.op_rvmprof_code, + i32::from(insns["rvmprof_code/ii"]) + ); + assert_eq!( + staticdata.liveness_info.as_slice(), + pyre_jit_trace::jitcode_runtime::all_liveness() + ); + + let mut builder = pyre_jit_trace::jitcode_runtime::build_pyre_production_bh_builder(); + builder.setup_cached_control_opcodes( + staticdata.op_live, + staticdata.op_catch_exception, + staticdata.op_rvmprof_code, + ); + let blackhole = builder.acquire_interp(); + assert_eq!(blackhole.op_live, staticdata.op_live as u8); + assert_eq!( + blackhole.op_catch_exception, + staticdata.op_catch_exception as u8 + ); + assert_eq!(blackhole.op_rvmprof_code, staticdata.op_rvmprof_code as u8); + } + #[allow(dead_code)] struct TestJitParamsGuard; diff --git a/pyre/pyre-macros/src/lib.rs b/pyre/pyre-macros/src/lib.rs index 04254676674..72d9ff1c126 100644 --- a/pyre/pyre-macros/src/lib.rs +++ b/pyre/pyre-macros/src/lib.rs @@ -1181,6 +1181,14 @@ struct PyreClassAttrs { /// Specifying this lets the GC consts retain one prefix while the /// PyType keeps its historical name. pytype_static: Option, + /// CPython 3.14 constructs this module type with `PyType_From*Spec`. + /// This is a public flag projection only; the PyPy TypeDef remains a + /// builtin internally. + cpython_heaptype: bool, + /// CPython permits mutation of this heap type's namespace. Implies + /// `cpython_heaptype`; absent this marker, heap extension types default to + /// IMMUTABLETYPE, matching the dominant 3.14 shape. + cpython_mutable: bool, } impl syn::parse::Parse for PyreClassAttrs { @@ -1190,12 +1198,26 @@ impl syn::parse::Parse for PyreClassAttrs { let mut type_id: Option = None; let mut static_name: Option = None; let mut pytype_static: Option = None; + let mut cpython_heaptype = false; + let mut cpython_mutable = false; while !input.is_empty() { input.parse::()?; if input.is_empty() { break; } let key: syn::Ident = input.parse()?; + match key.to_string().as_str() { + "cpython_heaptype" => { + cpython_heaptype = true; + continue; + } + "cpython_mutable" => { + cpython_heaptype = true; + cpython_mutable = true; + continue; + } + _ => {} + } input.parse::()?; match key.to_string().as_str() { "type_id" => type_id = Some(input.parse()?), @@ -1206,7 +1228,8 @@ impl syn::parse::Parse for PyreClassAttrs { key.span(), format!( "unknown `#[pyre_class]` key `{other}` — \ - expected `type_id` / `static_name` / `pytype_static`", + expected `type_id` / `static_name` / `pytype_static` / \ + `cpython_heaptype` / `cpython_mutable`", ), )); } @@ -1217,6 +1240,8 @@ impl syn::parse::Parse for PyreClassAttrs { type_id, static_name, pytype_static, + cpython_heaptype, + cpython_mutable, }) } } @@ -1228,6 +1253,8 @@ fn expand_pyre_class( let st_name = st.ident.clone(); let st_vis = st.vis.clone(); let name_lit = attrs.name; + let cpython_heaptype = attrs.cpython_heaptype; + let cpython_immutabletype = !attrs.cpython_mutable; // Derive static names from the struct name. // W_Random -> RANDOM_TYPE, W_RANDOM_GC_TYPE_ID, @@ -1388,6 +1415,8 @@ fn expand_pyre_class( const DESCRIPTOR: &'static ::pyre_object::lltype::PyreClassDescriptor = &#descriptor_static; const PYNAME: &'static str = #name_lit; + const CPYTHON_HEAPTYPE: bool = #cpython_heaptype; + const CPYTHON_IMMUTABLETYPE: bool = #cpython_immutabletype; } impl #st_name { @@ -2430,6 +2459,17 @@ fn expand_pyre_methods( #base_expr, <#self_ty as ::pyre_object::lltype::PyreClassPyTypeOf>::PYTYPE, ); + // [3.14-spec] The declaration records CPython's public + // owner/mutability axes while the PyPy TypeDef produced + // above remains structurally builtin. + unsafe { + ::pyre_object::w_type_set_cpython_type_flags( + tp, + <#self_ty as ::pyre_object::lltype::PyreClassPyTypeOf>::CPYTHON_HEAPTYPE, + !<#self_ty as ::pyre_object::lltype::PyreClassPyTypeOf>::CPYTHON_HEAPTYPE, + <#self_ty as ::pyre_object::lltype::PyreClassPyTypeOf>::CPYTHON_IMMUTABLETYPE, + ); + } ::pyre_object::pyobject::set_instantiate( unsafe { &*<#self_ty as ::pyre_object::lltype::PyreClassPyTypeOf>::PYTYPE diff --git a/pyre/pyre-object/src/lltype.rs b/pyre/pyre-object/src/lltype.rs index f00b410934f..2766ee1a157 100644 --- a/pyre/pyre-object/src/lltype.rs +++ b/pyre/pyre-object/src/lltype.rs @@ -248,6 +248,15 @@ pub trait PyreClassPyTypeOf { /// verbatim from `#[pyre_class("…", type_id = N)]`. Consumed by /// `#[pyre_methods]` so the impl block doesn't restate it. const PYNAME: &'static str; + /// CPython 3.14 exposes many module-native types as HEAPTYPE even though + /// their PyPy implementation remains a builtin `TypeDef`. The class + /// declaration records that public owner independently of the `PyType` + /// storage descriptor. + const CPYTHON_HEAPTYPE: bool; + /// CPython's IMMUTABLETYPE axis is independent of HEAPTYPE. Most modern + /// extension heap types carry both; a few (for example `_random.Random`) + /// are mutable. + const CPYTHON_IMMUTABLETYPE: bool; } /// `lltype.malloc(T, flavor='gc')` parity, *untyped* (no `GcType` bound). diff --git a/pyre/pyre-object/src/typeobject.rs b/pyre/pyre-object/src/typeobject.rs index fc1c04bbec4..1374ae8f8a2 100644 --- a/pyre/pyre-object/src/typeobject.rs +++ b/pyre/pyre-object/src/typeobject.rs @@ -131,6 +131,35 @@ pub struct W_TypeObject { pub mro_w: *mut crate::object_array::FixedObjectArray, /// typeobject.py:184 `flag_heaptype` — immutable after creation. pub flag_heaptype: bool, + /// [3.14-spec] CPython's public `Py_TPFLAGS_HEAPTYPE` ownership axis. + /// + /// PyPy deliberately collapses every interpreter `TypeDef` to + /// `flag_heaptype = False` (`TypeDef.heaptype`, `typeobject.py`). + /// CPython 3.14 instead builds many extension-module types through + /// `PyType_FromModuleAndSpec`, so they publish HEAPTYPE while remaining + /// PyPy-style builtin TypeDefs internally. Keep that observable owner + /// bit separate rather than changing the load-bearing PyPy field above. + pub flag_cpython_heaptype: bool, + /// CPython-internal `_Py_TPFLAGS_STATIC_BUILTIN` (bit 1). This is not the + /// inverse of HEAPTYPE: a legacy extension type readied through the public + /// `PyType_Ready` API has neither bit, whereas an interpreter-owned core + /// type initialized by `_PyStaticType_InitBuiltin` carries this one. + pub flag_cpython_static_builtin: bool, + /// [3.14-spec] CPython's orthogonal `Py_TPFLAGS_IMMUTABLETYPE` axis. + /// + /// A CPython heap type may still be immutable (the common extension-type + /// shape), and `PyType_Freeze` can make an existing heap type immutable. + /// PyPy uses `not flag_heaptype` for this question, so the 3.14 projection + /// needs its own field instead of overloading the PyPy owner bit. + pub flag_cpython_immutabletype: std::sync::atomic::AtomicBool, + /// Suppress CPython's public `Py_TPFLAGS_BASETYPE` without changing + /// PyPy's load-bearing `Layout.acceptable_as_base_class` field. + /// + /// PyPy can reject subclassing in a custom metaclass while leaving the + /// ordinary app-level layout acceptable. A CPython static counterpart + /// instead records the rejection directly in `tp_flags`; projecting that + /// observable difference must not perturb constructor dispatch. + pub flag_cpython_suppress_basetype: bool, /// typeobject.py `layout` — pointer to shared Layout object. pub layout: *const Layout, /// typeobject.py:179 `hasdict` — True when instances have __dict__. @@ -403,6 +432,10 @@ pub fn w_type_new(name: &str, bases: PyObjectRef, dict_ptr: *mut u8) -> PyObject bases, dict: dict_ptr, flag_heaptype: true, + flag_cpython_heaptype: true, + flag_cpython_static_builtin: false, + flag_cpython_immutabletype: std::sync::atomic::AtomicBool::new(false), + flag_cpython_suppress_basetype: false, layout: std::ptr::null(), hasdict: false, weakrefable: false, @@ -526,6 +559,10 @@ pub fn w_type_new_builtin( bases, dict: dict_ptr, flag_heaptype: false, + flag_cpython_heaptype: false, + flag_cpython_static_builtin: true, + flag_cpython_immutabletype: std::sync::atomic::AtomicBool::new(true), + flag_cpython_suppress_basetype: false, layout: std::ptr::null(), hasdict: false, weakrefable: false, @@ -1211,6 +1248,34 @@ pub unsafe fn w_type_issubtype(w_type: PyObjectRef, cls: PyObjectRef) -> bool { (*mro_ptr).as_slice().iter().any(|&t| std::ptr::eq(t, cls)) } +/// Whether a class pattern with one positional sub-pattern receives the +/// subject itself. CPython stores this as `_Py_TPFLAGS_MATCH_SELF`; PyPy's +/// pattern opcode recognizes the same builtin atomic families. Membership in +/// any family's MRO makes the property inherit exactly as CPython's +/// `inherit_special` does. +pub unsafe fn w_type_has_match_self(w_type: PyObjectRef) -> bool { + if w_type.is_null() || !is_type(w_type) { + return false; + } + for base in [ + get_instantiate(&INT_TYPE), + get_instantiate(&FLOAT_TYPE), + get_instantiate(&STR_TYPE), + get_instantiate(&LIST_TYPE), + get_instantiate(&TUPLE_TYPE), + get_instantiate(&DICT_TYPE), + get_instantiate(&crate::bytesobject::BYTES_TYPE), + get_instantiate(&crate::bytearrayobject::BYTEARRAY_TYPE), + get_instantiate(&crate::setobject::SET_TYPE), + get_instantiate(&crate::setobject::FROZENSET_TYPE), + ] { + if !base.is_null() && w_type_issubtype(w_type, base) { + return true; + } + } + false +} + /// typeobject.py find_best_base — the type base whose instance /// layout a subtype extends (most-derived layout among the type bases). /// Non-raising variant for the null-mro subtype fallback. @@ -1324,6 +1389,72 @@ pub unsafe fn w_type_set_heaptype(obj: PyObjectRef, value: bool) { (*(obj as *mut W_TypeObject)).flag_heaptype = value; } +/// Publish the CPython 3.14 owner and mutability axes for a type whose PyPy +/// storage owner remains unchanged. Construction sites call this once after +/// creating an interpreter builtin TypeDef; `PyType_Freeze` only changes the +/// immutable half through [`w_type_set_cpython_immutabletype`]. +/// # Safety +/// The caller must uphold every validity, runtime-type, aliasing, and lifetime +/// invariant required by the object and pointer arguments for the entire call. +pub unsafe fn w_type_set_cpython_type_flags( + obj: PyObjectRef, + heaptype: bool, + static_builtin: bool, + immutabletype: bool, +) { + let t = &mut *(obj as *mut W_TypeObject); + t.flag_cpython_heaptype = heaptype; + t.flag_cpython_static_builtin = static_builtin; + t.flag_cpython_immutabletype + .store(immutabletype, std::sync::atomic::Ordering::Release); +} + +/// CPython 3.14 public HEAPTYPE ownership, distinct from PyPy's +/// [`w_type_is_heaptype`] implementation classification. +/// # Safety +/// The caller must uphold every validity, runtime-type, aliasing, and lifetime +/// invariant required by the object and pointer arguments for the entire call. +pub unsafe fn w_type_is_cpython_heaptype(obj: PyObjectRef) -> bool { + (*(obj as *const W_TypeObject)).flag_cpython_heaptype +} + +/// Read CPython's internal STATIC_BUILTIN owner bit. +/// # Safety +/// The caller must uphold every validity, runtime-type, aliasing, and lifetime +/// invariant required by the object and pointer arguments for the entire call. +pub unsafe fn w_type_is_cpython_static_builtin(obj: PyObjectRef) -> bool { + (*(obj as *const W_TypeObject)).flag_cpython_static_builtin +} + +/// Set only CPython's public IMMUTABLETYPE axis (used by `PyType_Freeze`). +/// # Safety +/// The caller must uphold every validity, runtime-type, aliasing, and lifetime +/// invariant required by the object and pointer arguments for the entire call. +pub unsafe fn w_type_set_cpython_immutabletype(obj: PyObjectRef, value: bool) { + (*(obj as *const W_TypeObject)) + .flag_cpython_immutabletype + .store(value, std::sync::atomic::Ordering::Release); +} + +/// Read CPython's public IMMUTABLETYPE axis. +/// # Safety +/// The caller must uphold every validity, runtime-type, aliasing, and lifetime +/// invariant required by the object and pointer arguments for the entire call. +pub unsafe fn w_type_is_cpython_immutabletype(obj: PyObjectRef) -> bool { + (*(obj as *const W_TypeObject)) + .flag_cpython_immutabletype + .load(std::sync::atomic::Ordering::Acquire) +} + +/// Suppress CPython's public BASETYPE bit while preserving PyPy's canonical +/// layout-level subclassability field and all of its internal readers. +/// # Safety +/// The caller must uphold every validity, runtime-type, aliasing, and lifetime +/// invariant required by the object and pointer arguments for the entire call. +pub unsafe fn w_type_suppress_cpython_basetype(obj: PyObjectRef) { + (*(obj as *mut W_TypeObject)).flag_cpython_suppress_basetype = true; +} + /// typeobject.py `W_TypeObject.get_flags` — compute PyPy's public type flags /// from their canonical fields on `W_TypeObject`. /// # Safety @@ -1333,43 +1464,173 @@ pub unsafe fn w_type_get_flags(obj: PyObjectRef) -> i64 { if obj.is_null() || !is_type(obj) { return 0; } + const STATIC_BUILTIN: i64 = 1 << 1; const HEAPTYPE: i64 = 1 << 9; // copy_reg._HEAPTYPE + const INLINE_VALUES: i64 = 1 << 2; + const MANAGED_WEAKREF: i64 = 1 << 3; + const MANAGED_DICT: i64 = 1 << 4; const IMMUTABLETYPE: i64 = 1 << 8; + const DISALLOW_INSTANTIATION: i64 = 1 << 7; + const BASETYPE: i64 = 1 << 10; + const READY: i64 = 1 << 12; + const READYING: i64 = 1 << 13; const ABSTRACT: i64 = 1 << 20; + const MATCH_SELF: i64 = 1 << 22; + const HAVE_GC: i64 = 1 << 14; const PATMA_SEQUENCE: i64 = 1 << 5; const PATMA_MAPPING: i64 = 1 << 6; const METHOD_DESCRIPTOR: i64 = 1 << 17; + const LONG_SUBCLASS: i64 = 1 << 24; + const LIST_SUBCLASS: i64 = 1 << 25; + const TUPLE_SUBCLASS: i64 = 1 << 26; + const BYTES_SUBCLASS: i64 = 1 << 27; + const UNICODE_SUBCLASS: i64 = 1 << 28; + const DICT_SUBCLASS: i64 = 1 << 29; + const BASE_EXC_SUBCLASS: i64 = 1 << 30; + const TYPE_SUBCLASS: i64 = 1 << 31; let t = &*(obj as *const W_TypeObject); let mut flags = 0; - if t.flag_heaptype { + if t.flag_cpython_static_builtin { + flags |= STATIC_BUILTIN; + } + if t.flag_cpython_heaptype { flags |= HEAPTYPE; - } else { - // `Py_TPFLAGS_IMMUTABLETYPE` (`object.h`, read at v3.14.6): a type - // whose attributes cannot be set. `get_flags` publishes no such bit - // — it reports the heap flag and stops — while every type pyre builds - // itself already answers "cannot set 'x' attribute of immutable type" - // to a store, so the flag names something already true here rather - // than changing what any of them does. Measured over 39 builtins on - // 3.14: the bit is set on exactly the types that are not heap types, - // and those are exactly the ones that refuse the store, in both - // interpreters. `test_ctypes/_support.py` spells the constant and - // `test_win32.py:81` reads it back off `COMError`. + } + + if t.flag_cpython_heaptype { + // [3.14-spec] `type_new_descriptors` represents a dict slot added by + // a heap type with MANAGED_DICT (`typeobject.c`, read at v3.14.6). + // PyPy records the same ownership boundary as + // `self.hasdict and not self.layout.typedef.hasdict` when choosing a + // DictTerminator (typeobject.py:255-257). Project those canonical + // fields; do not infer ownership merely from the public capability, + // since module instances own their dict in the builtin layout. + let layout = t.layout; + let typedef_hasdict = !layout.is_null() && (*layout).typedef_hasdict; + if t.hasdict && !typedef_hasdict { + flags |= MANAGED_DICT; + // CPython's `type_ready_managed_dict` adds INLINE_VALUES only to + // fixed-size managed-dict types. This projection shares the + // layout typedef used by `type.__itemsize__`. + if !w_type_cpython_has_variable_items(obj) { + flags |= INLINE_VALUES; + } + } + if w_type_has_cpython_managed_weakref(obj) { + flags |= MANAGED_WEAKREF; + } + } + if t.flag_cpython_immutabletype + .load(std::sync::atomic::Ordering::Acquire) + { + // `Py_TPFLAGS_IMMUTABLETYPE` (`object.h`, read at v3.14.6) is + // orthogonal to HEAPTYPE: modern extension types commonly carry both. + // PyPy's `descr__flags__` reports neither axis for builtin TypeDefs; + // this field publishes CPython's observable split without changing + // PyPy's internal type ownership. flags |= IMMUTABLETYPE; } - // typeobject.py `flag_cpytype` marks cpyext-defined static types; pyre has - // no equivalent type owner, so its bit is always absent. if t.flag_abstract.load(std::sync::atomic::Ordering::Acquire) { flags |= ABSTRACT; } + // [3.14-spec] CPython `type_ready` exposes READYING while a custom + // metaclass's `mro()` is running, then replaces it with READY when the MRO + // is installed (`Objects/typeobject.c:450-466, 8986-8991`). PyPy's + // partially initialized `W_TypeObject` likewise has `mro_w is None` + // (`typeobject.py:1080-1084`), which is pyre's canonical readiness state. + if t.mro_w.is_null() { + flags |= READYING; + } else { + flags |= READY; + } + if t.flag_disallow_instantiation + .load(std::sync::atomic::Ordering::Acquire) + { + // [3.14-spec] CPython v3.14.6 `Include/object.h:540` assigns bit 7 + // to `Py_TPFLAGS_DISALLOW_INSTANTIATION`, and + // `Objects/typeobject.c:1407` exposes the complete `tp_flags` word. + // PyPy `typeobject.py:990-1004` publishes a smaller computed subset. + // Preserve that field-by-field shape while exposing the canonical + // flag that pyre's `type.__call__` already enforces. + flags |= DISALLOW_INSTANTIATION; + } + if w_type_get_acceptable_as_base_class(obj) && !t.flag_cpython_suppress_basetype { + // [3.14-spec] CPython v3.14.6 `Include/object.h:549` assigns bit 10 + // to `Py_TPFLAGS_BASETYPE`, and `Objects/typeobject.c:3638` uses that + // same bit to accept or reject a base class. PyPy + // `typeobject.py:990-1004` omits it from the public subset while + // `typeobject.py:1116-1118` enforces the canonical + // `layout.typedef.acceptable_as_base_class` value. Keep PyPy's + // field-by-field shape and publish that existing value. + flags |= BASETYPE; + } + if t.flag_have_gc { + // [3.14-spec] CPython v3.14.6 exposes the complete `tp_flags` word + // through `Objects/typeobject.c:1407`, and + // `Include/object.h:567` assigns this bit to `Py_TPFLAGS_HAVE_GC`. + // PyPy `typeobject.py:990-1004` computes a deliberately smaller + // public subset. Keep its field-by-field `descr__flags__` shape, but + // publish pyre's canonical per-type GC flag for the 3.14 surface. + flags |= HAVE_GC; + } if t.flag_method_descriptor { flags |= METHOD_DESCRIPTOR; } match t.flag_map_or_seq.load(std::sync::atomic::Ordering::Acquire) { b'M' => flags |= PATMA_MAPPING, - b'S' => flags |= PATMA_SEQUENCE, + // CPython deliberately omits the sequence-pattern flag from str, + // bytes and bytearray (`unicodeobject.c:15805`, + // `bytesobject.c:3118`, `bytearrayobject.c:2867`). Pyre keeps PyPy's + // internal S marker on these types for `issequence_w`; MATCH_SEQUENCE + // applies the same exclusion, so only the public bit is masked here. + b'S' if !w_type_issubtype(obj, get_instantiate(&STR_TYPE)) + && !w_type_issubtype(obj, get_instantiate(&crate::bytesobject::BYTES_TYPE)) + && !w_type_issubtype( + obj, + get_instantiate(&crate::bytearrayobject::BYTEARRAY_TYPE), + ) => + { + flags |= PATMA_SEQUENCE + } _ => {} } + if w_type_has_match_self(obj) { + // [3.14-spec] CPython `inherit_special` inherits MATCH_SELF from the + // dominant base (`typeobject.c:8204-8206`). PyPy implements the same + // observable class-pattern rule with its builtin atomic-type test; + // `w_type_has_match_self` centralizes that MRO classification for the + // opcode and this public flag. + flags |= MATCH_SELF; + } + // [3.14-spec] CPython v3.14.6 `Objects/typeobject.c:8175-8200` + // computes these mutually exclusive fast-subclass flags from the base + // MRO in this exact order. PyPy does not publish them from + // `descr__flags`, but its canonical classification is the MRO membership + // scan in `typeobject.py:603/1640`, ported as `w_type_issubtype`. + for (base, bit) in [ + ( + crate::interp_exceptions::lookup_exc_class_for_kind( + crate::interp_exceptions::ExcKind::BaseException, + ), + BASE_EXC_SUBCLASS, + ), + (get_instantiate(&TYPE_TYPE), TYPE_SUBCLASS), + (get_instantiate(&INT_TYPE), LONG_SUBCLASS), + ( + get_instantiate(&crate::bytesobject::BYTES_TYPE), + BYTES_SUBCLASS, + ), + (get_instantiate(&STR_TYPE), UNICODE_SUBCLASS), + (get_instantiate(&TUPLE_TYPE), TUPLE_SUBCLASS), + (get_instantiate(&LIST_TYPE), LIST_SUBCLASS), + (get_instantiate(&DICT_TYPE), DICT_SUBCLASS), + ] { + if !base.is_null() && w_type_issubtype(obj, base) { + flags |= bit; + break; + } + } flags } @@ -1400,6 +1661,49 @@ pub unsafe fn w_type_get_typedef_hasdict(obj: PyObjectRef) -> bool { (*layout).typedef_hasdict } } + +/// Whether CPython 3.14 projects a non-zero `tp_itemsize` for this PyPy +/// instance layout. The exact byte count remains in the interpreter's +/// `cpython_type_layout`; `type.__flags__` only needs the zero/non-zero split +/// used by `type_ready_managed_dict` to decide INLINE_VALUES. +unsafe fn w_type_cpython_has_variable_items(obj: PyObjectRef) -> bool { + let typedef = w_type_get_layout(obj); + std::ptr::eq(typedef, &TYPE_TYPE) + || std::ptr::eq(typedef, &INT_TYPE) + || std::ptr::eq(typedef, &LONG_TYPE) + || std::ptr::eq(typedef, &TUPLE_TYPE) + || std::ptr::eq(typedef, &crate::bytesobject::BYTES_TYPE) + || std::ptr::eq(typedef, &crate::memoryview::MEMORYVIEW_TYPE) +} + +/// CPython 3.14 `Py_TPFLAGS_MANAGED_WEAKREF`, projected through PyPy's +/// `weakrefable` inheritance and `find_best_base` layout owner. +/// +/// A heap type whose best base is not weakrefable introduced the managed +/// weakref slot itself. Otherwise the storage kind follows the best base: +/// another heap type propagates the managed flag, while a builtin owner such +/// as `type`, `set`, or `module` terminates the walk with an intrinsic slot. +/// `copy_flags_from_bases` may also obtain the capability from a compatible +/// secondary base; when the best base itself lacks it, CPython creates the +/// managed slot on the new type, which is the first arm below. +/// +/// This deliberately follows the owner chain instead of testing whether the +/// `__weakref__` descriptor is still present: deleting a descriptor cannot +/// rewrite the immutable type-layout flag. +unsafe fn w_type_has_cpython_managed_weakref(obj: PyObjectRef) -> bool { + if obj.is_null() || !is_type(obj) { + return false; + } + let t = &*(obj as *const W_TypeObject); + if !t.flag_heaptype || !t.weakrefable { + return false; + } + let bestbase = find_best_base(obj); + if bestbase.is_null() || !w_type_get_weakrefable(bestbase) { + return true; + } + w_type_has_cpython_managed_weakref(bestbase) +} /// Override acceptable_as_base_class by cloning the Layout. /// typedef.py:742,765,664 explicit overrides after initial creation. /// Layouts may be shared (reused from parent), so we clone to avoid @@ -1721,6 +2025,140 @@ mod tests { assert!(!Layout::expands_equal(root, true, true, root, false, true)); } + #[test] + fn type_flags_project_managed_dict_and_inline_values_from_layout_owner() { + const INLINE_VALUES: i64 = 1 << 2; + const MANAGED_DICT: i64 = 1 << 4; + const BASETYPE: i64 = 1 << 10; + const MASK: i64 = INLINE_VALUES | MANAGED_DICT; + + unsafe fn heap_type_with_layout( + typedef: *const PyType, + typedef_hasdict: bool, + ) -> PyObjectRef { + let w_type = w_type_new("C", PY_NULL, std::ptr::null_mut()); + let layout = leak_layout(Layout { + typedef, + nslots: 0, + newslotnames: vec![], + base_layout: std::ptr::null(), + acceptable_as_base_class: true, + typedef_hasdict, + }); + w_type_set_layout(w_type, layout); + w_type_set_hasdict(w_type, true); + w_type + } + + unsafe { + // A normal heap instance uses PyPy's DictTerminator and CPython's + // fixed-size inline-values representation. + let plain = heap_type_with_layout(&INSTANCE_TYPE, false); + assert_eq!(w_type_get_flags(plain) & MASK, MASK); + + // An app-level PyPy class may stand in for a CPython static + // builtin. Keep its internal heap owner, but do not publish the + // managed-layout bits CPython only assigns to heap types. + w_type_set_cpython_type_flags(plain, false, true, true); + assert!(w_type_is_heaptype(plain)); + assert_eq!(w_type_get_flags(plain) & MASK, 0); + assert_ne!(w_type_get_flags(plain) & BASETYPE, 0); + w_type_suppress_cpython_basetype(plain); + assert_eq!(w_type_get_flags(plain) & BASETYPE, 0); + + // A tuple subtype still owns a managed dict, but a non-zero + // CPython tp_itemsize excludes INLINE_VALUES. + let tuple_subclass = heap_type_with_layout(&TUPLE_TYPE, false); + assert_eq!(w_type_get_flags(tuple_subclass) & MASK, MANAGED_DICT); + + // A builtin typedef such as module owns its dict directly, so a + // heap subtype sharing that layout has neither managed bit. + let native_dict = heap_type_with_layout(&MODULE_TYPE, true); + assert_eq!(w_type_get_flags(native_dict) & MASK, 0); + } + } + + #[test] + fn type_flags_project_managed_weakref_through_the_best_base_owner() { + const MANAGED_WEAKREF: i64 = 1 << 3; + let layout = leak_layout(Layout { + typedef: &INSTANCE_TYPE, + nslots: 0, + newslotnames: vec![], + base_layout: std::ptr::null(), + acceptable_as_base_class: true, + typedef_hasdict: false, + }); + + unsafe fn builtin_with_layout(name: &str, layout: *const Layout) -> PyObjectRef { + let w_type = w_type_new_builtin(name, PY_NULL, std::ptr::null_mut(), &INSTANCE_TYPE); + w_type_set_layout(w_type, layout); + w_type + } + + unsafe fn heap_with_base( + name: &str, + base: PyObjectRef, + layout: *const Layout, + ) -> PyObjectRef { + let bases = crate::w_tuple_new(vec![base]); + let w_type = w_type_new(name, bases, std::ptr::null_mut()); + w_type_set_layout(w_type, layout); + w_type + } + + unsafe { + let object = builtin_with_layout("object", layout); + let plain = heap_with_base("Plain", object, layout); + w_type_set_weakrefable(plain, true); + assert_ne!(w_type_get_flags(plain) & MANAGED_WEAKREF, 0); + + let derived = heap_with_base("Derived", plain, layout); + w_type_set_weakrefable(derived, true); + assert_ne!(w_type_get_flags(derived) & MANAGED_WEAKREF, 0); + + let intrinsic = builtin_with_layout("intrinsic", layout); + w_type_set_weakrefable(intrinsic, true); + let intrinsic_subclass = heap_with_base("IntrinsicChild", intrinsic, layout); + w_type_set_weakrefable(intrinsic_subclass, true); + assert_eq!(w_type_get_flags(intrinsic_subclass) & MANAGED_WEAKREF, 0); + } + } + + #[test] + fn type_flags_keep_cpython_owner_and_immutability_orthogonal() { + const STATIC_BUILTIN: i64 = 1 << 1; + const IMMUTABLETYPE: i64 = 1 << 8; + const HEAPTYPE: i64 = 1 << 9; + const MASK: i64 = STATIC_BUILTIN | IMMUTABLETYPE | HEAPTYPE; + + unsafe { + // `_PyStaticType_InitBuiltin`: interpreter-owned static builtin. + let core = w_type_new_builtin("core", PY_NULL, std::ptr::null_mut(), &INSTANCE_TYPE); + assert_eq!( + w_type_get_flags(core) & MASK, + STATIC_BUILTIN | IMMUTABLETYPE + ); + + // `PyType_FromModuleAndSpec`: a heap-owned but immutable extension + // type. Its PyPy implementation owner remains a builtin TypeDef. + let extension = + w_type_new_builtin("extension", PY_NULL, std::ptr::null_mut(), &INSTANCE_TYPE); + w_type_set_cpython_type_flags(extension, true, false, true); + assert!(!w_type_is_heaptype(extension)); + assert_eq!(w_type_get_flags(extension) & MASK, HEAPTYPE | IMMUTABLETYPE); + + // A legacy static extension readied through public PyType_Ready is + // neither a core static builtin nor a heap type, but is immutable. + w_type_set_cpython_type_flags(extension, false, false, true); + assert_eq!(w_type_get_flags(extension) & MASK, IMMUTABLETYPE); + + // An app-level class remains mutable and heap-owned. + let user = w_type_new("User", PY_NULL, std::ptr::null_mut()); + assert_eq!(w_type_get_flags(user) & MASK, HEAPTYPE); + } + } + #[test] fn w_type_gc_type_id_matches_descr() { assert_eq!(W_TYPE_GC_TYPE_ID, 33); diff --git a/pyre/pyre-object/src/unicodeobject.rs b/pyre/pyre-object/src/unicodeobject.rs index d7db3876fae..21840600636 100644 --- a/pyre/pyre-object/src/unicodeobject.rs +++ b/pyre/pyre-object/src/unicodeobject.rs @@ -347,6 +347,76 @@ pub fn w_str_from_wtf8_managed(value: Wtf8Buf) -> PyObjectRef { } } +/// Allocate a dynamic exact string at a terminal, GC-safe return site. +/// +/// PyPy's `StdObjSpace.newutf8` constructs an ordinary movable +/// `W_UnicodeObject` (`objspace.py:391-394`). Most interpreter callers still +/// need the born-old stepping stone in [`w_str_from_wtf8_managed`] until their +/// Rust-stack live references have explicit roots. A caller that has consumed +/// all of its Python operands can use this direct translated allocation shape: +/// the separately allocated value box is the one live child rooted across the +/// collecting header allocation. +/// +/// # Safety +/// Every managed Python reference live across this call must already be visible +/// to the collector. In particular, a caller must not read an unrooted raw +/// `PyObjectRef` after this function returns. +pub unsafe fn w_str_from_wtf8_managed_collecting(value: Wtf8Buf) -> PyObjectRef { + if !crate::gc_interp::enabled() || unicode_value_gc_type_id() == 0 { + return w_str_from_wtf8_immortal(value); + } + let byte_len = value.len(); + let char_len = value.code_points().count(); + let value = crate::gc_storage::gc_alloc_storage_box(value, unicode_value_gc_type_id()); + let mut unicode = W_UnicodeObject { + ob_header: PyObject { + ob_type: &STR_TYPE as *const PyType, + w_class: get_instantiate(&STR_TYPE), + }, + value, + byte_len, + len: char_len, + w_slots: PY_NULL, + index_storage: std::ptr::null_mut(), + hash: 0, + }; + let value_slot = std::ptr::addr_of_mut!(unicode.value).cast::<*mut u8>(); + let mut needs_write_barrier = true; + let raw = unsafe { + crate::gc_hook::try_gc_alloc_collecting_rooted( + W_UNICODE_GC_TYPE_ID, + W_UNICODE_OBJECT_SIZE, + value_slot, + &mut needs_write_barrier, + ) + } + .filter(|raw| !raw.is_null()) + .unwrap_or(std::ptr::null_mut()); + if raw.is_null() { + let recovered = unsafe { (*unicode.value).clone() }; + return w_str_from_wtf8_immortal(recovered); + } + unsafe { + std::ptr::write(raw as *mut W_UnicodeObject, unicode); + } + // A nursery header needs no creation barrier. The collecting allocator + // can spill the header to old-gen; remember that old-to-young value edge + // exactly as listobject.py's rooted constructor does for its items block. + if needs_write_barrier { + crate::gc_hook::try_gc_write_barrier(raw); + } + raw as PyObjectRef +} + +/// UTF-8 convenience wrapper for [`w_str_from_wtf8_managed_collecting`]. +/// +/// # Safety +/// The caller must uphold the same root-safety contract as the wrapped +/// function. +pub unsafe fn w_str_new_managed_collecting(s: &str) -> PyObjectRef { + unsafe { w_str_from_wtf8_managed_collecting(Wtf8Buf::from_string(s.to_string())) } +} + /// `_utf8_sliced` (unicodeobject.py) — wrap a piece cut out of /// `recv`'s own WTF-8 storage. /// diff --git a/pyre/pyre-wasm-runner/src/main.rs b/pyre/pyre-wasm-runner/src/main.rs index 6e512a67b89..5ddf52aef76 100644 --- a/pyre/pyre-wasm-runner/src/main.rs +++ b/pyre/pyre-wasm-runner/src/main.rs @@ -106,6 +106,13 @@ struct Host { /// `PYRE_WASM_GUEST_PROFILE` sampling profiler; taken/restored around each /// epoch tick so `sample` can borrow the store it lives in. guest_profiler: Option, + /// Trace slots whose compile exported a `trace_wide`, and whose `slot + 1` + /// is therefore a published call target. The reserved spare slot holds the + /// narrow function when a compile had no wide entry, so the table alone + /// cannot tell the two apart; a replacement that would drop the wide entry + /// is rejected against this set rather than leaving `slot + 1` pointing at + /// the replaced compile. + wide_slots: std::collections::HashSet, } /// Report `PYRE_*` / `MAJIT_*` settings the guest cannot see. @@ -1519,10 +1526,18 @@ fn build_linker(engine: &Engine) -> Result> { // Only a trace slot may be cleared; nulling a main-module slot // (`id < trace_base`) would corrupt the shared dispatch table. if (func_id as u64) >= caller.data().trace_base { - // Release the table's hold on the trace function; the slot - // itself stays (wasm tables cannot shrink). + // Release the table's hold on the trace function; the slots + // themselves stay (wasm tables cannot shrink). `jit_compile` + // appends the entry as a pair, so `func_id + 1` holds this + // same trace — the wide entry where one was published, the + // spare copy of the narrow function otherwise. Clearing only + // the first half leaves the freed trace reachable through + // `call_indirect func_id + 1` and rooted for the store's + // lifetime, so both halves and the wide record go together. + caller.data_mut().wide_slots.remove(&func_id); if let Some(table) = caller.data().table { let _ = table.set(&mut caller, func_id as u64, Ref::Func(None)); + let _ = table.set(&mut caller, func_id as u64 + 1, Ref::Func(None)); } } }, @@ -1656,7 +1671,7 @@ fn jit_compile_trace( caller: &mut Caller<'_, Host>, bytes_ptr: u32, bytes_len: u32, -) -> Result<(Table, Func)> { +) -> Result<(Table, Func, Option)> { caller.data_mut().jit_compile_count += 1; let memory = caller .data() @@ -1745,18 +1760,31 @@ fn jit_compile_trace( let trace = instance .get_func(&mut *caller, "trace") .context("trace module is missing its `trace` export")?; + let trace_wide = instance.get_func(&mut *caller, "trace_wide"); - Ok((table, trace)) + Ok((table, trace, trace_wide)) } /// Compile and instantiate a trace, then append its export to the trace table. fn jit_compile(caller: &mut Caller<'_, Host>, bytes_ptr: u32, bytes_len: u32) -> Result { - let (table, trace) = jit_compile_trace(caller, bytes_ptr, bytes_len)?; + let (table, trace, trace_wide) = jit_compile_trace(caller, bytes_ptr, bytes_len)?; // Register the trace into the shared indirect function table so it is // reachable by table index. `grow` returns the newly appended slot. + // + // The pair is appended even for a narrow module. An emitted module names + // its wide entry `handle + 1`, and `jit_replace` may install a wide entry + // where this compile had none; without the reservation that write would + // land on the next trace's own entry. The spare slot holds the narrow + // function, which nothing calls: absence is encoded as `wide_slot == 0`. let slot = table - .grow(&mut *caller, 1, Ref::Func(Some(trace))) + .grow(&mut *caller, 2, Ref::Func(Some(trace))) .context("register trace into shared table")? as u32; + if let Some(wide) = trace_wide { + table + .set(&mut *caller, slot as u64 + 1, Ref::Func(Some(wide))) + .context("register wide trace entry into shared table")?; + caller.data_mut().wide_slots.insert(slot); + } Ok(slot) } @@ -1772,7 +1800,7 @@ fn jit_replace( "jit_replace_wasm: id {func_id} is not a trace slot" ))); } - let (table, trace) = jit_compile_trace(caller, bytes_ptr, bytes_len)?; + let (table, trace, trace_wide) = jit_compile_trace(caller, bytes_ptr, bytes_len)?; if !matches!( table.get(&mut *caller, func_id as u64), Some(Ref::Func(Some(_))) @@ -1781,12 +1809,28 @@ fn jit_replace( "jit_replace_wasm: id {func_id} is not a live trace" ))); } + // Modules emitted while this slot was wide carry `call_indirect func_id + + // 1` baked in. A narrow replacement cannot retract those, so accepting one + // would leave the pair straddling two compiles: `func_id` on the new trace + // and `func_id + 1` still on the old. Reject the shape change before + // either table is touched; a narrow-to-wide replacement stays allowed. + if trace_wide.is_none() && caller.data().wide_slots.contains(&func_id) { + return Err(Error::msg(format!( + "jit_replace_wasm: id {func_id} has a published wide entry the replacement does not" + ))); + } // The Store retains every instantiated module. Replacing this table entry // therefore leaves an old trace live when a non-tail indirect call still // has one of its frames on the guest stack. table .set(&mut *caller, func_id as u64, Ref::Func(Some(trace))) .context("replace trace in shared table")?; + if let Some(wide) = trace_wide { + table + .set(&mut *caller, func_id as u64 + 1, Ref::Func(Some(wide))) + .context("replace wide trace entry in shared table")?; + caller.data_mut().wide_slots.insert(func_id); + } Ok(func_id) } diff --git a/pyre/pyrex/src/lib.rs b/pyre/pyrex/src/lib.rs index 17a7a4fac0a..e2b11d79241 100644 --- a/pyre/pyrex/src/lib.rs +++ b/pyre/pyrex/src/lib.rs @@ -219,6 +219,7 @@ fn parse_args( match option.to_str() { Some("dev") => flags.dev_mode = true, Some("warn_default_encoding") => flags.warn_default_encoding = true, + Some("no_debug_ranges") => flags.no_debug_ranges = true, Some("utf8") | Some("utf8=1") => flags.utf8_mode = Some(1), Some("utf8=0") => flags.utf8_mode = Some(0), Some(value) if value.starts_with("utf8=") => {