Skip to content

Language

Jason E. Aten, Ph.D edited this page Jun 5, 2024 · 112 revisions

Reader Syntax

New anonymous hash map syntax: {}

As of v8.0.0, the curly braces can be used to create (anonymous) hashmaps, as in JSON.

New syntax uses curly braces:

> (def h {a:3 b:5 })
{a:3 b:5}
> 

The old style syntax for anonymous hash maps still works; the two are equivalent:

> (def h (hash a:3 b:5))
{a:3 b:5}
> 

The json2 function is new too. It prints hashes as JSON objects (commas inserted). The original json function returns raw bytes instead of string, and includes some meta-data that is not in the original JSON source.

> (def h (hash a:3 b:["pretty" "nice"]))
{a:3 b:["pretty" "nice"]}

zygo> (println (json2 h))

{"a":3, "b":["pretty", "nice"]}

With these changes, one can now (source "tests/sample.json") and have the JSON file read into zygo hash maps. Note that the default keys in zygo are symbols. The default keys in JSON are strings, and we do not (at the moment) convert them to symbols -- principally so that printing them out again via (json2) will keep the keys as strings, and thus remain parsable JSON. Hence access to members must use the h["element"] syntax instead of h.element:

> j=(source "tests/sample.json")

{"glossary":{"title":"example glossary" "GlossDiv":{"title":"S" "GlossList":{"GlossEntry":{"ID":"SGML" "SortAs":"SGML" "GlossTerm":"Standard Generalized Markup Language" "Acronym":"SGML" "Abbrev":"ISO 8879:1986" "GlossDef":{"para":"A meta-markup language, used to create markup languages such as DocBook." "GlossSeeAlso":["GML" "XML"]} "GlossSee":"markup"}}}}}

zyg> j.glossary   // querying for the symbol %glossary won't find it.
error in __main:3: hash has no field 'glossary' [err 1]

zygo> j["glossary"] // the string is the key in traditional JSON.

{"title":"example glossary" "GlossDiv":{"title":"S" "GlossList":{"GlossEntry":{"ID":"SGML" "SortAs":"SGML" "GlossTerm":"Standard Generalized Markup Language" "Acronym":"SGML" "Abbrev":"ISO 8879:1986" "GlossDef":{"para":"A meta-markup language, used to create markup languages such as DocBook." "GlossSeeAlso":["GML" "XML"]} "GlossSee":"markup"}}}}

zygo> j["glossary"]["title"]

"example glossary"

zygo> (pretty true)

zygo> j
{
    "glossary":{
        "title":"example glossary" 
        "GlossDiv":{
            "title":"S" 
            "GlossList":{
                "GlossEntry":{
                    "ID":"SGML" 
                    "SortAs":"SGML" 
                    "GlossTerm":"Standard Generalized Markup Language" 
                    "Acronym":"SGML" 
                    "Abbrev":"ISO 8879:1986" 
                    "GlossDef":{
                        "para":"A meta-markup language, used to create markup languages such as DocBook." 
                        "GlossSeeAlso":[
                            "GML"
                            "XML"
                            ] 
                    } 
                    "GlossSee":"markup" 
                } 
            } 
        } 
    } 
}

zygo> 

Zygo was already a great configuration language, as it allows comments and supports NaN, +Inf, -Inf in numbers. With these changes to be visually similar to JSON, zygo syntax provides a great (and familiar looking) configuration file experience.

Atoms

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

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)]
     body)

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

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

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
2
> 

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:)
                null)
          (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
100
> // i took on 0, 1, 2 in the above, although it wasn't used in this example.

Lists

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

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

[1 2 3 4]

Hashes

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.

Quoting

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)
%aSymbol

Functions

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.

Bindings

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

Conditionals

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.

(cond
    firstCondition firstExpression
    secondCondition secondExpression
    ...
    defaultExpression)

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".

Sequencing

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

Loops

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

Macros

  • 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))]
        (begin
          (mdef (quote ~key) (quote ~value) (hpair ~myHash i))
          ~@body))))
  • ^ 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

Arithmetic

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

Comparisons

  • <
  • >
  • <=
  • >=
  • == (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?

Printing

  • println
  • print
  • printf
  • str stringifies its argument
  • json / unjson for JSON encoding/decoding of records
  • json2 for simpler string JSON encoding of anonymous hashes; includes no meta-data like (json) provides.

symbol functions

  • str2sym
  • sym2str
  • gensym

serialization

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

utilities

  • 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
  • defined? checks if the symbol is in scope in the current environment

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:)
4
> (-> disney attraction: view:)
"spectacular"
> (-> disney name:)
"Cinderella"
> (:name disney)
"Cinderella"
> (:attraction disney)
 (tower rooms:4 view:"spectacular")
> (:rooms (:attraction disney))
4
> 
> (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)
`/Users/jaten/go/src/github.com/glycerine/zygomys.wiki`
zygo> 
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
`Go API.md
Home.md
Language.md`
zygo> 
zygo> // illustrate extracting a string from system output
zygo>
zygo> (nsplit (sys `ls -1`))  // split on newlines into an array
["Go API.md" "Home.md" "Language.md"]
zygo> (:2 (nsplit (sys `ls -1`))) // pick array index 2 (0-based always)
`Language.md`
zygo>

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
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")}
nope
zygo>

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'
zygo> 

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`)
"/Users/jaten/go/src/github.com/glycerine/zygomys"
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
"erDog"
zygo>

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]
zygo>