buzz

package module
v0.0.0-...-d120b58 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 23, 2026 License: GPL-3.0 Imports: 19 Imported by: 0

README

gopherbuzz

A pure-Go bytecode VM for the Buzz scripting language with JIT support. It targets Buzz 0.6.0-dev, tracking upstream buzz-language/buzz main at the commit pinned in version.go (UpstreamRef).

It implements a subset of the language. The goal is 100% compatibility; the running record of how far along that is lives in Upstream parity, and is enforced by a test rather than asserted by this file. If you are evaluating gopherbuzz as a Buzz implementation, read Where the skeletons are before the feature list -- it names every place this VM answers differently from upstream, or accepts source upstream refuses.

Upstream parity

Measured against UpstreamRef (0.5.0-265-g294d8f9) on 2026-08-12. Upstream ships six test directories; three are measurable here, and all three numbers are below rather than only the flattering one.

upstream suite files gopherbuzz what it asks
tests/behavior/ 85 83 pass does correct source produce the right answer?
tests/compile_errors/ 82 72 rejected does gopherbuzz REJECT what upstream rejects?
tests/fuzzed/ 644 0 panics can malformed input crash the front end?
tests/bench/ 11 not run upstream's benchmarks (ours are in benchmarks/)
tests/manual/ 9 not run interactive
tests/utils/ 10 n/a helper modules the behavior tests import

The two behavior files still failing are c-buzz-api.buzz and extern-library.buzz, both of which need upstream's C API rather than a language feature.

The compile-error row is the uncomfortable one and the most important. 10 of those 82 programs compile CLEAN here that upstream refuses. That is not a missing feature, it is missing strictness: gopherbuzz will accept source upstream tells you is wrong. If you are evaluating this VM as a Buzz implementation, weigh that at least as heavily as the behavior row -- a permissive checker is the failure mode a subset does not warn you about.

The three largest clusters are closed: match analysis (11 files -- exhaustiveness, duplicate conditions, overlapping ranges), terminal flow (8 -- unreachable code after a statement that transfers control away, plus the missing return), and yield propagation with the reserved-method signatures (6). What remains has no cluster bigger than a handful: out and block expressions, mutability, unused locals and shadowing, and assorted type checks.

Three things to know before adding a rule here.

Upstream's own suite is not internally consistent everywhere, so check a proposed rule against tests/behavior/ before writing it. unused-import is the worked example: the note in session.go records why that one cannot be promoted at all.

A strictness check has a DIRECTION. Reporting unreachable code over-claims (a wrong answer calls live code dead); reporting a missing return under-claims (a wrong answer invents an error on a correct function). terminates and terminatesForReturn exist because those two biases disagree about try/catch.

Most of what is left is a DIALECT DECISION or a disproportionate migration rather than a missing check. Each of these was implemented, measured, and reverted:

File(s) What it needs Measured cost
yield-location, yield-without-annotation require *> on any function that yields reverses a recorded choice (ast.YieldExpr, TestYieldOutsideFiberDismissed); ~18 fixtures here plus magus's s3-cache spell
fiber-error-location hold a direct throw to propagate-or-catch, as a CALL already is breaks seven of magus's suites: its spells, tour files and scripts throw without !>
unused-import make BZZ3001 an error impossible as stated -- see the note in session.go
selective-import stop assert resolving unimported blocked on registerBuiltins, which pre-defines the stdlib names on purpose
error-message rendering a thrown object's message the test body never runs under Exec, so nothing can surface it here

Each is a call to make deliberately, with the migration budgeted -- not a gap to patch. The one structural item worth naming: optionality is erased, which is what blocks two of them and would be the next real piece of type-system work.

The fuzz corpus is upstream's checked-in AFL output, not hand-written tests: the filenames are AFL's (id_000123,sig_06,src_000051,op_flip1,pos_1), where sig_06 is the signal that crashed the target and op_flip1/op_arith8 is the mutation applied. The contents are real Buzz programs with a byte corrupted -- mnssage: for message:, or invalid UTF-8 spliced mid-token. Passing means the front end REPORTS an error rather than crashing on them; note the scope, since it is measured over parse, check and compile and not over execution, and upstream blacklists an entry or two of its own.

The behavior baseline when this record started was 12 of 83.

Measure against the PINNED commit, not a local main checkout: a newer checkout has files that do not exist at the pin, which is how an earlier hand-count reached a wrong 13-of-84.

Nothing is copied into this repo. magus run conformance libs/gopherbuzz fetches buzz-language/buzz at the pinned sha into a cache directory and runs all three suites against it, so every number here is reproducible against the ref this VM claims to track.

Both allowlists -- testdata/upstream-behavior-allowlist.txt and testdata/upstream-compile-errors-allowlist.txt -- are the enforced source of truth, and each fails in both directions: a listed test that regresses, and an unlisted test that starts passing without being recorded. Parity can therefore only go up, and closing a gap forces the gain to be banked rather than quietly enjoyed.

What works

Objects (fields with defaults, methods, static fun and static fields, mut instances, optional unwrap if (x -> y), field punning), enums (enum<str>/enum<int> backing types and explicit case values), namespaces and imports, optionals with ??, as? and optional chaining ?./?[, nullable declarations that omit their initializer, default argument values, error sets on declarations plus try and multiple typed catch clauses, collection mutability as part of the TYPE (mut [int] is a distinct type that typeof renders, mut T is assignable to T and not the reverse, and the clone family re-types across it), fibers with resolve, ranges, string interpolation, pattern literals, zdef FFI, closures, generics as erasure, ranges with their full method set, and the collection/loop core (multi-clause for, labeled loops), and block expressions (from { ... out v; }), free identifiers (@"non-standard"), and generic object declarations, inline ifs, catch void, and maps keyed by any value (an object, an int, a bool -- not only a str). Three deliberate supersets: the contextual test keyword (below), named-argument labels, and compiled-bytecode serialization (next).

One superset worth naming: serialized bytecode

Upstream compiles to bytecode -- it is a bytecode VM, JITed through MIR -- but it has no way to PERSIST that bytecode. Its subcommands are test, check, fetch, format, help, init and version (running a script is the default path), none of which emits an artifact, and Chunk.zig builds a chunk in memory with no reader or writer beside it. Upstream's serialize is the buzz:serialize JSON module, a different thing entirely: it turns a runtime VALUE into text.

gopherbuzz adds the persistence: Chunk.Marshal emits a portable .bo blob that UnmarshalChunk runs without re-parsing or re-compiling, with source positions split into a companion .bdb. The distinction is narrow but it is the whole superset -- compiling to bytecode is upstream behaviour, writing it to a file is not.

This is the ordinary shape for a bytecode VM rather than an invention -- CPython's .pyc, the JVM's .class, luac and Lua's string.dump, and Erlang's .beam are all the same idea, and the split debug file mirrors a PDB or a DWARF .dwo. It is also load-bearing here: magus ships every built-in spell as a prebuilt .bo (internal/spellruntime/gen/*.bo), so a spell loads without a compiler on the critical path.

Because it is ours and not upstream's, it is ours to keep whole. Every constant kind the compiler can mint -- null, bool, int, float, str, enum def, object declaration, pattern, and type value -- has an encoding, so the encoder's "cannot serialize" arm is unreachable from compiled code. A type-value constant (<T>, typeof x) was the one that had been missed: Marshal failed outright on it, which silently barred any program using typeof from ever being a built-in spell. Bytecode version records what each format bump changed and why an older VM must reject a newer blob.

Read that list as "the shape parses and runs", not as "matches upstream in every detail". Several entries carry a caveat recorded under Where the skeletons are -- as coerces rather than asserts, a compound assign evaluates its target twice, and generics are erased.

What does not

No open gaps remain. Every one of the nine still-failing files is blocked by a property of the EMBEDDING rather than by unwritten code, so this list is not a backlog:

  • buzz's own native-extension ABI. extern-library and c-buzz-api are a different problem, and the failure mode hides it: extern-library looks like it only wants a shared library, and "null is not callable" is just its unbound extern fun sayHello(). But tests/utils/hello.zig takes a *api.NativeCtx, imports buzz_api.zig and LINKS AGAINST LIBBUZZ, exposing a hello(symbol) resolver keyed by name. Loading it would mean gopherbuzz reproducing upstream's NativeCtx layout and buzz_api entry points so a library compiled against buzz's VM can pull arguments from gopherbuzz's. c-buzz-api (buzz_c_api.c) is the same requirement stated openly. Building these with Zig would not help.
Where the skeletons are

These are ours, not missing features: places gopherbuzz answers differently from upstream, or answers where upstream would refuse. They are listed because a subset you can measure is more useful than a subset you have to discover. Each is a real, reproducible difference at the pinned ref.

Silently different answers. The dangerous class -- these do not error.

  • A bare as COERCES instead of asserting. 3.9 as int is 3 here; upstream's as is a statically checked cast, not a conversion. Only as? was fixed to be a real type test. gopherbuzz's own testdata depends on the coercion, which is why it still stands.
  • A compound assign evaluates its target twice. x op= v desugars to x = x op v sharing the target node, so f().n += 1 calls f() twice. Harmless for the plain variable and field cases; wrong for any side-effecting target.
  • A stored map key shadows a same-named builtin method. rec.map reads the field, not map.map. Deliberate -- an anonymous object literal is represented as a map, and upstream's anonymous objects have fields and no methods -- but it is a language-wide flip driven by one representation choice.
  • A backtick string interpolates, and an unparsable {...} stays literal. Upstream interpolates too, but would reject the malformed case; here it silently becomes text. That leniency is load-bearing (it is what lets a Mustache template and a zdef block live in a raw string), and it means a regex quantifier written `[0-9]{3}` becomes [0-9]3 with no diagnostic.

Checks that are recorded but not enforced. Declared and then trusted.

  • obj{...} annotations are erased in the checker, so an annotated discard (_: obj{ nope: str } = ...) asserts nothing statically. The RUNTIME test (x is obj{...}) does check field presence, so the two disagree.
  • match analysis is narrower than the runtime. Exhaustiveness, duplicate conditions and overlapping ranges are all checked now, but only over conditions that fold to a CONSTANT: 1 + 1 duplicates 2, while two conditions naming the same final do not. That is deliberately one-sided -- an unfoldable condition is recorded and never reported, because a false positive here rejects a correct program.
  • Generics are erased. There is no reified type argument, so assertOfType::<int> cannot inspect anything; gopherbuzz's own testing module takes a type NAME string instead. This is the one "cannot accommodate" above that is really a design choice.

Narrower gaps. Known, bounded, and unlikely to bite most programs.

  • A selective import (import a, b from "...") is honored for host modules but not yet for source or file modules, which bind every export.
  • Top-level placeholder hoisting made a forward reference from top-level code a bare runtime error instead of a positioned diagnostic.

Costs the design imposes. Not correctness, but you should know before adopting.

  • A local a closure captures is boxed, and boxing is keyed on the NAME via an over-approximating scan. An unrelated shadowing name inside a nested closure therefore boxes the outer local too, which costs the superinstruction fast paths and de-JITs the chunk: measured ~30% on a hot loop that differs only in an inner parameter's name.
  • Each boxed local allocates into a grow-only, never-freed global heap in the default NaN-boxed build, so a captured local declared inside a loop pins one entry per iteration: measured 2.7x RSS over 2M iterations.
  • A map keyed by anything other than str gets NO key->index hash at any size, so its lookups are O(n) where a str-keyed map is O(1) above smallMapThreshold. The hash is keyed by the key's display string, which stops being an identity the moment 1 and "1" can both be present; giving it a synthetic per-key identity would cost an allocation on every get of EVERY map to serve a shape neither this embedding nor upstream's suite builds at size.

A test is often blocked by more than one gap, so closing a single entry does not always flip a file green. The allowlist reports real progress; the table only explains it. types-as-value.buzz is the worked example: adding protocol declarations and exempting zdef from argument labeling both moved it forward, and it stays red because it also needs a native library this embedding cannot build.

Performance

A pure-Go VM with a baseline JIT: no cgo, no toolchain. Its standout case is a tight top-level numeric loop (LoopSum, sum 0..1e6), one shape the JIT compiles to native code (it also compiles the nested float loops of the Mandelbrot kernel; see benchmarks/):

xychart-beta
    title "LoopSum 0..1e6, warm, ms/op (lower is better)"
    x-axis ["gopherbuzz", "gopher-lua", "tengo", "goja"]
    y-axis "ms/op" 0 --> 430
    bar [5.7, 50.5, 84.0, 424]

That 5.7 ms is the JIT engaged; the same VM with the JIT off runs the loop in 40.6 ms, still ahead of the others, but the native-code path is the headline. Allocation is effectively zero either way: the NaN-boxed []uint64 stack has no GC-visible pointers.

The JIT compiles top-level numeric loops to native code; everything else runs on the interpreter. Its wheelhouse now covers both LoopSum and the Mandelbrot kernel. The baseline JIT learned the and short-circuit and int→float promotion, so Mandelbrot's nested float loop compiles to native SSE and runs in ~26 ms, an ~9× lead over gopher-lua's 246 ms. On the interpreter gopherbuzz wins the lighter scripting microbenchmarks (loops, calls, fib, collection iteration) and, with the float fast path in the arithmetic dispatch, is competitive on the heavy compute kernels: it trails gopher-lua on un-JIT'd MatMul, draws level with tengo on BinaryTrees and with gopher-lua on NBody, and string building still goes to gopher-lua. Allocation stays well under the dynamically typed peers throughout: kilobytes on lean workloads, and map/list iteration is allocation-free (foreach reuses a per-slot iterator); only string building reaches low single-digit MB. The full win-and-lose matrix (10 workloads, warm + fresh, plus an opt-in LuaJIT / Umka tier that is faster still) lives in benchmarks/, kept deliberately honest.

benchstat median, Go 1.25; gopherbuzz re-measured on an amd64 Xeon @ 2.10 GHz, the comparison engines on an amd64 Xeon @ 2.80 GHz (so the gap is conservative). Cross-language microbenchmarks differ in semantics (types, safety, GC), so read as order-of-magnitude, not a verdict.

Reproduce:

go test -run='^$' -bench=. -benchmem ./...                # in-tree (BUZZ_JIT=0 for interp)
cd benchmarks/comparison && GOWORK=off go test -bench=. . # cross-language

Why this matters

gopherbuzz is the interpreter behind magus, which fans out across a workspace and runs the tasks. The VM sits on the critical path of that flow, before any real work starts:

flowchart TD
    A([magus run]) --> B["gopherbuzz: evaluate<br/>magusfile.buzz + host-call glue"]
    B --> C{fan out across workspace}
    C -->|widens| B
    C --> D[run the real work]
    B:::hot
    classDef hot fill:#fde68a,stroke:#b45309,color:#111

Two constraints follow:

  • No second toolchain. magus is a single static Go binary: no cgo, no C library, nothing to install. A faster engine that requires a C toolchain (the extended tier: LuaJIT, Umka) would forfeit that, so the engine has to be pure Go.
  • It's on every task's critical path. The VM evaluates magusfile.buzz and the host-call glue on every run, and again as the fan-out widens. A slow or allocation-heavy layer pays that cost as latency and GC pressure on every build.

The goal is for the VM to stay invisible. The benchmarks above are deliberately heavy stress loops; a real magusfile.buzz is orders of magnitude smaller, so the VM's slice of any run sits well below the work it dispatches. Returns are diminishing now, and the perf design notes mostly exist to stop a future change from regressing what's here. The aim is to make the interpreter cheap enough that magus can treat it as free, without reaching for a second toolchain to get there.

Building

go build ./...
go test ./...

No cgo, no external toolchain. Pure-Go deps: purego (zdef() FFI) and golang-asm (JIT codegen, amd64).

After bumping BytecodeVersion, run go generate in ../internal/spellruntime to rebuild the embedded spell bytecode.

CLI

cmd/buzz is a standalone runner mirroring the upstream buzz CLI, built on the Go standard library alone (no third-party CLI framework):

go run ./cmd/buzz script.buzz          # run a file
echo 'return 1 + 2;' | go run ./cmd/buzz -   # run stdin
go run ./cmd/buzz -e 'import "std"; std.print("hi");'
go run ./cmd/buzz -c script.buzz       # type-check only
go run ./cmd/buzz -t script.buzz       # run its test "..." {} blocks
go run ./cmd/buzz --ast script.buzz    # dump the AST as JSON
go run ./cmd/buzz -L ./lib m.buzz      # add an import search path

The Buzz standard library is available; magus host bindings are not (use magus buzz for those, or magus buzz --workspace to load a magusfile).

Testing

Upstream Buzz's test "name" { … } blocks are supported. A block runs only under buzz -t / --test; a normal run skips it. A block fails when its body raises, typically a std.assert that did not hold:

import "std";

test "addition" {
    std.assert(1 + 1 == 2, "math broke");
}
go run ./cmd/buzz -t mytests.buzz
# ok    test "addition"
# ---
# 1 passed, 0 failed

Named arguments: upstream Buzz labels call arguments (f(a: 1, b: 2)) and requires the labels on multi-argument calls. gopherbuzz accepts them as a superset: labels resolve against the callee's parameter names at check time (any order; positional arguments first), while unlabeled calls keep working. For dynamically typed callees (host functions, any values) labels cannot be verified and arguments pass in written order. After label resolution, arguments evaluate in parameter order.

Deliberate divergence: upstream hard-reserves test as a keyword; gopherbuzz treats it as a contextual soft keyword. test introduces a block only in the test "…" { position and stays a normal identifier elsewhere. This runs every upstream test block verbatim while keeping test usable as an identifier, which the magus embedding needs (export fun test is a common target). It is therefore a strict superset of upstream, the same "match capabilities, diverge only where a Go embedding forces it" stance taken for FFI.

Build tags

Three mutually exclusive Value representations; one is compiled at a time.

Tag Value Use
(none) 8-byte NaN-box + handle table default production build
buzz_safe 24-byte interface + assertion, bounds-checked CI / differential testing
buzz_unsafe 24-byte pointer struct legacy baseline

The default build has zero GC write barriers on the push/arith/pop path (the operand stack is []uint64). buzz_safe is behaviorally identical and slower, which lets CI validate the fast build. The JIT is built with the default rep on amd64 and arm64 (on every OS, including Windows); every other config (safe/unsafe, other arches, wasm) uses a no-op stub. See which platforms this has actually run on before trusting a JIT result on a platform CI does not cover.

go test -tags buzz_safe ./...
go test -tags buzz_unsafe ./...

FFI (calling C)

zdef() binds functions (and data symbols like kCFBooleanTrue) from a C shared library at runtime, accepting both upstream-Buzz Zig declarations (fn sqrt(x: f64) f64;) and C prototypes, via purego, with no cgo and no build-time toolchain. The ffi module adds C-ABI type metadata and a pinned-memory API so scripts can drive the common patterns: scalar calls, pointer out-parameters, by-reference structs, and callbacks.

import "ffi";
final lib = zdef("libm", "double sqrt(double x);");
final r = lib.sqrt(9.0);                 // 3.0

Unlike upstream Buzz (whose FFI is Zig-ABI native and needs an embedded Zig compiler), gopherbuzz is C-ABI native: zdef takes C prototypes and ffi.sizeOf & friends take C type-name strings. Parsing works on every target; binding works where purego does, and returns a clear "unsupported" error elsewhere (e.g. wasm).

Full reference: docs/ffi.md · runnable demo: examples/ffi-c/ (go run .) · a larger showcase: examples/bubblegum/, an i3-flavored macOS tiling window manager written in pure Buzz on this FFI.

WebAssembly

The core is pure Go with no cgo, so it cross-compiles to wasm unmodified (zdef() returns "unsupported"; the JIT uses its stub). wasm/main.go (guarded by //go:build wasm) reads a program from stdin and prints a trailing return:

tinygo build -target=wasi -o buzz.wasm ./wasm        # ~1.6 MB; default scheduler (fibers use goroutines)
GOOS=wasip1 GOARCH=wasm go build -o buzz.wasm ./wasm # ~4 MB, no extra toolchain
echo 'return (1 + 2) * 10;' | wasmtime buzz.wasm     # 30

Both wasip1/wasm and js/wasm build. This makes gopherbuzz (to our knowledge the first Go implementation of Buzz) run in the browser: the magus docs site's Buzz playground (cmd/buzz-playground over internal/playground) evaluates Buzz live and dry-runs a magusfile.buzz, with host calls recorded.

Architecture

flowchart TD
    A[source] --> B[Parse]
    B --> C[ast.Program]
    C --> D[Checker]
    D --> E["Compiler<br/>FoldConsts, FusePeephole"]
    E --> F["Chunk<br/>bytecode"]
    F --> G["VM.Exec<br/>register-window stack"]
    G --> H[Value]
  • Instr {Op uint8, A, B int32}: word-coded, pointer-free, in a contiguous slice, fetched without bounds checks on the hot path.
  • Value: 8-byte NaN-boxed word. Immediates (int/float/bool/null) live in the payload; heap objects are indices into a per-VM handle table, so the operand stack is []uint64 with no GC-visible pointers.

Baseline JIT

On amd64 and arm64, a hot top-level chunk whose body is the numeric loop/arithmetic opcode subset is compiled to native code, deleting interpreter dispatch. On by default; disable with BUZZ_JIT=0 or vm.SetJIT(false).

  • The pointerless []uint64 stack lets native code run with no GC cooperation; every value sits at a static slot offset at each opcode boundary, so interpreter state is always materialized.
  • Each op has an int and a double (SSE) fast path. Anything else (mixed int/float, a non-number via any, NaN, float ÷0/%) deopts to the interpreter at the recorded ip; unsupported ops (calls, members, strings) make the chunk ineligible. The interpreter is the oracle, so the JIT is never wrong.
  • Loop back-edges poll cancellation every 256 iterations (one predicted branch).
  • Eligibility (depths()) is also validation, not just an opcode filter: every local slot, branch target, const index, fused sub-opcode and absorbed-nop slot is range-checked before a backend turns it into an address. The interpreter can assume a well-formed chunk (its indexing is bounds-checked, and the compiler emits nothing else); generated code cannot, on both counts -- and marshal.go decodes chunks from .bo bytes the compiler never wrote. A malformed chunk is declined, exactly like an unsupported opcode.
  • Code generation is serialized. golang-asm initializes package-level assembler tables from NewBuilder without synchronization, so two goroutines entering the generator at once race inside it -- on any two chunks, not just the same one. Compiling happens once per chunk, so the lock costs nothing; the cache read in front of it stays lock-free.
  • A compilation lives exactly as long as its *Chunk. The cache is keyed by a WEAK pointer, so it is not itself the reason a chunk can never be collected; when the chunk goes, a cleanup drops the entry and unmaps the executable pages. Reachability is what makes that unmap safe -- a chunk is reachable for the whole of its native run -- which is also why there is no LRU or size cap: neither can prove nobody is inside those pages. vm.JITMappedBytes() is the gauge, and it should plateau in a long-lived host rather than climb.
  • Every native exit is checked, not trusted. A deopt names a resume ip and a stack height, and the height has to equal base + LocalCount + entryDepth[ip] -- the same depth model the stub was emitted from, so this is an exact equality, not a range check. An exit that fails it (or names an ip outside the chunk, or a status no stub writes) is discarded: the entry locals are restored, the chunk is marked ineligible, and it re-runs interpreted from the top. Sound only because the eligibility filter rejects calls, which makes the locals window the entire observable effect of a native run.

Because that recovery is silent and correct, the counters are the only evidence it happened: vm.JITBadExitCount() and vm.JITCompileFailCount() are both zero on a healthy build, and a non-zero value is a bug in this package rather than anything about the program. Both also fire a fault hook (FaultJITBadExit, FaultJITCompile). The differential suite asserts them alongside the answers, since an answer-only assertion stays green while the JIT quietly stops engaging.

Codegen uses golang-asm: same machine code (so same runtime speed) as a hand emitter, but toolchain-verified. Only the trampolines (vm/jit_<arch>.s) are hand asm. Not yet JIT'd: calls, non-top-level frames, strings.

Which platforms this has actually run on

This is hand-written machine code, so "it compiles" and "it produces the right answer" are different claims. Only the second one matters, and it is only earned by executing the differential suite (TestJITMatchesInterpreter, TestJITComputesNatively, TestJITDeoptsOnRuntimeError) on the platform in question. Where each stands:

Platform Backend Executable memory Status
linux/amd64 jit_amd64.go mmap Exercised every CI run - the only platform CI covers.
darwin/arm64 jit_arm64.go mmap Exercised continuously by hand - primary development platform, not covered by CI.
linux/arm64 jit_arm64.go mmap Verified by hand - suite executed on arm64 hardware, 2026-08-04. Not covered by CI.
darwin/amd64 jit_amd64.go mmap Not executed here. Same backend and mapping as linux/amd64; only the OS differs.
windows/amd64 jit_amd64.go VirtualAlloc NEVER EXECUTED. Compiled and reviewed only.
windows/arm64 jit_arm64.go VirtualAlloc NEVER EXECUTED. Compiled and reviewed only.

The two Windows rows are the honest gap, and they are new: the JIT was excluded on Windows (!windows in every build tag) until it was enabled alongside the windows/arm64 release build. They share the two backends above, which are well-exercised elsewhere, but they are the only platforms using jit_mem_windows.go (VirtualAlloc + VirtualProtect + FlushInstructionCache instead of mmap + mprotect), and nothing executes them: CI runs on linux/amd64 only, and no Windows machine is in the loop.

What that does and does not mean:

  • safeCompileJIT recovers a codegen panic and falls back to the interpreter, so an unencodable instruction degrades to slow, not wrong. It now reports that (JITCompileFailCount) instead of caching the same silent "ineligible" verdict an unsupported opcode gets, so a backend that has stopped working is distinguishable from one that was never asked to. With depths() validating the chunk, nothing reaches that recovery any more except a genuine codegen bug -- which is why the test for it substitutes a panicking generator rather than feeding in a malformed chunk.
  • A miscompile that produces an unresumable exit is caught by the exit check above and recovered. A miscompile that computes the wrong number is not: nothing cross-checks arithmetic the interpreter never re-runs, so if the Windows mapping handed back memory that was subtly wrong, the failure mode is still a wrong answer.
  • A fault inside generated code is not recoverable at all. jitEntry is NOSPLIT with no stack map, so a SIGSEGV in the generated bytes is fatal error: unexpected fault address, which recover() cannot see and debug.SetPanicOnFault does not cover (it applies to faults in Go code). Every guard here is a compile-time or exit-time check; none of them is a net under the native run itself. Windows is where that matters, because jit_mem_windows.go is the one executable-memory path nothing has ever executed.
  • BUZZ_JIT=0 disables the JIT entirely and is the mitigation if a Windows result is ever suspect. Reporting that BUZZ_JIT=0 changes an answer is the single most useful bug report this component can receive.

Performance design

The interpreter's throughput rests on a few load-bearing tricks. Before touching the hot path, baseline with benchstat over -bench=. -count=10 and re-check under buzz_safe.

  • Exec is I-cache-bound (~50 KB single switch). Adding a new full case regresses all benchmarks 25-55%. Add small branches inside existing handlers, or move cold code to //go:noinline helpers, never a new case body.
  • Superinstructions (FusePeephole): OpBinLC, OpBinLL, OpCmpLC fuse the dominant GetLocal/LoadConst/<op>/JumpFalse patterns.
  • SetLocal absorption: fused ops peek ahead and write x = x op y straight to the slot.
  • Static int proof: bit 31 of a fused op's B means "both operands proven int" (drops the tag checks); sub-opcode is masked & 0x7F / & 0x7FFF. Sound because OpCheckType guards every any → int narrowing.
  • Inline caches: per-VM mcache (member access) and field-slot hints (OpGetField/OpSetField): pointer/index compares, no string scan. Per-VM, not per-Chunk (chunks are shared; verified -race).
  • NaN-box + handle table: zero write barriers on push/pop; the table pins objects for the VM's life (fine for short per-target sessions).

Bytecode version

Bump vm.BytecodeVersion (in vm/marshal.go) when opcode numbering, the Instr/Chunk/UpvalInfo layout, the fused-op encoding, or the serializable Value/AST set changes.

Contributing gotchas

  1. No new Exec case bodies (I-cache; see above).
  2. Value changes must pass under all three build tags (CI runs default + buzz_safe; spot-check buzz_unsafe).
  3. Fused-op sub-opcode masking (& 0x7F / & 0x7FFF) must track any new flag bits, in both chunk.go and the VM handlers.
  4. slotTypeInt = 1 (vm chunk.go) mirrors buzz.sInt so they must be kept in sync.
  5. mcache/ncache are per-VM, never per-Chunk (chunks are shared).
  6. Re-check escapes with go build -gcflags='-m=2' ./vm/ after hot-path changes.

Documentation

Overview

Package buzz is a stack-based bytecode interpreter for the Buzz scripting language embedded in magusfiles. It is a Go reimplementation of the upstream Buzz language. gopherbuzz targets Buzz 0.6.0-dev: it tracks buzz-language/buzz main (0.6.0 is unreleased), synced and validated against the exact commit pinned by UpstreamRef; see LanguageVersion. The latest published language reference is https://buzz-lang.dev/0.5.0/reference/ .

Architecture: source is lexed and parsed (Parse), type-checked, then compiled to a flat instruction stream (CompileWith) that the register-window VM ([VM.Run]) executes.

The primary embedding entry point is NewSession; host code injects globals with Session.SetGlobal and registers target callbacks that Buzz can invoke via Session.Targets.

Value equality

Equality (==) is structural for scalars and strings but reference-based for collections (lists, maps, objects). Two distinct list or map values are never == even if their contents match — this avoids O(n) comparison costs in the common case. Compare elements explicitly when content equality is needed.

Heap lifetime

Heap objects (strings, lists, maps, objects, userdata) are pinned for the process lifetime: the VM never collects or compacts its heap. Fine for magus's short-lived sessions; a long-running embedder should isolate workloads in separate processes.

Index

Constants

View Source
const (
	// Type-check errors (checker.go).
	UndefinedName    diagnostics.Code = "BZZ1001" // reference to a variable or function that is not in scope
	UndefinedType    diagnostics.Code = "BZZ1002" // reference to a type name that is not defined
	NonBoolCondition diagnostics.Code = "BZZ1003" // an if/while/for condition whose type is not bool
	ArgumentError    diagnostics.Code = "BZZ1004" // a call with the wrong count, an unknown/duplicate name, or a missing argument
	TypeMismatch     diagnostics.Code = "BZZ1005" // an assignment, return, yield, or operand whose type does not match what is expected
	UnhandledRaise   diagnostics.Code = "BZZ1006" // a call to a !> function from a caller that neither declares !> nor catches it
	UnknownMember    diagnostics.Code = "BZZ1007" // access to a member an imported module does not export

	// Session / runtime errors (session.go).
	UnresolvedImport diagnostics.Code = "BZZ2001" // an import that cannot be resolved to a module or file
	FiberMisuse      diagnostics.Code = "BZZ2002" // resume/resolve called wrong: not a fiber, missing argument, or a running fiber

	// Warnings (parser.go). Unlike every code above, a warning never fails Exec/Compile -
	// see Severity.
	UnusedImport diagnostics.Code = "BZZ3001" // an import whose namespace binding is never referenced

	// Warnings (checker.go).
	StringAccumulation diagnostics.Code = "BZZ3002" // a string rebuilt from itself with + inside a loop
)

BZZ diagnostic codes. Each names a distinct, documented buzz error kind. There is deliberately NO catch-all code: a type error the checker has not classified carries NO code at all (just its message), matching Rust and TypeScript, where an error either earns a specific code or has none. A code is a lookup handle for a documented failure, not a completeness checkbox.

View Source
const (
	CVoid        = vmpackage.CVoid
	CBool        = vmpackage.CBool
	CInt         = vmpackage.CInt
	CUint        = vmpackage.CUint
	CFloat       = vmpackage.CFloat
	CDouble      = vmpackage.CDouble
	CCharPtr     = vmpackage.CCharPtr
	CVoidPtr     = vmpackage.CVoidPtr
	CAddr        = vmpackage.CAddr
	CPoint2D     = vmpackage.CPoint2D
	CRect4D      = vmpackage.CRect4D
	CUnsupported = vmpackage.CUnsupported
)

C type constants.

View Source
const (
	// LabelUpstream marks a module that tracks upstream Buzz's standard library: a
	// clean-room reimplementation whose names, signatures, and semantics match it.
	LabelUpstream = "upstream"
	// LabelGopherbuzz marks a module that originates in gopherbuzz, with no
	// counterpart in upstream Buzz.
	LabelGopherbuzz = "gopherbuzz"
)

Well-known module labels, classifying a module by origin. Labels are free-form strings; these are the vocabulary gopherbuzz applies to its own stdlib, and a host defines additional labels as needed (e.g. "host", "wasm").

View Source
const LanguageVersion = "0.6.0-dev"

LanguageVersion is the Buzz language version gopherbuzz targets. Buzz 0.6.0 is unreleased: gopherbuzz tracks buzz-language/buzz `main`, which sits between the released 0.5.0 and the eventual 0.6.0, so the honest label is the in-development series. gopherbuzz stays compatible with released 0.5.0 and additionally implements the 0.6.0-dev conventions present at UpstreamRef -- namespace-decl resolution, `=>` arrow-body functions, and the `buzz:` stdlib import scheme.

View Source
const UpstreamRef = "0.5.0-265-g294d8f9"

UpstreamRef pins the exact buzz-language/buzz commit gopherbuzz is measured against, as a `git describe`: the 0.5.0 tag plus the commits since (0.5.0-<N>-g<shortsha>). Because 0.6.0 is not tagged, a commit -- not a version number -- is the only precise statement of which upstream this is compared with.

It is a comparison point, NOT a compatibility claim. gopherbuzz implements a subset. Do NOT restate the score here: this comment carried "26 of 83" long after the real figure moved, and a second stale number lived in conformance_test.go at the same time, so the tree asserted three different scores at once. The authority is testdata/upstream-behavior-allowlist.txt - its line count IS the passing count, because the conformance test enforces the list in both directions. The README's parity section carries the running record in prose. Bump this ref and re-run the conformance target on every sync.

Variables

View Source
var (
	// CTypeLayout returns the size and alignment in bytes of a C type name.
	CTypeLayout = vmpackage.CTypeLayout
	// IsPointerCType reports whether a C/Zig type spelling is a pointer (carried
	// as a heap-boxed `ud` to preserve the full 64-bit address).
	IsPointerCType = vmpackage.IsPointerCType
	// StructLayout computes size, alignment, and field offsets of a C struct.
	StructLayout = vmpackage.StructLayout
	// AllocFFI pins n zeroed bytes at a fixed address and returns it.
	AllocFFI = vmpackage.AllocFFI
	// AllocCString copies a string into a NUL-terminated C block.
	AllocCString = vmpackage.AllocCString
	// ReadCString reads a NUL-terminated C string, terminator included.
	ReadCString = vmpackage.ReadCString
	// ForeignStructTypes returns a zdef struct's C field types by name.
	ForeignStructTypes = vmpackage.ForeignStructTypes
	// WriteFFIBytes copies bytes into a block returned by AllocFFI.
	WriteFFIBytes = vmpackage.WriteFFIBytes
	// FreeFFI releases a block previously returned by AllocFFI.
	FreeFFI = vmpackage.FreeFFI
	// ReadScalar reads a C scalar from an alloc block at addr+offset.
	ReadScalar = vmpackage.ReadScalar
	// WriteScalar writes a C scalar into an alloc block at addr+offset.
	WriteScalar = vmpackage.WriteScalar
	// MakeCallback wraps a Buzz function as a C function pointer (its address).
	MakeCallback = vmpackage.MakeCallback
)

FFI memory and C-ABI type metadata, backing the `ffi` std module. These are portable (no cgo, no purego) — see vm/ffi_mem.go.

View Source
var DebugOnly = vmpackage.DebugOnly

DebugOnly makes Marshal emit the debug-info (.bdb) blob instead of the executable bytecode (.bo). See vm.DebugOnly for full documentation.

View Source
var DefaultSearchPaths = []string{
	"./?.buzz",
	"./?/main.buzz",
	"./?/src/main.buzz",
	"./?/src/?.buzz",
	"/usr/share/buzz/?.buzz",
	"/usr/share/buzz/?/main.buzz",
	"/usr/share/buzz/?/src/main.buzz",
	"/usr/share/buzz/?/src/?.buzz",
	"/usr/local/share/buzz/?.buzz",
	"/usr/local/share/buzz/?/main.buzz",
	"/usr/local/share/buzz/?/src/main.buzz",
	"/usr/local/share/buzz/?/src/?.buzz",
	"$BUZZ_PATH/?.buzz",
	"$BUZZ_PATH/?/main.buzz",
	"$BUZZ_PATH/?/src/main.buzz",
	"$BUZZ_PATH/?/src/?.buzz",
}

DefaultSearchPaths is the ordered list of path templates an unconfigured Session searches to resolve `import "<name>"` to a file. In each template `?` is replaced with the import path and environment variables are expanded (a template referencing an unset variable is skipped, so an unset $BUZZ_PATH drops its entries rather than searching the filesystem root).

It mirrors the upstream Buzz search order (https://buzz-lang.dev); module files use the `.buzz` extension. Override per-session with WithSearchPaths.

View Source
var GetFFIProvider = vmpackage.GetFFIProvider

GetFFIProvider returns the currently installed FFI provider.

View Source
var ParseCDecls = vmpackage.ParseCDecls

ParseCDecls parses one or more C function prototypes separated by semicolons.

View Source
var ParseZigDecls = vmpackage.ParseZigDecls

ParseZigDecls parses Zig-style declarations (the upstream-Buzz zdef dialect).

View Source
var RegisterFFIProvider = vmpackage.RegisterFFIProvider

RegisterFFIProvider installs p as the FFI backend used by zdef().

View Source
var SetFFIProvider = vmpackage.SetFFIProvider

SetFFIProvider sets the FFI provider (accepts nil, for tests).

View Source
var UnmarshalChunk = vmpackage.UnmarshalChunk

UnmarshalChunk deserializes a Chunk produced by Chunk.Marshal.

Functions

func AncestorsFromContext

func AncestorsFromContext(ctx context.Context) []string

AncestorsFromContext returns the current dispatch ancestor stack stored by the pool.

func CompileWith

func CompileWith(prog *ast.Program, opts CompileOptions) (*vmpackage.Chunk, error)

CompileWith compiles prog under opts. See CompileOptions. Pass the zero CompileOptions{} for a self-contained program whose top-level variables are slot-based locals (the one-shot fast path); set SharedGlobals for the session model. (This is the standalone counterpart to Session.Compile, which compiles source against a session's shared scope.)

func IsReservedIdent

func IsReservedIdent(name string) bool

IsReservedIdent reports whether name is a word upstream Buzz reserves, so it cannot be used as a plain binding name. A code GENERATOR emitting Buzz needs this: a Go field named Type mirrors to a field named `type`, which does not parse, and the generator has to reach for a free identifier (@"type") instead. Exported so there is one list rather than a copy that silently drifts from the parser's.

func Parse

func Parse(src string) (*ast.Program, error)

Parse tokenizes src and returns a Program using upstream Buzz's rules: the program top level may contain only declarations, imports, and expression statements (no control flow), and call arguments after the first must be labeled. This is the default because it matches upstream — leniency is the deviation, not strictness, so it must be opted into explicitly (ParseEmbedded).

func ParseEmbedded

func ParseEmbedded(src string) (*ast.Program, error)

ParseEmbedded relaxes the two script-conformance rules Parse enforces (top-level statements and labeled args) for gopherbuzz's embedded use: the REPL, magus eval, magusfile loading, and interactive snippets, where top-level statements are the whole point. It is the named, deliberate deviation from upstream Buzz.

func WithAncestors

func WithAncestors(ctx context.Context, stack []string) context.Context

WithAncestors installs stack as the dispatch ancestor stack. The pool maintains it itself while dispatching; it is exported for the two boundaries the pool cannot see: the entry target (invoked directly rather than dispatched, so it must seed the stack with its own name or a dependency can cycle back into it undetected) and a cross-project dispatch (which enters a project where these names mean nothing, and passes nil to clear them).

func WithObserver

func WithObserver(ctx context.Context, obs TargetObserver) context.Context

WithObserver returns ctx carrying obs, which Pool.execute notifies for each target.

func WithPoolObserver

func WithPoolObserver(ctx context.Context, obs PoolObserver) context.Context

WithPoolObserver returns ctx carrying obs, which a Pool notifies as it acquires, warms, and releases sessions. Pass it on the same ctx you hand to Dispatch.

func WithPoolRegistry

func WithPoolRegistry(ctx context.Context, reg *PoolRegistry) context.Context

WithPoolRegistry stores reg in ctx for retrieval inside the Buzz call stack.

func WithTargetMemo

func WithTargetMemo(ctx context.Context, m *TargetMemo) context.Context

WithTargetMemo returns ctx carrying m as the invocation-scoped target memo.

func WrapDirect

func WrapDirect(name string, fn vmpackage.Callable, obs DirectObserver) vmpackage.Callable

WrapDirect returns a Callable that runs fn and reports its duration and outcome to obs under name; register the result with vm.DirectValue as usual. When obs is nil it returns fn unchanged, so an unobserved binding pays nothing. This is the recommended way to time native calls (host bindings, stdlib directs) without touching the interpreter dispatch loop. The wrapper passes args straight through and does not retain them, preserving the Callable no-retain contract.

Types

type CFuncSig

type CFuncSig = vmpackage.CFuncSig

CFuncSig is a parsed C function prototype.

type CParam

type CParam = vmpackage.CParam

CParam is one parameter of a C function signature.

type CType

type CType = vmpackage.CType

CType is a C type from the zdef() declaration subset.

type CompileObserver

type CompileObserver interface {
	// Phase reports one finished compile sub-phase: which phase, how long it ran,
	// and the error it produced (nil on success). Fired in pipeline order (parse,
	// then check, then compile) for each compiled chunk. Diagnostics runs parse and
	// check only, so it fires those two without a following compile.
	Phase(phase CompilePhase, elapsed time.Duration, err error)
	// Import reports one resolved import: its path (as written, e.g. "buzz:os"),
	// how it resolved, how long resolution took, and any error. For a flat file or
	// source import the duration includes executing the imported module, whose own
	// compile phases fire separately on this same observer.
	Import(importPath string, outcome ImportOutcome, elapsed time.Duration, err error)
}

CompileObserver is notified as a Session compiles source into a runnable chunk: the parse/check/compile phase timings and each import it resolves.

It is optional: attach one with Session.SetCompileObserver. With none set the session compiles unchanged, so this adds no cost and no behaviour change.

type CompileOptions

type CompileOptions struct {
	// SharedGlobals compiles top-level declarations as runtime Env bindings
	// (OpDefName/OpLoadName) rather than stack slots. Set it when several chunks
	// execute against one shared Env and must observe each other's top-level
	// definitions — the magus multi-magusfile model and the REPL both rely on
	// this. When false (the default), top-level variables are slot-based locals:
	// faster (no per-access map hashing), but private to a single Run. Function
	// bodies always use slots regardless of this flag.
	SharedGlobals bool

	// DebugLines records a source line for every emitted instruction (Chunk.lines)
	// so the debugger can report a paused frame's current line and drive
	// line-level step hooks. Off by default — the one-shot fast path pays nothing;
	// the session path (Session.Compile) turns it on so magus.pry() works.
	DebugLines bool

	// PromoteTopLevel, only meaningful together with SharedGlobals, slot-promotes
	// a top-level var/const that is provably chunk-private: not exported and never
	// referenced from inside any function/fiber body (where it would either need to
	// outlive the top-level frame or change from a live-Env read to a by-value
	// upvalue snapshot). Promoted vars become stack slots — the same fast path
	// block-locals and function bodies already use — while exported and
	// closure-captured top-level names stay Env bindings, so cross-chunk visibility
	// is unchanged for them. Leave it false for the REPL/incremental path, where a
	// later chunk may reference any earlier top-level name by name.
	PromoteTopLevel bool

	// ImportedTypes are the exported object/enum declarations of flat-imported
	// modules (the same set handed to the checker). They are seeded into the
	// compiler's typeDecls so an object literal of an imported type
	// (`config\Config{...}`) applies that type's field defaults, exactly as a
	// local-type literal does. Without this, an imported-type literal only carries
	// the fields it sets and leaves the rest null — upstream Buzz applies the
	// defaults, so this is a parity fix, not an extension.
	ImportedTypes []ast.Node
}

CompileOptions controls how a program's top-level scope is compiled.

type CompilePhase

type CompilePhase int

CompilePhase identifies a sub-phase of turning Buzz source into a runnable chunk.

const (
	PhaseParse   CompilePhase = iota // lexer + parser: source -> AST
	PhaseCheck                       // type checker over the AST
	PhaseCompile                     // AST -> bytecode chunk
)

func (CompilePhase) String

func (p CompilePhase) String() string

String names the compile phase for logs and metric labels (plain ASCII).

type Diagnostic

type Diagnostic struct {
	Line, Col int
	Code      diagnostics.Code
	Msg       string
	Severity  Severity
}

Diagnostic is a positioned diagnostic for editor tooling. Line and Col are 1-based; a zero Line means no position was recoverable (Col is only meaningful beside a nonzero Line). Msg has the "buzz: line L:C:" prefix stripped - the position travels in the fields instead. Code is the BZZ diagnostic code (empty for a parse error, which has no code). Severity's zero value is SeverityError, matching every diagnostic before this field existed (a parse error has no Severity set either, so it reads as an error, correctly). Msg/Line/Col/Code/Severity mirror the unexported checker typeError; keep the two shapes in sync if either gains a field.

func (Diagnostic) String

func (d Diagnostic) String() string

String renders d the same shape typeError.Error() renders a hard error in - "[CODE] buzz: line L:C: <severity: >msg", plus a "see: <url>" line when Code is set - so a warning a caller prints reads consistently with the errors this package already produces.

type DirectObserver

type DirectObserver interface {
	// DirectCall reports one finished direct call: the binding name, its wall-clock
	// duration, and the error it returned (nil on success).
	DirectCall(name string, elapsed time.Duration, err error)
}

DirectObserver is notified when a wrapped native (direct) callable returns. It is the recommended seam for timing host calls: wrap a Callable with WrapDirect at binding registration, which leaves the VM's hot direct-dispatch arm untouched. See WrapDirect.

type FFIProvider

type FFIProvider = vmpackage.FFIProvider

FFIProvider binds parsed C function signatures from a shared library into callable Buzz values.

type ImportOutcome

type ImportOutcome int

ImportOutcome classifies how a Session resolved one import statement.

const (
	ImportBound    ImportOutcome = iota // already bound or already loaded; skipped
	ImportNative                        // a host-native module value
	ImportDecls                         // host-supplied embedded declarations
	ImportResolver                      // resolved by the host module resolver
	ImportFile                          // a .buzz file on the search path
	ImportNotFound                      // nothing resolved the import (an error)
)

func (ImportOutcome) String

func (o ImportOutcome) String() string

String names the import outcome for logs and metric labels (plain ASCII).

type MarshalOption

type MarshalOption = vmpackage.MarshalOption

MarshalOption configures what Chunk.Marshal emits.

type Module

type Module struct {
	// Name is the bare string a program imports, e.g. "os" in `import "os"`.
	Name string
	// Labels classify the module for filtering. See the Label* constants for the
	// vocabulary gopherbuzz applies to its own stdlib; a host adds its own
	// (e.g. "host", "wasm").
	Labels []string
	// Bind wires the module onto sess. It may install a fresh module (via
	// Session.SetNativeModule / SetModuleDecls) or read back and extend one an
	// earlier Module already provided under Name (host methods over the stdlib).
	Bind func(sess *Session, env ModuleEnv) error
}

Module describes one importable Buzz module for registration on a session: the bare name a program imports, free-form Labels that classify it (provenance, WASM-safety, ...), and a Bind hook that wires it onto a session -- installing a fresh module, or merging onto one an earlier Module provided under the same name.

It is the single shape gopherbuzz's stdlib and a host embedder (e.g. magus's os/vcs/http surface) both use to describe a module, so a session's whole import surface is one ordered, labeled list: Session.Provide applies it, and a caller filters by label to derive a subset (the WASM playground, a strict-conformance run, a docs index).

This is the *registration* descriptor -- how to install a module -- and is distinct from a host's richer *API* descriptor (magus/std.Module carries a module's methods and fields for documentation and binding codegen). A given module may have both: one says how to install it, the other what it exposes.

func (Module) HasLabel

func (m Module) HasLabel(label string) bool

HasLabel reports whether m carries label.

type ModuleEnv

type ModuleEnv struct {
	Ctx context.Context
	Out io.Writer
}

ModuleEnv carries what a Bind hook may need beyond the session itself: the context a host module captures, and the writer std's `print` should target. A Module ignores the fields it does not use.

type Option

type Option func(*Session)

Option configures a Session at construction. See NewSession.

func WithEmbedded

func WithEmbedded() Option

WithEmbedded relaxes the upstream-Buzz script-conformance rules (top-level statements and labeled args) for this session. Embedding hosts (REPL, magus eval, magusfile loading) must set it; without it a session parses strictly, matching upstream.

func WithREPL

func WithREPL() Option

WithREPL marks this session as an interactive REPL, suppressing the BZZ3001 unused-import warning (see Session.repl). A REPL host should pass this alongside WithEmbedded.

func WithSearchPaths

func WithSearchPaths(paths ...string) Option

WithSearchPaths replaces the session's import search path templates (see DefaultSearchPaths for the syntax). Passing no paths is a no-op, leaving the session on DefaultSearchPaths. A host that wants to confine imports to its own layout passes its own templates here (e.g. magus restricts resolution to `magusfiles/?.buzz` under the project and workspace roots).

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

Pool is a per-source bounded pool of pre-warmed Buzz sessions. Safe for concurrent use.

Concurrency model: each Submit spawns a goroutine that acquires one semaphore slot (bounding real parallelism), checks out a warmed session from the idle list, runs the target, and returns the session. Because there is no fixed set of worker goroutines, a target that dispatches children via Dispatch and blocks until they finish never starves the children of a goroutine to run on — nested dispatch cannot deadlock on GOROUTINE OR SEMAPHORE availability, regardless of fan-out. Parallelism is bounded by the semaphore, which Dispatch yields (via getSem.Yield) so a child can acquire the slot its parent holds, even at MAGUS_CONCURRENCY=1.

That invariant does not, by itself, rule out every deadlock: two in-flight SIBLINGS that mutually depend on each other (B needs C, C needs B) each hold a goroutine and a slot just fine, but would block forever on each other's TargetMemo entry — neither name appears in the other's static ancestor stack, so the ancestor-chain cycle check never fires. TargetMemo.TryRun detects this dynamically (its waitingFor wait-for graph) and errors instead of hanging; see TargetMemo's doc comment.

func (*Pool) Close

func (p *Pool) Close() error

Close shuts down all idle sessions after in-flight jobs finish and release theirs.

func (*Pool) Dispatch

func (p *Pool) Dispatch(ctx context.Context, names []string, ancestors []string) error

Dispatch fans out names concurrently, yielding the caller's buzz slot if held so that children can acquire it (deadlock-free at MAGUS_CONCURRENCY=1). TargetMemo deduplication is applied when a memo is present in ctx: a target already in-flight is subscribed to (not re-submitted); the waitFn is called without holding the slot, so it cannot deadlock.

func (*Pool) Submit

func (p *Pool) Submit(ctx context.Context, name string, ancestors []string) <-chan error

Submit dispatches name and returns a channel delivering one error. Returns a closed-error channel if the pool is closed or a cycle is detected.

type PoolObserver

type PoolObserver interface {
	// SessionAcquire fires when the pool checks out a session to run a target.
	// reused is true when an idle warm session was taken; false when none was idle
	// and the pool must warm a fresh one (a cold start, reported next by
	// SessionWarm). idle is the idle-session count remaining right after checkout.
	SessionAcquire(ctx context.Context, reused bool, idle int)
	// SessionWarm fires when the pool warms a fresh session, reporting how long
	// construction took and its error (nil on success). Always preceded by a
	// SessionAcquire with reused=false.
	SessionWarm(ctx context.Context, elapsed time.Duration, err error)
	// SessionRelease fires when a finished session returns to the pool. evicted is
	// true when the pool was full or closed and the session was closed instead of
	// retained; idle is the idle-session count right after the release.
	SessionRelease(ctx context.Context, evicted bool, idle int)
}

PoolObserver is notified of a Session pool's lifecycle as it serves target runs: session checkout (reuse vs cold warm), warm cost, and release or eviction.

It is optional: attach one with WithPoolObserver. With none set the pool runs unchanged, so this adds no cost and no behaviour change to callers that do not opt in.

type PoolRegistry

type PoolRegistry struct {
	// contains filtered or unexported fields
}

PoolRegistry maps string keys to per-source Pools. Safe for concurrent use.

func NewPoolRegistry

func NewPoolRegistry(getSem func(ctx context.Context) Semaphore, capacity int) *PoolRegistry

NewPoolRegistry returns an empty registry. getSem is called per-execute to derive the semaphore from ctx (pass nil for no concurrency budget). capacity<=0 defaults to NumCPU.

func PoolRegistryFromContext

func PoolRegistryFromContext(ctx context.Context) *PoolRegistry

PoolRegistryFromContext retrieves the PoolRegistry stored by WithPoolRegistry, or nil.

func (*PoolRegistry) Close

func (r *PoolRegistry) Close() error

Close closes every Pool in the registry.

func (*PoolRegistry) Get

func (r *PoolRegistry) Get(key string, newSession WorkerFunc) *Pool

Get returns the Pool for key, creating it with newSession on first call. newSession is ignored on cache hits.

type Semaphore

type Semaphore interface {
	Acquire(ctx context.Context) error
	Release()
	// Yield releases one slot for the duration of fn and re-acquires it before
	// returning. The caller must hold a slot; use only when buzzSlotHeld is true.
	Yield(ctx context.Context, fn func() error) error
}

Semaphore is the concurrency budget the pool draws from. *cache.Limiter satisfies this interface (Acquire/Release/Yield are defined on it).

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session is a single Buzz execution context. Not safe for concurrent use; ensure one goroutine owns it at a time.

func NewSession

func NewSession(ctx context.Context, opts ...Option) *Session

NewSession creates a Buzz execution context. Inject globals with SetGlobal and register target callbacks via Targets. Close releases the context.

Imports resolve against DefaultSearchPaths unless WithSearchPaths overrides it. BUZZ_INCLUDE_PATH (colon-separated on Unix, semicolon-separated on Windows) is read to populate the additional include directory list, searched after the templates; the host may override it with SetIncludeDirs.

func (*Session) CallDepth

func (s *Session) CallDepth() int

CallDepth reports the number of active frames in the current VM. Used by step-over to detect frame-boundary crossings.

func (*Session) CallValue

func (s *Session) CallValue(ctx context.Context, fn vmpackage.Value, args []vmpackage.Value) (vmpackage.Value, error)

CallValue invokes a Buzz function (or direct callable) Value with the given arguments. Host code (e.g. magus target dispatch) uses this to call back into Buzz.

func (*Session) ClearStepHook

func (s *Session) ClearStepHook()

ClearStepHook removes any installed step hook from the session and the current VM.

func (*Session) Close

func (s *Session) Close() error

Close releases the session's resources.

func (*Session) Compile

func (s *Session) Compile(code string) (*vmpackage.Chunk, error)

Compile parses, type-checks, and returns a runnable Chunk bound to this session's shared-globals scope. Pass the result to ExecChunk to run it, optionally multiple times without re-parsing.

func (*Session) DeclareModuleTypes

func (s *Session) DeclareModuleTypes(boundName, src string)

DeclareModuleTypes parses src and registers its exported object/enum types under boundName's namespace immediately, without waiting for a matching `import` statement to trigger it (contrast SetModuleDecls, whose src is only collected lazily, when resolveImport processes a real `import "<importPath>";`).

Every native module (crypto, io, os, vcs, ...) can use the lazy path, because nothing binds its name into the session env before the import runs. It does NOT work for a module whose native value a host binds some OTHER way before any import is processed - e.g. a namespace meant to be callable without an explicit import, via SetGlobal. resolveImport's "already bound" check fires before it ever consults moduleDecls, so a SetModuleDecls registered under that same name would never be collected. Call DeclareModuleTypes directly instead, once, when setting up such a namespace, to get the same "import this path, get its types" outcome SetModuleDecls gives every other module.

func (*Session) Diagnostics

func (s *Session) Diagnostics(code string) []Diagnostic

Diagnostics parses and type-checks code against the session's shared scope and returns every diagnostic the editor should surface: a single parse error (checking cannot proceed past it), or otherwise every type error the checker found. Unlike Exec and Compile it does not stop at the first error, so it can drive live squiggles per keystroke.

It is NOT side-effect-free. Resolving the program's imports executes each imported module's top-level code and reads its file from disk, so the checker can see the globals and types they define (there is no check-only import pass). It also mutates session state (loadedPaths, env, importedTypes). Call it on a fresh or throwaway session - the embedded playground path (dry.Diagnostics) makes a new one per call - never on a live session you still intend to Exec, or a later real import will be skipped as already-loaded.

func (*Session) DoString

func (s *Session) DoString(code string) error

DoString executes code using the session's own context. Embedders needing per-call cancellation use Exec directly. Required by the cross-engine engine.Session interface (every backend implements DoString), so it stays even though it is a thin wrapper over Exec.

func (*Session) Eval

func (s *Session) Eval(ctx context.Context, code string) (vmpackage.Value, error)

Eval compiles and runs code against the session's shared scope and returns the program's result value (the value of a trailing `return <expr>`, else Null). The REPL uses it to print bare expressions.

func (*Session) EvalChunk

func (s *Session) EvalChunk(ctx context.Context, chunk *vmpackage.Chunk) (vmpackage.Value, error)

EvalChunk runs a previously compiled Chunk and returns its result value. The REPL driver compiles first (to tell a syntax error — fall back to the statement form — from a runtime error) then runs exactly once via this, so a snippet with side effects never executes twice.

func (*Session) Exec

func (s *Session) Exec(ctx context.Context, code string) error

Exec parses, type-checks, compiles, and executes Buzz source code in the session's environment. Type errors are returned as hard errors (Buzz is statically typed).

func (*Session) ExecBytecode

func (s *Session) ExecBytecode(ctx context.Context, data []byte) error

ExecBytecode deserializes a Chunk from data and executes it in this session.

func (*Session) ExecChunk

func (s *Session) ExecChunk(ctx context.Context, chunk *vmpackage.Chunk) error

ExecChunk runs a previously compiled Chunk in the session's environment.

func (*Session) Exports

func (s *Session) Exports() map[string]vmpackage.Value

Exports returns the subset of Globals() whose names were declared with export in a file executed via Exec or ExecChunk. The map is a fresh snapshot; mutations don't affect the session.

func (*Session) Frames

func (s *Session) Frames() []vmpackage.DebugFrame

Frames returns the active call stack of the currently-executing VM, innermost first. Empty when no run is in progress.

func (*Session) GetGlobal

func (s *Session) GetGlobal(name string) vmpackage.Value

GetGlobal returns the value bound to name, or Null if unbound. The signature matches the cross-engine engine.Session interface (which returns a bare Value); absence and an explicit null binding both yield Null.

func (*Session) Globals

func (s *Session) Globals() map[string]vmpackage.Value

Globals returns a snapshot of the session's top-level bindings (name → value), including host-injected globals. The REPL filters host names for .globals.

func (*Session) IncludeDirs

func (s *Session) IncludeDirs() []string

IncludeDirs returns the current include directory list.

func (*Session) Locals

func (s *Session) Locals(level int) map[string]vmpackage.Value

Locals returns the named locals of the frame at level (0 = innermost) in the current VM, read from its stack register window. Empty when the level is out of range or no debug-name info was compiled.

func (*Session) NativeModule

func (s *Session) NativeModule(importPath string) (vmpackage.Value, bool)

NativeModule returns the value registered for importPath via SetNativeModule, or ok=false if none is registered. It lets a host that layers its own methods onto a stdlib module under a shared name (e.g. magus merging host methods onto Buzz's bare "os"/"fs"/"crypto") read the registered module back so it can be extended in place rather than replaced.

func (*Session) NewChild

func (s *Session) NewChild() *Session

NewChild creates an isolated session that inherits this session's import resolution (search paths, include dirs, native modules, module resolver) but starts with a fresh top-level scope and its own loaded-path set. io.runFile uses it so a run file cannot see or mutate the caller's globals — parity with upstream buzz, whose runFile executes the file in its own scope, not the caller's.

func (*Session) Provide

func (s *Session) Provide(env ModuleEnv, mods ...Module) error

Provide binds each module onto the session in order. Order is significant: a later module may merge onto an earlier one that shares its Name (a host layering methods onto the stdlib), so lower-precedence modules come first. Provide stops at the first Bind error and returns it.

func (*Session) SetCompileObserver

func (s *Session) SetCompileObserver(obs CompileObserver)

SetCompileObserver attaches obs, notified as this session compiles source (parse/check/compile phase timings) and resolves imports. Pass nil to detach. With none set the session compiles unchanged, adding no cost.

func (*Session) SetFaultHook

func (s *Session) SetFaultHook(cb func(vmpackage.FaultKind))

SetFaultHook installs cb to fire when a VM executing this session's code faults (see vm.FaultKind: a recovered internal panic, or a host callable error raised as a throw). It is applied to each VM the session runs, gated exactly like the debugger step hook, so an unset hook costs nothing. Pass nil to detach.

func (*Session) SetGlobal

func (s *Session) SetGlobal(name string, v vmpackage.Value)

SetGlobal binds name to v in the session's global Env.

func (*Session) SetIncludeDirs

func (s *Session) SetIncludeDirs(dirs []string)

SetIncludeDirs replaces the directories searched for file-based imports. The host (e.g. internal/interp) calls this to enforce workspace sandboxing before running any user code.

func (*Session) SetModuleDecls

func (s *Session) SetModuleDecls(importPath, src string)

SetModuleDecls registers src as the embedded Buzz source imported by `import "<importPath>"`. It resolves before the includeDirs file search and flat-merges (its exported object/enum types become visible to the importer's checker, which a native value module cannot provide). Use it for shipped Buzz library modules that have no file on the include path.

func (*Session) SetModuleResolver

func (s *Session) SetModuleResolver(fn func(importPath string) (vmpackage.Value, bool))

SetModuleResolver installs fn as the on-demand resolver for path-style imports (see the moduleResolver field). fn is called with the import path and binds its returned value under the path's basename (or alias) when it reports ok; a false return leaves the import for the includeDirs file search.

func (*Session) SetNativeModule

func (s *Session) SetNativeModule(importPath string, v vmpackage.Value)

SetNativeModule registers v as the module imported by `import "<importPath>"`. The import binds v under the path's basename (e.g. "util" for "magus/extra"), or under an explicit alias. Host-provided modules resolve before any file search, so they need no .buzz file on disk.

func (*Session) SetPromoteTopLevel

func (s *Session) SetPromoteTopLevel(on bool)

SetPromoteTopLevel enables top-level slot promotion for every chunk this session compiles (see Session.promoteTopLevel and CompileOptions.PromoteTopLevel). The magusfile execution path turns it on for faster top-level code; the REPL must leave it off so a later prompt line can resolve earlier top-level names.

func (*Session) SetStepHook

func (s *Session) SetStepHook(mask vmpackage.StepMask, cb func(vmpackage.StepEvent, vmpackage.DebugFrame))

SetStepHook installs cb to fire on the current VM for events matching mask. cb runs synchronously on the execution goroutine and may re-enter the pry REPL. It applies to the VM currently executing; if none is active the hook is stored and applied to the next run that starts.

func (*Session) Targets

func (s *Session) Targets() map[string]vmpackage.Callable

Targets returns the session's dispatchable target map. The embedder owns its contents: magus registers exported magusfile targets and spell ops into it.

func (*Session) Tests

func (s *Session) Tests() []TestEntry

Tests returns the test blocks registered while executing this session's code, in source order. A normal run never executes their bodies; a test runner calls each Fn (e.g. via CallValue) and treats a returned error as a failure.

func (*Session) Upvalues

func (s *Session) Upvalues(level int) map[string]vmpackage.Value

Upvalues returns the captured upvalues of the frame at level (0 = innermost), keyed by name. Empty when the frame is not a closure or no names were compiled.

func (*Session) Warnings

func (s *Session) Warnings() []Diagnostic

Warnings returns the non-fatal diagnostics (currently just BZZ3001 unused imports) found by the most recent Exec or Compile call on this session - see lastWarnings for why it is last-compile rather than accumulated. Nil before any compile.

Unlike Diagnostics, this is a plain read of state compileShared already computed on the run path: it does not re-resolve imports or re-execute anything, so it is safe to call right after Exec/Compile on a live session you intend to keep using.

type Severity

type Severity int

Severity classifies a BZZ diagnostic. The zero value is SeverityError, so every diagnostic built before Severity existed - and every one the checker still builds without setting it explicitly - keeps its current meaning: it fails Exec/Compile exactly as before this type was introduced. Only a diagnostic that opts in (UnusedImport, StringAccumulation) is a warning, which Exec/Compile must never fail on.

const (
	SeverityError Severity = iota
	SeverityWarning
)

func (Severity) String

func (sv Severity) String() string

String renders the severity the way a diagnostic message prefixes it ("warning: "); SeverityError renders as "" since an error carries no prefix today (see typeError.Error).

type TargetMemo

type TargetMemo struct {
	// contains filtered or unexported fields
}

TargetMemo is a per-invocation run-once tracker. It ensures a target executes at most once within one top-level dispatch, even when concurrent `depends_on` callers name the same target (diamond dependencies). Safe for concurrent use.

It also detects the cross-dependency cycle the static ancestor-chain check in Submit/dispatchInner cannot see: two in-flight SIBLINGS that depend on each other (B needs C, C needs B) never appear in each other's ancestor stack, so without this they would each subscribe to the other's still-running entry and deadlock forever. waitingFor records the dynamic wait-for graph (which caller is blocked on which name) so a caller about to block can detect the loop closing back to itself first. See TryRun.

func NewTargetMemo

func NewTargetMemo() *TargetMemo

NewTargetMemo returns a fresh, empty TargetMemo for one invocation scope.

func TargetMemoFromContext

func TargetMemoFromContext(ctx context.Context) *TargetMemo

TargetMemoFromContext retrieves the TargetMemo stored by WithTargetMemo, or nil.

func (*TargetMemo) Complete

func (m *TargetMemo) Complete(name string, err error)

Complete records err for name and unblocks any waiters. Must be called exactly once by the goroutine that received isNew=true from TryRun.

func (*TargetMemo) TryRun

func (m *TargetMemo) TryRun(caller, name string) (isNew bool, waitFn func(ctx context.Context) error)

TryRun checks whether name has already run or is running. caller is the name of the target on whose behalf this call is being made (the last entry of its ancestor stack), or "" for a top-level dispatch with no enclosing target; it is used only to record/detect the wait-for cycle below.

Returns (true, nil) when name is new — caller must run the target then call Complete. Returns (false, waitFn) when name is already in-flight or done — caller invokes waitFn(ctx) to get the result. waitFn blocks until the in-flight execution finishes, ctx is cancelled, or a cross-dependency cycle through name is detected; call it WITHOUT holding a limiter slot.

type TargetObserver

type TargetObserver interface {
	// TargetEnd reports a finished target: its name, how long its function ran, and the
	// error it returned (nil on success). Called after the target function returns.
	TargetEnd(ctx context.Context, name string, elapsed time.Duration, err error)
}

TargetObserver is notified as the pool runs targets. The pool calls it once per target per run (the target memo collapses repeat dependents into a single run), so an observer sees each target exactly once with its wall-clock duration and outcome.

It is optional: attach one with WithObserver. With none set the pool runs unchanged, so this adds no cost and no behaviour change to callers that do not opt in.

type TestEntry

type TestEntry struct {
	Name string
	Fn   vmpackage.Value
}

TestEntry is one registered `test "Name" { … }` block: its name and the zero-argument closure that runs its body.

type WorkerFunc

type WorkerFunc func(ctx context.Context) (*WorkerSession, error)

WorkerFunc creates a pre-warmed Buzz session and target map for the pool. The session is owned by the pool worker and must not be used concurrently.

type WorkerSession

type WorkerSession struct {
	Session *Session
	Targets map[string]vmpackage.Callable
}

WorkerSession is the pre-warmed pair a WorkerFunc returns: a freshly-executed Buzz session plus the target map derived from its exports. The two are produced together and always returned together, so bundling them in one struct keeps callers from having to handle the "half-loaded" nil-nil-error shape the old three-return signature carried.

Directories

Path Synopsis
Package ast defines the Buzz abstract syntax tree node types.
Package ast defines the Buzz abstract syntax tree node types.
Package buzzgen mirrors Go types into Buzz.
Package buzzgen mirrors Go types into Buzz.
cmd
buzz command
Command buzz is a standalone runner for the Buzz language, mirroring the upstream `buzz` CLI (https://buzz-lang.dev).
Command buzz is a standalone runner for the Buzz language, mirroring the upstream `buzz` CLI (https://buzz-lang.dev).
examples
ffi-c command
Command ffi-c is a runnable demonstration of gopherbuzz's C FFI.
Command ffi-c is a runnable demonstration of gopherbuzz's C FFI.
internal
Package std provides Buzz's standard library modules as native modules for the magus/buzz interpreter.
Package std provides Buzz's standard library modules as native modules for the magus/buzz interpreter.
Package token defines the lexical token types and scanner for Buzz source.
Package token defines the lexical token types and scanner for Buzz source.
Package types defines the Buzz static type system used by the type checker.
Package types defines the Buzz static type system used by the type checker.
Command buzz-wasm is a minimal WebAssembly entry point for the interpreter.
Command buzz-wasm is a minimal WebAssembly entry point for the interpreter.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL