On this page:
1.1 Building and Running Zuo
1.2 Library Modules and Startup Performance
1.3 Embedding Zuo in Another Application
1.4 Zuo Datatypes
1.5 Zuo Implementation and Macros
1.6 Zuo Module Protocol
1.7 Path Handling
8.12

1 Zuo Overview🔗ℹ

Zuo is a Racket variant in the sense that program files start with #lang, and the module path after #lang determines the parsing and expansion of the file content. Zuo, however, has a completely separate implementation. So, even though its programs start with #lang, Zuo programs are not meant to be run via Racket.

While #lang zuo/base accesses a base language, the primary intended use of Zuo is with #lang zuo, which includes the zuo/build library for using Zuo as a make replacement.

The name “Zuo” is derived from the Chinese word for “make.”

1.1 Building and Running Zuo🔗ℹ

Compile "zuo.c" from the Zuo sources with a C compiler. No additional files are needed for compilation, other than system and C-library headers. No compiler flags should be needed, although flags like -o zuo or -O2 are a good idea.

You can also use configure, make, and make install, where make targets mostly invoke a Zuo script after compiling "zuo.c". If you don’t use configure but compile to zuo in the current directory, then ./zuo build.zuo and ./zuo build.zuo install (omit the ./ on Windows) will do the same thing as make and make install with a default configuration.

The Zuo executable runs only modules:

Additional Zuo arguments are delivered to that program via the runtime-env procedure. When the initial script module has a main submodule (see module+), that submodule is run.

Changed in version 1.1: Added the -c flag.

1.2 Library Modules and Startup Performance🔗ℹ

Except for the built-in zuo/kernel language module, Zuo finds languages and modules through a collection of libraries. By default, Zuo looks for a directory "lib" relative to the executable as the root of the library-collection tree. You can supply an alternate collection path with the -X command-line flag.

You can also create an instance of Zuo with a set of libraries embedded as an image. Embedding an image has two advantages:

The "local/image.zuo" script included with the Zuo sources generates a ".c" file that is a copy of "zuo.c" plus embedded modules. By default, the zuo module and its dependencies are included, but you can specify others with ++lib. In addition, the default collection-root path is disabled in the generated copy, unless you supply --keep-collects when running "image.zuo".

When you use configure and make ./zuo build.zuo to build Zuo, the default build target creates a "to-run/zuo" that embeds the zuo library, as well as a "to-install/zuo" that has the right internal path to find other libraries after make install or ./zuo build.zuo install.

You can use images without embedding. The dump-image-and-exit Zuo kernel primitive creates an image containing all loaded modules, and a -B or --boot command-line flag for Zuo uses the given boot image on startup. You can also embed an image created with dump-image-and-exit by using "local/image.zuo" with the --image flag.

A boot image is machine-independent, whether in a stand-alone file or embedded in ".c" source.

1.3 Embedding Zuo in Another Application🔗ℹ

Zuo can be embedded in a larger application, with or without an embedded boot image. To support embedding, compile "zuo.c" or the output of "local/image.zuo" with the ZUO_EMBEDDED preprocessor macro defined (to anything); the "zuo.h" header will be used in that case, and "zuo.h" should also be used by the embedding application. Documentation for the embedding API is provided as comments within "zuo.h".

1.4 Zuo Datatypes🔗ℹ

Zuo’s kernel supports the following kinds of data:

Notable omissions include floating-point numbers, characters, Unicode strings, and vectors. Paths are represented using byte strings (with an implied UTF-8 encoding for Windows wide-character paths).

See Zuo S-Expression Reader for information on reading literal values as S-expression.

1.5 Zuo Implementation and Macros🔗ℹ

The "zuo.c" source implements zuo/kernel, which is a syntactically tiny language plus around 100 primitive procedures. Since Zuo is intended for scripting, it’s heavy on filesystem, I/O, and process primitives, and almost half of the primitives are for those tasks (while another 1/3 of the primitives are just for numbers, strings, and hash tables).

Zuo data structures are immutable except for variable values, and even a variable is set-once; attempting to get a value of the variable before it has been set is an error. (Variables are used to implement letrec, for example.) Zuo is not purely functional, because it includes imperative I/O and errors, but it actively discourages in-process state by confining imperative actions to external interactions. Along those lines, an error in Zuo always terminates the program; there is no exception system (and therefore no way within Zuo to detect early use of an unset variable).

The zuo language is built on top of zuo/kernel, but not directly. There’s an internal “looper” language that just adds simple variants of letrec, cond, and let*, because working without those is especially tedious. Then there’s an internal “stitcher” language that is the only use of the “looper” language; it adds its own lambda (with implicit begin) let (with multiple clauses), let*, letrec (with multiple binding clauses), and, or, when, unless, and a kind of define and include.

Two macro implementations are built with the “stitcher” layer. One is based on the same set-of-scopes model as Racket, and that macro system is used for and provided by zuo/hygienic. The other is non-hygienic and uses a less expressive model of scope, which a programmer might notice if, say, writing macro-generating macros; that macro system is used for and provided by zuo, because it’s a lot faster and adequate for most scripting purposes. The two macro system implementations are mostly the same source, which is parameterized over the representation of scope and binding, and implemented through a combination of zuo/datum and the “stitcher” layer’s include.

Naturally, you can mix and match zuo and zuo/hygienic modules in a program, but you can’t use macros from one language within the other language. More generally, Zuo defines a #lang protocol that lets you build arbitrary new languages (from the character/byte level), as long as they ultimately can be expressed in zuo/kernel.

1.6 Zuo Module Protocol🔗ℹ

At Zuo’s core, a module is represented as a hash table. There are no constraints on the keys of the hash table, and different layers on top of the core module protocol can assign meanings to keys. For example, the zuo and zuo/hygienic layers use a common key for accessing provided bindings, but different keys for propagating binding information for macro expansions.

The core module system assigns a meaning to one key, 'read-and-eval, which is supplied by a module that implements a #lang language. The value of 'read-and-eval is a procedure of three arguments:

The procedure must return a hash table representing the evaluated module. A 'read-and-eval procedure might use string-read to read input, it might use kernel-eval to evaluate read or generated terms, and it might use module->hash to access other modules in the process of parsing a new module—but a 'read-and-eval procedure is under no obligation to use any of those.

A call (module->hash M) primitive checks whether the module M is already loaded and returns its hash table if so. The zuo/kernel module is always preloaded, but other modules may be preloaded in an image that was created by dump-image-and-exit. If a module M is not already loaded, module->hash reads the beginning of M’s source to parse the #lang specification and get the path of the language module L; a recursive call (module->hash L) gets L, and L’s 'read-and-eval procedure is applied to the source of M to get M’s representation as a hash. That representation is both recorded for future use and returned from the original (module->hash M) call.

The Zuo startup sequence assigns a meaning to a second key in a module’s hash table: 'submodules. The value of 'submodules should be a hash table that maps keys to thunks, each representing a submodule. When Zuo runs an initial script, it looks for a 'main submodule and runs it (i.e., calls the thunk) if present.

The zuo, zuo/base, and zuo/hygienic languages do not specify how their provided-variable information is represented in a module hash table, but they do specify that 'dynamic-require is mapped to the dynamic-require function, and then dynamic-require can be used to access provided values.

Changed in version 1.2: Added the 'dynamic-require key for zuo and related languages.

1.7 Path Handling🔗ℹ

Working with paths is a central issue in many scripting tasks, and it’s certainly a key problem for a build system. Zuo embeds some specific choices about how to work with paths: