What Is Bytecode?

You now have two representations of a program in hand: the text you write and the tree the parser builds. There is a third, and it is where much of the interesting engineering in language runtimes happens. Bytecode is the program flattened one more time, from a tree of nested nodes into a straight list of small, numbered instructions, each one an operation so simple it cannot be broken down further: load this value, add the top two, jump back eight instructions.

If source code is the recipe as a chef writes it, and the tree is that recipe organized into steps and sub-steps, bytecode is the version taped above the station for the line cook: a flat numbered list of tiny actions, chop this, stir that, go back to step 4, with all nuance already decided. The line cook never needs to understand the dish. They execute step after step, and the dish appears.

Why not just walk the tree?

An interpreter can execute the AST directly, and simple ones do. But tree-walking is slow in a specific, mechanical way: at every node, the interpreter must ask what kind of node it is, find its children, and recurse, over and over, every time that code runs. A loop that executes a million times re-answers the same structural questions a million times. Compiling the tree to bytecode answers those questions once, ahead of time, and freezes the results into a flat list that can be replayed cheaply forever after.

  • Flat beats nested: stepping to the next instruction in a list is one increment, while walking a tree means chasing child pointers.
  • Decided beats re-decided: precedence, scope, and jump targets are resolved during compilation and baked into the instruction order.
  • Compact beats verbose: an instruction is a couple of bytes, which is where the name bytecode comes from, so real programs shrink dramatically at this stage.

Reading a bytecode listing

Here is a small program in the academy's teaching language, and beside it, a plausible bytecode listing for it. The instruction names are generic teaching names; every real runtime spells its own differently, but the shape is universal.

; source                    ; bytecode
; total = 0                 0: PUSH 0
;                           1: STORE total
; i = 1                     2: PUSH 1
;                           3: STORE i
; while i <= 3 {            4: LOAD i
;                           5: PUSH 3
;                           6: LESSEQ
;                           7: JUMP_IF_FALSE 14
;   total = total + i       8: LOAD total
;                           9: LOAD i
;                          10: ADD
;                          11: STORE total
; ... i = i + 1 elided ...
; }                        13: JUMP 4
; print total              14: LOAD total
;                          15: PRINT

Look at what happened to the while loop. In the source it is a shape you can see; in the tree it is a node containing a body. In bytecode it has dissolved into arithmetic on instruction numbers: a conditional jump forward past the body, and an unconditional jump back to the check. Structure became addresses. This is the single most important thing to understand about bytecode, and it is why decompilers exist: turning those jumps back into readable loops and ifs takes real reconstruction work.

Walk the whole journey

The visualizer below carries one program through every representation you have learned in this level: source, tokens, tree, a rewrite pass, and back to code. Click through the stages and notice how each one is a different shape for the same behaviour. Bytecode is simply one more such shape, one step below the last stage shown here.

The rewrite stage above performs constant folding, computing 10 + 5 and 2 * 3 at compile time. Real compilers do this before emitting bytecode so the instruction stream never wastes cycles on arithmetic whose answer never changes. It is a first taste of Level 4: passes that change the representation without changing the behaviour.

A machine that does not exist

Machine code is a list of instructions for a physical CPU. Bytecode is a list of instructions for an invented one, a virtual machine defined entirely in software. That indirection buys portability: compile a program to bytecode once, and it runs anywhere someone has written the VM, regardless of the physical chip underneath. It also buys control, since the VM can enforce limits and safety checks a raw CPU never would. The price is speed, one layer of software between your instructions and the metal, which is why high-performance runtimes add machinery to compile hot bytecode down to real machine code while the program runs.

Design note: instruction sets for VMs come in two main families. Stack machines say ADD and take operands implicitly from a value stack; register machines say something like ADD r3, r1, r2 and name their operands. Stack bytecode is smaller and simpler to generate; register bytecode executes fewer instructions per unit of work. Both appear in production runtimes, and the next lesson steps through a stack machine because it is the design where the mechanics are easiest to watch.

A second note that matters later in the academy: because bytecode is just data, a program can carry its own instruction format. Nothing forces PUSH to be the same number in two different programs. An obfuscator can exploit that by inventing a fresh, private instruction encoding per build and shipping a matching interpreter alongside it, which forces a reader to reverse the interpreter before the instruction stream means anything. That technique, virtualization, is covered properly in a later level; what makes it possible is exactly the property you learned here, that bytecode is an invented language for an invented machine.

Bytecode is the program as a flat, compact list of tiny instructions for a software-defined machine: structure dissolved into jumps, decisions baked into order, ready to be replayed cheaply. You can now hold all three shapes of a program in your head, text, tree, and instruction list. Next, you will watch a machine actually run one.

Frequently asked questions

Which real languages use bytecode?

Most of the widely used ones. Java, Python, Lua, PHP, Ruby, and the .NET languages all compile source to bytecode for their virtual machines, and JavaScript engines compile to internal bytecode before optimizing further. Fully ahead-of-time languages like C and Go skip the VM and emit machine code directly.

Can bytecode be turned back into source code?

To a useful degree, yes. Decompilers reconstruct readable source from bytecode, recovering loops and conditionals from jump patterns. What is genuinely gone is anything the compiler discarded, comments always, and often local names, depending on the runtime. Level 5 examines what decompilers can and cannot recover.

Is bytecode the same as assembly?

They rhyme but differ in target. Assembly is a human-readable spelling of machine code for a physical CPU. Bytecode targets a virtual machine defined in software. The next lesson in this level runs a stack machine on bytecode, and the lesson after it descends to the physical CPU.

Keep learning

  • What a Compiler Actually Does
  • How a Stack Machine Runs Bytecode
  • Machine Code and the CPU
  • Lua Obfuscator
  • Python Obfuscator
  • Java & JAR Obfuscator

All lessons