Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
marshal: write and read co_consts through the runtime bag
serialize_code gains a serialize_code_with variant that writes each
co_consts entry through a caller-supplied writer; serialize_code keeps
the BorrowedConstant writer as its default.  The VM writer passes its own
write_object_depth, so a code constant that Literal holds but
BorrowedConstant cannot describe reaches the stream instead of panicking
in borrow_obj_constant, and a constant shared with the enclosing object
takes an entry in the writer's reference table.

PyMarshalBag implements constant_ref_from_value, bytes_from_value,
str_from_value and tuple_elements_from_value, which
deserialize_code_value_inner requires to decode code fields through the
runtime bag.

Assisted-by: Claude
  • Loading branch information
youknowone committed Aug 13, 2026
commit ec7f273e9aab9ee3aee2dc4e8e9feb40b8837911
22 changes: 21 additions & 1 deletion crates/compiler-core/src/marshal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,25 @@ pub fn serialize_value<W: Write, D: Dumpable>(
/// Split varnames/cellvars/freevars are reassembled into
/// co_localsplusnames/co_localspluskinds.
pub fn serialize_code<W: Write, C: Constant>(buf: &mut W, code: &CodeObject<C>) {
serialize_code_with(buf, code, |buf, constant| {
serialize_value(buf, constant.borrow_constant().into()).unwrap_or_else(|x| match x {});
Ok::<(), core::convert::Infallible>(())
})
.unwrap_or_else(|x| match x {})
}

/// Serialize a code object, writing each `co_consts` entry through
/// `write_constant`.
///
/// A runtime caller passes its own object writer so that values its constant
/// representation carries but `BorrowedConstant` cannot describe — lists,
/// dicts, sets — reach the stream, and so a constant shared with the enclosing
/// object keeps its entry in that writer's reference table.
pub fn serialize_code_with<W: Write, C: Constant, E>(
buf: &mut W,
code: &CodeObject<C>,
mut write_constant: impl FnMut(&mut W, &C) -> core::result::Result<(), E>,
) -> core::result::Result<(), E> {
// 1–5: scalar fields
buf.write_u32(code.arg_count);
buf.write_u32(code.posonlyarg_count);
Expand All @@ -1414,7 +1433,7 @@ pub fn serialize_code<W: Write, C: Constant>(buf: &mut W, code: &CodeObject<C>)
buf.write_u8(Type::Tuple as u8);
write_len(buf, code.constants.len());
for constant in &*code.constants {
serialize_value(buf, constant.borrow_constant().into()).unwrap_or_else(|x| match x {})
write_constant(buf, constant)?;
}

// 8: co_names (tuple of strings)
Expand Down Expand Up @@ -1457,6 +1476,7 @@ pub fn serialize_code<W: Write, C: Constant>(buf: &mut W, code: &CodeObject<C>)
// 16: co_exceptiontable
buf.write_u8(Type::Bytes as u8);
write_vec(buf, &code.exceptiontable);
Ok(())
}

fn write_marshal_str<W: Write>(buf: &mut W, s: &str) {
Expand Down
30 changes: 29 additions & 1 deletion crates/vm/src/stdlib/marshal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,14 @@ mod decl {
}
} else if let Some(co) = obj.downcast_ref::<PyCode>() {
buf.write_u8(b'c');
marshal::serialize_code(buf, &co.code);
// `Literal` holds the exact object a constant was built from, so
// route `co_consts` back through the object writer: it reaches the
// values `BorrowedConstant` cannot describe and shares the one
// reference table the reader indexes against.
marshal::serialize_code_with(buf, &co.code, |buf, constant| {
let constant = PyObjectRef::from(constant.clone());
write_object_depth(buf, &constant, refs, version, vm, depth - 1)
})?;
} else if let Some(sl) = obj.downcast_ref::<crate::builtins::PySlice>() {
if version < 5 {
return Err(vm.new_value_error("unmarshallable object"));
Expand Down Expand Up @@ -570,6 +577,27 @@ mod decl {
fn constant_bag(self) -> Self::ConstantBag {
PyVmBag(self.vm)
}
/// `Literal` wraps any object, so a decoded `co_consts` entry is
/// already its own compiler-side constant — no placeholder is needed
/// and `make_code_with_constants` keeps the default.
fn constant_ref_from_value(&self, value: &Self::Value) -> Option<Literal> {
Some(Literal::from(value.clone()))
}
fn bytes_from_value(&self, value: &Self::Value) -> Option<Vec<u8>> {
value
.downcast_ref::<PyBytes>()
.map(|bytes| bytes.as_bytes().to_vec())
}
fn str_from_value(&self, value: &Self::Value) -> Option<String> {
value
.downcast_ref::<PyStr>()
.map(|str| str.to_string_lossy().into_owned())
}
fn tuple_elements_from_value(&self, value: &Self::Value) -> Option<Vec<Self::Value>> {
value
.downcast_ref::<PyTuple>()
.map(|tuple| tuple.as_slice().to_vec())
}
}

fn deserialize_value(
Expand Down
21 changes: 21 additions & 0 deletions extra_tests/snippets/stdlib_marshal.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@ def test_roundtrip(self):

assert eval(loaded) == eval(orig)

def test_roundtrip_non_constant_co_consts(self):
# `code.replace` accepts any marshalable object, including values the
# compiler constant representation cannot describe.
orig = compile("1 + 1", "", "eval").replace(
co_consts=([1, 2], {"a": 3}, {4, 5}, 6)
)

loaded = marshal.loads(marshal.dumps(orig))

self.assertEqual(loaded.co_consts, ([1, 2], {"a": 3}, {4, 5}, 6))

def test_roundtrip_shared_co_const(self):
# A constant shared with the enclosing object is written once and both
# readers resolve the same reference.
shared = ["shared"]
orig = compile("1 + 1", "", "eval").replace(co_consts=(shared,))

loaded_code, loaded_shared = marshal.loads(marshal.dumps((orig, shared)))

self.assertIs(loaded_code.co_consts[0], loaded_shared)


if __name__ == "__main__":
unittest.main()
Loading