Evaluation
In the previous chapter, we introduced the fact that GameLisp code is made out of a tree of simple values. Those values are sometimes parsed directly from a text file, but they can also be generated dynamically.
Unlike Rust, GameLisp code does not need to be compiled into a binary before it can be run. You just pass your code to GameLisp, and it's executed straight away.
This chapter will describe precisely what happens when GameLisp is asked to run a particular value as a piece of code.
Basic Evaluation
"Evaluating" a value means executing it as a piece of code. When we're talking about evaluation, we tend to say "form" rather than "value", but they're just two different names for the same thing. When a form is evaluated, it either returns another value or triggers an error.
Values of type nil
, bool
, int
, flo
, char
, str
or tab
are self-evaluating. If you
evaluate a form of one of these types, no code is executed; the "evaluation" simply returns the
form itself.
Symbols evaluate to the current value of the variable which they name. Local variable bindings take precedence over global bindings. If there are no local or global bindings for the symbol which is being evaluated, it's an error.
Arrays are a little more complicated:
-
Empty arrays are self-evaluating.
-
When the array's first element is a symbol which names a special form, such as
quote
,if
ordo
, then that form's special evaluation rules are applied. -
Otherwise, the array is evaluated as a function call. All of its elements are evaluated in order, from left to right. The result of evaluating the leftmost form becomes the "callee", the function which is being called. The results of evaluating all of the other forms (if any) are passed to the call as its arguments. The function call's return value becomes the result of the evaluation.
Attempting to evaluate a form of any non-representable type is an error.
A callee is usually a function. Functions can be defined either in GameLisp or in Rust. GameLisp comes with a very large number of built-in functions in its standard library.
For now, the main built-in functions you need to know are:
-
prn
takes any number of arguments and prints their text representation to stdout, separated by spaces and followed by a newline.pr
does the same thing without the newline. Both functions return#n
. -
int?
,tab?
and similar functions accept a single argument. They return#t
if the argument belongs to the specified type, or#f
otherwise. -
<
,==
,>=
and so on perform numeric comparisons.+
,-
,%
and so on perform basic arithmetic on numbers. It's an error to pass a non-number argument to any of these functions.- Note that GameLisp's arithmetic and comparisons use prefix notation rather than the
more familiar infix notation. Rather than writing
1 + 2 + 3
, write(+ 1 2 3)
; and rather than writingi < len
, write(< i len)
.
- Note that GameLisp's arithmetic and comparisons use prefix notation rather than the
more familiar infix notation. Rather than writing
-
arr
constructs a new array which contains all of its arguments. For example,(arr)
will return a new empty array, and(arr 1 2 3)
will return a new array which contains those three numbers.- You can "splay" the contents of one array into another with the abbreviation
..
; for example,(arr 10 ..src)
will create a new array which contains the integer10
, followed by all of the elements from the arraysrc
.
- You can "splay" the contents of one array into another with the abbreviation
A few examples of nested function calls:
(prn (> (+ 1 1 1) 2)) ; prints #t
(prn (pr "a ") (pr "b ")) ; prints a b #n #n
(prn (nil? #n) (int? 10) (bool? (int? "hello"))) ; prints #t #t #t
(prn (nil? 1 2)) ; error: too many arguments
Special Forms
"Special forms" are those which don't follow the basic evaluation rules described above. Because GameLisp's macro facilities are so powerful, it has a relatively small number of special forms, compared to languages like Rust.
do
has the same effect as a block { }
in Rust. (do a b c)
evaluates a
, then
evaluates b
, then finally evaluates c
, returning its value. (do)
, with no arguments,
evaluates to #n
.
Many other forms establish an "implicit do
". They contain zero or more child forms which are
evaluated one after the other, and they return the result of evaluating their last child form.
quote
suppresses evaluation: (quote a)
returns the value of a
without
evaluating it. Remember that quote
is usually abbreviated
as '
, so we would write (quote val)
as 'val
. Quote is most often used either to stop a
symbol from being evaluated as a variable, or to stop an array from being evaluated as a function
call:
; without the quotes, this would be evaluated as printing the values of
; variables named "hello" and "world". with the quotes, it prints two
; symbols instead.
(prn 'hello 'world)
; without the quote, this would be evaluated as a call to the function
; named "alice", passing in the values of the variables "betty"
; and "carlo" as arguments, and then printing the call's return value.
; with the quote, it prints a literal array instead.
(prn '(alice betty carlo))
if
performs conditional evaluation. It must always receive three forms: a
"condition" form, a "then" form, and an "else" form. First, the "condition" form is evaluated.
If its result is truthy, the "then" form is evaluated and returned;
otherwise, the "else" form is evaluated and returned.
; prints 5, but doesn't print 4
(prn (if (> 2 1)
5
(prn 4)))
Local Variables
GameLisp's local variables are lexically scoped and always mutable. They can be declared either
at the toplevel of a source file, or within an implicit do
.
Just like Rust, you declare a new local variable by using the let
special form.
Its first argument should be a symbol, and its optional second argument is a form which will be
evaluated to initialize the variable. When the second argument is absent, the variable's initial
value will be #n
.
(let a 20)
(let b (* 20 a))
(prn b) ; prints 400
When let
has three or more arguments, it's equivalent to multiple consecutive let
forms
with one or two arguments each. We could rewrite the above as:
(let a 20, b (* 20 a))
(prn b) ; prints 400
let
's first argument can be an arbitrary pattern rather than a symbol, but we'll discuss that
in a later chapter.
Global Variables
When evaluating a symbol which isn't bound to a local variable, it's assumed to refer to a
global variable instead. The symbol a-name
will evaluate to the current value of the
global variable binding for the symbol a-name
, or it will trigger an error if there's no
such binding.
GameLisp's global variables use late binding. This means that every time a global variable is accessed or mutated, that variable's binding is looked up at the last possible moment. If you're trying to access a global variable which doesn't exist, you won't find out when the code is loaded - the error is delayed until you actually try to access it. On the other hand, late binding means that there's no need to declare your variables in advance, so you won't have to struggle with order-of-declaration problems or circular dependencies.
Global bindings can be dynamically created, destroyed, accessed or mutated by calling the built-in
functions bind-global!
, del-global!
,
global
and global=
.
Built-in function calls like (prn 'hello)
make use of global variables. When the GameLisp runtime
starts up, the symbol prn
is bound to a global variable. The initial value of that global
variable is set to a Rust function which prints its arguments.
A local variable "shadows" a global variable which is bound to the same name:
(prn 'hello) ; prints hello
(let prn 10)
(prn 'world) ; error: callee is an int
Late binding enables some useful tricks. For example, if you're creating a 2D game and you find it inconvenient to look up your sprites in a hash table, you could arrange for them to be automatically bound to global variables instead:
(draw spr:zombie-head (- x 10) (- y 10))
(draw spr:zombie-body (- x 10) y)
Functions
Functions in GameLisp are like closures in Rust, in that they can be stored as the value of a
variable. Functions defined in GameLisp have the type fn
, while functions defined in Rust
have the type rfn
.
The fn
special form defines a new function, which can then be called just like one of the
built-in functions:
(let triple (fn (n)
(let doubled (+ n n))
(+ n doubled)))
(prn (triple 70)) ; prints 210
The body of a function establishes an "implicit do
", like the do
special form. Each of
its body forms are evaluated one after the other, and the value of the final form is returned as
the result of the function call. You can return early using the return
special
form.
By default, a function only accepts a fixed number of arguments:
(prn (triple)) ; error: too few arguments
(prn (triple 1 2)) ; error: too many arguments
However, a fn's parameter list is a pattern. Among other things, this
allows you to describe an optional parameter with the special syntax (? name default-val)
, or
capture zero or more arguments with the special syntax ..name
.
(let sum-and-triple (fn (..nums)
(* 3 (+ ..nums))))
(prn (sum-and-triple 10 20 30)) ; prints 180
(let print-multi (fn (item (? times 1))
(forn (_ times)
(pr item " "))
(prn)))
(print-multi 'badgers) ; prints badgers
(print-multi 'badgers 3) ; prints badgers badgers badgers
Functions capture the lexical environment at their definition site. This means that you don't need to worry about local variables going out of scope:
(let f (do
(let captured 10)
(fn ()
captured)))
(prn (f)) ; prints 10, even though `captured` is out-of-scope