F11. Two tables on the side - #129
Merged
Merged
Conversation
The line table and the exception table, both decoded by hand and both checked against the interpreter's own decoders. The angle is that the two are the same idea twice. A traceback needs to know which line and which columns an instruction came from, and a raise needs to know which handler covers an offset, and neither is needed while a program is going right. So both live beside the bytecode rather than in it, and nothing reads either until something has already gone wrong. The exception table decoder agrees with dis._parse_exception_table. The location table decoder agrees with co_positions() on 299 code objects out of four standard library modules. The zero cost claim is checked rather than asserted: the instructions inside a try are the same list as the same loop with no try around it, and SETUP_FINALLY and POP_BLOCK never reach the bytecode. One detail worth the trip: the two tables encode their integers with the six bit chunks in opposite orders, and the reason is that the exception table gets binary searched on its first number. Six diagrams, seven cells with version notes, and the lesson runs end to end in Pyodide.
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.
The twenty fifth lesson, and the eleventh of the front end part. F10 opened the code object and named
co_linetableandco_exceptiontablewithout saying what is in them. This one decodes both.The angle is that the two tables are the same idea twice. A traceback needs to know which line and which columns an instruction came from. A raise needs to know which handler covers an offset. Neither is needed while a program is going right, so neither is allowed to cost anything while it is. Both got moved out of the instruction stream into a compressed blob that nothing reads until something has already gone wrong.
What the lesson does
It opens with the zero cost claim and then checks it rather than asserting it. Wrap a loop in a
try, take the instructions inside the covered range, and compare them against the same loop with notryat all. Same list.SETUP_FINALLYandPOP_BLOCKstill exist in the compiler, and F08 watched the optimizer move them around, but the assembler deletes both and writes a table instead, so neither reaches the bytecode.Then it decodes an exception table entry byte by byte. Four numbers, all in code units, six bits to a byte, and the top bit of the first byte marking where an entry starts. That marker is the interesting part: it is what lets a raise binary search a table whose entries are not all the same length, because you can land anywhere and walk backwards to a boundary. Twelve lines of Python decode the whole thing and the cell checks its answer against
dis._parse_exception_table.The second half is the location table. Four numbers per instruction, six entry forms, and the assembler picks the smallest one that fits. The lesson rebuilds a traceback's caret from the column numbers by hand, then writes a thirty line decoder and runs it against 299 code objects out of
argparse,dataclasses,disandjson.decoder. Zero disagreements withco_positions().It closes on a detail I did not expect. The two tables use the same six bit varint and put the chunks in opposite orders.
parse_varintreads most significant first,write_varintwrites least significant first, and they are forty lines apart in the same header. It is not sloppiness. The exception table is binary searched on the first number of each entry, and reading the big end first is what makes that comparison cheap.One thing that took a while
My location decoder disagreed with
co_positions()at exactly one instruction, and the internal documentation did not explain why.write_location_info_long_forminPython/assemble.cwritesloc.col_offset + 1rather thanloc.col_offset, so that a stored zero can mean "no column here", andadvance_with_locationstakes the one back off on the way out. NeitherInternalDocs/code_objects.mdnor the format comment mentions it. The lesson says so, because it is a decent reminder that the source is the specification.Checks
Every code cell has a
variesnote, because all seven of them print offsets or byte counts and 3.14 compiles these functions a little smaller than 3.15. The prose holds on both, and the two lines that have to be exact,dis agrees on every one of themand0 of them decoded differently, say the same thing on both.Six diagrams,
.excalidrawand.svg, all under 1100 px. The lesson runs end to end in Pyodide. Both READMEs have their rows.just checkandjust versionsare green locally.Part of M3, issue #22.