Liz tries to mimic syntax of Clojure/EDN and map concepts to Zig semantics. Being familiar with either one the languages is helpful. You can always refer to Zig documentation for more details.
Comments start with semicolon ;. By convention line comments use two semicolons and comments on end of line use one. comment macro can be also used, it discards all forms nested inside of it.
;; Line comment
;; Another line
(+ 3 4) ; End of line comment
(comment
(+ 1 2))nil maps to null and is used for optionals:
nil ; => nullundefined is used for uninitialized variables:
undefined ; => undefinedBooleans are directly equivalent to Zig booleans:
true ; => true
false ; => falseNumber literals can be written in following ways:
- decimal
1231.23 - exponent
12e31.2e31.2e-3 - hexadecimal
0x123 - octal
0123 - binary
2r0110 - arbitrary base
NrXXXwhere (<= 2 N 36) and X is in [0-9, A-Z]
Note that in Zig each number has a type. For example u8 for bytes/chars, i32 as signed integer, f64 for floating doubles, c_int for compatibility with C types, etc.
String literals follow Clojure/Java escaping rules. String literals are represented as sentinel-terminated arrays with zero byte as terminating sentinel. Use backslash to escape single characters and \newline, \space, \tab, \formfeed, \backspace, \return for special characters.
(const bytes "hello")
(assert (= bytes.len 5))
(assert (= (aget bytes 1) \e))
(assert (= (aget bytes 5) 0))
(assert (= \e \u0065))
(assert (= 0x1f4a9 128169))
(assert (.eql mem u8 "hello" "h\u0065llo")))Valid symbol characters are limited to make it simple to interop with Zig. Unlike Clojure, dash - is not a valid character for identifier. It is recommended to follow Zig's naming conventions:
snake_casefor variables and constantscamelCasefor functionsTitleCasefor types
Define private functions with defn-.
For functions to be used by other modules use defn.
Anonymous functions are defined with fn.
Note that unlike Clojure there is not implicit return in Liz, use return to return values from a function.
(defn- ^int32 add [^int32 a ^int32 b]
(return (+ a b)))
(defn ^void main []
(print "{}" [(add 1 2)]))Functions can have additional metadata keywords:
^:export- makes a function externally visible for use with C ABI^:extern- when linking statically resolve at link time, or when linking dunamically resolve at runtime
(defn ^{:extern "c"} ^f64 atan2 [^f64 a ^f64 b])For more complicated types that are not valid symbols, use string tags.
Example specifying a different call convention:
;; Force a function to be inlined by specifying a calling convention.
(defn ^"callconv(.Inline) u32" shiftLeftOne [^u32 a]
(return (<< a 1)))Parameters can have additional metadata keywords:
^:comptime- parameter values is known at the compile time^:noalias- TODO
(defn ^usize typeNameLength [^:comptime ^type T]
(return (.-len (@typeName T))))Use const and var for constants of variable declarations.
Usually the type can be inferred, but for literals you can explicitly specify it.
Use set! to modify value of a variable or struct field.
(const ^i32 a 123)
(var b a)
(set! b 456)
;; Use :pub modifier to make top-level declarations public
(const ^:pub ^f64 pi 3.14)Zig does not allow name shadowing, so to keep the semantics similar Liz does not implement let binding.
(+ a b) ; add numbers (+ 1 2) => 3
(- a b) ; subract (- 2 5) => -3
(- a) ; negate (- 2) => -2
(* a b) ; multiply (* 2 5) => 10
(/ a b) ; divide (/ 10 5) => 2
(mod a b) ; division modulus (mod 5 3) => 1
(rem a b) ; division remainder (rem -10 3) => -1
(inc a) ; increment by one (inc 2) => 3
(dec a) ; decrement by one (dec 2) => 1
;; Append = to operators to modify variables
(+= a b) ; same as (set! a (+ a b))
(-= a b) ; same as (set! a (- a b))
(*= a b) ; same as (set! a (* a b))
(/= a b) ; same as (set! a (/ a b))
;; Mutating increment/decrement
(inc! a) ; increment, same as (+= a 1)
(dec! a) ; decrement, same as (-= a 1)(= a b) ; equal
(not= a b) ; not equal
(zero? a) ; same as (= a 0)
(pos? a) ; positive number, same as (> a 0)
(neg? a) ; negative number, same as (< a 0)
(even? a) ; even number
(odd? a) ; odd numberUnlinke in Clojure = is a binary operator for now. Instead of (= a b c) you need to write (and (= a b) (= b c)).
(and a b) ; logical and (and true true) => true
(or a b) ; logical or (and true false) => true
(not x) ; negation (not true) => false(bit-not x) ; Bitwise complement
(bit-and a b) ; Bitwise and, alias for `&`
(bit-or a b) ; Bitwise or, alias for `|`
(bit-xor a b) ; Bitwise exclusive or
(bit-shift-left x n) ; Bitwise shift left, alias for `<<`
(bit-shift-right x n) ; Bitwise shift right, alias for `>>`
(bit-flip x n) ; Flip bit at index n
(bit-set x n) ; Set bit at index n
(bit-test x n) ; Test bit at index nUse if for if-then-else conditions. Multiple forms in a branch can be grouped with do.
Use when for conditions without else which allows you to write multiple then forms.
(if (zero? x)
(print "zero")
(print "something else"))
(if (zero? x)
(do (print "zero")
(print "still zero")))
(when (zero? x)
(print "zero")
(print "still zero")))Use if-not and when-not as a shortcut for negated conditions to save on parentheses.
(if (not x) ...)
(if-not x ...)Use if-some or when-some to test optionals and unwrapping values.
(const ^?u32 optional_a 0)
(when-some [value optional_a]
(expect (= value 0)))cond is for multiple if else statements
(cond
(pos? x) (print "greater than zero")
(neg? x) (print "less than zero")
:else (print "zero"))case also known as switch in other languages.
(case a
;; Multiple cases can be combined together.
[1 2 3] 0
;; Ranges can be specified using (range). These are inclusive both ends.
(range 5 100) 1
;; Branches can be arbitrarily complex.
101 (block :blk
(const ^u64 c 5)
(break :blk (* c (+ 2 1))))
;; The else branch catches everything not already captured.
;; Else branches are mandatory unless the entire range of values
;; is handled.
9)More details: if, switch in Zig docs.
Group multiple forms into a scoped block with do. Mostly useful for multiple forms in conditionals.
(do
(var ^i32 a 1)
(inc! a))
;; a is out of scope hereUse block for labeled blocks. break with label can be used to return a value from the block.
block
break
while
basic loop that iterates while the condition holds true
continue
break
else
while-step
step evaluated on every iteration including when using continue
while-some iterates while the expression is optional with value and binds the value
iterates over elements of array or slice. like Clojure doseq
for
dotimes macro is provided for convenience
note that parameter can only be a constant or variable
Arrays store elements of the same type with length known at compile time.
Slices have a pointer and length known at run time. Slices usually act as a view into array or can be returned when allocating memory of dynamic size.
The difference to arrays in C represented by pointers is that Arrays and Slices have always associated length andprotect against out of bounds access and overflows.
;; array uses vector notation and a type tag
(const message ^"[_]u8" [\h \e \l \l \o])
;; same as above
(const message "hello")
;; coerce string literal into slice
(const ^"[]const u8" message_slice "hello")
;; create slice from array specifying beginning and end
(const arr_slice (slice arr 0 5))
;; omitting end slices to the end
(const message_slice (slice message 0))
;; get size of array/slice with `len`
(.-len message) ; => 5
message.len ; also works
;; aget to get element
(aget message 0) ; => \h
;; aset for setting element at a given index
(aset arr 0 value)
;; aget/aset can take multiple indices for nested arrays
(aget pixels 5 10)
(aset pixels 5 10 value)
;; concatenate arrays with ++, works if the values are known at compile time
(const all_of_it (++ part_one part_two))struct enum union error
Expressions that define structs can be parametrized by comptime parameters which acts like generics known from other languages.
(.method x arg) ; => x.method(arg)(.-attr x) ; => x.attr
(set! (.-attr x) value) ; => x.attr = valueMemory address is done with & and dereference with *.
(& arr)
;; can be used also as part of the symbol
&arr
(.-* ptr)
;; can be used also as part of the symbol
ptr.*
@import usingnamespace
https://ziglang.org/documentation/0.7.1/#import https://ziglang.org/documentation/0.7.1/#usingnamespace
defer will execute an expression at the end of the current scope.
errdeher is similar to defer, but will only execute if the scope returns with an error.
?
if-some, when-some, while-some bind
for errors in else branch
https://ziglang.org/documentation/0.7.1/#Optionals
error values
syntax mimics Clojure
try orelse catch
https://ziglang.org/documentation/0.7.1/#Errors
suspend resume await async
https://ziglang.org/documentation/0.7.1/#Async-Functions
Use zig* as an escape hatch to emit Zig code directly. Useful when manipulating types or for writing down assembly.
(const bytes "hello")
(assert (= (@TypeOf bytes) (zig* "*const [5:0]u8")))In case when Liz special forms would clash with Zig names, you can use quote.
('when 1) ; => when(1);and io - print
test / testing expect
https://ziglang.org/documentation/0.7.1/#Zig-Test
Macros are used internally to implement some special forms, but user-defined macros are not implemented. Zig's comptime is less powerful, but I am interested to explore its limitations first before introducing macros.
Questions
- like ClojureScript - macros written in Clojure
- self-hosted macros - would need to implement higher-level datasctructures convenient for manipulating code as data