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