← Contents

Chapter 2

The Shape of the Source

Before the mechanisms, the map. This chapter is the one to come back to when a later chapter names a file and you want to know what else lives near it.

The numbers

src/ holds about 147,000 lines of C++. That figure is misleading on its own, because 79,400 of them are generated Unicode tables — character names, properties, collation weights, normalization data — emitted from the pinned UCD and UCA 17.0 files in tools/ucd/. Nobody reads those, and nobody edits them.

The hand-written implementation is about 67,700 lines, and it is very unevenly distributed:

FileLinesWhat it is
Interpreter.cpp20,787the tree walk, calls, dispatch, modules, concurrency
Builtins.cpp9,887named built-ins, Test, the head of the method chain
Parser.cpp7,210statements, expressions, declarations, interpolation
MethodCallPart2.cpp3,549the method chain, continued
Regex.cpp2,785the regex and grammar engine
MethodCallPart3.cpp2,652the method chain, continued
Codegen.cpp2,586the --exe transpiler
Lexer.cpp2,351tokenizer
MethodCallTail.cpp2,277the method chain, the end of it
Interpreter.h1,363the interpreter's own interface, plus the rt* helpers
main.cpp1,281the CLI and the four compile drivers
Value.cpp1,093coercions, comparison, gist, flatten

Two shapes stand out and both are deliberate.

Interpreter.cpp is enormous. It is the tree walk, and the tree walk touches everything: scopes, calls, operators, assignment, control flow, module loading, the FFI marshaller, the concurrency runtime. Splitting it by topic would mostly move #includes around, because the pieces share the interpreter's private state rather than a clean interface.

The method dispatcher is split across four files for one reason. It used to be a single 9,138-line function, methodCallInner, and that stopped being compilable in a reasonable time. It is now four ordered segmentsBuiltins.cpp holds the head, then MethodCallPart2, MethodCallPart3, MethodCallTail — each returning std::optional<Value>, where nullopt means "not handled here, try the next segment".

The critical property, stated in the source and worth repeating: these are segments, not categories. The chain is order-sensitive. Later arms deliberately catch what earlier ones decline. An arm belongs where its priority is, not where it reads nicely. MethodCallSegment.h exists so that the four files share one include prologue and cannot drift apart.

The library boundary

  src/main.cpp        the rakupp CLI          ─┐
                                               ├─► both link librakupp_rt.a
  generated stub.cpp  a --exe/--aot binary    ─┘

Everything except main.cpp and Repl.cpp compiles into librakupp_rt.a. The REPL lives in the executable rather than the library on purpose: nothing about an interactive session should be linked into the standalone binaries the compiling modes produce.

What each file is for

Front end

FileRole
Token.hthe token struct: kind, text, position, spaceBefore
Lexer.{h,cpp}source text to a flat vector<Token>
Ast.hevery node type, and the NK tag enum
Parser.{h,cpp}tokens to a Program; the live user-operator tables
Pod.{h,cpp}the $=pod DOM

Values

FileRole
Value.{h,cpp}the fat tagged struct, CowStr, ClassInfo, Callable
IStr.hthe interned-string field
MethodName.hMName, the packed method name used by the dispatch chain
BigInt.{h,cpp}arbitrary-precision integers, base 10^9
IntOps.hportable overflow-checked arithmetic and bit intrinsics

Execution

FileRole
Interpreter.{h,cpp}the tree walk and nearly everything it reaches
Builtins.cppthe built-in routine table and the method chain's head
MethodCall*.cppthe rest of the method chain
BuiltinsShared.hhelpers the split forced out of file scope
Runtime.{h,cpp}the shared entry points, and the big-stack thread
IOSpec.cppIO::Spec::* path algorithms

Engines

FileRole
Regex.{h,cpp}regex compilation, the matcher, GrammarMatcher
LtmNfa.{h,cpp}the declarative-prefix NFA for longest-token matching
Unicode.{h,cpp}normalization, grapheme segmentation, collation, properties
unicode_*_gen.cppthe generated tables those read
unicode_names.cppcharacter names — the single largest file in the tree

Back ends and tooling

FileRole
Codegen.{h,cpp}--exe: AST to C++
AstEmit.cpp--aot: C++ that rebuilds the AST
AstSerial.{h,cpp}the binary AST format behind the precompiled parse
AstDump.cpp--dump-ast
Lint.{h,cpp}--lint, static analysis over the parsed tree
Highlight.{h,cpp}--highlight, parse-aware syntax colouring
Profiler.{h,cpp}--profile, the routine-level wall-time profiler
Repl.{h,cpp}the interactive session

Boundaries

FileRole
Ffi.{h,cpp}the libffi backend: loader, ABI probe, type registry
rakupp_ext.hthe C ABI extension modules compile against
ExtApi.cppthe host side of that ABI
Platform.hthe Windows/POSIX split, in one place

Reading conventions in the source

Three habits recur, and knowing them saves a lot of confusion.

*Comments explain why, and often carry the measurement.* A comment in this tree is rarely a restatement of the code. It is much more often the reason the obvious version was rejected, sometimes with a number attached:

// src/Value.h — the CowStr rationale, trimmed
// Value is copied by value everywhere ... so holding a bare std::string
// meant a long string was memcpy'd on each of those. The cost is
// O(length) per OPERATION, which makes any pure-Raku tokenizer O(n^2):
// JSON::Fast spent 13.9 s on a 421 KB document that Rakudo parses in
// 50 ms, and the profile was all copying, not parsing.

When this book explains a decision, it is usually expanding a comment like that one.

A "decided once" field is a fact about the syntax, not a cached result. Several node types carry a small mutable field that starts at a sentinel and is written on first evaluation:

// src/Ast.h
template <typename T> struct DecidedOnce {
    std::atomic<T> v;
    operator T() const { return v.load(std::memory_order_relaxed); }
    DecidedOnce& operator=(T x) {
        v.store(x, std::memory_order_relaxed); return *this;
    }
};

The atomic is not for synchronisation. It is there because a node is shared between threads, every writer computes the same idempotent answer, and a plain field would make that a data race that ThreadSanitizer correctly reports. Relaxed atomics make it defined at plain-load cost on the architectures that matter. Chapter 18 is entirely about what these fields hold and, more importantly, what they must never hold.

rt* functions are the compiled backend's vocabulary. Anything named rtAdd, rtIndexRef, rtAttrGet, rtCallB is a runtime entry point that Codegen emits calls to. They are declared in Interpreter.h and are the contract between the transpiler and the runtime — which is why they are inline where the fast path matters. Chapters 25 to 27 are about them.

Building it

cmake -S . -B build
cmake --build build -j

CMakeLists.txt builds librakupp_rt from all of src/ except main.cpp, then the rakupp executable. The glob is CONFIGURE_DEPENDS, but CMake caches it, so re-run the configure step after adding a source file.

The compiler matters more than usual here. Clang produces a binary between 1.2 and 2 times faster than GCC's on this codebase — the method dispatch chain and the tree walk are both inlining-sensitive in ways GCC handles less well — so Clang is what ships and GCC is kept as a portability gate. Link-time optimisation and -mcpu=native were both measured and both did nothing.

The test surface

WhereWhat
Roastthe Raku specification suite, run by tools/run-roast.raku
t/run.rakuthe local suite: examples and showcases, byte-compared to golden output
t/regression/one file per fixed bug
t/stress/concurrency and memory stress, also run under TSan and ASan
tools/perf-guard.rakuthe performance gate, compared against a recorded baseline
the showcase interpretersJavaScript, Perl, Python and Lisp, written in Raku

The release checklist in docs/dev/RELEASING.md gates on all of them. Chapter 35 is about why the performance gate is there and what happens when it is skipped.