Skip to content

How a compiler works

A compiler is a program that reads a program. It answers two broad questions:

  1. Does this source make sense?
  2. How should another machine or runtime execute it?

That is enough of a definition to begin reading TypeRB.

Here is TypeRB source text:

def main()
puts("hello")
end

To us, it says “define the starting function and print a string.” A compiler has to build that meaning one small decision at a time.

  1. Source text
  2. Tokens
  3. Syntax tree
  4. Resolved and checked program
  5. Intermediate form
  6. Output code

The names above are not six unrelated systems. They are six views of the same program.

The lexer reads characters and groups them into tokens. Tokens are the small pieces of the language: def, main, (, ), a string, and end.

Why keep this step? Later code can ask “is this token a function name?” instead of repeatedly asking “what do these four characters mean?”

The parser turns the token sequence into a syntax tree, often shortened to AST (abstract syntax tree). The tree records that main is a function and that puts("hello") is a call inside its body.

It is a tree because programs contain smaller programs: a function has a body, the body has a call, and the call has an argument.

The resolver answers “which declaration does this name refer to?” It connects a call such as greet() to the correct function, and an imported name to the correct module.

This is separate from parsing. A parser can recognize the shape of greet() without yet knowing whether greet exists.

The type checker verifies relationships such as these:

  • a function receives the right number of arguments;
  • an Integer value is not used where a String is required; and
  • every return value agrees with the function’s declared result.

Resolver and checker failures become diagnostics. A good diagnostic explains what is wrong, where it happened, and—when possible—what the author can do.

An intermediate representation, or IR, is a compiler-friendly form of the checked program. It usually makes types, control flow, and operations more explicit than the original source.

Why not generate output directly from the syntax tree? An IR gives several backends one shared, checked input. It is also a useful boundary for validation and optimization.

The two TypeRB repositories have different IR responsibilities:

  • the reference compiler lowers to its portable typed IR before generating Go, Ruby, or TypeScript;
  • TypeRB Native owns Native MIR and other internal checked models for native layout, runtime, and QBE emission.

The backend translates the checked intermediate form into a lower-level target.

The reference compiler emits another high-level language. A Go toolchain, Ruby runtime, or JavaScript runtime then does the remaining work. The Native path emits QBE IL; QBE and the platform C toolchain turn that into machine code.

Compiler developers often use one larger split:

Part Main question Typical stages
Frontend What does this TypeRB source mean? lex, parse, resolve, check
Middle What explicit checked form should we work with? lower, verify, optimize
Backend How should this target execute it? emit, assemble, link

The boundary is helpful, but it is not a law of nature. Real repositories add project loading, package resolution, diagnostics, formatting, editor services, tests, runtimes, and command-line code around the central pipeline.

You can now read both maps with six questions:

  1. Where does source enter?
  2. Where are tokens and syntax trees made?
  3. Where are names resolved?
  4. Where are types checked?
  5. Which checked intermediate form crosses the next boundary?
  6. Which tool finally turns the output into something runnable?

You do not need to memorize every type or directory. Use the reference compiler map or the Native compiler map to answer those questions in one repository at a time.

Keep the contributor glossary nearby for terms such as ABI, conformance, bootstrap, seed, and fixed point that become important after the central pipeline is clear.