F10: inside a code object - #128
Merged
Merged
Conversation
What compiling a file actually hands you. The lesson walks a module down to a method through co_consts, because a nested definition is stored as a constant next to the numbers and strings, and then takes the record apart. The part worth the lesson is localsplus. co_varnames, co_cellvars and co_freevars look like three separate tuples and are three views of one array, filtered by tag bytes, and a parameter a nested function reads is tagged both local and cell so it appears in two of them. Seven slots come out of _varname_from_oparg and the tuples add up to eight. That is also why a closure's first instruction runs before line one: MAKE_CELL has to put the cell in the slot before anything can read it. Then co_flags one bit at a time, then equality, which compares the bytecode in its unspecialized form and never compares the filename. Two compiles of the same source from two different files are equal and hash the same, and a function run five thousand times still equals a fresh compile while _co_code_adaptive underneath it does not. Then replace and the AttributeError you get for trying to assign. Eighteen cells, six diagrams, six claims each with a cell under it, eight citations into Include/cpython/code.h, Objects/codeobject.c and Python/assemble.c.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
F09 ended with a code object falling out of the assembler. This one opens it up.
The hook is that a code object holds no values. Not one. It has the instructions, the literals, the names to look up later and the list of slots a frame will need, and every actual value your program computes lives somewhere else. That is what makes one code object able to serve every call, and it is the thing to have straight before F13 builds a frame.
What is in it
It starts by compiling a small file with a class, a method and a generator in it, then walking
co_constsrecursively. A nested definition is not a separate thing the compiler hands back alongside the module, it is stored as a constant of the enclosing code object, sitting next to the numbers and the strings. Printing the tree makes that concrete in about four lines.The part that earns the lesson is localsplus.
co_varnames,co_cellvarsandco_freevarsread like three tuples and are three filtered views of one array, and fordef outer(a, b=2, *args, c, **kw)with a nested function readinga, the array has seven slots and the three tuples add up to eight.ais tagged both local and cell,get_localsplus_namestests one bit at a time, and soacomes out of two of the filters. The lesson enumerates the real array with_varname_from_opargand prints which tuples each slot turns up in, which makes the overcount obvious rather than surprising.That tag is also the answer to a question a reader has probably had for a while, which is why
dissometimes shows two instructions before the first line of the function.MAKE_CELL 0has to replace the value in slot 0 with a cell holding it before anything can read it, so it runs before your code does.Equality
code_richcomparecompares the name, the argument counts, the flags, the first line number, the constants, the names, the side tables, and the bytecode through_Py_GetBaseCodeUnit, which is the unspecialized form. It never comparesco_filename. So the same source compiled from/one/place.pyand from/somewhere/else.pygives two code objects that are equal and hash the same, and the filename is carried for tracebacks rather than for identity.The specialization half is nicer than it sounds. Run a function five thousand times, and it still equals a fresh compile, and
co_codeis byte for byte identical, and_co_code_adaptiveunderneath it is not. Three lines, and the reader has seen the quiet copy the interpreter has been writing into the whole time.Then
replace, which gives a new object rather than changing this one, and theAttributeErroryou get for assigning toco_name, printed rather than described.The usual
Eighteen cells, six diagrams, six claims each with a runnable cell under it and no unobservable ones, eight citations into
Include/cpython/code.h,Include/internal/pycore_code.h,Objects/codeobject.candPython/assemble.c. No new glossary terms:code object,frame,celland the rest were all already in there. One version note, the usual one saying which interpreter you are on.just checkpasses and the notebook runs top to bottom on Pyodide as well as on 3.14 and 3.15.F11 takes the two side tables this one only names, the line table and the exception table.