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:
If you run Zuo with no command-line arguments, then it loads "main.zuo" in the current directory.
As long as the -c is not used and the first argument is not the empty string, the first argument to Zuo is used as a file to run or a directory containing a "main.zuo" to run.
Note that starting Zuo with the argument "." is equivalent to the argument "./main.zuo", so zuo . is a convenient replacement for make while still passing arguments.
If the -c flag is provided to Zuo, the first argument is treated as the text of a module to run, instead of the name of a file or directory.
If the first argument to Zuo is the empty string (which would be invalid as a file path), the module to run is read from standard input.
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:
No extra directory of library modules is necessary, as long as all relevant libraries are embedded.
Zuo can start especially quickly, competitive with the fastest command-line programs.
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:
booleans;
integers as 64-bit two’s complement with modular arithmetic;
strings as byte strings (optionally prefixed with # and with \n, \r, \t, \", \\, and octal escapes);
symbols, both interned (never garbage collected) and uninterned;
lists;
hash tables, which are symbol-keyed persistent maps (and don’t actually employ hashing internally);
procedures, including first-class continuations reified as procedures;
variables, which are named, set-once, single-valued containers;
opaque objects that pair a key and a value, where the value can be accessed only by supplying the key (which is typically kept private using lexical scope); and
handles, which represent system resources like files or processes.
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:
a string for the text of a module using the language,
a position within the string that starts a module body after #lang and the language name, and
a module path that will be mapped to the result of evaluating the module (i.e., the path to the text’s source).
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—
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:
Zuo relies on syntactic normalization of paths. For example, starting with "a/b" and building "../c" from there produces the path "a/c", even if "a/b" on the filesystem is a symbolic link to to the relative path "x/y/z"—
in which case the filesystem would resolve "a/b/../c" the same as "a/x/y/z/../c", which is "a/x/y/c" and not "a/c". In short, mixing directory symbolic links with Zuo’s path functions can be different than what the filesystem would do, so take care to avoid cases that would not work. Symbolic links to files will not create problems, so consider just never using directory links.
There is no way to change the working directory of the Zuo process. Having a fixed current directory means that relative paths work in many more situations than they would otherwise. Relative paths are communicated to system facilities still in relative form, leaving it up to the operating system to resolve the path relative to the current working directory.
When starting a subprocess, you can pick the working directory for the subprocess. In that case, you must take care to adjust relative paths communicated to the process, and find-relative-path can help.
Zuo uses and propagates relative paths as much as possible. This convention is partly enabled by the fact that the working directory cannot change within the Zuo process. It also helps avoid trouble from a mismatch between syntactic and filesystem-based path resolution, as might be created with symbolic directory links; for example, even if you used symbolic links or one of multiple filesystem mounts to access a Zuo working tree, staying within that tree avoids complications with the path that reaches the tree.
The way that you start a Zuo script affects the script’s operation in terms of absolute or relative paths. If you start a Zuo script with a relative path, such as zuo scripts/go.zuo, the quote-module-path form will report a relative path for the enclosing script. If you start it with an absolute path, such as zuo /home/racket/scripts/go.zuo, then quote-module-path reports an absolute path. Similarly, with zuo/build, when you use a relative path to refer to a dependency, information about the dependency can be recorded in relative form, but referring to a dependency with an absolute path means that information is recorded with an absolute path (even if that could be made relative to the dependent target’s path).
The build/build library encourages an explicit distinction between “source” and “build” directories, neither of which necessarily corresponds to the current working directory. This distinction, along with the fact that the working directory doesn’t change, helps to create composable build scripts. See Building Targets for more information.