Odin Speaks Odin ยท Chapter 1

Chapter 1: Why embed a language at all

This chapter:

  • Watch the finished project do the thing that justifies the whole book: a boss fight's behaviour changed by editing a text file, with the game still running.
  • Understand what "embedding a language" actually buys an engine, concretely, not as marketing.
  • Confront the honest question: why build one instead of reaching for Lua.
  • See the five-layer map this book builds, and why none of the five layers get compressed.
  • Set up the project skeleton and the musc tool you'll use for most of Parts I to III.

Where we left off: nothing yet; this is the start.


1.1 A tour of the finished thing

Picture Embers, the small arcade game this book ends with, already running. A boss, Ashling, is mid-fight: she drifts to a point on screen, pauses, fires a ring of shots, waits for the player to find the gap, then drifts somewhere else and repeats. None of that sequencing is Odin code. It's this, sitting in a file called boss_ashling.mus:

func phase_one(boss) {
    move_to(boss, 0, -80, 120)
    wait(0.6)
    emit(boss, "ring_shot")
    wait_until(player_found_gap)
    move_to(boss, 60, -40, 120)
}

Now picture editing that file while the game keeps running: change 0.6 to 1.4, save, and on the boss's next attack the pause is visibly longer. No recompile. No restart. The Odin binary running Embers never changed; only the script did, and the game picked it up live.

That is the single sentence this entire book is building toward, and Chapter 19 is where you'll build the mechanism that makes it possible. Everything before it, the lexer, the parser, the tree-walker, the compiler, the bytecode VM, the embedding layer, exists to make that one edit-and- save moment real rather than a slide in a talk.

1.2 What "embedding a language" actually buys you

It's worth being concrete about this before writing a single line of code, because "scripting language" is one of those phrases that sounds self-evidently useful and is worth interrogating anyway.

  • Iteration speed for content, separate from iteration speed for the engine. Recompiling Embers takes however long odin build takes. Reloading a .mus file takes however long it takes the file watcher to notice the mtime changed, milliseconds. Once wave and boss behaviour lives in scripts, the person tuning a boss fight's timing never touches the Odin build at all.
  • Data-driven content instead of recompiled logic. A .mus file is data your program reads, the same category of thing as a level file or a config file, except this "data" can express sequencing, conditionals and loops, because it's a real, if small, programming language.
  • A sandboxed place for behaviour that isn't fully trusted. Not "sandboxed" in the security sense yet, Chapter 18 is explicit that fault isolation here defends against bugs, not malice, but sandboxed in the sense that a script's mistakes are contained: a broken boss pattern degrades to "this one enemy stops updating," not "the whole game crashes."

None of this is unique to games. Anywhere the shape of the problem is "let other code, or a future version of yourself, or someone who isn't a programmer at all, change behaviour without shipping a recompiled binary," this is the pattern: a simulation whose scenario logic is scripted, a CLI tool with a plugin system, a server whose request-handling rules are configurable per customer without a redeploy. Embers is this book's concrete example, not the limit of the idea.

1.3 Why not just use Lua?

Here's the honest answer, stated once and then left alone: in production, you probably wouldn't build this. You'd embed Lua, or Wren, or another existing, battle-tested, genuinely excellent embeddable language, and spend your engineering time on your actual game instead of on a lexer. Lua's C API is decades of real-world hardening this book cannot and does not try to match in eighteen chapters.

Design decision: build Muspel from scratch, rather than embed an existing language. Alternatives considered: embed Lua (via Odin's C bindings), embed a smaller existing embeddable language such as Wren. Trade-off: an existing language gets you a correct, fast, well-documented implementation immediately, at the cost of never seeing what it's actually doing under the value marshalling, the bytecode dispatch, the coroutine mechanism, all the parts that make "just embed Lua" feel like magic the first time you use it. Choice: build Muspel from scratch, because this book's actual subject is understanding that magic well enough to make different design choices confidently, in Odin, for your own language. A reader who finishes this book and then reaches for Lua on their next project will understand precisely what Lua is doing for them, and that's worth more than the convenience of not having built it once.

This is also why the book leans so heavily on Design decision callouts throughout: the goal isn't "here is Muspel," it's "here is how you'd reason about building your language," with Muspel as the worked example.

1.4 The seven-layer map

A language implementation is seven genuinely separate engineering disciplines, and this book gives each of them room rather than compressing them into a single "and then we interpret it" chapter:

  1. Lexing (Part I): turning raw source text into a stream of tokens, numbers, strings, identifiers, operators, each carrying enough position information to produce a good error message later.
  2. Parsing (Part I): turning that token stream into an abstract syntax tree that reflects Muspel's actual grammar, including real error recovery when the input is malformed.
  3. Structuring data (Part I): giving the language two ways to name a shape, a struct for grouping related values and an enum for a small closed set of named states, plus a fixed-capacity array to hold many of them, the vocabulary a game engine's script API actually needs instead of long positional argument lists and magic numbers.
  4. Tree-walking (Part II): a deliberate stepping stone, built to validate the language's semantics in isolation from any performance concern, before a single line of bytecode exists to get wrong instead.
  5. Compiling and executing (Parts III to IV): a bytecode compiler and a resumable stack-based virtual machine, built once the tree-walker has proven the semantics are right and Chapter 9 has measured, not asserted, why tree-walking alone isn't enough for a script that runs every frame.
  6. Modularising (Part V): letting one script import named declarations from another, so a growing script codebase shares one struct and one set of helper functions instead of copy-pasting them into every file.
  7. Embedding (Part V): the boundary layer, host-callable builtins, value marshalling, event callbacks, fault isolation, live reload, that turns "an interpreter that runs a file" into "a thing driving somebody else's program."

Seven layers, seven different sets of engineering trade-offs. A register machine versus a stack machine is a different kind of decision than panic-mode error recovery versus stopping at the first syntax error, which is different again from whether a native builtin is allowed to suspend a running script, or whether a struct's field access resolves by a byte offset or a name. Each gets its own chapters because each is a whole skill in a real language implementation, and a book that flattened them would leave you able to describe an interpreter, not one you'd actually built and could defend under questioning.

1.5 The project layout

Two packages, kept deliberately apart:

odin-speaks-odin/
โ”œโ”€โ”€ muspel/        # the language + VM, a standalone package (Parts I to V)
โ”œโ”€โ”€ embers/         # the capstone host game (Part VI)
โ”œโ”€โ”€ tools/musc/     # a CLI: lex/parse/compile/disassemble/run a .mus file
โ”œโ”€โ”€ build.sh
โ”œโ”€โ”€ run.sh
โ””โ”€โ”€ README.md

muspel/ never imports embers/. embers/ imports muspel/ and supplies the host callbacks muspel/ calls into. This is the single most important structural lesson in the book, stated now so it's visible before a single line of muspel/ exists: a scripting layer that's entangled with its first host was never actually reusable. Appendix F sketches attaching this exact same muspel/ package to a second host, yo_odin's Destroids, as evidence the separation held.

tools/musc is the tool you'll live in for most of Parts I to III: musc lex, musc parse, musc check, musc compile, musc run, musc debug, one subcommand added per layer as it's built. It exists precisely so every early chapter has a satisfying, runnable checkpoint long before Embers exists to embed the VM in anything, which matters given how far off the capstone is.

Right now, in a fresh project of your own, musc does almost nothing: it reads a file and prints it back. That's deliberate, it's the whole milestone for this chapter, proof the toolchain and project layout work before any real compiler code exists:

// tools/musc/main.odin
package musc

import "core:fmt"
import "core:os"

main :: proc() {
	if len(os.args) < 2 {
		fmt.eprintln("usage: musc <file.mus>")
		os.exit(1)
	}

	path := os.args[1]
	data, err := os.read_entire_file_from_path(path, context.allocator)
	if err != nil {
		fmt.eprintfln("musc: could not read %s: %v", path, err)
		os.exit(1)
	}
	defer delete(data)

	fmt.print(string(data))
}

Try it, in that fresh project:

./run.sh examples/hello.mus
// hello.mus: the book's first sample script, read back verbatim by Chapter 1's musc shell.
var greeting = "hello, muspel"
print(greeting)

That's it. No lexer, no parser, nothing that understands Muspel yet, just a program that reads a file and prints it, and a project that builds. Chapter 2 makes it actually look at what's in that file.

Note on this book's own code/odin-speaks-odin. That command works exactly as shown in a project you're building yourself from this chapter's listing. It won't work against this book's own shipped reference code: code/odin-speaks-odin/tools/musc/main.odin is a single, final file that every later chapter keeps editing in place, so by the time you clone this repository it's already grown into the subcommand dispatcher section 1.5's project layout describes (musc lex, musc parse, musc check, one argument for the subcommand, one for the file). Running ./run.sh examples/hello.mus against that main.odin, with no subcommand, prints a usage error and exits 1, not the file's contents; that's expected, not a bug, code/ is the finished project the whole book converges on, not a chapter-by-chapter snapshot. The first command in this book that's meant to be run against the shipped code/odin-speaks-odin as-is is Chapter 2's musc lex, and from there on every "Try it" in this book matches that shipped project exactly, verified by actually running it.

Checkpoint

In your own project, ./run.sh examples/hello.mus prints the file's contents back out. The project skeleton exists, musc builds and runs, and you have a concrete picture, from section 1.1, of what you're building toward.

Exercises

  1. In your own project, add a second .mus file under examples/ and confirm musc prints it back too.
  2. Read os.read_entire_file_from_path's signature in the Odin standard library and note what its Error return type distinguishes (file not found versus a permissions error, for instance). You'll want this kind of error precision again once musc is reporting its own errors from Chapter 2 onward, not just the operating system's.
  3. Sketch, on paper, one more real-world use of "embed a small language" beyond a game, from your own work if you can. Keep it in mind: it's a useful gut-check for whether a given book of Muspel's design decisions would still make sense in that other setting.

Further reading