Skip to content

T03, the language under the language - #41

Merged
tamnd merged 1 commit into
mainfrom
lesson-t03
Sep 6, 2026
Merged

tamnd merged 1 commit into
mainfrom
lesson-t03

Conversation

@tamnd

@tamnd tamnd commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Third lesson of the tour, and the one that answers the question most people have never asked out loud. T02 read every column of the assembly except the code. This reads the code.

The lesson writes its own disassembler rather than showing the output of somebody else's. That is the design decision the whole page rests on, so it is worth saying why.

The disassembler is thirty lines and it is on the page

There is no IL disassembler in the box, and adding a tool dependency to a lesson that is supposed to run in a browser tab was not an option. It turns out none is needed.

System.Reflection.Emit.OpCodes has one public static field per instruction, and every one of them knows its own number, the shape of its operand and what it does to the stack. Six lines of reflection over that class gives you the whole instruction table without typing any of it, and once you have the table, decoding a method body is an opcode, an operand whose length the opcode already told you, and repeat. Resolving the operands is the same token lookup T02 was about.

So the reader does not get shown IL. They get shown how to read it, and then read it.

What is on the page

Fifteen blocks. All of them run against L1, the shared program from lessons/shared/l1 that T02 introduced.

  • The instruction set counted: two hundred and twenty six opcodes, a hundred and ninety nine of them one byte and twenty seven of them behind an escape byte.
  • The disassembler itself, in two blocks, one for the decode loop and one for turning operands into names.
  • Square.Area in six instructions, with the byte offsets explained.
  • A stack walk of that method, using the push and pop behaviour that comes with each opcode, ending on the fact that the deepest it goes is two and the header claims eight.
  • Circle.Area, where Math.PI turns out to be eight bytes of operand rather than a field load.
  • Every method header in the program in one table, showing seven one byte headers and one of twenty eight bytes, plus the abstract method whose body address is zero.
  • Every instruction in the program that carries a method token, which is where call and callvirt separate.
  • The whole of <Main>$, two hundred bytes, with its five local slots and its one exception region, followed by ten notes on the places where it does not match the source.
  • The program counted: seven bodies, two hundred and eighty bytes, ninety instructions, twenty eight distinct opcodes.

Three prediction gates. What a small method header says the maximum stack depth is, which instruction calls a non virtual method and why, and whether calling IDisposable.Dispose on a struct enumerator boxes it. The third one closes the loop on the dispose gate in T02, which asked why the reference was there and could not yet say what it cost.

One new diagram, docs/diagrams/evaluation-stack.dg, on why the middle language is a stack machine rather than a register machine. It is the one part of the lesson the instruction listings cannot show on their own. No numbers in it, same rule as last time.

Things the page can now say that T02 could not

T02 left three loose ends and this picks all of them up. The <side>P name in the string heap turns out to be a field token in an ldfld. The single StandAloneSig row turns out to be the local variable list of the one method that has locals. The gap at slot two in the pdb, where a local had no name, turns out to be the enumerator.

How it is checked

Eight captured outputs, committed, including every instruction of every method and their byte offsets. That is the most sensitive thing this repository has committed so far and it is deliberate. If a compiler update changes the lowering of a foreach or of an interpolated string, this lesson goes red on the next run, which is exactly what a book about internals should do.

Two dropped blocks with assertions, on the same pattern as T02. The scale block compares the program's total IL against System.Private.CoreLib and asserts the ratio is over two thousand, which is under half the real figure today so a shrinking library does not turn a true statement red. The machine block asserts a supported platform and a three part release version. Every claim has a written reason.

Verified before opening this

Locally on osx-arm64 and on server3 on linux-x64, both against the committed expected output, byte for byte. IL should be identical everywhere, since the compiler emits the same bytes whatever it is running on, but the opcode count comes from the installed framework rather than from the standard, so it is worth having watched it on a second machine before the CI legs do.

Not in here

No runtime: citations, same reason as T01 and T02. The page cites ECMA-335 Partition III for the instruction set and II.25.4 for the method body layout, and says plainly why there is nothing from the runtime tree yet.

Reads the one column T02 skipped. The lesson writes its own IL
disassembler in about thirty lines, using the instruction table that
already ships on System.Reflection.Emit.OpCodes, and then reads L1 with
it: the smallest method, a stack walk of it, the constant the compiler
baked in, every method header in the program, every instruction that
carries a method token, and the whole of <Main>$ with notes on the ten
places where it does not match the source.

Picks up three loose ends from T02. The <side>P name in the string heap
is a field token in an ldfld, the single StandAloneSig row is the local
variable list of the one method with locals, and the pdb's unnamed slot
two is the enumerator the foreach needed.

Three prediction gates, on the maximum stack depth in a small header, on
why a non virtual method is called with callvirt, and on whether
disposing a struct enumerator boxes it. The last one answers the dispose
gate T02 had to leave open.

Adds docs/diagrams/evaluation-stack.dg on why the middle language is a
stack machine rather than a register machine.

Verified against the committed expected output on osx-arm64 and on
server3 on linux-x64. Both matched byte for byte.
@tamnd tamnd added this to the M2 The tour milestone Sep 6, 2026
@tamnd tamnd added priority/p0 Blocks the current milestone area/pedagogy Sequencing, the beginner ramp and assessment labels Sep 6, 2026
@tamnd
tamnd merged commit 3248cd3 into main Sep 6, 2026
22 checks passed
@tamnd
tamnd deleted the lesson-t03 branch September 6, 2026 02:00
@tamnd tamnd mentioned this pull request Sep 6, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/pedagogy Sequencing, the beginner ramp and assessment priority/p0 Blocks the current milestone

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant