Skip to content

Commit c22a3a6

Browse files
committed
co_consts
1 parent 0ef6055 commit c22a3a6

7 files changed

Lines changed: 148 additions & 61 deletions

File tree

crates/codegen/src/compile.rs

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3281,12 +3281,19 @@ impl Compiler {
32813281
// Set qualname
32823282
self.set_qualname();
32833283

3284-
// Handle docstring
3284+
// Handle docstring - store in co_consts[0] if present (CPython 3.14+)
32853285
let (doc_str, body) = split_doc(body, &self.opts);
3286-
self.current_code_info()
3287-
.metadata
3288-
.consts
3289-
.insert_full(ConstantData::None);
3286+
if let Some(doc) = &doc_str {
3287+
// Docstring present: store in co_consts[0] and set HAS_DOCSTRING flag
3288+
self.current_code_info()
3289+
.metadata
3290+
.consts
3291+
.insert_full(ConstantData::Str {
3292+
value: doc.to_string().into(),
3293+
});
3294+
self.current_code_info().flags |= bytecode::CodeFlags::HAS_DOCSTRING;
3295+
}
3296+
// If no docstring, don't add None to co_consts (CPython 3.14 behavior)
32903297

32913298
// Compile body statements
32923299
self.compile_statements(body)?;
@@ -3306,16 +3313,8 @@ impl Compiler {
33063313
// Create function object with closure
33073314
self.make_closure(code, funcflags)?;
33083315

3309-
// Handle docstring if present
3310-
if let Some(doc) = doc_str {
3311-
emit!(self, Instruction::Copy { index: 1_u32 });
3312-
self.emit_load_const(ConstantData::Str {
3313-
value: doc.to_string().into(),
3314-
});
3315-
emit!(self, Instruction::Swap { index: 2 });
3316-
let doc_attr = self.name("__doc__");
3317-
emit!(self, Instruction::StoreAttr { idx: doc_attr });
3318-
}
3316+
// Note: docstring is now retrieved from co_consts[0] by the VM
3317+
// when HAS_DOCSTRING flag is set, so no runtime __doc__ assignment needed
33193318

33203319
Ok(())
33213320
}
@@ -6166,10 +6165,7 @@ impl Compiler {
61666165
in_async_scope: false,
61676166
};
61686167

6169-
self.current_code_info()
6170-
.metadata
6171-
.consts
6172-
.insert_full(ConstantData::None);
6168+
// Lambda cannot have docstrings, so no None is added to co_consts (CPython 3.14+)
61736169

61746170
self.compile_expression(body)?;
61756171
self.emit_return_value();

crates/compiler-core/src/bytecode.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,13 +389,16 @@ pub struct CodeObject<C: Constant = ConstantData> {
389389

390390
bitflags! {
391391
#[derive(Copy, Clone, Debug, PartialEq)]
392-
pub struct CodeFlags: u16 {
392+
pub struct CodeFlags: u32 {
393393
const OPTIMIZED = 0x0001;
394394
const NEWLOCALS = 0x0002;
395395
const VARARGS = 0x0004;
396396
const VARKEYWORDS = 0x0008;
397397
const GENERATOR = 0x0020;
398398
const COROUTINE = 0x0080;
399+
/// If a code object represents a function and has a docstring,
400+
/// this bit is set and the first item in co_consts is the docstring.
401+
const HAS_DOCSTRING = 0x4000000;
399402
}
400403
}
401404

crates/compiler-core/src/marshal.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ pub fn deserialize_code<R: Read, Bag: ConstantBag>(
202202
})
203203
.collect::<Result<Box<[(SourceLocation, SourceLocation)]>>>()?;
204204

205-
let flags = CodeFlags::from_bits_truncate(rdr.read_u16()?);
205+
let flags = CodeFlags::from_bits_truncate(rdr.read_u32()?);
206206

207207
let posonlyarg_count = rdr.read_u32()?;
208208
let arg_count = rdr.read_u32()?;
@@ -660,7 +660,7 @@ pub fn serialize_code<W: Write, C: Constant>(buf: &mut W, code: &CodeObject<C>)
660660
buf.write_u32(end.character_offset.to_zero_indexed() as _);
661661
}
662662

663-
buf.write_u16(code.flags.bits());
663+
buf.write_u32(code.flags.bits());
664664

665665
buf.write_u32(code.posonlyarg_count);
666666
buf.write_u32(code.arg_count);

crates/vm/src/builtins/code.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ pub struct ReplaceArgs {
152152
#[pyarg(named, optional)]
153153
co_names: OptionalArg<Vec<PyObjectRef>>,
154154
#[pyarg(named, optional)]
155-
co_flags: OptionalArg<u16>,
155+
co_flags: OptionalArg<u32>,
156156
#[pyarg(named, optional)]
157157
co_varnames: OptionalArg<Vec<PyObjectRef>>,
158158
#[pyarg(named, optional)]
@@ -177,6 +177,13 @@ pub struct ReplaceArgs {
177177
#[repr(transparent)]
178178
pub struct Literal(PyObjectRef);
179179

180+
impl Literal {
181+
/// Get a reference to the inner PyObjectRef
182+
pub fn as_object(&self) -> &PyObjectRef {
183+
&self.0
184+
}
185+
}
186+
180187
impl Borrow<PyObject> for Literal {
181188
fn borrow(&self) -> &PyObject {
182189
&self.0
@@ -411,7 +418,7 @@ pub struct PyCodeNewArgs {
411418
kwonlyargcount: u32,
412419
nlocals: u32,
413420
stacksize: u32,
414-
flags: u16,
421+
flags: u32,
415422
co_code: PyBytesRef,
416423
consts: PyTupleRef,
417424
names: PyTupleRef,
@@ -628,7 +635,7 @@ impl PyCode {
628635
}
629636

630637
#[pygetset]
631-
const fn co_flags(&self) -> u16 {
638+
const fn co_flags(&self) -> u32 {
632639
self.code.flags.bits()
633640
}
634641

crates/vm/src/builtins/function.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ impl PyFunction {
7272
}
7373
});
7474

75+
// Get docstring from co_consts[0] if HAS_DOCSTRING flag is set (CPython 3.14+)
76+
let doc = if code.code.flags.contains(bytecode::CodeFlags::HAS_DOCSTRING) {
77+
code.code
78+
.constants
79+
.first()
80+
.map(|c| c.as_object().clone())
81+
.unwrap_or_else(|| vm.ctx.none())
82+
} else {
83+
vm.ctx.none()
84+
};
85+
7586
let qualname = vm.ctx.new_str(code.qualname.as_str());
7687
let func = Self {
7788
code: PyMutex::new(code.clone()),
@@ -85,7 +96,7 @@ impl PyFunction {
8596
annotations: PyMutex::new(None),
8697
annotate: PyMutex::new(None),
8798
module: PyMutex::new(module),
88-
doc: PyMutex::new(vm.ctx.none()),
99+
doc: PyMutex::new(doc),
89100
#[cfg(feature = "jit")]
90101
jitted_code: OnceCell::new(),
91102
};

crates/vm/src/builtins/object.rs

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,7 @@ fn type_slot_names(typ: &Py<PyType>, vm: &VirtualMachine) -> PyResult<Option<sup
188188
fn object_getstate_default(obj: &PyObject, required: bool, vm: &VirtualMachine) -> PyResult {
189189
// Check itemsize - CPython lines 7260-7264
190190
if required && obj.class().slots.itemsize > 0 {
191-
return Err(vm.new_type_error(format!(
192-
"cannot pickle {:.200} objects",
193-
obj.class().name()
194-
)));
191+
return Err(vm.new_type_error(format!("cannot pickle {:.200} objects", obj.class().name())));
195192
}
196193

197194
let state = if obj.dict().is_none_or(|d| d.is_empty()) {
@@ -228,9 +225,7 @@ fn object_getstate_default(obj: &PyObject, required: bool, vm: &VirtualMachine)
228225

229226
// Compare: if actual type's basicsize > expected basicsize, fail (CPython lines 7297-7304)
230227
if obj.class().slots.basicsize > basicsize {
231-
return Err(
232-
vm.new_type_error(format!("cannot pickle '{}' object", obj.class().name()))
233-
);
228+
return Err(vm.new_type_error(format!("cannot pickle '{}' object", obj.class().name())));
234229
}
235230
}
236231

@@ -675,9 +670,7 @@ fn reduce_newobj(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult {
675670
// Check if type has tp_new
676671
let cls = obj.class();
677672
if cls.slots.new.load().is_none() {
678-
return Err(
679-
vm.new_type_error(format!("cannot pickle '{}' object", cls.name()))
680-
);
673+
return Err(vm.new_type_error(format!("cannot pickle '{}' object", cls.name())));
681674
}
682675

683676
let (args, kwargs) = get_new_arguments(&obj, vm)?;
@@ -686,13 +679,13 @@ fn reduce_newobj(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult {
686679

687680
let has_args = args.is_some();
688681

689-
let (newobj, newargs): (PyObjectRef, PyObjectRef) = if kwargs.is_none() || kwargs.as_ref().is_some_and(|k| k.is_empty()) {
682+
let (newobj, newargs): (PyObjectRef, PyObjectRef) = if kwargs.is_none()
683+
|| kwargs.as_ref().is_some_and(|k| k.is_empty())
684+
{
690685
// Use copyreg.__newobj__
691686
let newobj = copyreg.get_attr("__newobj__", vm)?;
692687

693-
let args_vec: Vec<PyObjectRef> = args
694-
.map(|a| a.as_slice().to_vec())
695-
.unwrap_or_default();
688+
let args_vec: Vec<PyObjectRef> = args.map(|a| a.as_slice().to_vec()).unwrap_or_default();
696689

697690
// Create (cls, *args) tuple
698691
let mut newargs_vec: Vec<PyObjectRef> = vec![cls.to_owned().into()];
@@ -703,10 +696,16 @@ fn reduce_newobj(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult {
703696
} else {
704697
// Use copyreg.__newobj_ex__
705698
let newobj = copyreg.get_attr("__newobj_ex__", vm)?;
706-
let args_tuple: PyObjectRef = args.map(|a| a.into()).unwrap_or_else(|| vm.ctx.empty_tuple.clone().into());
707-
let kwargs_dict: PyObjectRef = kwargs.map(|k| k.into()).unwrap_or_else(|| vm.ctx.new_dict().into());
708-
709-
let newargs = vm.ctx.new_tuple(vec![cls.to_owned().into(), args_tuple, kwargs_dict]);
699+
let args_tuple: PyObjectRef = args
700+
.map(|a| a.into())
701+
.unwrap_or_else(|| vm.ctx.empty_tuple.clone().into());
702+
let kwargs_dict: PyObjectRef = kwargs
703+
.map(|k| k.into())
704+
.unwrap_or_else(|| vm.ctx.new_dict().into());
705+
706+
let newargs = vm
707+
.ctx
708+
.new_tuple(vec![cls.to_owned().into(), args_tuple, kwargs_dict]);
710709
(newobj, newargs.into())
711710
};
712711

@@ -720,7 +719,9 @@ fn reduce_newobj(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult {
720719

721720
let (listitems, dictitems) = get_items_iter(&obj, vm)?;
722721

723-
let result = vm.ctx.new_tuple(vec![newobj, newargs, state, listitems, dictitems]);
722+
let result = vm
723+
.ctx
724+
.new_tuple(vec![newobj, newargs, state, listitems, dictitems]);
724725
Ok(result.into())
725726
}
726727

Lines changed: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,108 @@
1+
"""
2+
Test co_consts behavior for Python 3.14+
3+
4+
In Python 3.14+:
5+
- Functions with docstrings have the docstring as co_consts[0]
6+
- CO_HAS_DOCSTRING flag (0x4000000) indicates docstring presence
7+
- Functions without docstrings do NOT have None added as placeholder for docstring
8+
9+
Note: Other constants (small integers, code objects, etc.) may still appear in co_consts
10+
depending on optimization level. This test focuses on docstring behavior.
11+
"""
12+
13+
14+
# Test function with docstring - docstring should be co_consts[0]
15+
def with_doc():
16+
"""This is a docstring"""
17+
return 1
18+
19+
20+
assert with_doc.__code__.co_consts[0] == "This is a docstring", with_doc.__code__.co_consts
21+
assert with_doc.__doc__ == "This is a docstring"
22+
# Check CO_HAS_DOCSTRING flag (0x4000000)
23+
assert with_doc.__code__.co_flags & 0x4000000, hex(with_doc.__code__.co_flags)
24+
25+
26+
# Test function without docstring - should NOT have HAS_DOCSTRING flag
27+
def no_doc():
28+
return 1
29+
30+
31+
assert not (no_doc.__code__.co_flags & 0x4000000), hex(no_doc.__code__.co_flags)
32+
assert no_doc.__doc__ is None
33+
34+
35+
# Test async function with docstring
136
from asyncio import sleep
237

338

4-
def f():
5-
def g():
6-
return 1
39+
async def async_with_doc():
40+
"""Async docstring"""
41+
await sleep(1)
42+
return 1
743

8-
assert g.__code__.co_consts[0] == None
9-
return 2
1044

45+
assert async_with_doc.__code__.co_consts[0] == "Async docstring", async_with_doc.__code__.co_consts
46+
assert async_with_doc.__doc__ == "Async docstring"
47+
assert async_with_doc.__code__.co_flags & 0x4000000
1148

12-
assert f.__code__.co_consts[0] == None
1349

50+
# Test async function without docstring
51+
async def async_no_doc():
52+
await sleep(1)
53+
return 1
54+
55+
56+
assert not (async_no_doc.__code__.co_flags & 0x4000000)
57+
assert async_no_doc.__doc__ is None
1458

15-
def generator():
59+
60+
# Test generator with docstring
61+
def gen_with_doc():
62+
"""Generator docstring"""
1663
yield 1
1764
yield 2
1865

1966

20-
assert generator().gi_code.co_consts[0] == None
67+
assert gen_with_doc.__code__.co_consts[0] == "Generator docstring"
68+
assert gen_with_doc.__doc__ == "Generator docstring"
69+
assert gen_with_doc.__code__.co_flags & 0x4000000
2170

2271

23-
async def async_f():
24-
await sleep(1)
25-
return 1
72+
# Test generator without docstring
73+
def gen_no_doc():
74+
yield 1
75+
yield 2
76+
2677

78+
assert not (gen_no_doc.__code__.co_flags & 0x4000000)
79+
assert gen_no_doc.__doc__ is None
2780

28-
assert async_f.__code__.co_consts[0] == None
2981

82+
# Test lambda - cannot have docstring
3083
lambda_f = lambda: 0
31-
assert lambda_f.__code__.co_consts[0] == None
84+
assert not (lambda_f.__code__.co_flags & 0x4000000)
85+
assert lambda_f.__doc__ is None
86+
87+
88+
# Test class method with docstring
89+
class cls_with_doc:
90+
def method():
91+
"""Method docstring"""
92+
return 1
93+
3294

95+
assert cls_with_doc.method.__code__.co_consts[0] == "Method docstring"
96+
assert cls_with_doc.method.__doc__ == "Method docstring"
3397

34-
class cls:
35-
def f():
98+
99+
# Test class method without docstring
100+
class cls_no_doc:
101+
def method():
36102
return 1
37103

38104

39-
assert cls().f.__code__.co_consts[0] == None
105+
assert not (cls_no_doc.method.__code__.co_flags & 0x4000000)
106+
assert cls_no_doc.method.__doc__ is None
107+
108+
print("All co_consts tests passed!")

0 commit comments

Comments
 (0)