Jason E. Aten edited this page Nov 25, 2018 · 100 revisions

Reader Syntax


zygomys has seven types of Atoms: ints (which are int64 in Go), floats (which are float64), strings, chars (runes), bools, and symbols. There is also an atom for raw bytes. Raw bytes corresponds to a []byte in Go, and is constructed with (raw content). Raw bytes are mostly used to manipulate packets or byte buffers received from external parties.

The following are different kinds of literal syntax for these atoms.

3 ; an int
-21 ; a negative int
nil ; the nil value; the empty list () is also represented by nil.
0x41 ; int in hexadecimal
0o755 ; int in octal
0b1110 ; int in binary
4.1 ; a float
-2.3 ; a negative float
1.3e20 ; a float in scientific notation
'c' ; the character 'c'
'\n' ; the newline character
"asdfsd" ; a string
asdfsd ; a symbol
true ; the "true" boolean
false ; the "false" boolean
`a raw string literal` ; like Go raw strings
hello:  ; the same symbol as %hello. The colon quotes and ends the symbol.

Comments are Go style, with /* block comments */ and // for comment-until-end-of-line. The syntax for symbols is the same as that for Go identifiers. Symbols name program entities such as variables and types. Symbols are a sequence of one or more letters and digits. The first character in an identifier must be a letter.

To avoid conflicts between user functions and future features, symbols that begin with an _ underscore are reserved for system functions and extensions.


Assignment is done using either the (def) or the (set) operator.

(def x 10) will always define a new variable x in the current scope. If there is already an x in the current scope, it will be updated. No scope up the stack will ever be effected. def should be your choice for most operations. Notice that a for loop does create a new scope, hence (def) expressions inside a for loop cannot modify variables established before the loop, and a set is needed.

While def is the workhorse, sometimes set is needed. set is more powerful and thus more dangerous; it can modify non-local variables. Like def expression (set x 10) will update the value of x with 10 if x is already defined in the current scope, and define a new binding if there is no x anywhere on the stack. However, if x is not found in the current scope, we will search up the scope stack for an earlier binding to x. If x is indeed found up the stack, the value of x in that higher scope will be updated to 10. If no binding is found, a local one is created. The non-local update of 'set' is essential is some cases, but should be used with care.

Using the infix notation within curly-braces, set may be expressed as in Go:

{a = 10}

Multiple assignement

(mdef a b c (list 1 2 3)) will assign value 1 to symbol a, value 2 to symbol b, and value 3 to symbol c. The last element of an mdef must be a list. If the list has fewer elements than the target symbols that are provided, it is an error. However it is perfectly acceptible to assign to fewer symbols than the list holds. mdef is often used in conjunction with hpair to fetch a key and its corresponding value. For example,

(mdef key val (hpair myhash 0))

will fetch the first (because it was given the 0 argument) key and value from myhash.

To support Go-like multiple assignment, the = assignment operator now supports multiple values on either side of the = sign:

(a, b, c = true, false, true)

For loops

For loops in zygomys are patterned after loops in C. There are three parts to the loop expression: the initializer, the predicate, and the advance. These three parts are written in an array before the body expressions. Optionally a label can be attached to the for loop (the label is a quoted symbol, typically quoted using a trailing colon). The unlabeled for loop looks like:

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

In contrast, the labeled for loop looks like the following, where outerLoop: is the label.

(for outerLoop: [(initializer) (test) (advance)]

For example, here is counting to 0..9, and summing as we go:

(def sum 0)
(for [(def i 0) (< i 10) (def i (+ 1 i))] 
     (println i)
     (set sum (+ sum i)))

Notice how set use is required here to update the sum variable which is before the loop, as for-loops create a new nested-scope for the pre-amble and body. The new scope avoids clobbering a parent loop varaible by mistake -- the child loop does not have to try to guess what the parent called its variables. This is especially valuable when sourcing unknown other code from within a loop.

Here is an example using a for loop to go though a hash. While it is easier to use the range loop (below) for this purpose, the example illustrates just how much code the range macro saves you.

> (def h (hash a:3 b:5))
(hash a 3 b 5)
> (def k (keys h))
[a b]
> (for [(def i 0) (< i (len k)) (def i (+ 1 i))] 
     (printf "my hash value %v for key %v\n" 
        (hget h (aget k i)) 
        (str (aget k i))))
my hash value 3 for key a
my hash value 5 for key b

Last but not least, here is an example of a nested for loop using a labeled break and a labeled continue. We make use of the ++ macro which is easy to read, and simply increments its argument by 1 using set.

(def isum 0)
(def jsum 0)
(for outerLoop: [(def i 1) (< i 5) (++ i)]
     (set isum (+ isum i))

     (for innerLoop: [(def j 1) (< j 5) (++ j)]
          (set jsum (+ jsum j))
          (cond (> j 2) (continue outerLoop:)
                (and (> i 2) (> j 3)) (break outerLoop:)
          (set jsum (+ jsum 1000))
(printf "isum is %d\n" isum)
(printf "jsum is %d\n" jsum)
(assert (== isum 10))
(assert (== jsum 8024))

Ranging over hashes and arrays

The range macro acts like a for-range loop in Go, assigning successive keys and values to the variables you name. In the example below, k gets each key in turn, and v gets each value from hash h. You can use range to iterate through arrays too.

> (def h (hash a:44 b:55 c:77 d:99))
> (def s "")
> (range k v h (set s (concat s " " (str k) "-maps->" (str v))))
> s
" a-maps->44 b-maps->55 c-maps->77 d-maps->99"
> // 
> (def sum 0) (range i value [10 50 40] (+= sum value)); range over array demo
> sum
> // i took on 0, 1, 2 in the above, although it wasn't used in this example.


Lists are just cons-cell lists like in other LISP dialects and are delimited by parentheses

(aFunction arg1 arg2
    (anotherFunction arg1 arg2))

You can also just describe plain pairs (cons-cells in which the tail is not necessarily a list) using the \ character. The backslash \ replaces the dot of traditional lisp syntax, because dot is re-purposed for dot-symbol/element selection.

(a \ b)


Arrays correspond to Go slices and are delimited by square braces.

[1 2 3 4]


zygomys has mutable hashmaps which use Go maps internally. The literal syntax uses the (hash ) form.

(hash %a 3
 %b 2)

Using the : alternative quoting mechanism, the same map can be written more readably as

(hash a:3 b:2)

The colon does not become a part of the symbols a and b in the above example. It simply acts like a quote % operation, but on the right-hand-side of the symbol.

The hash above maps a to 3 and b to 2. Hash keys can only be integers, strings, chars, or symbols.


The quote symbol % indicates that the following expression should be interpreted literally. This is useful for declaring symbols and lists. Note that this is deliberately different from the tradition in Lisp of using the single quote ', since we prefer to be compatible with Go's syntax, which uses the single quote for rune literals.

%(1 2 3 4)


An anonymous function can be declared in zygomys like so

(fn [a b] (+ a b))

A function can be declared with a name using defn.

(defn add3 [a] (+ a 3))

Note that like in Clojure, the argument list is given in an array instead of a list.


A binding can be added in the current scope using def. You can also create a new scope and declare bindings in it using let or letseq (letseq replaces let*).

(def a 3)

(let [a 3
      b 4]
    (* a b))
; returns 12

(letseq [a 2
       b (+ a 1)]
    (+ a b))
; returns 5

The difference between let and letseq is that let creates bindings all at once, so you will not be able to access earlier bindings in later bindings. The letseq form creates bindings one by one, sequentially, so each binding can access bindings declared before it.

Calling functions

Functions can be called in the regular way.

(defn add3 [a] (+ a 3))
(add3 2) ; returns 5

They can also be called indirectly using apply.

(apply + [1 2 3]) ; returns 6
(apply + %(1 2 3)) ; same as above

This works exactly the same with anonymous functions

((fn [a b] a) 2 3) ; returns 2
(apply (fn [a b] a) [2 3]) ; same as above


zygomys by default uses the universal conditional statement, cond. (If using the infix syntax, 'if' and 'else' are also now implemented.) The syntax of cond is as follows.

    firstCondition firstExpression
    secondCondition secondExpression

The cond statement will check the conditions in order. If the condition is true, it will return the result of the corresponding expression. If not, it will move on the next condition. If none of the conditions are true, it will return the result of the default expression. The default expression is the only required portion of this statement. The way to think of this is that the first condition/expression pair is an if statement, the second is an else if statement, and the default is the else statement.

zygomys also provides the short-circuit boolean operators and and or. The and expression will return the first "falsy" sub-expression or, if all sub-expressions are "truthy", the last sub-expression is returned. The or expression is the opposite, returning the first "truthy" expression or the last expression.

The boolean false, the null value (empty list), the integer 0, and the null character are considered "falsy". All other values are considered "truthy".


The begin statement is used to sequence expressions. It will run all sub-expressions and return the result of the final expression. The top-level, function bodies, and let-statement bodies have implicit begin statements.

Builtin Functions

The following builtin functions are provided by the language runtime.

Running code

  • source run code from named path


  • for loop; as in C
  • range loops over hashes; as in Go.


  • defmac defines a macro. Macros use function syntax, with arguments in [] an array.
  • vargs after & support, for example the range macro is defined as:
(defmac range [key value myHash & body]
  ^(let [n (len ~myHash)]
      (for [(def i 0) (< i n) (def i (+ i 1))]
          (mdef (quote ~key) (quote ~value) (hpair ~myHash i))
  • ^ is the syntax-quote shortcut (rather than backtick).
  • ~ is unquote. Use (quote ~sym) to capture the actual symbol name used in the argument (as above in the range example).
  • ~@ is splicing-unquote. See the body vararg in the range example.
  • macexpand given an expression, returns the quoted expansion without running it. Useful for debugging macros.

Integer shift operations

  • sll (shift-left logical)
  • sra (shift-right arithmetic)
  • srl (shift-right logical)

Bitwise operations

  • bitAnd
  • bitOr
  • bitXor
  • bitNot (one's complement)

Boolean operations

  • and (short circuits)
  • or (short circuits)
  • not


  • +
  • -
  • *
  • /
  • mod (modulo)
  • ** exponentiation
  • += -= update with set
  • ++ -- update with set


  • <
  • >
  • <=
  • >=
  • == (equality check)
  • !=

Type Introspection

  • type? (returns the type as a string)
  • zero? (returns true only for 0, 0.0, and null character)
  • null? (returns true only for ())
  • empty? (returns true only for (), [], and {})
  • list?
  • array?
  • number? (int, char, or float)
  • int?
  • float?
  • char?
  • string?
  • symbol?
  • hash?


  • println
  • print
  • printf
  • str stringifies its argument
  • json / unjson for JSON encoding/decoding of records

symbol functions

  • str2sym
  • sym2str
  • gensym


  • json / unjson
  • msgpack / unmsgpack
  • a native Go struct can be associated with each record
  • togo to reify a Go struct that matches the record


  • slurpf read newline delimited file into an array of strings
  • (writef val "path") function write val to path
  • owritef same as writef but will clobber any existing path
  • system shell out and return the combined output as a string
  • flatten turn nested lists of strings into one flat array

Array Functions

The array function can be used to construct an array. It is identical to the square brace literal syntax.

The makeArray function creates an array of a given length. By default, the items in the array are intialized to null.

(makeArray 3) ; => [() () ()]
(makeArray 3 0) ; => [0 0 0]

The aget function indexes into an array.

(aget [0 1 2] 1) ; returns 1

aget accepts a 3rd default value in case the access is out of bounds

(assert (== %outOfBoundsValue (aget [0 1 2] 99 %outOfBoundsValue)))

The : macro can be used in place of aget for more compact notation

(assert (== %33 (:0 [33 44 55])))
(assert (== %44 (:1 [33 44 55])))
(assert (== %55 (:2 [33 44 55])))
(assert (== %outOfVoundsValue (:99 [33 44 55] %outOfBoundsValue)))

The aset function modifies the value in the array at the given index

(def arr [0 1 2])
(aset arr 1 3)
; arr should now be [0 3 2]

Arrays are mutable in zygomys.

List Functions

The list, cons, first, and rest functions operate the same as in other LISP dialects. Note, however, that first and rest can also work on arrays. The second function on lists and arrays gets the second element; this different from rest which returns a sequence after the first.

The flatten function will convert a nested list of strings and symbols into a flattened array of strings.

String Functions

The sget function is similar to the aget function, except it operates on characters of a string instead of elements of a list.

Hashmap functions

The hget function retrieves a value from the hashmap by key.

(def h (hash %a 2 %b 3))
(hget h %a) ; => 2

You can give the hget function a third argument, which is the default value that will be returned if the key is not found. If no default is given and the key is not found, a runtime error will occur.

(hget (hash %a 3) %b 0) ; => 0

The hset function works similar to aset but using a key instead of an index.

(def h (hash %a 3))
(hset h %a 2) ; h is now (hash %a 2)

The hdel function takes a hash and a key and deletes the given key from the hash.

(def h (hash %a 3 %b 2))
(hdel h %a) ; h is now (hash %b 2)

Record definition

The (defmap) macro creates a named record type. The underlying hashmap can be accessed with the same tools as above. The ':' and '->' operators can also be deployed.

> (defmap castle)
> (defmap tower)
> (def disney (castle name:"Cinderella" attraction: (tower rooms:4 view:"spectacular")))
> disney
 (castle name:"Cinderella" attraction: (tower rooms:4 view:"spectacular"))
> (-> disney attraction: rooms:)
> (-> disney attraction: view:)
> (-> disney name:)
> (:name disney)
> (:attraction disney)
 (tower rooms:4 view:"spectacular")
> (:rooms (:attraction disney))
> (hset (:attraction disney) rooms: 34)
> (:attraction disney)
 (tower rooms:34 view:"spectacular")
> disney
 (castle name:"Cinderella" attraction: (tower rooms:34 view:"spectacular"))

Generic Container Operations

The append function can append an expression to the end of an array or a character onto the end of a string.

(append [0 1] 2) ; => [0 1 2]
(append "ab" 'c') ; => "abc"
(append [0 1] [2 3]) ; => [0 1 [2 3]]

The appendslice will join two arrays together without creating an array within an array, much like calling Go's append(a, b...).

(appendslice [0 1] [2 3]) ; => [0 1 2 3]

The concat function overlaps with append and appendslice. It will concatenate any number of arrays, strings, or lists.

(concat [0 1] [2 3] [4 5]) ; => [0 1 2 3 4 5]
(concat "ab" "cd" "ef") ; => "abcdef"
(concat %(1 2) %(3 4) %(5 6)) ; => (1 2 3 4 5 6)

The len function returns the number of elements in an array, keys in a hash, or number of characters in a string.

new scopes

  • (newScope ...) introduces a new scope. The statements in the ... will not leak bindings or change variables outside the scope -- that is, unless (set) is used to do so deliberately.

Debug facilities

Mostly for system development, several REPL-only special commands are recognized that let one see exactly what the interpreter is doing as it generates and executes expressions.

  • .debug an .undebug turn on and off the system tracing facility -- warning, very low level! :-)
  • .dump will show the current environment's functions instruction and stack
  • .dump function will dump a specific function by name
  • .gls (global listing) will display the symbol table, all bindings.
  • .ls will show the internal stacks. Placing (_ls) in your code will produce a stack dump at that point in execution, and continue executing.
  • (infixExpand {}) will show the converted infix expression in its s-expression form.

system calls

The sys macro calls the system function; this issues a shell command and returns the stdout and stderr as a string. The sys must have a space after it. For example,

zygo> (sys pwd)
zygo> (sys ls -1)  // not all symbols come through the macro cleanly
error in system:-1: Error calling 'system': flatten on '[]zygo.Sexp{zygo.SexpPair{Head:zygo.SexpSymbol{name:"ls", number:157, isDot:false, ztype:""}, Tail:zygo.SexpPair{Head:-1, Tail:0}}}' failed with error 'arguments to system must be strings; instead we have zygo.SexpInt / val = '-1''
in __main:2
zygo> (sys `ls -1`) // so, simply: pass a string to avoid the -1 parsing as a number token
zygo> // illustrate extracting a string from system output
zygo> (nsplit (sys `ls -1`))  // split on newlines into an array
["Go" "" ""]
zygo> (:2 (nsplit (sys `ls -1`))) // pick array index 2 (0-based always)

Infix syntax

Statements placed within the '{' and '}' curly braces are treated as infix, and parsed accordingly. For example, 'if' and 'else', comparisons, and arithmetic work as expected in infix mode. Function calls always use prefix syntax.

zygo> a = 10
zygo> if a * 10 + 3 > 100 + 2 { (println "over the one-oh-two-mark") } else { (println "nope")}
over the one-oh-two-mark
zygo> if a * 10 + 3 > 100 + 5 { (println "over the one-oh-five-mark") } else { (println "nope")}

Notice this works because the repl is implicitly wrapped in an infix block. This allows the repl to work as a calculator easily.

Packages and imports

Mirroring Go, you can partition code into packages. Packages use the same visibility rules as Go: lower-case identifiers are private, upper-case identifiers are publically accessible.

zygo>  stuff = (package stuff (def b "i am a private string") (def B "I am a Public string"))
(package stuff  
     elem 0 of :  global
         (global scope - omitting content for brevity)
     elem 1 of :  scope Name: 'stuff'
         B -> "I am a Public string"
         b -> "i am a private string"
zygo> stuff.B
"I am a Public string"
zygo> stuff.b
error in __main:9: Cannot access private member 'b' of package 'stuff'

Notice that the private (lower-case) variable was not available outside the package.

If you store a package in a file, then -- just as in Go -- you can use the import statement to import the package from the path to the file. Here we'll use the example package helloKit from the tests directory, giving it the alias pk.

zygo> (sys `pwd`)
zygo> (import pk "tests/prepackage")
(package helloKit  
     elem 0 of :  global
         (global scope - omitting content for brevity)
     elem 1 of :  scope Name: 'helloKit'
         Funky -> (defn Funky [a] (concat a " rov" Rov " chases " Kit))
         Kit -> "cat"
         Rov -> "erDog"
         UsePriv -> (defn UsePriv [] (concat "my number is " (str privetLane)))
         privetLane -> 42
zygo> pk.Rov

map function

The map function takes a function as the first argument. It applies that function to the second argument, which can be either a list or an array.

zygo> (map (fn [x] (** x 3)) %(1 2 3 4 5))
(1 8 27 64 125)
zygo> (map (fn [x] (** x 3)) [1 2 3 4 5])
[1 8 27 64 125]
zygo> (map (fn [x] (+ x 1)) [1 2 3 4 5])
[2 3 4 5 6]
Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.