Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

zuri.compile

import zuri

Everything here is re-exported by zuri, so import zuri is enough and the names are called as zuri.*. Importing zuri.compile on its own works too and reaches the same definitions.

The Instr type zuri.compile() returns: a generic representation of one VM instruction, tagged by op ('LoadConst', 'Add', 'Jmp', …) with a fields dict holding that opcode’s own operands. As with zuri.ast.Node, there’s one Instr class rather than one class per opcode. The instruction set has around sixty opcodes, and a dedicated class per opcode would be sixty near-identical classes to keep in sync with the compiler forever after.

import zuri

for instr in zuri.compile('var x = 1 + 2') {
  echo '${instr.op} ${instr.fields}'
}

Resolved operands

compile() returns a flat instruction list, matching the VM’s own Instr type, but several opcodes (LoadConst, GetGlobal, GetField, Invoke, …) only carry a raw index into the compiler’s own constant pool, which isn’t otherwise exposed. Rather than force every caller to also separately fetch and correlate a constants table, each such field gets its already-resolved value embedded right on the instruction too, alongside the raw index: e.g. a LoadConst node’s fields has both const_idx (the raw index) and value (what it actually points at). A resolved field is never literally named after the raw index’s own subject when that word is a reserved keyword (const, class, …), since a dict key can’t be read back with plain dot-access syntax then; class becomes class_reg (it’s a register index anyway, not a resolved class value), and similarly for a handful of others.

A Closure instruction’s resolved value is a nested function prototype ({ name, arity, variadic, instructions }), whose own instructions is recursively wrapped the exact same way, so a script with functions or methods in it exposes their bodies too, not just the code immediately around them.

Every instruction node carries the source line it was compiled from, but never a column. The compiler’s own line tracking is line-only (statement granularity), unlike a zuri.token.Token’s or zuri.ast.Node’s position, which both have real column info from the lexer/parser.

Functions

walk_instrs()

zuri.walk_instrs(instrs, visitor)

Recursively visits every instruction reachable from instrs (typically zuri.compile()’s own result, or any single Instr plucked out of it), including inside a Closure instruction’s own nested function body, in the order they appear, calling visitor for each one.

visitor is either a plain function, called with every instruction (visitor(instr)), or a dict from opcode name to function, where only a matching instruction gets called (visitor[instr.op](instr)). Reach for the dict form when you only care about one or two opcodes.

Always visits the whole tree; there’s no way for visitor to skip descending into a nested function body.

import zuri

var instrs = zuri.compile(source)

# every global name this script writes to, anywhere, including
# inside nested function bodies
var written = []
zuri.walk_instrs(instrs, {
  SetGlobal: @(instr) { written.append(instr.fields.name) },
})

Parameters

  • instrs (Instr|list[Instr])
  • visitor (function|dict)

find_instrs()

zuri.find_instrs(instrs, op) -> list[Instr]

Every instruction reachable from instrs whose op is op, in the order they appear. Shorthand for the common “just give me a flat list of these” case walk_instrs() itself is built on.

import zuri

var instrs = zuri.compile(source)
for jmp in zuri.find_instrs(instrs, 'Jmp') {
  echo jmp.fields.offset
}

Parameters

  • instrs (Instr|list[Instr])
  • op (string)

Returns list[Instr]

compile()

zuri.compile(source) -> list[Instr]

Compiles source as a standalone script and returns its instructions as a flat list of Instr. See this module’s own doc comment for exactly what each instruction carries.

Raises (rather than returning partial bytecode) on a lexer, parser, or compiler error.

Parameters

  • source (string) — The Zuri source to compile

Returns list[Instr]

Raises Error if source doesn’t compile

compile_file()

zuri.compile_file(path) -> list[Instr]

Reads the file at path and compiles its contents; exactly compile(file(path).read()), for the common case of compiling a real file rather than a source string you’ve already got in hand.

Parameters

  • path (string) — Path to the Zuri source file to compile

Returns list[Instr]

Raises Error if path can’t be opened or read, or doesn’t compile

check()

zuri.check(source: string) -> list[Diagnostic]

Every problem compiling source would report, as a list of zuri.Diagnostic in the order they occur, without running anything and without raising. An empty list means source compiles.

Source that does not parse reports its syntax errors, exactly as parse_partial() finds them. Source that parses goes on to the compiler, which reports a name declared twice in one scope, self or parent outside a method, parent in a class with no superclass, break or continue outside a loop, an assignment to a constant, a private member reached through anything other than self or parent, a class extension declaring a field or an instance method, and a function too large for its registers.

import zuri

for problem in zuri.check('if true {\n  break\n}') {
  echo problem  # 2:3: 'break' used outside of a loop
}

echo zuri.check('var x = 1').length()  # 0

Parameters

  • source (string) — The Zuri source to check

Returns list[Diagnostic]

check_file()

zuri.check_file(path: string) -> list[Diagnostic]

Reads the file at path and checks it; exactly check(file(path).read()).

Parameters

  • path (string) — Path to the Zuri source file to check

Returns list[Diagnostic]

Raises Error if path can’t be opened or read

Classes

Instr

class zuri.Instr

One VM instruction.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

zuri.Instr(op, line, fields)

Returns a new instance of an Instr directly, from already- wrapped field values. Ordinary Zuri code should use zuri.compile() instead; this constructor does no wrapping of its own.

Parameters

  • op (string) — The opcode name, e.g. 'LoadConst', 'Add'
  • line (?number) — The source line this instruction was compiled from, or nil if the compiler never stamped one
  • fields (dict) — This opcode’s own operands, already wrapped

Instr.to_string()

zuri.Instr.to_string() -> string

This instruction as op@line, e.g. 'LoadConst@3', or just op when it carries no line. A one-line summary; use dump() to actually see what’s inside an instruction.

Returns string

Instr.dump()

zuri.Instr.dump() -> string

A readable, indented, multi-line dump of this instruction and everything inside it, recursively (including a Closure instruction’s own nested function body). Same idea as zuri.ast.Node.dump: the actual way to find out what an opcode’s fields looks like is to run something and read the dump, not to guess a key name from the opcode’s name alone.

import zuri

var instrs = zuri.compile('var x = 1')
echo instrs[0].dump()

prints

LoadConst@0
  dst: 0
  const_idx: 1
  value: 1

Returns string


2026, Richard Ore and Zuri contributors