# zygomys v9 uses an iter-based parser, so zygo no longer needs a background goroutine

> zygomys is a Lisp interpreter written in Go whose main job is letting a script build trees of Go structs through reflection and then call compiled Go methods on them. Version 9 replaced the background parsing goroutine with the iter package, and the standalone REPL is called zygo.

**glycerine/zygomys** — Zygo is a Lisp interpreter written in 100% Go. Central use case: dynamically compose Go struct trees in a zygo script, then invoke compiled Go functions on those trees. Makes Go reflection easy.

- Repository: https://github.com/glycerine/zygomys
- Website: https://github.com/glycerine/zygomys/wiki
- Stars: 1,790 · Forks: 85
- Language: Go
- License: BSD-2-Clause
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/glycerine-zygomys

## Building it is a clone and a make, and the make bakes in the git version

Installation is documented in two lines:

```bash
$ git clone https://github.com/glycerine/zygomys
$ cd zygomys && make
```

The default Makefile goal is all, which runs build and then test. The build target does something worth understanding rather than copying blindly. It writes a Go source file, zygo/gitcommit.go, containing an init function that records the output of git describe --abbrev=0 --tags and git rev-parse HEAD into GITLASTTAG and GITLASTCOMMIT:

```bash
build:
	/bin/echo "package zygo" > zygo/gitcommit.go
	cd cmd/zygo; go install .
```

So the build depends on a real git checkout with tags. A downloaded source archive without history produces a file with empty version strings, and the interpreter loses the ability to report which build it is. That is fine for a local clone and a real constraint for distribution.

Test is a shell script plus the Go test suite:

```bash
test:
	tests/testall.sh && echo "running 'go test'" && cd zygo && go test -v
```

The .zy files under tests/ are the executable documentation. The README points at them by name, including tests/package.zy for the package mechanism and tests/declare.zy for struct definition and type checking.

## The reflection path is the reason to use this instead of encoding/json

The stated central use case is to compose Go struct trees in a zygo script and then invoke compiled Go functions on those trees. The script builds the objects; the methods that run on them are ordinary Go methods running at compiled speed.

That is a different offer from a configuration format. JSON and Msgpack support serialization and deserialization, while zygomys can build typed trees of Go structs from scripts. With zygomys, a value declared as a Go type comes back as that Go type, so a method can assume its fields are the right Go types rather than anything that arrived over the wire.

The feature list names the mechanisms that make this work. There is struct definition and type checking, with tests/declare.zy as the example. There is readable nested method calls, where (a.b.c.Fly) calls method Fly on object c that lives within objects a and b, which is what lets a script reach into a tree and act on the leaves. There are record definitions, (defmap), described as making configuration a breeze. And there is a raw bytes type, (raw string), for zero-copy []byte manipulation.

JSON and Msgpack interop is present, so the same program can read a plain JSON document or a script. The choice is whether you want untyped data or typed objects.

## Parenthesis is always a call, and curly braces switch the parser

One syntax rule runs through everything: parenthesis always mean a function call or a native lisp form, and function calls always use outer parentheses. There is no infix call syntax outside the optional infix mode.

That infix mode is a subset of Go, entered by putting an expression inside curly braces. The REPL assumes infix by default, which the README says is there to facilitate math:

```
zygo> x := 3; y := 5; if x + y == 8 { (println "we add up") } else { (println "wat?" ) }
we add up
```

Note what that line contains: Go-style short variable declarations with :=, arithmetic with operators rather than Lisp function names, and if/else with curly braces, while the actual call stays parenthesized as (println "we add up"). Mixing the two in one expression is the intended usage.

Since v8.0.0 the curly brace syntax also declares anonymous hash maps, which the README frames as a way to ease parsing of JSON. So the same delimiter that switches to infix also builds a map, and that dual use is worth knowing before you read someone else's script and misread a brace.

Quoting and strings follow Go rather than Clojure in a few places. Backticks give raw multiline strings as in Go, and Lisp-expression quoting uses % because the apostrophe delimits runes as in Go. Syntax quoting uses a caret, ^(), instead of a backtick.

## Loops, macros and packages are the parts you will borrow from Go habits

The language borrows from Clojure and from the Go and C tradition, and the for-loop is explicitly one of the Go-shaped parts.

```
(for [(initializer) (test) (advance)] (body))
```

Break and continue are available inside, and both can be labelled so you can exit nested loops from the inside, which is the case that s-expression languages usually leave awkward.

The range loop is the hash and array equivalent, written (range key value hash_or_array (body)), acting like a Go for-range over hashes or arrays.

Macros exist in the Lisp sense, and the macro system has a specific affordance: (macexpand (yourMacro)) makes writing and debugging macros easier by expanding a form on demand. Syntax quoting with ^() supports that workflow.

The package mechanism is listed as supporting modularity and isolation of scripts, packages and libraries from each other, with tests/package.zy holding examples. That word isolation matters: the point of the mechanism is that two scripts can define the same name without colliding, which is the failure mode a naive embedded interpreter tends to have once a configuration grows past a page.

The extension story is concrete. The README names repl/random.go, repl/regexp.go and repl/time.go as examples of how to add functionality, and it points at Clojure-style threading, (-> hash field1: field2:) and (:field hash) selection, for people arriving from Clojure.

## Version 9 removed the goroutine, and the CHANGELOG-shaped clues are in the feature list

The README headline for 9.0.0 says version 9 uses the iter package to avoid needing a background parsing goroutine. That is an architectural change, not a feature, and it is the kind that shows up as reduced complexity rather than new capability. If you were reading about zygomys years ago and saw goroutine and channel support mentioned, the feature list also marks channel and goroutine support as deprecated as of v6.0.7 because it is data race-y. The parser is no longer the place where a race could hide.

Other items in the 9 list are smaller but real: NaN handling that matches typical expectations and Go's answers, which matters if script output feeds numeric code; the package mechanism; and struct definition with type checking.

The module path tells you the versioning story as clearly as anything: go.mod declares module github.com/glycerine/zygomys/v9 and go 1.24.2. Go's major-version suffix means v9 is a distinct import path, so upgrading from v8 is not a drop-in change at the import line. The repository has no GitHub releases, so there is no tag list to consult beyond what the repository itself contains.

Dependencies include github.com/glycerine/greenpack, github.com/glycerine/liner for command-line editing with tab-complete, github.com/glycerine/blake2b, github.com/tinylib/msgp and github.com/ugorji/go/codec for serialization, and github.com/glycerine/goconvey for tests. Several are pseudo-versions pinned to commits, which is a maintenance signal to weigh before depending on the module.

## Where zygomys is the wrong tool

Four cases.

A large scripting ecosystem is the first. There is no package manager for the language, no published library index, and no version compatibility story for script authors. If your users are expected to write nontrivial scripts, a language with a registry and a migration path is a safer bet.

Documentation reachability is the second. The full documentation lives in the GitHub wiki, not in the repository, which means it can be edited separately from the code and is not versioned with the releases. There is no release history to check it against. Anyone evaluating this should read the wiki Language page, which the README links for the hash map syntax, before committing.

Distribution without git is the third, and it comes straight from the Makefile. Because zygo/gitcommit.go is generated from git describe and git rev-parse, building from a tarball loses the version metadata. Pin a commit rather than a branch if you are vendoring.

Sandboxing is the fourth, and it is a caveat rather than a blocker. The README advertises a sandbox-able environment via zygo -sandbox and the NewGlispSandbox() function, which is what you want before running untrusted scripts. Note also that the language is Lisp-shaped, so a script is code with full access to whatever the host environment exposes, and the sandbox is a flag you have to pass rather than a mode you get by default.

## Conclusion

zygomys fits a Go project that wants users to configure behaviour with a small DSL, especially one whose configuration is naturally nested structs with methods on them, since the reflection path keeps the objects native Go and the calls compiled. It does not fit a team that needs a mature, well-documented language with a large package ecosystem, and the documentation lives in the GitHub wiki rather than in the repository, so evaluate it against that page rather than the README. Before embedding it, read the module path github.com/glycerine/zygomys/v9 and note that the repository has no GitHub releases, so the only version marker is the v9 in the path and the v9 feature list in the README.

## FAQ

### How do I install zygomys and get the zygo REPL?

Clone the repository and run make, which builds cmd/zygo and installs it, then runs the tests. The build writes zygo/gitcommit.go from git describe and git rev-parse HEAD, so it needs a real git checkout rather than a source archive.

### What is zygo sandbox mode?

The README lists a sandbox-able environment as a feature, reachable by running zygo -sandbox, with a corresponding NewGlispSandbox() function. It is opt-in via the flag rather than the default.

### What is new in zygomys version 9?

Version 9 uses the iter package so the parser no longer needs a background goroutine. The 9.0.0 list also covers the package mechanism for isolating scripts, NaN handling that matches Go, and struct definition with type checking.

### How does zygomys call methods on nested Go structs?

You configure trees of Go structs in a script and then call Go methods on them at compiled speed. Nested calls are written readably, so (a.b.c.Fly) calls method Fly on the object c inside objects a and b.

### What Go version does zygomys require?

go.mod declares go 1.24.2 and the module path github.com/glycerine/zygomys/v9, so the major version is part of the import path. The repository has no GitHub releases.

### Does zygomys support JSON and Msgpack?

Yes. The README lists JSON and Msgpack interop for serialization and deserialization, and since v8.0.0 anonymous hash maps can be declared with curly brace syntax to ease parsing of JSON.

## Sources

- [glycerine/zygomys on GitHub](https://github.com/glycerine/zygomys)
- [Issues](https://github.com/glycerine/zygomys/issues)
- [License: BSD-2-Clause](https://github.com/glycerine/zygomys/blob/master/LICENSE)
- [Project website](https://github.com/glycerine/zygomys/wiki)
- [README](https://github.com/glycerine/zygomys/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/glycerine-zygomys
