From bea8d762584a5f3b216a15be200d0f6c80df80f1 Mon Sep 17 00:00:00 2001 From: zyros-dev Date: Tue, 23 Dec 2025 12:14:56 +1000 Subject: [PATCH 1/2] Add CPython teaching materials for Record type project - CLAUDE.md: Teaching mode instructions and architecture overview - teaching-todo.md: 5-phase curriculum building toward Record type - teaching-notes.md: Detailed implementation notes (for Claude reference) The learning project implements a Record type (immutable named container) and BUILD_RECORD opcode, covering PyObject fundamentals, type slots, the evaluation loop, and build system integration. --- CLAUDE.md | 112 +++++++++++++++ teaching-notes.md | 356 ++++++++++++++++++++++++++++++++++++++++++++++ teaching-todo.md | 209 +++++++++++++++++++++++++++ 3 files changed, 677 insertions(+) create mode 100644 CLAUDE.md create mode 100644 teaching-notes.md create mode 100644 teaching-todo.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000000000..80615331d5e1b5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,112 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Teaching Mode + +This repository is being used as a learning environment for CPython internals. The goal is to teach the user how CPython works, not to write code for them. + +**Behavior Guidelines:** +- Describe implementations and concepts, don't write code unless explicitly asked +- Ask questions to verify understanding ("What do you think ob_refcnt does?") +- Point to specific files and line numbers for the user to read +- When the user is stuck, give hints before giving answers +- Reference `teaching-todo.md` for the structured curriculum +- Reference `teaching-notes.md` for detailed research (student should not read this) +- Encourage use of `dis` module, GDB, and debug builds for exploration + +**The learning project:** Implementing a `Record` type and `BUILD_RECORD` opcode (~300 LoC). This comprehensive project covers: +- PyObject/PyVarObject fundamentals (custom struct, refcounting) +- Type slots (tp_repr, tp_hash, tp_dealloc, tp_getattro, sq_length, sq_item) +- The evaluation loop (BUILD_RECORD opcode in ceval.c) +- Build system integration + +A working solution exists on the `teaching-cpython-solution` branch for reference. + +## Build Commands + +```bash +# Debug build (required for learning - enables assertions and refcount tracking) +./configure --with-pydebug +make + +# Smoke test +./python.exe --version +./python.exe -c "print('hello')" + +# Run specific test +./python.exe -m test test_sys +``` + +After modifying opcodes or grammar: +```bash +make regen-all # Regenerate generated files +make # Rebuild +``` + +## Architecture Overview + +### The Object Model (start here) +- `Include/object.h` - PyObject, PyVarObject, Py_INCREF/DECREF +- `Include/cpython/object.h` - PyTypeObject (the "metaclass" of all types) +- `Objects/*.c` - Concrete type implementations + +### Core Data Structures +| Type | Header | Implementation | +|------|--------|----------------| +| int | `Include/cpython/longintrepr.h` | `Objects/longobject.c` | +| tuple | `Include/cpython/tupleobject.h` | `Objects/tupleobject.c` | +| list | `Include/cpython/listobject.h` | `Objects/listobject.c` | +| dict | `Include/cpython/dictobject.h` | `Objects/dictobject.c` | +| set | `Include/setobject.h` | `Objects/setobject.c` | + +### Execution Engine +- `Include/opcode.h` - Opcode definitions +- `Lib/opcode.py` - Python-side opcode definitions (source of truth) +- `Include/cpython/code.h` - Code object structure +- `Include/cpython/frameobject.h` - Frame object (execution context) +- `Python/ceval.c` - **The interpreter loop** - giant switch on opcodes, stack machine + +### Compiler Pipeline +- `Grammar/python.gram` - PEG grammar +- `Parser/` - Tokenizer and parser +- `Python/compile.c` - AST to bytecode +- `Python/symtable.c` - Symbol table building + +## Key Concepts for Teaching + +**Everything is a PyObject:** +```c +typedef struct { + Py_ssize_t ob_refcnt; // Reference count + PyTypeObject *ob_type; // Pointer to type object +} PyObject; +``` + +**The stack machine:** Bytecode operates on a value stack. `LOAD_FAST` pushes, `BINARY_ADD` pops two and pushes one, etc. + +**Type slots:** `PyTypeObject` has function pointers (tp_hash, tp_repr, tp_call) that define behavior. `len(x)` calls `x->ob_type->tp_as_sequence->sq_length`. + +## Useful Commands for Learning + +```bash +# Disassemble Python code +./python.exe -c "import dis; dis.dis(lambda: [1,2,3])" + +# Check reference count (debug build) +./python.exe -c "import sys; x = []; print(sys.getrefcount(x))" + +# Show total refcount after each statement (debug build) +./python.exe -X showrefcount + +# Run with GDB +gdb ./python.exe +(gdb) break _PyEval_EvalFrameDefault +(gdb) run -c "1 + 1" +``` + +## External Resources + +- Developer Guide: https://devguide.python.org/ +- CPython Internals Book: https://realpython.com/products/cpython-internals-book/ +- PEP 3155 (Qualified names): Understanding how names are resolved diff --git a/teaching-notes.md b/teaching-notes.md new file mode 100644 index 00000000000000..c39fd138525911 --- /dev/null +++ b/teaching-notes.md @@ -0,0 +1,356 @@ +# Teaching Notes (For Claude - Not for Student) + +This file contains detailed research and implementation guidance for teaching CPython internals through building a Record type. + +--- + +## Phase 1: PyObject Fundamentals + +### PyObject Structure +**File:** `Include/object.h:105-109` +```c +typedef struct _object { + _PyObject_HEAD_EXTRA + Py_ssize_t ob_refcnt; + PyTypeObject *ob_type; +} PyObject; +``` + +### PyVarObject Structure +**File:** `Include/object.h:115-118` +```c +typedef struct { + PyObject ob_base; + Py_ssize_t ob_size; +} PyVarObject; +``` + +### Py_INCREF/DECREF +**File:** `Include/object.h:461-508` +- INCREF: Simply `op->ob_refcnt++` +- DECREF: Decrements, calls `_Py_Dealloc(op)` when reaches 0 +- Debug builds track `_Py_RefTotal` and detect negative refcounts + +### Teaching Questions - Answers + +**Q: What are the two fields every Python object has?** +A: `ob_refcnt` (reference count) and `ob_type` (pointer to type object) + +**Q: Why reference counting instead of tracing GC?** +A: Deterministic destruction (know exactly when objects die), simpler implementation, good cache locality. Downside: can't handle cycles (hence the cycle collector supplement). + +**Q: Should Record use PyObject or PyVarObject?** +A: PyVarObject - because Record has variable number of fields. The `ob_size` will store field count. + +--- + +## Phase 2: Data Structures for Record + +### Tuple as Reference +**File:** `Include/cpython/tupleobject.h:5-11` +```c +typedef struct { + PyObject_VAR_HEAD + PyObject *ob_item[1]; // Flexible array member +} PyTupleObject; +``` + +### Record Memory Layout Design +```c +typedef struct { + PyObject_VAR_HEAD // includes ob_size = field count + Py_hash_t r_hash; // cached hash (-1 if not computed) + PyObject *r_names; // tuple of field names (strings) + PyObject *r_values[1]; // flexible array of values +} RecordObject; +``` + +**Design decisions:** +- `r_names` is a tuple (shared across records with same fields) +- `r_values` is inline for cache locality +- `r_hash` cached because immutable (like tuple) +- Use `ob_size` for field count + +**Q: Tradeoff tuple vs dict for names?** +A: Tuple is simpler and faster for small N. Dict would be O(1) lookup but more memory. For typical record sizes (2-10 fields), linear scan of tuple is fine. Could optimize with dict for large records. + +### Key Functions to Study +- `Objects/tupleobject.c:tuple_hash` (line ~350) - hash combining algorithm +- `Objects/tupleobject.c:tuple_richcompare` (line ~600) - element-by-element comparison +- `Objects/tupleobject.c:tuple_dealloc` (line ~250) - DECREF each element + +--- + +## Phase 3: Type Slots Implementation + +### Slot Reference Table + +| Slot | Signature | Purpose | +|------|-----------|---------| +| `tp_dealloc` | `void (*)(PyObject *)` | Release resources | +| `tp_repr` | `PyObject *(*)(PyObject *)` | `repr(obj)` | +| `tp_hash` | `Py_hash_t (*)(PyObject *)` | `hash(obj)` | +| `tp_richcompare` | `PyObject *(*)(PyObject *, PyObject *, int)` | Comparisons | +| `tp_getattro` | `PyObject *(*)(PyObject *, PyObject *)` | `obj.attr` | +| `sq_length` | `Py_ssize_t (*)(PyObject *)` | `len(obj)` | +| `sq_item` | `PyObject *(*)(PyObject *, Py_ssize_t)` | `obj[i]` | + +### Implementation Patterns + +**record_dealloc:** +```c +static void +record_dealloc(RecordObject *r) +{ + Py_ssize_t i, n = Py_SIZE(r); + PyObject_GC_UnTrack(r); // If GC tracked + Py_XDECREF(r->r_names); + for (i = 0; i < n; i++) { + Py_XDECREF(r->r_values[i]); + } + Py_TYPE(r)->tp_free((PyObject *)r); +} +``` + +**record_hash (based on tuple_hash):** +```c +static Py_hash_t +record_hash(RecordObject *r) +{ + if (r->r_hash != -1) + return r->r_hash; + + Py_hash_t hash = 0x345678L; + Py_ssize_t n = Py_SIZE(r); + Py_hash_t mult = 1000003L; + + for (Py_ssize_t i = 0; i < n; i++) { + Py_hash_t h = PyObject_Hash(r->r_values[i]); + if (h == -1) return -1; + hash = (hash ^ h) * mult; + mult += 82520L + n + n; + } + hash += 97531L; + if (hash == -1) + hash = -2; + r->r_hash = hash; + return hash; +} +``` + +**record_getattro:** +```c +static PyObject * +record_getattro(RecordObject *r, PyObject *name) +{ + // First check if it's a field name + if (PyUnicode_Check(name)) { + Py_ssize_t n = Py_SIZE(r); + for (Py_ssize_t i = 0; i < n; i++) { + PyObject *field = PyTuple_GET_ITEM(r->r_names, i); + if (PyUnicode_Compare(name, field) == 0) { + PyObject *val = r->r_values[i]; + Py_INCREF(val); + return val; + } + } + } + // Fall back to generic attribute lookup (for methods, etc.) + return PyObject_GenericGetAttr((PyObject *)r, name); +} +``` + +### Common Pitfalls +1. Forgetting to INCREF return values from sq_item/getattro +2. Not handling negative indices in sq_item +3. Forgetting to handle hash == -1 (error indicator) +4. Not calling PyObject_GC_UnTrack in dealloc if type is GC-tracked + +--- + +## Phase 4: Evaluation Loop + +### Key Locations +- `_PyEval_EvalFrameDefault`: `Python/ceval.c:1577` +- Stack macros: `Python/ceval.c:1391-1433` +- `BUILD_TUPLE`: `Python/ceval.c:2615-2630` +- `BUILD_MAP`: `Python/ceval.c:2648-2700` + +### BUILD_TUPLE Pattern +```c +case TARGET(BUILD_TUPLE): { + PyObject *tup = PyTuple_New(oparg); + if (tup == NULL) + goto error; + while (--oparg >= 0) { + PyObject *item = POP(); + PyTuple_SET_ITEM(tup, oparg, item); + } + PUSH(tup); + DISPATCH(); +} +``` + +### BUILD_RECORD Design +Stack layout: `[..., name1, val1, name2, val2, ..., nameN, valN]` +Oparg: N (number of fields) +Pops: 2*N items +Pushes: 1 record + +```c +case TARGET(BUILD_RECORD): { + Py_ssize_t n = oparg; + PyObject *names = PyTuple_New(n); + if (names == NULL) + goto error; + + // Collect names and values from stack (reverse order) + PyObject **values = PyMem_Malloc(n * sizeof(PyObject *)); + if (values == NULL) { + Py_DECREF(names); + goto error; + } + + for (Py_ssize_t i = n - 1; i >= 0; i--) { + PyObject *val = POP(); + PyObject *name = POP(); + if (!PyUnicode_Check(name)) { + // Error: field name must be string + // cleanup and goto error + } + PyTuple_SET_ITEM(names, i, name); // steals ref + values[i] = val; // we own this ref + } + + PyObject *record = PyRecord_New(names, values, n); + PyMem_Free(values); + if (record == NULL) { + Py_DECREF(names); + goto error; + } + + PUSH(record); + DISPATCH(); +} +``` + +--- + +## Phase 5: Implementation Details + +### File Structure + +**Include/recordobject.h:** +```c +#ifndef Py_RECORDOBJECT_H +#define Py_RECORDOBJECT_H + +#include "Python.h" + +typedef struct { + PyObject_VAR_HEAD + Py_hash_t r_hash; + PyObject *r_names; + PyObject *r_values[1]; +} RecordObject; + +PyAPI_DATA(PyTypeObject) PyRecord_Type; + +#define PyRecord_Check(op) PyObject_TypeCheck(op, &PyRecord_Type) + +PyAPI_FUNC(PyObject *) PyRecord_New(PyObject *names, PyObject **values, Py_ssize_t n); + +#endif +``` + +### Build Integration + +**Makefile.pre.in additions:** +```makefile +OBJECT_OBJS= \ + ... \ + Objects/recordobject.o +``` + +**Python/bltinmodule.c:** +Add to `_PyBuiltin_Init`: +```c +SETBUILTIN("Record", &PyRecord_Type); +``` + +### Opcode Number Selection +Check `Lib/opcode.py` for gaps. In 3.10: +- Gap at 35-48 +- Gap at 58 +- Could use 35 for BUILD_RECORD + +Add to `Lib/opcode.py`: +```python +def_op('BUILD_RECORD', 35) +hasconst.append(35) # or maybe not, depends on design +``` + +--- + +## Teaching Strategies + +### Phase 1 Approach +1. Have student grep for "typedef struct _object" +2. Ask them to explain each field before revealing +3. Demo with: `import sys; x = []; print(sys.getrefcount(x))` +4. Show how INCREF/DECREF work with print statements in debug build + +### Phase 2 Approach +1. Compare tuple and list side by side - why different structures? +2. Have student sketch Record layout before showing solution +3. Discuss space/time tradeoffs + +### Phase 3 Approach +1. Start with dealloc - "what happens when refcount hits 0?" +2. Work through repr next - visible feedback +3. Leave hash for after they understand the algorithm from tuple + +### Phase 4 Approach +1. Use GDB to step through BUILD_TUPLE +2. Print stack_pointer values before/after +3. Have student write pseudocode before C + +### Phase 5 Approach +1. Start with minimal working type (just dealloc + repr) +2. Add features incrementally, test each +3. Opcode last, after type fully works + +--- + +## Quick Reference: Key Line Numbers (3.10) + +| Item | File | Line | +|------|------|------| +| PyObject | Include/object.h | 105 | +| PyVarObject | Include/object.h | 115 | +| Py_INCREF | Include/object.h | 461 | +| Py_DECREF | Include/object.h | 477 | +| PyTypeObject | Include/cpython/object.h | 191 | +| PyTupleObject | Include/cpython/tupleobject.h | 5 | +| tuple_hash | Objects/tupleobject.c | ~350 | +| tuple_richcompare | Objects/tupleobject.c | ~600 | +| tuple_dealloc | Objects/tupleobject.c | ~250 | +| PyTuple_Type | Objects/tupleobject.c | ~750 | +| _PyEval_EvalFrameDefault | Python/ceval.c | 1577 | +| Stack macros | Python/ceval.c | 1391 | +| BUILD_TUPLE | Python/ceval.c | 2615 | +| BUILD_MAP | Python/ceval.c | 2648 | + +--- + +## Solution Branch Reference + +The `teaching-cpython-solution` branch contains: +- `Include/recordobject.h` - Complete header +- `Objects/recordobject.c` - Full implementation (~200 lines) +- Modified `Lib/opcode.py` - BUILD_RECORD definition +- Modified `Python/ceval.c` - BUILD_RECORD handler +- Modified build files +- Test script demonstrating all features + +Use this as reference when student gets stuck, but guide them to discover solutions themselves first. diff --git a/teaching-todo.md b/teaching-todo.md new file mode 100644 index 00000000000000..ef709f534c3f8b --- /dev/null +++ b/teaching-todo.md @@ -0,0 +1,209 @@ +# CPython Internals Learning Path + +A structured curriculum for understanding CPython's implementation by building a custom `Record` type and `BUILD_RECORD` opcode. + +## The Project + +Build a lightweight immutable record type (like a simplified namedtuple): + +```python +r = Record(x=10, y=20, name="point") +r.x # 10 (attribute access) +r[0] # 10 (sequence protocol) +len(r) # 3 +hash(r) # hashable (can be dict key) +repr(r) # Record(x=10, y=20, name='point') +r == r2 # comparable +``` + +--- + +## Phase 1: The Object Model + +### 1.1 PyObject - The Universal Base +- [ ] Read `Include/object.h` - find `PyObject` struct (around line 105) +- [ ] Understand: What are the two fields every Python object has? +- [ ] Find where `Py_INCREF` and `Py_DECREF` are defined +- [ ] Question: Why does CPython use reference counting instead of tracing GC? + +### 1.2 PyVarObject - Variable-Length Objects +- [ ] Find `PyVarObject` in the headers +- [ ] Question: What's the difference between PyObject and PyVarObject? +- [ ] Which built-in types use PyVarObject? (hint: list, tuple, but not dict) +- [ ] **For Record:** Should Record use PyObject or PyVarObject? Why? + +### 1.3 Type Objects +- [ ] Read `Include/cpython/object.h` - find `PyTypeObject` (around line 191) +- [ ] Identify the "slots" - tp_hash, tp_repr, tp_dealloc, tp_getattro, etc. +- [ ] Question: How does Python know what `len(x)` should call for a given type? +- [ ] Find where `PyTuple_Type` is defined in `Objects/tupleobject.c` - study its structure + +--- + +## Phase 2: Concrete Data Structures + +### 2.1 Tuples (Our Reference Implementation) +- [ ] Read `Include/cpython/tupleobject.h` and `Objects/tupleobject.c` +- [ ] Study the PyTupleObject struct - how does it store elements? +- [ ] Find `tuple_hash` - how does tuple compute its hash? +- [ ] Find `tuple_richcompare` - how does equality work? +- [ ] **For Record:** Our Record will store values like tuple, but add field names + +### 2.2 Dictionaries (For Field Name Lookup) +- [ ] Read `Include/cpython/dictobject.h` - understand PyDictObject basics +- [ ] Question: How would we map field names to indices efficiently? +- [ ] Study `PyDict_GetItem` - how to look up a key + +### 2.3 Designing Record's Memory Layout +- [ ] Sketch the RecordObject struct: + - What fields do we need? (field names, values, cached hash?) + - Should we store field names per-instance or share them? +- [ ] Question: What's the tradeoff between storing names as tuple vs dict? + +--- + +## Phase 3: Type Slots Deep Dive + +### 3.1 Essential Slots for Record +- [ ] `tp_dealloc` - Study tuple's dealloc. What must we DECREF? +- [ ] `tp_repr` - Study tuple's repr. How do we build the output string? +- [ ] `tp_hash` - Study tuple's hash. What makes a good hash for immutable containers? +- [ ] `tp_richcompare` - Study tuple's compare. Handle Py_EQ at minimum + +### 3.2 Sequence Protocol (for indexing) +- [ ] Find `PySequenceMethods` in headers +- [ ] Study `sq_length` - returns `Py_ssize_t` +- [ ] Study `sq_item` - takes index, returns item (with INCREF!) +- [ ] **For Record:** Implement these so `r[0]` and `len(r)` work + +### 3.3 Attribute Access (for field names) +- [ ] Study `tp_getattro` - how does attribute lookup work? +- [ ] Look at how namedtuple does it (it's in Python, but concept applies) +- [ ] **For Record:** Map `r.fieldname` to the correct value + +### 3.4 Constructor +- [ ] Study `tp_new` vs `tp_init` - what's the difference? +- [ ] For immutable types, which one do we need? +- [ ] **For Record:** Design the C function signature for creating records + +--- + +## Phase 4: The Evaluation Loop + +### 4.1 ceval.c Overview +- [ ] Open `Python/ceval.c` - find `_PyEval_EvalFrameDefault` (around line 1577) +- [ ] Understand the main dispatch loop structure +- [ ] Find the stack macros: `PUSH()`, `POP()`, `TOP()`, `PEEK()` (around line 1391) + +### 4.2 Study Similar Opcodes +- [ ] Find `BUILD_TUPLE` implementation - how does it pop N items and push a tuple? +- [ ] Find `BUILD_MAP` implementation - how does it handle key-value pairs? +- [ ] Question: What error handling pattern do these opcodes use? + +### 4.3 Design BUILD_RECORD +- [ ] Decide on stack layout: `BUILD_RECORD n` where n is field count +- [ ] Stack before: `[..., name1, val1, name2, val2, ...]` (or different order?) +- [ ] Stack after: `[..., record]` +- [ ] What validation do we need? (names must be strings, no duplicates?) + +--- + +## Phase 5: Implementation + +### 5.1 Create the Header File +- [ ] Create `Include/recordobject.h` +- [ ] Define `RecordObject` struct +- [ ] Declare `PyRecord_Type` +- [ ] Declare `PyRecord_New()` constructor function + +### 5.2 Implement the Type +- [ ] Create `Objects/recordobject.c` +- [ ] Implement `record_dealloc` +- [ ] Implement `record_repr` +- [ ] Implement `record_hash` +- [ ] Implement `record_richcompare` +- [ ] Implement `record_length` (sq_length) +- [ ] Implement `record_item` (sq_item) +- [ ] Implement `record_getattro` (attribute access by name) +- [ ] Define `PyRecord_Type` with all slots filled +- [ ] Implement `PyRecord_New()` - the C API constructor + +### 5.3 Add the Opcode +- [ ] Add `BUILD_RECORD` to `Lib/opcode.py` (pick unused number, needs argument) +- [ ] Run `make regen-opcode` and `make regen-opcode-targets` +- [ ] Implement `BUILD_RECORD` handler in `Python/ceval.c` + +### 5.4 Build System Integration +- [ ] Add `recordobject.c` to the build (Makefile.pre.in or setup.py) +- [ ] Add header to appropriate include lists +- [ ] Register type in Python initialization + +### 5.5 Build and Test +- [ ] Run `make` - fix any compilation errors +- [ ] Test basic creation via C API +- [ ] Test via manual bytecode or compiler modification +- [ ] Verify all operations: indexing, len, hash, repr, equality, attribute access + +--- + +## Verification Checklist + +After implementation, verify each feature: + +```python +# Creation (via whatever mechanism we build) +r = Record(x=10, y=20) + +# Repr +assert repr(r) == "Record(x=10, y=20)" + +# Indexing (sequence protocol) +assert r[0] == 10 +assert r[1] == 20 +assert len(r) == 2 + +# Attribute access +assert r.x == 10 +assert r.y == 20 + +# Hashing (for use as dict key) +d = {r: "value"} +assert d[r] == "value" + +# Equality +r2 = Record(x=10, y=20) +assert r == r2 + +# Immutability (should raise) +# r.x = 30 # AttributeError +# r[0] = 30 # TypeError +``` + +--- + +## Files We'll Create/Modify + +| File | Action | ~Lines | +|------|--------|--------| +| `Include/recordobject.h` | Create | 25 | +| `Objects/recordobject.c` | Create | 200 | +| `Lib/opcode.py` | Modify | 2 | +| `Python/ceval.c` | Modify | 30 | +| `Makefile.pre.in` | Modify | 5 | +| `Python/bltinmodule.c` | Modify | 10 | + +--- + +## How to Use This Guide + +1. Read the specified files - don't skim, trace through the code +2. Answer questions before moving on (write answers down) +3. Use `./python.exe -c "..."` to experiment +4. Use GDB when confused: `gdb ./python.exe` +5. The `dis` module shows bytecode: `import dis; dis.dis(func)` + +Debug build helpers: +```bash +./python.exe -c "import sys; print(sys.getrefcount(x))" +./python.exe -X showrefcount +``` From ebb65219232310788243eda71a7438d401b7de8e Mon Sep 17 00:00:00 2001 From: zyros-dev Date: Tue, 23 Dec 2025 12:26:04 +1000 Subject: [PATCH 2/2] Implement Record type and BUILD_RECORD opcode Add a new immutable Record type with named fields (similar to namedtuple but implemented entirely in C). This serves as a teaching reference implementation demonstrating core CPython concepts: - PyVarObject with variable-length data (r_values array) - Type slots: tp_repr, tp_hash, tp_richcompare, tp_dealloc, tp_traverse - Sequence protocol: sq_length, sq_item - Attribute access: tp_getattro for field name lookup - GC integration with _PyObject_GC_TRACK and Py_TRASHCAN_* - Bytecode evaluation loop (BUILD_RECORD opcode) Features: - Attribute access: r.field_name - Indexing: r[0], r[1], r[-1], len(r) - Hashing: hash(r) - usable as dict key - Equality: r1 == r2 (compares both names and values) - Python constructor: Record(x=10, y=20) Files added: - Include/recordobject.h: Header with RecordObject struct and API - Objects/recordobject.c: Full implementation (~490 lines) Files modified: - Lib/opcode.py: Added BUILD_RECORD opcode (166) - Include/opcode.h: Regenerated with BUILD_RECORD - Python/opcode_targets.h: Regenerated opcode jump table - Python/ceval.c: Added BUILD_RECORD handler in eval loop - Makefile.pre.in: Added recordobject.o and header - Objects/object.c: Added PyRecord_Type initialization - Python/bltinmodule.c: Exposed Record as builtin --- Include/opcode.h | 1 + Include/recordobject.h | 56 +++++ Lib/opcode.py | 1 + Makefile.pre.in | 2 + Objects/object.c | 2 + Objects/recordobject.c | 493 ++++++++++++++++++++++++++++++++++++++++ Python/bltinmodule.c | 2 + Python/ceval.c | 59 +++++ Python/opcode_targets.h | 2 +- 9 files changed, 617 insertions(+), 1 deletion(-) create mode 100644 Include/recordobject.h create mode 100644 Objects/recordobject.c diff --git a/Include/opcode.h b/Include/opcode.h index 52039754bd88ea..a9146bda8b63a8 100644 --- a/Include/opcode.h +++ b/Include/opcode.h @@ -135,6 +135,7 @@ extern "C" { #define SET_UPDATE 163 #define DICT_MERGE 164 #define DICT_UPDATE 165 +#define BUILD_RECORD 166 #ifdef NEED_OPCODE_JUMP_TABLES static uint32_t _PyOpcode_RelativeJump[8] = { 0U, diff --git a/Include/recordobject.h b/Include/recordobject.h new file mode 100644 index 00000000000000..e542cf8b00a12d --- /dev/null +++ b/Include/recordobject.h @@ -0,0 +1,56 @@ +/* Record object interface - immutable named container */ + +#ifndef Py_RECORDOBJECT_H +#define Py_RECORDOBJECT_H +#ifdef __cplusplus +extern "C" { +#endif + +#include "pyport.h" +#include "object.h" + +/* + * RecordObject: An immutable container with named fields. + * Similar to a simplified namedtuple implemented in C. + * + * Features: + * - Attribute access by name: r.field_name + * - Indexing by position: r[0], r[1] + * - Hashable (can be used as dict key) + * - Equality comparison + * - Nice repr: Record(x=10, y=20) + */ + +typedef struct { + PyObject_VAR_HEAD + Py_hash_t r_hash; /* Cached hash, -1 if not yet computed */ + PyObject *r_names; /* Tuple of field names (strings) */ + PyObject *r_values[1]; /* Flexible array of field values */ +} RecordObject; + +PyAPI_DATA(PyTypeObject) PyRecord_Type; + +#define PyRecord_Check(op) PyObject_TypeCheck(op, &PyRecord_Type) +#define PyRecord_CheckExact(op) Py_IS_TYPE(op, &PyRecord_Type) + +/* Create a new Record from names tuple and values array. + * names: tuple of strings (field names) - reference is stolen + * values: array of PyObject* (field values) - references are stolen + * n: number of fields + * Returns: new Record object, or NULL on error + */ +PyAPI_FUNC(PyObject *) PyRecord_New(PyObject *names, PyObject **values, Py_ssize_t n); + +/* Get field by index (returns borrowed reference) */ +PyAPI_FUNC(PyObject *) PyRecord_GetItem(PyObject *record, Py_ssize_t index); + +/* Get field by name (returns new reference) */ +PyAPI_FUNC(PyObject *) PyRecord_GetFieldByName(PyObject *record, PyObject *name); + +/* Get number of fields */ +#define PyRecord_GET_SIZE(op) Py_SIZE(op) + +#ifdef __cplusplus +} +#endif +#endif /* !Py_RECORDOBJECT_H */ diff --git a/Lib/opcode.py b/Lib/opcode.py index 37e88e92df70ec..9d08af1c6ed969 100644 --- a/Lib/opcode.py +++ b/Lib/opcode.py @@ -212,5 +212,6 @@ def jabs_op(name, op): def_op('SET_UPDATE', 163) def_op('DICT_MERGE', 164) def_op('DICT_UPDATE', 165) +def_op('BUILD_RECORD', 166) # Number of name/value pairs del def_op, name_op, jrel_op, jabs_op diff --git a/Makefile.pre.in b/Makefile.pre.in index fa99dd86c416ed..dedadfb460a650 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -434,6 +434,7 @@ OBJECT_OBJS= \ Objects/obmalloc.o \ Objects/picklebufobject.o \ Objects/rangeobject.o \ + Objects/recordobject.o \ Objects/setobject.o \ Objects/sliceobject.o \ Objects/structseq.o \ @@ -1091,6 +1092,7 @@ PYTHON_HEADERS= \ $(srcdir)/Include/pythonrun.h \ $(srcdir)/Include/pythread.h \ $(srcdir)/Include/rangeobject.h \ + $(srcdir)/Include/recordobject.h \ $(srcdir)/Include/setobject.h \ $(srcdir)/Include/sliceobject.h \ $(srcdir)/Include/structmember.h \ diff --git a/Objects/object.c b/Objects/object.c index 0bef2e9dfb5e35..c15c0cac3bc049 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -14,6 +14,7 @@ #include "pycore_unionobject.h" // _PyUnion_Type #include "frameobject.h" #include "interpreteridobject.h" +#include "recordobject.h" #ifdef Py_LIMITED_API // Prevent recursive call _Py_IncRef() <=> Py_INCREF() @@ -1863,6 +1864,7 @@ _PyTypes_Init(void) INIT_TYPE(PyProperty_Type); INIT_TYPE(PyRangeIter_Type); INIT_TYPE(PyRange_Type); + INIT_TYPE(PyRecord_Type); INIT_TYPE(PyReversed_Type); INIT_TYPE(PySTEntry_Type); INIT_TYPE(PySeqIter_Type); diff --git a/Objects/recordobject.c b/Objects/recordobject.c new file mode 100644 index 00000000000000..3b670c2a6d1fce --- /dev/null +++ b/Objects/recordobject.c @@ -0,0 +1,493 @@ +/* Record object implementation - immutable named container */ + +#include "Python.h" +#include "pycore_object.h" // _PyObject_GC_TRACK +#include "structmember.h" // PyMemberDef +#include "recordobject.h" + +/* Hash constants - same as used by tupleobject.c */ +#if SIZEOF_PY_UHASH_T > 4 +#define _PyHASH_XXPRIME_1 ((Py_uhash_t)11400714785074694791ULL) +#define _PyHASH_XXPRIME_2 ((Py_uhash_t)14029467366897019727ULL) +#define _PyHASH_XXPRIME_5 ((Py_uhash_t)2870177450012600261ULL) +#define _PyHASH_XXROTATE(x) ((x << 31) | (x >> 33)) /* Rotate left 31 bits */ +#else +#define _PyHASH_XXPRIME_1 ((Py_uhash_t)2654435761UL) +#define _PyHASH_XXPRIME_2 ((Py_uhash_t)2246822519UL) +#define _PyHASH_XXPRIME_5 ((Py_uhash_t)374761393UL) +#define _PyHASH_XXROTATE(x) ((x << 13) | (x >> 19)) /* Rotate left 13 bits */ +#endif + +/* + * RecordObject implementation + * + * A Record is an immutable container with named fields, similar to + * a simplified namedtuple but implemented entirely in C. + * + * Memory layout: + * PyObject_VAR_HEAD (includes ob_size = number of fields) + * r_hash (cached hash value, -1 if not computed) + * r_names (tuple of field name strings) + * r_values[n] (array of field values) + */ + +/* Forward declarations */ +static PyObject *record_repr(RecordObject *r); +static Py_hash_t record_hash(RecordObject *r); +static PyObject *record_richcompare(PyObject *v, PyObject *w, int op); +static Py_ssize_t record_length(RecordObject *r); +static PyObject *record_item(RecordObject *r, Py_ssize_t i); +static PyObject *record_getattro(RecordObject *r, PyObject *name); +static int record_traverse(RecordObject *r, visitproc visit, void *arg); +static void record_dealloc(RecordObject *r); + + +/* ----------------- Construction ----------------- */ + +PyObject * +PyRecord_New(PyObject *names, PyObject **values, Py_ssize_t n) +{ + RecordObject *record; + Py_ssize_t i; + + /* Validate names is a tuple of strings */ + if (!PyTuple_CheckExact(names) || PyTuple_GET_SIZE(names) != n) { + PyErr_SetString(PyExc_TypeError, + "names must be a tuple with correct size"); + return NULL; + } + + for (i = 0; i < n; i++) { + if (!PyUnicode_Check(PyTuple_GET_ITEM(names, i))) { + PyErr_SetString(PyExc_TypeError, + "all field names must be strings"); + return NULL; + } + } + + /* Allocate the record object */ + record = PyObject_GC_NewVar(RecordObject, &PyRecord_Type, n); + if (record == NULL) { + return NULL; + } + + /* Initialize fields */ + record->r_hash = -1; /* Not yet computed */ + Py_INCREF(names); + record->r_names = names; + + /* Copy values (stealing references) */ + for (i = 0; i < n; i++) { + record->r_values[i] = values[i]; + } + + _PyObject_GC_TRACK(record); + return (PyObject *)record; +} + + +/* ----------------- Deallocation ----------------- */ + +static void +record_dealloc(RecordObject *r) +{ + Py_ssize_t i, n = Py_SIZE(r); + + PyObject_GC_UnTrack(r); + Py_TRASHCAN_BEGIN(r, record_dealloc) + + Py_XDECREF(r->r_names); + for (i = 0; i < n; i++) { + Py_XDECREF(r->r_values[i]); + } + + Py_TYPE(r)->tp_free((PyObject *)r); + + Py_TRASHCAN_END +} + + +/* ----------------- GC Traversal ----------------- */ + +static int +record_traverse(RecordObject *r, visitproc visit, void *arg) +{ + Py_ssize_t i, n = Py_SIZE(r); + + Py_VISIT(r->r_names); + for (i = 0; i < n; i++) { + Py_VISIT(r->r_values[i]); + } + return 0; +} + + +/* ----------------- Repr ----------------- */ + +static PyObject * +record_repr(RecordObject *r) +{ + Py_ssize_t i, n = Py_SIZE(r); + _PyUnicodeWriter writer; + int first = 1; + + i = Py_ReprEnter((PyObject *)r); + if (i != 0) { + return i > 0 ? PyUnicode_FromString("Record(...)") : NULL; + } + + _PyUnicodeWriter_Init(&writer); + writer.overallocate = 1; + writer.min_length = 8 + n * 10; /* "Record(" + fields + ")" */ + + if (_PyUnicodeWriter_WriteASCIIString(&writer, "Record(", 7) < 0) + goto error; + + for (i = 0; i < n; i++) { + PyObject *name = PyTuple_GET_ITEM(r->r_names, i); + PyObject *value = r->r_values[i]; + PyObject *value_repr; + + if (!first) { + if (_PyUnicodeWriter_WriteASCIIString(&writer, ", ", 2) < 0) + goto error; + } + first = 0; + + /* Write "name=" */ + if (_PyUnicodeWriter_WriteStr(&writer, name) < 0) + goto error; + if (_PyUnicodeWriter_WriteChar(&writer, '=') < 0) + goto error; + + /* Write repr(value) */ + value_repr = PyObject_Repr(value); + if (value_repr == NULL) + goto error; + if (_PyUnicodeWriter_WriteStr(&writer, value_repr) < 0) { + Py_DECREF(value_repr); + goto error; + } + Py_DECREF(value_repr); + } + + if (_PyUnicodeWriter_WriteChar(&writer, ')') < 0) + goto error; + + Py_ReprLeave((PyObject *)r); + return _PyUnicodeWriter_Finish(&writer); + +error: + _PyUnicodeWriter_Dealloc(&writer); + Py_ReprLeave((PyObject *)r); + return NULL; +} + + +/* ----------------- Hash ----------------- */ + +static Py_hash_t +record_hash(RecordObject *r) +{ + Py_ssize_t i, n = Py_SIZE(r); + Py_uhash_t acc; + + if (r->r_hash != -1) { + return r->r_hash; + } + + /* Use the same XXH3-inspired algorithm as tuple */ + acc = _PyHASH_XXPRIME_5; + for (i = 0; i < n; i++) { + Py_uhash_t lane = PyObject_Hash(r->r_values[i]); + if (lane == (Py_uhash_t)-1) { + return -1; + } + acc += lane * _PyHASH_XXPRIME_2; + acc = _PyHASH_XXROTATE(acc); + acc *= _PyHASH_XXPRIME_1; + } + + /* Also incorporate the field names into the hash */ + Py_uhash_t names_hash = PyObject_Hash(r->r_names); + if (names_hash == (Py_uhash_t)-1) { + return -1; + } + acc ^= names_hash; + + acc += n ^ (_PyHASH_XXPRIME_5 ^ 3527539UL); + + if (acc == (Py_uhash_t)-1) { + acc = 1546275796; + } + + r->r_hash = acc; + return acc; +} + + +/* ----------------- Comparison ----------------- */ + +static PyObject * +record_richcompare(PyObject *v, PyObject *w, int op) +{ + RecordObject *vr, *wr; + Py_ssize_t i, n; + int names_equal; + + if (!PyRecord_Check(v) || !PyRecord_Check(w)) { + Py_RETURN_NOTIMPLEMENTED; + } + + vr = (RecordObject *)v; + wr = (RecordObject *)w; + + /* Records must have same size */ + if (Py_SIZE(vr) != Py_SIZE(wr)) { + if (op == Py_EQ) Py_RETURN_FALSE; + if (op == Py_NE) Py_RETURN_TRUE; + Py_RETURN_NOTIMPLEMENTED; + } + + n = Py_SIZE(vr); + + /* For equality, field names must also match */ + if (op == Py_EQ || op == Py_NE) { + names_equal = PyObject_RichCompareBool(vr->r_names, wr->r_names, Py_EQ); + if (names_equal < 0) { + return NULL; + } + if (!names_equal) { + if (op == Py_EQ) Py_RETURN_FALSE; + if (op == Py_NE) Py_RETURN_TRUE; + } + + /* Compare all values */ + for (i = 0; i < n; i++) { + int cmp = PyObject_RichCompareBool(vr->r_values[i], + wr->r_values[i], Py_EQ); + if (cmp < 0) return NULL; + if (!cmp) { + if (op == Py_EQ) Py_RETURN_FALSE; + if (op == Py_NE) Py_RETURN_TRUE; + } + } + + if (op == Py_EQ) Py_RETURN_TRUE; + if (op == Py_NE) Py_RETURN_FALSE; + } + + /* For ordering comparisons, we don't support them */ + Py_RETURN_NOTIMPLEMENTED; +} + + +/* ----------------- Sequence Protocol ----------------- */ + +static Py_ssize_t +record_length(RecordObject *r) +{ + return Py_SIZE(r); +} + +static PyObject * +record_item(RecordObject *r, Py_ssize_t i) +{ + Py_ssize_t n = Py_SIZE(r); + + /* Handle negative indices */ + if (i < 0) { + i += n; + } + + if (i < 0 || i >= n) { + PyErr_SetString(PyExc_IndexError, "record index out of range"); + return NULL; + } + + PyObject *value = r->r_values[i]; + Py_INCREF(value); + return value; +} + +static PySequenceMethods record_as_sequence = { + (lenfunc)record_length, /* sq_length */ + 0, /* sq_concat */ + 0, /* sq_repeat */ + (ssizeargfunc)record_item, /* sq_item */ + 0, /* sq_slice (deprecated) */ + 0, /* sq_ass_item (immutable) */ + 0, /* sq_ass_slice (deprecated) */ + 0, /* sq_contains */ + 0, /* sq_inplace_concat */ + 0, /* sq_inplace_repeat */ +}; + + +/* ----------------- Attribute Access ----------------- */ + +static PyObject * +record_getattro(RecordObject *r, PyObject *name) +{ + Py_ssize_t i, n = Py_SIZE(r); + + /* First check if it's a field name */ + if (PyUnicode_Check(name)) { + for (i = 0; i < n; i++) { + PyObject *field = PyTuple_GET_ITEM(r->r_names, i); + int cmp = PyUnicode_Compare(name, field); + if (cmp == 0) { + PyObject *value = r->r_values[i]; + Py_INCREF(value); + return value; + } + if (PyErr_Occurred()) { + return NULL; + } + } + } + + /* Fall back to generic attribute lookup (for methods, __class__, etc.) */ + return PyObject_GenericGetAttr((PyObject *)r, name); +} + + +/* ----------------- Public API ----------------- */ + +PyObject * +PyRecord_GetItem(PyObject *record, Py_ssize_t index) +{ + if (!PyRecord_Check(record)) { + PyErr_BadInternalCall(); + return NULL; + } + RecordObject *r = (RecordObject *)record; + if (index < 0 || index >= Py_SIZE(r)) { + PyErr_SetString(PyExc_IndexError, "record index out of range"); + return NULL; + } + /* Return borrowed reference */ + return r->r_values[index]; +} + +PyObject * +PyRecord_GetFieldByName(PyObject *record, PyObject *name) +{ + if (!PyRecord_Check(record)) { + PyErr_BadInternalCall(); + return NULL; + } + return record_getattro((RecordObject *)record, name); +} + + +/* ----------------- Python Constructor ----------------- */ + +static PyObject * +record_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + /* Record() only accepts keyword arguments */ + if (PyTuple_GET_SIZE(args) != 0) { + PyErr_SetString(PyExc_TypeError, + "Record() takes no positional arguments"); + return NULL; + } + + if (kwds == NULL || !PyDict_Check(kwds) || PyDict_Size(kwds) == 0) { + PyErr_SetString(PyExc_TypeError, + "Record() requires at least one keyword argument"); + return NULL; + } + + Py_ssize_t n = PyDict_Size(kwds); + PyObject *names = PyTuple_New(n); + if (names == NULL) { + return NULL; + } + + PyObject **values = PyMem_Malloc(n * sizeof(PyObject *)); + if (values == NULL) { + Py_DECREF(names); + return PyErr_NoMemory(); + } + + /* Iterate through kwargs */ + PyObject *key, *value; + Py_ssize_t pos = 0; + Py_ssize_t i = 0; + while (PyDict_Next(kwds, &pos, &key, &value)) { + Py_INCREF(key); + Py_INCREF(value); + PyTuple_SET_ITEM(names, i, key); + values[i] = value; + i++; + } + + PyObject *record = PyRecord_New(names, values, n); + PyMem_Free(values); + + if (record == NULL) { + Py_DECREF(names); + return NULL; + } + + return record; +} + + +/* ----------------- Type Object ----------------- */ + +PyDoc_STRVAR(record_doc, +"Record(name=value, ...)\n\ +\n\ +Immutable container with named fields.\n\ +\n\ +Records support:\n\ + - Attribute access: r.field_name\n\ + - Indexing: r[0], r[1], len(r)\n\ + - Hashing: hash(r) (usable as dict key)\n\ + - Equality: r1 == r2\n\ + - Nice repr: Record(x=10, y=20)"); + +PyTypeObject PyRecord_Type = { + PyVarObject_HEAD_INIT(&PyType_Type, 0) + "Record", /* tp_name */ + sizeof(RecordObject) - sizeof(PyObject *), /* tp_basicsize */ + sizeof(PyObject *), /* tp_itemsize */ + (destructor)record_dealloc, /* tp_dealloc */ + 0, /* tp_vectorcall_offset */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_as_async */ + (reprfunc)record_repr, /* tp_repr */ + 0, /* tp_as_number */ + &record_as_sequence, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + (hashfunc)record_hash, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + (getattrofunc)record_getattro, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | + Py_TPFLAGS_SEQUENCE, /* tp_flags */ + record_doc, /* tp_doc */ + (traverseproc)record_traverse, /* tp_traverse */ + 0, /* tp_clear */ + record_richcompare, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + PyType_GenericAlloc, /* tp_alloc */ + record_new, /* tp_new */ + PyObject_GC_Del, /* tp_free */ +}; diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 6ea20bfc5f7c94..0381c42b22957c 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -2,6 +2,7 @@ #include "Python.h" #include +#include "recordobject.h" // PyRecord_Type #include "pycore_ast.h" // _PyAST_Validate() #include "pycore_compile.h" // _PyAST_Compile() #include "pycore_object.h" // _Py_AddToAllObjects() @@ -3036,6 +3037,7 @@ _PyBuiltin_Init(PyInterpreterState *interp) SETBUILTIN("str", &PyUnicode_Type); SETBUILTIN("super", &PySuper_Type); SETBUILTIN("tuple", &PyTuple_Type); + SETBUILTIN("Record", &PyRecord_Type); SETBUILTIN("type", &PyType_Type); SETBUILTIN("zip", &PyZip_Type); debug = PyBool_FromLong(config->optimization_level == 0); diff --git a/Python/ceval.c b/Python/ceval.c index 9f4ef6be0e1f2a..b74c7da47e7af1 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -28,6 +28,7 @@ #include "frameobject.h" #include "opcode.h" #include "pydtrace.h" +#include "recordobject.h" #include "setobject.h" #include "structmember.h" // struct PyMemberDef, T_OFFSET_EX @@ -3372,6 +3373,64 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag) DISPATCH(); } + case TARGET(BUILD_RECORD): { + /* BUILD_RECORD oparg + * Stack: [name1, val1, name2, val2, ..., nameN, valN] + * Creates a Record with oparg fields. + * Pops 2*oparg items, pushes 1 Record. + */ + Py_ssize_t i, n = oparg; + PyObject *names = PyTuple_New(n); + if (names == NULL) + goto error; + + /* Allocate temporary array for values */ + PyObject **values = PyMem_Malloc(n * sizeof(PyObject *)); + if (values == NULL) { + Py_DECREF(names); + PyErr_NoMemory(); + goto error; + } + + /* Pop name/value pairs from stack in reverse order */ + for (i = n - 1; i >= 0; i--) { + PyObject *val = POP(); + PyObject *name = POP(); + + if (!PyUnicode_Check(name)) { + Py_DECREF(name); + Py_DECREF(val); + /* Clean up already-collected items */ + for (Py_ssize_t j = i + 1; j < n; j++) { + Py_DECREF(PyTuple_GET_ITEM(names, j)); + Py_DECREF(values[j]); + } + Py_DECREF(names); + PyMem_Free(values); + _PyErr_SetString(tstate, PyExc_TypeError, + "Record field names must be strings"); + goto error; + } + + PyTuple_SET_ITEM(names, i, name); /* Steals reference */ + values[i] = val; /* We own this reference */ + } + + /* Create the Record object */ + PyObject *record = PyRecord_New(names, values, n); + PyMem_Free(values); + + if (record == NULL) { + /* PyRecord_New takes ownership of names on success, + * but we need to clean up on failure */ + Py_DECREF(names); + goto error; + } + + PUSH(record); + DISPATCH(); + } + case TARGET(DICT_MERGE): { PyObject *update = POP(); PyObject *dict = PEEK(oparg); diff --git a/Python/opcode_targets.h b/Python/opcode_targets.h index 951f8f8a5569f7..b76099c60dcc57 100644 --- a/Python/opcode_targets.h +++ b/Python/opcode_targets.h @@ -165,7 +165,7 @@ static void *opcode_targets[256] = { &&TARGET_SET_UPDATE, &&TARGET_DICT_MERGE, &&TARGET_DICT_UPDATE, - &&_unknown_opcode, + &&TARGET_BUILD_RECORD, &&_unknown_opcode, &&_unknown_opcode, &&_unknown_opcode,