From b33adafe86392bc1b23fa31da72152671e170831 Mon Sep 17 00:00:00 2001 From: dot Date: Mon, 12 Oct 2026 08:58:14 +0900 Subject: [PATCH] Project private constant specializations in public adaptive bytes Traverse immutable canonical boundaries and preserve arguments and cache cells. Retain public specialization IDs and reject the unsupported legacy iterator encoding. Add ordinary warmed and monitoring regressions. Assisted-by: OpenAI Codex:model identifier unavailable --- crates/compiler-core/src/bytecode.rs | 7 ++ crates/vm/src/builtins/code.rs | 48 +++++++-- .../snippets/adaptive_public_export_315.py | 101 ++++++++++++++++++ .../COMPILER_315_DEVELOPMENT.md | 12 ++- 4 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 extra_tests/snippets/adaptive_public_export_315.py diff --git a/crates/compiler-core/src/bytecode.rs b/crates/compiler-core/src/bytecode.rs index d00cd7fb0..d0c1ef92a 100644 --- a/crates/compiler-core/src/bytecode.rs +++ b/crates/compiler-core/src/bytecode.rs @@ -807,6 +807,13 @@ impl CodeUnits { } } + /// Number of code units, without borrowing the live mutable instruction slice. + /// Constructors and Clone keep the out-of-line counter array at this length. + #[must_use] + pub fn unit_count(&self) -> usize { + self.adaptive_counters.len() + } + /// Replace the opcode at `index` in-place without changing the arg byte. /// Uses atomic Release store to ensure prior cache writes are visible /// to threads that subsequently read the new opcode with Acquire. diff --git a/crates/vm/src/builtins/code.rs b/crates/vm/src/builtins/code.rs index ed34aad96..aa7556473 100644 --- a/crates/vm/src/builtins/code.rs +++ b/crates/vm/src/builtins/code.rs @@ -1198,15 +1198,47 @@ impl Py { } #[pygetset] - pub fn _co_code_adaptive(&self, vm: &VirtualMachine) -> crate::builtins::PyBytesRef { - // Return current (possibly quickened/specialized) bytecode - let bytes = unsafe { - core::slice::from_raw_parts( - self.code.instructions.as_ptr() as *const u8, - self.code.instructions.len() * 2, - ) + pub fn _co_code_adaptive(&self, vm: &VirtualMachine) -> PyResult { + let incompatible = || vm.new_value_error("incompatible adaptive bytecode layout"); + let legacy = + || vm.new_value_error("legacy private GET_YIELD_FROM_ITER has no public 3.15 encoding"); + let (canonical, []) = self.canonical_code.as_chunks::<2>() else { + return Err(incompatible()); }; - vm.ctx.new_bytes(bytes.to_vec()) + let instructions = &self.code.instructions; + if canonical.len() != instructions.unit_count() { + return Err(incompatible()); + } + let mut bytes = Vec::with_capacity(self.canonical_code.len()); + let mut index = 0; + while index < canonical.len() { + let base = Instruction::try_from(canonical[index][0]).map_err(|_| incompatible())?; + if matches!(base, Instruction::GetYieldFromIter) { + return Err(legacy()); + } + let next = index + .checked_add(1 + base.cache_entries()) + .filter(|&next| next <= canonical.len()) + .ok_or_else(incompatible)?; + // Boundaries come only from this code's immutable canonical backing. + // Never decode a cache payload or use a live monitoring wrapper's width. + let op = match instructions.read_op(index) { + op @ (Instruction::LoadConstImmortal | Instruction::LoadConstMortal) => { + op.deoptimize() + } + Instruction::GetYieldFromIter => return Err(legacy()), + op => op, + }; + bytes.push(u8::from(op)); + bytes.push(u8::from(instructions.read_arg(index))); + for cache in index + 1..next { + bytes.extend_from_slice(&instructions.read_cache_u16(cache).to_ne_bytes()); + } + index = next; + } + // Per-cell atomic observation, not a coherent cross-thread snapshot. + // This does not repair the interpreter's other live-storage access paths. + Ok(vm.ctx.new_bytes(bytes)) } #[pygetset] diff --git a/extra_tests/snippets/adaptive_public_export_315.py b/extra_tests/snippets/adaptive_public_export_315.py new file mode 100644 index 000000000..2c1856121 --- /dev/null +++ b/extra_tests/snippets/adaptive_public_export_315.py @@ -0,0 +1,101 @@ +"""Public adaptive bytes preserve layout without exposing private constant opcodes.""" + +import dis +import marshal +import opcode +import sys + +FILENAME = "adaptive_public_export.py" +CONSTANT_SOURCE = ( + 'def warm_const():\n return ("ordinary constant payload", 123456789)\n' +) +LOOP_SOURCE = "def total(n):\n result = 0\n for value in range(n):\n result += value\n return result\n" +PUBLIC = set(opcode.opmap.values()) | set(opcode._specialized_opmap.values()) + + +def make(source, name): + namespace = {} + exec(compile(source, FILENAME, "exec"), namespace) + return namespace[name] + + +def check_layout(code, canonical, offsets): + adaptive = code._co_code_adaptive + assert len(adaptive) == len(canonical) + for offset in offsets: + assert adaptive[offset] in PUBLIC, (offset, adaptive[offset]) + assert adaptive[offset] not in (219, 220, 221) + assert adaptive[offset + 1] == canonical[offset + 1] + assert code.co_code == canonical + return adaptive + + +function = make(CONSTANT_SOURCE, "warm_const") +code = function.__code__ +canonical = code.co_code +offsets = tuple(i.offset for i in dis.get_instructions(code)) +cold = check_layout(code, canonical, offsets) +constant_offset = next( + i.offset for i in dis.get_instructions(code) if i.opname == "LOAD_CONST" +) +assert cold[constant_offset] == opcode.opmap["LOAD_CONST"] +before = marshal.dumps(code, 6) +for _ in range(1000): + assert function() == ("ordinary constant payload", 123456789) +assert marshal.dumps(code, 6) == before +adaptive = check_layout(code, canonical, offsets) +constants = [ + i for i in dis.get_instructions(code, adaptive=True) if i.opname == "LOAD_CONST" +] +assert len(constants) == 1 +assert constants[0].argval == ("ordinary constant payload", 123456789) +assert adaptive[constants[0].offset] == opcode.opmap["LOAD_CONST"] + +loop = make(LOOP_SOURCE, "total") +code = loop.__code__ +canonical = code.co_code +offsets = tuple(i.offset for i in dis.get_instructions(code)) +for _ in range(1000): + assert loop(10) == 45 +first = check_layout(code, canonical, offsets) +second = check_layout(code, canonical, offsets) +assert first == second +assert any(i.cache_info for i in dis.get_instructions(code)) +rows = tuple(dis.get_instructions(code, adaptive=True)) +assert tuple(i.offset for i in rows) == offsets +assert any(i.opcode in opcode._specialized_opmap.values() for i in rows) +assert all(first[i.offset] == i.opcode for i in rows) + +monitoring = sys.monitoring +available = [tool for tool in range(6) if monitoring.get_tool(tool) is None] +assert available +TOOL = available[0] +monitoring.use_tool_id(TOOL, "adaptive-public-export") +try: + for event in (monitoring.events.LINE, monitoring.events.INSTRUCTION): + function = make(LOOP_SOURCE, "total") + code = function.__code__ + canonical = code.co_code + offsets = tuple(i.offset for i in dis.get_instructions(code)) + observed = [] + + def inspect(actual_code, location): + assert actual_code is code + if not observed: + # First adaptive export for this code happens under active wrappers. + observed.append(check_layout(actual_code, canonical, offsets)) + assert marshal.loads(marshal.dumps(actual_code, 6)).co_code == canonical + + monitoring.register_callback(TOOL, event, inspect) + monitoring.set_local_events(TOOL, code, event) + try: + assert function(10) == 45 + assert len(observed) == 1 + finally: + monitoring.set_local_events(TOOL, code, 0) + monitoring.register_callback(TOOL, event, None) + check_layout(code, canonical, offsets) + assert function(5) == 10 +finally: + monitoring.set_events(TOOL, 0) + monitoring.free_tool_id(TOOL) diff --git a/tools/opcode_metadata/COMPILER_315_DEVELOPMENT.md b/tools/opcode_metadata/COMPILER_315_DEVELOPMENT.md index e42d95391..bd5a61c4d 100644 --- a/tools/opcode_metadata/COMPILER_315_DEVELOPMENT.md +++ b/tools/opcode_metadata/COMPILER_315_DEVELOPMENT.md @@ -53,8 +53,16 @@ they do not alter the three strict comparison domains. Canonical `co_code` uses immutable prepublication bytes and a stable bytes owner; the common marshal writer uses that actual field rather than live monitoring -instructions. The adaptive getter remains a separate gap: private Rust -`LOAD_CONST` specializations can appear as unknown IDs in canonical `dis`. +instructions. The adaptive getter projects private Rust `LOAD_CONST` IDs220/221 +to public `LOAD_CONST` at immutable canonical instruction boundaries, preserving +arguments, public specialization IDs and cache payload bytes. Legacy private +ID219 has no equivalent one-unit public encoding and raises `ValueError` on +adaptive export. Length and truncated canonical cache spans are checked; arbitrary +same-length postpublication embedding mutation remains outside the finalized-code +contract. Per-cell atomic reads do not establish a coherent concurrent snapshot +or repair inherited shared-storage accessor and mixed-width dispatch concerns. +The projection neither mutates executable instructions nor changes canonical or +marshal bytes. Runtime specialization choices need not match CPython. Retained private `GET_YIELD_FROM_ITER` must never be emitted by the compiler. The current local tree includes the separately reviewed builtin `frozendict` prerequisite and exact final `Lib/opcode.py`, `Lib/_opcode_metadata.py`, and