Engines
A magusfile is written in Buzz and runs on the
embedded Buzz VM through a small internal seam. A magusfile.buzz exposes the
magus.* API and composes spells, targets, and
charms. This page covers the seam and how a new language
would plug in.
The same gopherbuzz pipeline runs every Buzz program magus touches: a magusfile,
a spell, a magus buzz script and the browser playground. What differs per host
is the set of modules registered before the program runs. The magusfile engine
gets every host module; the playground's wasm build installs only the modules
marked WASM. How gopherbuzz runs Buzz follows one program from source
through the compiler to the VM and the JIT.
The engine interface
The backend implements one small interface, engine.Engine, which is a
factory for engine.Session:
// internal/interp/engine/engine.go
type Engine interface {
ID() string
NewSession(ctx context.Context) (Session, error)
}
type Session interface {
Close() error
SetGlobal(name string, v Value)
GetGlobal(name string) Value
NewTable() Table
LoadString(code string) (Value, error)
DoString(code string) error
Call(p CallParams, args ...Value) error
}
Value and Table are engine-neutral handles, so host code reads and writes
script values without knowing the concrete VM. The backend registers itself at
init() time:
// internal/interp/engine/buzz/buzz.go
func init() { engine.Register("buzz", engineImpl{}) }
and is found by name through engine.Lookup(name). The registered backend in a
stock magus binary is buzz.
The backend is pulled into the binary by blank import in
cmd/magus/packs_interp.go:
_ ".../internal/interp/engine/buzz"
The spell contract
A spell exports a fixed set of mgs_-prefixed functions (see
spells). The list of optional functions
and the decoder keys they map to is single-sourced in
internal/spell/contract.go as
OptionalContract, and the Buzz resolver (internal/spell/resolve.go) iterates
that one list. A spell's mgs_ functions decode to a Spec for every scalar and
list contribution (needs, provides, version_cmd, opaque) and
for record-shaped ops ({bin, args, charms}).
An op is a command or a service; a function-valued op is fun(Target) > Command or
fun(Target) > Service, called once at load to record the declarative value it
returns (a Command's {bin, args, charms} or a Service's {command, readiness?, stop?},
a long-running process magus run blocks on). The op's kind is inferred from that
value, so one spell mixes both. A remote cache backend
is not an op: it is a spell that exports the backend functions
(enabled/get_artifact/put_artifact/has_artifact/prune), detected by name and wired with
magus\cache.remote (see Remote caching).
Doc-comment capture. Buzz captures a handler's doc comment at compile time
(the parser binds the comment to the function node; FunDoc reads it back), and
magus doctor enforces one on each function-handler target. Note Buzz's
Chunk.Doc is in-memory only and not serialized to bytecode, so Buzz captures
docs only for freshly-compiled workspace .buzz spells, never the embedded
built-ins.
Host modules are a superset of Buzz's stdlib
magus layers its host methods onto Buzz's own stdlib modules under the same bare
names: import "os" carries both Buzz's os.* (sleep, env, execute) and magus's
additions (proc\exec, proc\which, ...); import "fs" carries Buzz's fs plus
fs\glob/readFile; and magus adds whole modules Buzz lacks (vcs, archive,
http, charm, ...). One import per domain covers the union, with no separate
extra namespace to remember which side a call lives on.
Where a method overlaps a Buzz stdlib call, the magus form is sandbox-aware
while the bare stdlib is not. For example, env\get/lookup honor the env
allowlist, whereas Buzz's os\env is raw. Those overlaps are noted per-method in the
module reference (either works); the cross-reference lives in
std/buzz_stdlib.go.
A few entries are not treated as duplicates because the magus behavior the
stdlib can't reproduce: magus's os\exit raises a lifecycle error (Buzz's
hard-exits the process), magus's os\sleep is cancellable (Buzz's blocks), and
magus's crypto.*_file hashes a file (Buzz's hash only takes a string). These
stay on the magus surface.
A workspace spell lives at spells/<name>/spell.buzz (or flat
spells/<name>.buzz).
"Built-in spell" vs language "builtins"
A built-in spell is a spell whose bytecode is compiled from
spells/<name>/spell.buzz and embedded in the magus binary (go,
typescript, docker, ...; see spells). This is a magus
concept and is unrelated to Buzz's language builtins (spawn, list/map
methods, etc.), which are part of the Buzz language itself. The docs always write
"built-in spell" when they mean the former.
Adding a new language
The engine interface is the stable, clean part of the seam. Plugging in a second language today, however, also touches a handful of hard-coded dispatch spots above the interface. This is the current state, not the end state:
- Implement
engine.Engine/Sessionfor the VM andengine.Registerit (the clean part). - Map the file extension to the engine in
engineForExt(internal/interp/source.go) and add the glob toscriptExts/ themagusfile.<ext>lists. - Branch the runtime where it special-cases an engine by name
(
src.Engine == "buzz"ininternal/interp/runtime.go). - Provide the per-engine host bindings (the
magus.*surface), asinternal/interp/bindings/buzz.godoes today.
Future direction: registry-driven discovery. The intent is to derive
extensions, magusfile filenames, and dispatch from the engine registry itself, so
adding a language means registering a backend (with its extensions and binding
installer) and nothing else, with no edits to source.go, runtime.go, or
switch statements. That refactor is deliberately out of scope for now. The
hard-coded spots above are the seam's known leaks, documented so they
are visible rather than surprising.
See also
- spells: the
mgs_spell contract and how spells compose. - targets: the runnable unit and its CLI grammar.
- modules/index: the
magus.*host module reference.