Skip to content

Commit 47ff358

Browse files
committed
Add implementation lessons learned to teaching notes
Document practical issues encountered during Record type implementation: - Hash constants not publicly exposed (copy from tupleobject.c) - GC trashcan macros for deep recursion protection - PyUnicode_Compare error handling - Opcode number selection (166 after DICT_UPDATE) - Python-level constructor (tp_new) for testing - Type initialization order in object.c and bltinmodule.c - Correct allocator (PyObject_GC_NewVar) - tp_basicsize calculation for flexible array members Also added testing checklist for verifying implementation.
1 parent bea8d76 commit 47ff358

File tree

1 file changed

+174
-4
lines changed

1 file changed

+174
-4
lines changed

teaching-notes.md

Lines changed: 174 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -347,10 +347,180 @@ hasconst.append(35) # or maybe not, depends on design
347347

348348
The `teaching-cpython-solution` branch contains:
349349
- `Include/recordobject.h` - Complete header
350-
- `Objects/recordobject.c` - Full implementation (~200 lines)
351-
- Modified `Lib/opcode.py` - BUILD_RECORD definition
350+
- `Objects/recordobject.c` - Full implementation (~490 lines)
351+
- Modified `Lib/opcode.py` - BUILD_RECORD opcode 166
352352
- Modified `Python/ceval.c` - BUILD_RECORD handler
353-
- Modified build files
354-
- Test script demonstrating all features
353+
- Modified `Makefile.pre.in` - Build integration
354+
- Modified `Objects/object.c` - Type initialization
355+
- Modified `Python/bltinmodule.c` - Builtin registration
355356

356357
Use this as reference when student gets stuck, but guide them to discover solutions themselves first.
358+
359+
---
360+
361+
## Lessons Learned During Implementation
362+
363+
These are practical issues encountered during actual implementation:
364+
365+
### 1. Hash Constants Not Publicly Exposed
366+
367+
The XXH3-style hash constants (`_PyHASH_XXPRIME_1`, `_PyHASH_XXPRIME_2`, `_PyHASH_XXPRIME_5`) used by `tupleobject.c` are defined locally in that file, not in a header. Had to copy them into `recordobject.c`:
368+
369+
```c
370+
#if SIZEOF_PY_UHASH_T > 4
371+
#define _PyHASH_XXPRIME_1 ((Py_uhash_t)11400714785074694791ULL)
372+
#define _PyHASH_XXPRIME_2 ((Py_uhash_t)14029467366897019727ULL)
373+
#define _PyHASH_XXPRIME_5 ((Py_uhash_t)2870177450012600261ULL)
374+
#define _PyHASH_XXROTATE(x) ((x << 31) | (x >> 33))
375+
#else
376+
#define _PyHASH_XXPRIME_1 ((Py_uhash_t)2654435761UL)
377+
#define _PyHASH_XXPRIME_2 ((Py_uhash_t)2246822519UL)
378+
#define _PyHASH_XXPRIME_5 ((Py_uhash_t)374761393UL)
379+
#define _PyHASH_XXROTATE(x) ((x << 13) | (x >> 19))
380+
#endif
381+
```
382+
383+
**Teaching point:** Internal implementation details are often not exposed. Look at how existing code solves the same problem.
384+
385+
### 2. GC Trashcan for Deep Recursion
386+
387+
Deallocation needs `Py_TRASHCAN_BEGIN`/`Py_TRASHCAN_END` to prevent stack overflow when destroying deeply nested structures:
388+
389+
```c
390+
static void
391+
record_dealloc(RecordObject *r)
392+
{
393+
PyObject_GC_UnTrack(r);
394+
Py_TRASHCAN_BEGIN(r, record_dealloc)
395+
396+
// ... cleanup code ...
397+
398+
Py_TRASHCAN_END
399+
}
400+
```
401+
402+
**Teaching point:** Check how `tuple_dealloc` and `list_dealloc` handle this.
403+
404+
### 3. PyUnicode_Compare Error Handling
405+
406+
`PyUnicode_Compare` can raise exceptions (e.g., if comparison fails). Must check `PyErr_Occurred()`:
407+
408+
```c
409+
int cmp = PyUnicode_Compare(name, field);
410+
if (cmp == 0) {
411+
// found match
412+
}
413+
if (PyErr_Occurred()) {
414+
return NULL;
415+
}
416+
```
417+
418+
### 4. Opcode Number Selection
419+
420+
Originally planned opcode 35, but ended up using 166 (right after `DICT_UPDATE` at 165). The gaps in the opcode table exist for historical reasons and some are reserved.
421+
422+
**Teaching point:** Look at recent additions to see where new opcodes go. `Lib/opcode.py` is the source of truth.
423+
424+
### 5. Python-Level Constructor (tp_new)
425+
426+
To test the Record type without emitting bytecode, need a Python constructor:
427+
428+
```c
429+
static PyObject *
430+
record_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
431+
{
432+
// Record() only accepts keyword arguments
433+
if (PyTuple_GET_SIZE(args) != 0) {
434+
PyErr_SetString(PyExc_TypeError,
435+
"Record() takes no positional arguments");
436+
return NULL;
437+
}
438+
// ... iterate kwds, build names tuple and values array
439+
}
440+
```
441+
442+
This allows: `Record(x=10, y=20)` without needing compiler changes.
443+
444+
### 6. Type Initialization Order
445+
446+
The type must be initialized in `_PyTypes_Init()` in `Objects/object.c`:
447+
448+
```c
449+
INIT_TYPE(PyRecord_Type);
450+
```
451+
452+
And exposed as builtin in `Python/bltinmodule.c`:
453+
454+
```c
455+
SETBUILTIN("Record", &PyRecord_Type);
456+
```
457+
458+
### 7. Correct Allocator for GC-Tracked VarObjects
459+
460+
Use `PyObject_GC_NewVar` for variable-size objects that participate in GC:
461+
462+
```c
463+
record = PyObject_GC_NewVar(RecordObject, &PyRecord_Type, n);
464+
```
465+
466+
Not `PyObject_NewVar` (which doesn't set up GC tracking).
467+
468+
### 8. tp_basicsize Calculation
469+
470+
For variable-size objects, `tp_basicsize` should be the size WITHOUT the variable part:
471+
472+
```c
473+
sizeof(RecordObject) - sizeof(PyObject *) /* tp_basicsize */
474+
sizeof(PyObject *) /* tp_itemsize */
475+
```
476+
477+
The `- sizeof(PyObject *)` accounts for the `r_values[1]` flexible array member.
478+
479+
---
480+
481+
## Testing Checklist
482+
483+
After implementation, verify:
484+
485+
```python
486+
# Basic construction
487+
r = Record(x=10, y=20)
488+
489+
# repr
490+
assert repr(r) == "Record(x=10, y=20)"
491+
492+
# Attribute access
493+
assert r.x == 10
494+
assert r.y == 20
495+
496+
# Indexing
497+
assert r[0] == 10
498+
assert r[-1] == 20
499+
assert len(r) == 2
500+
501+
# Hashing (usable as dict key)
502+
d = {r: "value"}
503+
r2 = Record(x=10, y=20)
504+
assert d[r2] == "value"
505+
506+
# Equality
507+
assert r == r2
508+
assert r != Record(x=10, y=30)
509+
assert r != Record(a=10, b=20) # different names
510+
511+
# Error handling
512+
try:
513+
r[5]
514+
except IndexError:
515+
pass
516+
517+
try:
518+
Record() # no kwargs
519+
except TypeError:
520+
pass
521+
522+
try:
523+
Record(1, 2, 3) # positional args
524+
except TypeError:
525+
pass
526+
```

0 commit comments

Comments
 (0)