Symbols are very important and central to LispBM and also perhaps a bit different from identifiers/names used in languages such as C. A short introduction to symbols could be a good place to start.
One way to think about a symbol is as a name. Used as a name, a symbol can identify a value or function in the environment. A symbol can also be used as data in and of itself, more on this later.
NOTE Symbols are expressed as strings in your
program such as a, let, define,
+ or orange. The "reader", the part of LBM
that parses code, translates each symbol into a 28bit value. The string
orange for example is only of interest if you print a
symbol and then the runtime system will look up what string corresponds
to the 28bit identifier you want to print. So the runtime system is
never wasting time comparing strings to see if a symbol is this or that
symbol, it's all integer comparisons.
You associate values with symbols using, define, let and you can change the value bound to a "variable" using set, setq or setvar.
Not all symbols are treated the same in LBM. Some symbols are treated
as special because of their very fundamental nature. Among these special
symbols you find define, let and
lambda for example. These are things that you should not be
able to redefine and trying to redefine them leads to an error. Symbols
that start with ext- are special and reserved for use
together with extensions that are loaded and bound at runtime.
Examples of symbols used as data are nil and
t. nil represents "nothing", the empty list or
other similar things and t represents true. But any symbol
can be used as data by quoting it ', see
Quotes and Quasiquotation
.
A symbol is a string of characters following the rules: 1. The first character is a one of 'a' - 'z' or 'A' - 'Z' or '+-/=<>#!'. 2. The rest of the characters are in 'a' - 'z' or 'A' - 'Z' or '0' - '9' or '+-/=<>!?_'. 3. At most 256 characters long.
Note that lower-case and upper-case alphabetical letters are
considered identical so the symbol apa is the same symbol
as APA.
examples of valid symbols:
apa apa? !apa kurt_russel_is_great
LBM supports signed and unsigned integer types as well as float and double. The numerical types in LBM are
The byte and the char value have identical representation and type, thus char is an unsigned 8 bit type in LBM.
An integer literal is interpreted to be of type i, a
28/56bit signed integer value. A literal with decimal point is
interpreted to be a type f32 or float value.
To specify literals of the other types, the value must be postfixed
with a qualifier string. The qualifiers available in LBM are:
b, i, u, i32,
u32, i64, u64, f32
and f63. The i and f32 qualifiers
are never strictly needed but can be added if one so wishes. An
alternative way of writing byte literals is using Character literals (e.g.
\#a).
So for example:
1b - Specifies a byte typed value of 11.0f64 - Specifies a 64bit float with value 1.0.\#a - Specifies a byte type value of 97.Note that it is an absolute requirement to include a decimal when writing a floating point literal in LBM.
We are trying to make type conversions feel familiar to people who
know a bit of C programming. On a 32bit platform LBM numerical types are
ordered according to:
byte < i < u < i32 < u32 < i64 < u64 < float < double.
Operations such as (+ a b), figures out the largest type
according to the ordering above and converts all the values to this
largest type.
Example:
(+ 1u 3i32) - Promotes the 1u value type i32 and
performs the addition, resulting in 4i32.(+ 1 3.14) - Here the value 1 is of type
i which is smaller than f32, the result
4.14f32.A potential source of confusion is that f32 is a larger
type than i64 and u64. this means that if you,
for example, add 1.0 to an i64 value you will get an
f32 back. If you instead wanted the float to be converted
into a double before the addition, this has to be done manually.
Example:
(+ (to-double 1.0) 5i64) - Manually convert a value to
double.The type-of operation can be used to query a value for
its type. On the numerical types the type-of operation
answers as follows:
(type-of 1b) -> type-char(type-of 1) -> type-i(type-of 1u) -> type-u(type-of 1i32) -> type-i32(type-of 1u32) -> type-u32(type-of 1i64) -> type-i64(type-of 1u64) -> type-u64(type-of 1.0) -> type-float(type-of 1.0f64) -> type-doubleOperations on fixed bitwidth numerical types can lead to overflow. The ranges representable in 32bit LBMs integer types are the following:
type-char : 0 - 255type-i : -134217728 - 1342177272type-u : 0 - 268435455type-i32 : -2147483648 - 2147483647type-u32 : 0- 4294967295type-i64 : -9223372036854775808 -
9223372036854775807type-u64 : 0 - 18446744073709551615| Example | Result |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
All Values in LBM are encoded in one way or another. The encoded value holds additional information about type and garbage collection mark bit. Operations that operate on an LBM value needs to unpack this encoded format and extract the actual numerical information from the representation. This has a cost and operations on numbers are in general a bit slower than what one gets in, for example C.
The chart below shows the time it takes to perform 10 million additions on the x86 architecture (a i7-6820HQ) in 32 and 64 Bit mode.

In 64Bit mode the x86 version of LBM shows negligible differences in cost of additions at different types.
For addition performance on embedded systems, we use the the EDU VESC motorcontroller as the STM32F4 candidate and the VESC EXPRESS for a RISCV data point.

In general, on 32Bit platforms, the cost of operations on numerical types that are 32Bit or less are about equal in cost. The costs presented here was created by timing a large number of 2 argument additions. Do not see these measurements as the "truth carved in stone", LBM performance keeps changing over time as we make improvements, but use them as a rough guiding principle.
LBM supports string literals, consisting of a pair of double quotes
(") with a string of characters in between. These evaluate
to a byte array containing the bytes of these characters (in the source
code's encoding), followed by a zero byte.
Special characters can be written using escape sequences. They take
the form of a backslash character (\) followed by some
character from the list below and are replaced with their corresponding
character at read time. A backslash followed by any other character is a
read error.
| Escape sequence | ASCII value | C equivalent | Character represented |
|---|---|---|---|
\0 |
0x0 | \0 |
Zero byte |
\a |
0x7 | \a |
Bell character |
\b |
0x8 | \b |
Backspace |
\t |
0x9 | \t |
Horizontal Tab |
\n |
0xA | \n |
Newline |
\v |
0xB | \v |
Vertical tab |
\f |
0xC | \f |
Form feed |
\r |
0xD | \r |
Carriage return |
\e |
0x1B | \e |
Escape character |
\s |
0x20 | - | Space |
\" |
0x22 | \" |
Double quote " |
\\ |
0x5C | \\ |
Backslash
\ |
\d |
0x7F | - | Delete character |
Note that unlike other languages, single quotes (')
can't be used to form string literals as it's busy being used for quoting! Therefore it doesn't need
to be escaped within string literals.
Individual characters can be written using character literals. They
take the form of \# followed by any ASCII character (e.g.
\#a or \#X), and evaluate to their
corresponding numerical byte value (note the type!). Like strings,
character literals also support escape sequences, in which case they
evaluate to its value from the above table, and any invalid characters
result in a read error.
| Example | Result |
|
|
|
|
|
|
|
|
|
|
|
|
Opinions on Lisp syntax varies widely depending on a persons programming experience and preferences. If you look around, or ask around you could find any of the following, and probably more views on lisp syntax:
Lisp programs are written using S-expressions, a notation introduced
by McCarthy. An
S-expression describes a tree in an unambiguous way. An example of an
S-expression is (+ 1 2) and the tree it represents is shown
below:

Another example (+ (* a a) (* b b)) which as a lisp
program means a2 + b2:

In Lisp, which stands for "LISt Processor", a list is a right leaning
tree ending in the symbol "nil". By convention these right leaning
expressions are easy to write and requires only a few parentheses. The
example below shows how the list created by lisp program
(list 1 2 3 4) is represented as a tree:

A left leaning structure requires full parenthesization and can be
expressed in lisp as
(cons (cons (cons (cons nil 4) 3) 2) 1).

The conventions strongly favor the right leaning case.
There are no two different trees that correspond to a given S-expression and thus parsing of S-expressions is unambiguous. The unambiguous nature of S-expressions is useful in areas other than lisp programming as well. KiCad uses S-expressions to represent tree data in some of its file formats. Apparently WebAssembly uses S-expressions as well to describe WebAssembly modules
S-expressions are built from two things, Atoms and Pairs of S-expressions. So an S-expression is either:
(a . b)In LispBM the set of atoms consist of:
1, 2, 3.14, 65b,
2u32a,
lambda, define, kurt-russel
...In LispBM a pair of S-expressions is created by an application of
cons as (cons a b) which creates the pair
(a . b). Convention is that (e0 e1 ... eN) =
(e0 . ( e1 . ... ( eN . nil))).
A structure such as (e0 e1 ... eN) is called a list.
The S-expressions discussed in the previous section are merely tree structures. The Lisp evaluator provides a computational interpretation for these trees. However, not all trees are valid Lisp programs. This section focuses on those trees that do make sense as Lisp programs and their meaning to the Lisp evaluator.
Values and expressions
The LispBM evaluator transforms expressions into values. For
instance, the expression (+ 1 2) is evaluated to the value
3.
| Example | Result |
|
|
In LispBM the distinction between expressions and values is often blurred. For example, it is possible to write a function that returns a result that can itself be interpreted as code
| Example | Result |
|
|
|
|
The result of evaluating (mk-code 10) is the list
containing a +, 10 and 1. This
list is the value that (mk-code 10) evaluates to. Now, the
result of (mk-code 10), since it is valid lisp, can be
evaluated.
| Example | Result |
|
|
In most cases this is quite natural and our functions will result in, Strings, lists and numbers that are easily and naturally understood as values.
Still, it is worthwhile to remember that values can be expressions and expressions can be values.
Errors
Some times evaluation is impossible. This could be because the
program is malformed, a type mismatch or a division by zero (among many
other possibilities). Errors terminate the evaluation of the expression.
To recover from an error the programmer needs to explicitly
trap it.
| Example | Result |
|
|
Environments
LispBM expressions are evaluated in relation to a global and a local environment. An environment is a key-value store where the key is a lisp symbol and the value is any lisp value.
The rest of this section will now explain the meaning of LBM programs by informally showing expressions, what values they evaluate into and how they change and depend on the environments
Atoms
Some atoms, such as Numbers, Strings and byte arrays cannot be further evaluated.
| Example | Result |
|
|
|
|
|
|
Symbols evaluate by a lookup in the environment. First, the local
environment is searched for a binding of the symbol. If unable to find a
binding in the local environment, the global environment is searched. If
unable to find a binding in the global environment as well, the runtime
system attempts to dynamically load a binding using a system provided
callback function. If all of the above fails to provide a value a
variable_not_bound error is produced.
Composite forms
A composite form, such as (e1 ... eN) is evaluated in
different ways depending on what e1 is. There are three
major categories that e1 can fall into. Either
e1 is something that represents a function and
(e1 ... eN) is a function application. Or e1
is a so-called special-form that form the core of the LBM
language. Or lastly, e1 is anything else and the composite
form is malformed and will ultimately result in an error.
The composite form (e1 ... eN) is evaluated by first
checking if e1 is a special form or not. if e1
is a special form the composite form is passed to a special-form
evaluator. if e1 is not a special form, the composite form
is evaluated as a function application. These two major branches of
composite form evaluation are described below.
Special form evaluation
Below are a selection of basic special-forms in lispBM together with their evaluation process
(quote a) evaluates to a for
any a(define s e), e
is evaluated into v and the global environment is augmented
with the pair (s . v)(lambda params body) is
evaluated into '(closure params body env). env` is the
local environment there the lambda expression is evaluated.(if e1 e2 e3) is evaluated by
evaluating e1 into v1 if v1 is
nil, e3 is evaluated otherwise e2 is
evaluated.(progn e1 e2 ... eN) is
evaluated by evaluating e1 then e2 and so on
until eN. The value v that eN
evaluats into is the value (progn e1 e2 ... eN) evaluates
to.(and e1 e2 ... eN) evaluates the
eI expressions from left to right as long as they result in
a non-nil value.(or e1 e2 ... eN) evaluates the
eI expressions from left to right until there is a non-nil
result.and, or, progn and
if evaluates expressions in sequence. if
evaluates first the condition expression and then either the true or
false branch. progn evaluates all of the expressions in
sequence. In the case of and, or,
progn and if, the constituent expressions are
all evaluated in the same local environment. Any extensions to the local
environment performed by an expression in the sequence is only visible
within that expression itself.
(let ((s1 e1) (s2 e2) ... (sN eN) e) eI are evaluated in
order into vI. The local environment is extended with
(sI . vI). sI is visible in eJ
for J >= I. e is then evaluated in the
extended local environment.(setq s e) is evaluated by first
evaluating e into v. The environments are then
scanned for a binding of s. local environment is searched
first followed by global. If a binding of s is found it is
modified into (s . v).If no binding of s is found when evaluating
(setq s e) a variable_not_bound error is
triggered.
Function application evaluation
The evaluation strategies explained here are applied to composite
expressions of the (e1 ... eN) form where e1
does not fall into the category of "special forms".
In LispBM an (e1 ... eN) is evaluated by first
evaluating e1. This is because depending on what kind of
function object e1 evaluates into, the application if
evaluated in different ways.
e1 should evaluate into a closure, a
"fundamental operation" or an "extension". fundamental operations and
extensions take their arguments passed on the stack while a closure is
applied in an environment extended with the argument value bindings.
Depending on the value of e1 the arguments are either
evaluated left to right and the results are pushed onto the stack, or
they are evaluated left to right and used to extend the environment.
The quote and the quasiquote
The LBM parser (Reader) expands usages of the character sequences:
', `, , and ,@. The
' as in 'a is expanded into
(quote a) for any a. The remaining `,
, and ,@ are expanded into applications of
quote, append and cons using the
algorithms described by Bawden in quasiquotation
in lisp.
TODO: Finish section.
To differentiate from Imperative and Functional, think of imperative programs as sequences of operations that update a state and functional programs as transformations of values through application of compositions of functions. Functional programming languages often let functions be values, which means that functions can be stored in lists, returned from other functions and so on
LispBM is a multiparadigm programming language. Most languages are a mix of functional and imperative and differ in what style it makes most convenient. At one end of this spectrum we find C which makes imperative easy and functional hard, and in the other end Haskell with the opposite favoritism. In LispBM we try to not unfairly favour any particular style over the other.
Picking a functional or an imperative style does have consequences though. Functional LispBM programs have properties such as persistence of data, that can be broken using the imperative part of the language.
With the imperative features of the language it is also in some places possible to peek under the hood of the runtime system. you can detect when and how environments are shared or copied for example. Please avoid exploiting the power of destructive updates for evil purposes.
The list below shows imperative operations from the core of LispBM. In the extension libraries there are many more of the kind.
Adds up an arbitrary number of values. The form of a +
expression is (+ expr1 ... exprN).
| Example | Result |
|
|
|
|
|
|
|
|
Subtract an arbitrary number of values from a value. The form of a
- expression is (- expr1 ... exprN).
| Example | Result |
|
|
|
|
|
|
|
|
Multiplying an arbitrary number of values. The form of a
* expression is (* expr1 ... exprN).
| Example | Result |
|
|
|
|
|
|
|
|
Division. The form of a / expression is
(/ expr1 ... exprN). The resulting type is the same as the
inputs (after their types have been promoted of course).
| Example | Result |
|
|
|
|
|
|
|
|
Modulo operation. The form of a mod expression is
(mod expr1 exp2). The modulo operation is not generalised
to n arguments.
| Example | Result |
|
|
|
|
|
|
Integer division operation. Like normal division except if the result
is a floating point value it is cast to an integer, which floors the
result. The form of a // expression is
(// expr1 ... exprN). Can be used as a elegant complement
to mod, with // returning the quotient and
mod returning the remainder of a division.
| Example | Result |
|
|
|
|
Compare values for equality. The eq operation implements
structural equality. The form of an 'eqexpression is(eq
expr1 ... exprN)`. Structural equality means that the values must have
the identical in memory representations to be considered equal.
| Example | Result |
|
|
|
|
|
|
|
|
|
|
|
|
not-eq implements the negation of eq. In other words,
(not-eq a b c) evaluates to the same result as
(not (eq a b c)).
| Example | Result |
|
|
|
|
|
|
|
|
|
|
|
|
The = operation can only be used on numerical arguments.
If you know you are comparing numbers, it will be more efficient to use
=. An important difference between eq and
= is that = compare the numerical values of
the arguments. A 3 is a 3 independent of them being different types.
eq on the other hand compares the representations of the
arguments exactly and they must match in structure, type and value to be
considered equal.
| Example | Result |
|
|
|
|
|
|
|
|
Greater than comparison. A greater than comparison has the form
(> expr1 ... exprN) and evaluates to t if
expr1 is greater than all of expr2 ... exprN.
| Example | Result |
|
|
|
|
|
|
|
|
Less than comparison. A less than comparison has the form
(> expr1 ... exprN) and evaluates to t if
expr1 is less than all of expr2 ... exprN.
| Example | Result |
|
|
|
|
|
|
|
|
Greater than or equal comparison. A greater than comparison has the
form (>= expr1 ... exprN) and evaluates to
t if expr1 is greater than or equal to all of expr2 ...
exprN.
| Example | Result |
|
|
|
|
|
|
|
|
|
|
Less than or equal comparison. A less than or equal comparison has
the form (<= expr1 ... exprN) and evaluates to
t if expr1 is less than or equal to all of expr2 ...
exprN.
| Example | Result |
|
|
|
|
|
|
|
|
|
|
Special form
Boolean and operation between n arguments. The form of
an and expression is (and expr1 ... exprN).
This operation treats all non-nil values as true. Boolean
and is "short-circuiting" and only evaluates until a false
is encountered.
| Example | Result |
|
|
|
|
|
|
Special form
Boolean or operation between n arguments. The form of an
or expression is (or expr1 ... exprN). This
operation treats all non-nil values as true. Boolean or is
"short-circuiting" and only evaluates until a true is encountered.
| Example | Result |
|
|
|
|
|
|
|
|
|
|
Boolean not takes one argument. The form of a
not expression is (not expr). All non-nil
values are considered true.
| Example | Result |
|
|
|
|
|
|
the list? predicate is true for all lists, empty (nil)
or not.
| Example | Result |
|
|
|
|
|
|
|
|
|
|
|
|
the number? predicate is true for all numbers.
| Example | Result |
|
|
|
|
|
|
|
|
|
|
|
|
The shift left operation takes two arguments. The first argument is a value to shift and the second argument is the number of bit positions to shift the value.
| Example | Result |
|
|
|
|
|
|
The shift right operation takes two arguments. The first argument is a value to shift and the second argument in the number of bit positions to shift the value.
| Example | Result |
|
|
|
|
|
|
Performs the bitwise and operation between two values. The type of the result is the same type as the first of the arguments.
| Example | Result |
|
|
Performs the bitwise or operation between two values. The type of the result is the same type as the first of the arguments.
| Example | Result |
|
|
Performs the bitwise exclusive or operation between two values. The type of the result is the same type as the first of the arguments.
| Example | Result |
|
|
Performs the bitwise negation operations on a value. The result is of same type as the argument.
| Example | Result |
|
|
Represents the empty list. The nil value is also considered to be
false by conditionals. nil is a symbol but it cannot be
redefined and will always evaluate to itself.
| Example | Result |
|
|
|
|
|
|
All non nil values are considered true in conditionals.
t should be used in cases where an explicit true makes
sense. t is a symbol but it cannot be redefined and will
always evaluate to itself.
| Example | Result |
|
|
|
|
|
|
false is an alias for nil.
| Example | Result |
|
|
|
|
|
|
true is an alias for t.
| Example | Result |
|
|
|
|
|
|
Code and data share the same representation, it is only a matter of how you look at it. The tools for changing view, or interpretation, are the quotation and quasiquotation operations.
Special form
When the ' quote operator is encountered by the reader
it is removed and the code to the right is wrapped in
(quote ...). Evaluating a quoted expression
(quote a) results in a unevaluated.
| Example | Result |
|
|
|
|
|
|
The backwards tick operator ` is called the quasiquote.
It is similar to ' but allows splicing in results of
computations using the , and the ,@ operators.
The result of '(+ 1 2) and `(+ 1 2) are
similar in effect. Both result in the result value of
(+ 1 2), that is a list containing +, 1 and 2. When
`(+ 1 2) is read into the heap it is expanded into the
expression
(append (quote (+)) (append (quote (1)) (append (quote (2)) (quote nil))))
which evaluates to the list (+ 1 2).
| Example | Result |
|
|
|
|
|
|
The comma is used to splice the result of a computation into a quasiquotation.
The expression `(+ 1 ,(+ 1 1)) is expanded by the
reader into
(append (quote (+)) (append (quote (1)) (append (list (+ 1 1)) (quote nil)))).
Evaluating the expression above results in the list
(+ 1 2).
| Example | Result |
|
|
The comma-at operation is used to splice in the result of a computation (that returns a list) into a list when quasiquoting.
| Example | Result |
|
|
The identity function takes one argument which it directly returns.
The form of an identity expression is
(identity expr), where expr is an arbitrary lisp
expression. (identity expr) is a function application so
all normal function application rules apply. The most important property
of function applications in this case is that the argument is evaluated
which differentiates identity from quote which
returns the argument unevaluated.
| Example | Result |
|
|
|
|
|
|
|
|
|
|
rest-args are related to user defined functions. As such
rest-args is also given a brief explanation in the section
about the lambda.
rest-args is a mechanism for handling optional arguments
in functions. Say you want to define a function with 2 arguments and an
optional 3rd argument. You can do this by creating a 3 argument function
and check if argument 3 is valid or not in the body of the function
| Example | Result |
|
|
|
|
|
|
This approach works well if your function has 1,2 or some other small
number of optional arguments. However, functions with many optional
arguments will look messy at the application site,
(my-fun 1 2 nil nil nil nil 32 nil kurt-russel) for
examples
Functions you create, using lambda or defun, do actually take an arbitrary number of arguments. In other words, it is no error to pass in 5 arguments to a 2 argument defun or lambda function. The extra arguments will by default just be ignored.
| Example | Result |
|
|
|
|
|
|
all of those extra arguments, 100 200 300 400 500 passed
into my-fun are ignored. But if we want to, we can access these extra
arguments through the rest-args operation.
| Example | Result |
|
|
|
|
|
|
rest-args gives a clean looking interface to functions
taking arbitrary optional arguments. Functions that make use of
rest-args must, however, be written specifically to do so
and are themself responsible for the figuring out the positional
semantics of extra arguments.
One way to explicitly carry the semantics of an optional argument
into the function body is to add optional arguments as key-value pairs
where the key states the meaning. Then rest-args becomes
essentially an association list that you query using assoc.
For example:
| Example | Result |
|
|
|
|
|
|
|
|
The rest-args operation also, itself, takes an optional
numerical argument that acts as an index into the list of rest
arguments.
| Example | Result |
|
|
|
|
|
|
|
|
The set form is used to change the value of some
variable in an environment. You can use set to change the
value of a global definition or a local definition. An application of
the set form looks like
(set var-expr val-expr) where var-expr should
evaluate to a symbol. The val-expr is evaluated before
rebinding the variable. set returns the value that
val-expr evaluates to.
| Example | Result |
|
|
set works in local environments too such as in the body
of a let or in a progn-local variable created
using var.
| Example | Result |
|
|
See setq for a version which does't
evaluate var-expr, similarly to define.
setvar is an alias of set.
A definition in the global environment can be removed using undefine.
The form of an undefine expression is (undefine name-expr)
where name-expr should evaluate to a symbol (for example
'apa).
| Example | Result |
|
|
It is also possible to undefine several bindings at the same time by providing a list of names.
| Example | Result |
|
|
Evaluate data as an expression. The data must represent a valid
expression. The form of an eval expression is
(eval expr). An optional environment can be passed in as
the first argument: (eval env-expr expr).
| Example | Result |
|
|
|
|
|
|
|
|
Evaluate a list of data where each element represents an expression.
The form of an eval-program expression is
(eval-program program-expr). A program-expr is
a list of expressions where each element in the list can be evaluated by
eval.
Note that eval-program can not take any extra environment argument.
| Example | Result |
|
|
|
|
|
|
|
|
An apply expression has the form
(apply fun-expr args-expr), and it applies
args-expr to fun-expr. It can loosely be
emulated by (eval (cons fun-expr args-expr)), except for
the property that the elements of args-expr aren't
evaluated when you use apply. args-expr itself
is of course evaluated, but given the emulated form, the individual
elements of the list would be evaluated again. This is a problem when
your argument list may include symbols or any other values which change
when evaluated.
Note that fun-expr doesn't necessarily have to be a user
defined function, but can be any value which can be applied, that is, a
value which can be the first item in a list expression:
(fun ...). This includes (but is not limited to) user
define functions (which end up as closure values), built-in functions,
extensions, macros, and even special
forms (but see Footgun: Special forms below).
| Example | Result |
|
|
|
|
|
|
|
|
|
|
|
|
Footgun: Special forms
Special forms are built in functions which
control how their arguments are evaluated. This means that their
implementations has custom logic for how to evaluate the arguments,
which has the unfortunate consequence that to apply a list of arguments
to them "without evaluating" said arguments would require specific logic
for each special form's implementation. Given LispBM's nature of running
in an embedded environment where binary size is a luxury this becomes
highly impractical. Therefore the argument list is passed on unmodified
to special forms. So (apply def '(a 'symbol)) is
precisely equivalent to
(eval (cons def '(a 'symbol))), 'symbol will
be evaluated!.
This is most noticeable with and and or.
They are special forms due to their short-circuiting behavior, where
later arguments may not be evaluated depending on the previous
arguments. Given that you only given them the symbol t or
nil their special form status isn't a problem when it comes
to apply since those symbols evaluate to themselves. The
problem appears when you give and or or other
"unstable" values like arbitrary symbols. Since they're special forms
they will evaluate them another time.
This behavior may be changed in the future, so please avoid writing
programs which depend on this behavior. For the case of and
and or you can define your own function which re-implement
their behavior, which would be subject to apply's
non-evaluating property (see example below).
| Example | Result |
|
|
|
|
|
|
|
|
Parses a string resulting in either an expression or the
read_error in case the string can not be
parsed into an expression. The form of a read expression is
(read string).
| Example | Result |
|
|
|
|
|
|
Parses a string containing multiple sequenced expressions. The
resulting list of expressions can be evaluated as a program using
eval-program. The form of a read-program
expression is (read-program string).
| Example | Result |
|
|
Parses and evaluates a program incrementally.
read-eval-program reads a top-level expression then
evaluates it before reading the next.
| Example | Result |
|
|
read-eval-program supports the @const-start and @const-end annotations which move all
global definitions created in the program to constant memory
(flash).
| Example | Result |
|
|
The type-of function returns a symbol that indicates
what type the argument is. The form of a type-of expression
is (type-of expr).
| Example | Result |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The sym2str function converts a symbol to its string
representation. The resulting string is a copy of the original so you
cannot destroy built in symbols using this function.
| Example | Result |
|
|
|
|
The str2sym function converts a string to a symbol.
| Example | Result |
|
|
The sym2u function returns the numerical value used by
the runtime system for a symbol.
| Example | Result |
|
|
|
|
The u2sym function returns the symbol associated with
the numerical value provided. This symbol may be undefined in which case
you get as result a unnamed symbol.
| Example | Result |
|
|
|
|
The gc function runs the garbage collector so that it
can reclaim values from the heap and LBM memory that are no longer
needed.
Note that one should not need to run this function. GC is run automatically when needed.
| Example | Result |
|
|
Special forms are like functions except that they choose what arguments are evaluated. This is in contrast to "normal functions", usually referred to as "forms" in other Lisps, which always evaluate every argument they are given.
Note that some special forms are located in other sections to improve the document flow. These are:
Special form
Conditionals are written as
(if cond-expr then-expr else-exp). If the
cond-expr evaluates to nil
the else-expr will be evaluated. For any other value of
cond-expr the then-expr will be evaluated.
| Example | Result |
|
|
|
|
Special form
cond is a generalization of if to discern
between n different cases based on boolean expressions. The form of a
cond expression is:
(cond ( cond-expr1 expr1) (cond-expr2 expr2) ... (cond-exprN exprN)).
The conditions are checked from first to last and for the first
cond-exprN that evaluates to true, the corresponding
exprN is evaluated.
If no cond-exprN evaluates to true, the result of the
entire conditional is nil.
| Example | Result |
|
|
|
|
Special form
You create an anonymous function with lambda. The function can be
given a name by binding the lambda expression using
define or let. A lambda
expression has the form (lambda param-list body-expr).
| Example | Result |
|
|
|
|
You can give more arguments to a function created using lambda. The
extra arguments can be accessed in the lambda body by calling the
rest-args function which gives back auxiliary arguments as
a list.
| Example | Result |
|
|
|
|
rest-args takes an optional numerical argument that is
used to index into the list containing the rest of the arguments.
| Example | Result |
|
|
|
|
|
|
|
|
Special form
A lambda expression evaluates into a closure
which is very similar to a lambda but extended
with a captured environment for any names unbound in the param-list
appearing in the body-expr. The form of a closure is
(closure param-list body-exp environment).
| Example | Result |
|
|
|
|
|
|
Special form
Local environments are created using let. The let binding in lispbm
allows for mutually recursive bindings. The form of a let is
(let list-of-bindings body-expr) and evaluating this
expression means that body-expr is evaluted in an environment extended
with the list-of-bindings.
| Example | Result |
|
|
|
|
You can deconstruct composite values while let binding.
| Example | Result |
|
|
|
|
Special form
loop allows to repeatedly evaluate an expression for as long as a
condition holds. The form of a loop is
(loop list-of-local-bindings condition-exp body-exp).
The list-of-local-bindings are very similar to how
let works, just that here the body-exp is
repeated.
| Example | Result |
|
|
Special form
You can give names to values in a global scope by using define. The
form of define is (define name expr). The given expression
is evaluated and the result is stored in the global environment under
name. In lispbm you can redefine already defined values. It
is also possible to remove bindings from the global environment, see undefine.
| Example | Result |
|
|
Special form
The setq special-form is similar to set and
to setvar but expects the first argument to be a symbol.
The first argument to setq is NOT evaluated.
| Example | Result |
|
|
Just like set and setvar, setq
can be used on variables that are bound locally such as in the body of a
let or a progn-local variable created using
var.
| Example | Result |
|
|
Special form
The progn special form allows you to sequence a number of
expressions. The form of a progn expression is
(progn expr1 ... exprN).
The evaluation result of a progn sequence is the value that the last
exprN evaluated to. This is useful for sequencing of
side-effecting operations.
| Example | Result |
|
|
|
|
Special form
The curlybrace { syntax is a short-form (syntactic
sugar) for (progn. The reader replaces occurrences of
{ with (progn. The { should be
closed with an }.
These two programs are thus equivalent:
(progn
(define a 10)
(define b 20)
(+ a b))And
{
(define a 10)
(define b 20)
(+ a b)
}The closing curlybrace } should be used to close an
opening { but purely for esthetic reasons. The
} is treated identically to a regular closing parenthesis
).
The opening { and closing } curlybraces are
used as a short-form for progn-blocks of sequences
expressions.
Special form
The var special form allows local bindings in a progn expression. A
var expression is of the form (var symbol expr) and the symbol
symbol is bound to the value that expr
evaluates to withing the rest of the progn expression.
| Example | Result |
|
|
|
|
You can deconstruct composite value while var binding.
| Example | Result |
|
|
|
|
Special form
trap lets you catch an error rather than have the
evaluation context terminate. The form of a trap expression is
(trap expr). If expr crashes with an error e
then (trap expr) evaluates to (exit-error e).
If expr successfully runs and returns r, then
(trap expr) evaluates to (exit-ok r).
| Example | Result |
|
|
|
|
trap catches any error except for fatal errors. A fatal
error will still lead to the context being terminated.
Lists are built using cons cells. A cons cell is represented by the
lbm_cons_t struct in the implementation and consists of two fields named
the car and the cdr. There is no special
meaning associated with the car and the cdr
each can hold a lbm_value. See cons and
list for two ways to create structures of cons cells
on the heap.

A cons cell can be used to store a pair of values. You create a pair
by sticking a value in both the car and cdr field of a cons cell using
either '(1 . 2) or (cons 1 2).

A list is a number of cons cells linked together where the car fields
hold values and the cdr fields hold pointers (the last cdr field is
nil). The list below can be created either as '(1 2 3) or
as (list 1 2 3).

Use car to access the car field of a cons
cell. A car expression has the form
(car expr).
Taking the car of a number of symbol type is in general
a type_error.
| Example | Result |
|
|
|
|
first is an alternative name for the car
operation. Use first to access the first element of a list
or pair. A first expression has the form
(first expr).
| Example | Result |
|
|
|
|
Use cdr to access the cdr field of a cons
cell. A cdr expression has the form
(cdr expr).
| Example | Result |
|
|
|
|
rest is an alternative name for the cdr
operation. Use rest to access all elements except the first
one of a list, or to access the second element in a pair. A
rest expression has the form (rest expr).
| Example | Result |
|
|
|
|
The cons operation allocates a cons cell from the heap
and populates the car and the cdr fields of
this cell with its two arguments. The form of a cons
expression is (cons expr1 expr2). To build well formed
lists the innermost cons cell should have nil in the cdr field.
| Example | Result |
|
|
|
|
|
|
|
|
The dot, ., operation creates a pair. The form of a dot
expression is (expr1 . expr2). By default the evaluator
will attempt to evaluate the result of (expr1 . expr2)
unless it is prefixed with '.
| Example | Result |
|
|
|
|
The list function is used to create proper lists. The
function takes n arguments and is of the form
(list expr1 ... exprN).
| Example | Result |
|
|
Computes the length of a list. The length function takes
one argument and is of the form (length expr).
| Example | Result |
|
|
The range function computes a list with integer values
from a range specified by its endpoints. The form of a range expression
is (range start-expr end-expr). The end point in the range
is excluded.
| Example | Result |
|
|
|
|
|
|
The append function combines two lists into a longer
list. An append expression is of the form
(append expr1 expr2).
| Example | Result |
|
|
Index into a list using the ix function. The form of an
ix expression is (ix list-expr index-expr).
Indexing starts from 0 and if you index out of bounds the result is nil.
A negative index accesses values starting from the end of the list.
| Example | Result |
|
|
|
|
Destructively update an element in a list. The form of a
setix expression is
(setix list-expr index-expr value-expr). Indexing starts
from 0 and if you index out of bounds the result is nil. A negative
value -n will update the nth value from the end of the list.
| Example | Result |
|
|
|
|
Check if a value is included in list. The form of an
member expression is
(member value-expr list-expr). Equality is checked
structurally, in the same way as eq,
meaning if you're checking numbers the types must match (see the
following examples).
| Example | Result |
|
|
|
|
|
|
This function can be used as a readable and efficient way of checking
if a value is in some constant set of values. This often results in
significantly less code than unrolling it as a series of eqs inside an or expression.
| Example | Result |
|
|
|
|
|
|
The setcar is a destructive update of the car field of a
cons-cell.
| Example | Result |
|
|
|
|
The setcdr is a destructive update of the cdr field of a
cons-cell.
| Example | Result |
|
|
|
|
take creates a list containing the n first
elements of another list. The form of a take expression is
(take list-exp n-exp).
| Example | Result |
|
|
drop creates a list from another list by dropping the
n first elements of that list. The form of a
drop expression is (drop list-exp n-exp).
| Example | Result |
|
|
reverse creates a list containing the same elements as
an existing list but in reverse order. The form of a
reverse expression is (reverse list-exp).
| Example | Result |
|
|
rotate creates a list containing the same elements as an
existing list but rotated some number of step along a direction. The
form of a rotate expression is
(rotate list-exp dist-expr). The sign of the value
dist-expr evaluates to, decides direction of rotation.
| Example | Result |
|
|
|
|
|
|
|
|
|
|
Rotating a list in the negative direction is slightly faster than rotating in the positive direction. The chart below shows the time 1 Million 3 step rotations take in each direction at varying list lengths. The data is collected on x86.

merge merges two lists that are ordered according to a
comparator into a single ordered list. The form of a merge
expression is
(merge comparator-exp list-exp1 list-exp2).
| Example | Result |
|
|
sort orders a list of values according to a comparator.
The sorting algorithm used is an in-place merge-sort. A copy of the
input list is created at the beginning of the sort to provide a
functional interface from the user's point of view. The form of a sort
expression is (sort comparator-exp list-exp)
| Example | Result |
|
|
Association lists (alists) are, just like regular lists, built out of cons-cells. The difference is that an alist is a list of pairs where the first element in each par can be thought of as a key and the second element can be thought of as the value. So alists implement a key-value lookup structure.
(list '(1 . horse) '(2 . donkey) '(3 . shark)) is an
example of an alist with integer keys and symbol values.
The acons form is similar to cons, it
attaches one more element onto an alist. The element that is added
consists of a key and a value so acons takes one more
argument than cons. The form of an acons
expression is (acons key-expr val-expr alist-expr). The
alist-expr should evaluate to an alist but there are no
checks to ensure this.
Example that adds the key 4 and associated value
lemur to an existing alist.
| Example | Result |
|
|
The assoc function looks up the first value in an alist
matching a given a key. The form of an assoc expression is
(assoc alist-expr key-expr)
| Example | Result |
|
|
The cossa function looks up the first key in an alist
that matches a given value. The form of an cossa expression
is (cossa alist-expr value-expr)
| Example | Result |
|
|
The setassoc function destructively updates a key-value
mapping in an alist. The form of a setassoc expression is
(setassoc alist-expr key-expr value-expr). If you assign a
key which doesn't exist in the original alist, it is left unchanged,
while another association pair is added to the returned list.
| Example | Result |
|
|
|
|
|
|
Create an array of bytes. The form of a bufcreate
expression is (bufcreate size-expr).
| Example | Result |
|
|
|
|
Alternatively a buffer can be allocated from a compactable memory region (defrag mem).
| Example | Result |
|
|
|
|
For more information about defragmentable memory see Defragmentable memory.
Returns the size of a buffer in number of bytes. The form of an
buflen expression is (buflen buf-expr) where
buf-expr has to evaluate into a buffer.
| Example | Result |
|
|
Read a value from a buffer. The contents of a buffer can be read as a
sized integer or unsigned value using as many bytes from the buffer as
the X portion of the function name implies. The form of a bufget
expression is (bufget-[X] buf-expr ix-expr) where
ix-expr evaluates to a number indicating the byte position
to start reading from.
| Example | Result |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The bufset functions performs a destructive updates to a
buffer. The form of a bufset expression is
(bufset-[X] buf-expr ix-expr val-expr) where
ix-expr evaluates to a number indicating where in the
buffer to start writing and val-expr is the value to
write.
| Example | Result |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
To clear a byte array the function bufclear can be used
(bufclear arr optByte optStart optLen) Where arr is the
byte array to clear, optByte is the optional argument of what to clear
with (default 0), optStart is the optional argument of which position to
start clearing (default 0) and optLen is the optional argument of how
many bytes to clear after start (default the entire array). Example:
| Example | Result |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Byte buffer literals can be created using the [ and
] syntax to enclose values to initialize the array with.
The [ and ] syntax is complete resolved in the
reader and thus cannot contain arbitrary lisp terms. the values listed
between the [ and the ] must be literals!
The form of the [ and ] syntax is
[ val1 ... valN ].
| Example | Result |
|
|
LispBM supports arrays of arbitrary lisp values (including other arrays).
An array literal are specified as a sequence of lisp values between
[| and |]. Values in a literal array are not
evaluated.
| Example | Result |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
All arrays have an associated heap-cell that acts as a liaison in relation to the garbage collector. When garbage collection frees the liaison, it also frees the array data in buffers and arrays memory (lbm_memory).

array takes n arguments and creates an array holding
those arguments as values. The form of an array expression
is (array expr1 ... exprN).
| Example | Result |
|
|
|
|
Allocate an array with mkarray. Arrays are allocated in
arrays and byte buffer memory. The form an mkarray
expression is (mkarray num-expr).
Note that there is currently no literal syntax for arrays.
The example below allocates an array in "lbm_memory" (arrays and byte-buffer memory).
| Example | Result |
|
|
Index into an array using the ix function. The form of
an ix expression is
(ix array-expr index-expr). Indexing starts from 0 and if
you index out of bounds the result is nil. A negative index accesses
values starting from the end of the array.
| Example | Result |
|
|
|
|
Destructively update an element in an array. The form of a
setix expression is
(setix arr-expr index-expr value-expr). Indexing starts
from 0 and if you index out of bounds the result is nil. A negative
value -n will update the nth value from the end of the list.
| Example | Result |
|
|
|
|
LBM has two types of memory, the HEAP and the LBM_MEMORY. Lists and pairs are all stored on the heap. Arrays and large values (such as 64bit numbers are stored on LBM_MEMORY. The HEAP has a nice property that all allocations on it are the same size and therefore the HEAP is immune to the problems caused by fragmentation. On LBM_MEMORY arbitrarily sized arrays can be allocated and fragmentation can cause an allocation to fail even though there is enough free bytes.
One way to resolve the fragmentation problem is to use a compacting garbage collector. We have opted to not use a compacting garbage collector on the LBM_MEMORY as it is quite complicated. It is extra complicated given how this memory is a shared resource between C extensions and the lisp runtime system.
Our solution is to allow the programmer to create a memory block inside of the LBM_MEMORY in which we will run a defragmentation routine when needed. The defragmentable memory can only be used to allocate non-zero sized byte arrays on the lisp side. The idea is that the programmer calculates the maximum size of simultaneously used arrays (+ the overhead of 3 words per allocation) needed for a small critical set of arrays used in the program and allocates a defragmentable memory of that size.
The LBM (non-compacting) gabage collector frees arrays from a defragmentable memory area automatically. An allocation in the defragmentable memory area that fails triggers garbage collection followed by compaction (if needed).
dm-create creates a region of defragmentable memory for
bytearrays within LBM memory. The form of a dm-create
expression is (dm-create size-expr).
| Example | Result |
|
|
dm-alloc is used to allocate a byte-array from a region
of defragmentable memory. The form of a dm-alloc expression
is (dm-alloc DM-expr size-expr). where DM-expr
evaluates to the defragmentable region to allocate from and
size-expr is the number of bytes to allocate. Each
allocation uses up 12 extra bytes of header that you do not include in
size-expr.
| Example | Result |
|
|
|
|
Special form
Pattern-matching is expressed using match. The form of a match
expression is (match expr (pat1 expr1) ... (patN exprN)).
Pattern-matching compares the shape of an expression to each of the
pat1 ... patN and evaluates the expression
exprM of the pattern that matches. In a pattern you can use
a number of match-binders or wildcards: _, ?,
?i,?u,?float.
| Example | Result |
|
|
The no_match symbol is returned from pattern matching if
no case matches the expression.
_.The underscore pattern matches anything.
| Example | Result |
|
|
The ? pattern matches anything and binds that anything
to variable. Using the ? pattern is done as
(? var) and the part of the expression that matches is
bound to var.
| Example | Result |
|
|
Patterns used in a match expressions can be augmented with a boolean
guard to further discern between cases. A pattern with a guard is of the
form (pattern-expr guard-expr expr). A pattern with a
guard, matches only if the pattern structurally matches and if the
guard-expr evaluates to true in the match environment.
| Example | Result |
|
|
|
|
The concurrency support in LispBM is provided by the set of
functions, spawn, wait, yield and
atomic described below. Concurrency in LispBM is scheduled
by a round-robin scheduler that splits the runtime system evaluator
fairly (with caveats, below) between all running processes.
When a process is scheduled to run, made active, it is given a quota of evaluator "steps" to use up. The process then runs until that quota is exhausted or the process itself has signaled it wants to sleep by yielding or blocking (for example by waiting for a message using the message passing system).
A process can also request to not be "pre-empted" while executing a
certain expression by invoking atomic. One should take care
to make blocks of atomic code as small as possible as it disrupts the
fairness of the scheduler. While executing inside of an atomic block the
process has sole ownership of the shared global environment and can
perform atomic read-modify-write sequences to global data.
Use spawn to launch a concurrent process. Spawn takes a
closure and arguments to pass to that closure as its arguments. The form
of a spawn expression is
(spawn opt-name opt-stack-size closure arg1 ... argN).
Each process has a runtime-stack which is used for the evaluation of expressions within that process. The stack size needed by a process depends on 1. How deeply nested expressions evaluated by the process are. 2. Number of recursive calls (Only if a function is NOT tail-recursive). 3. The Number of arguments that functions called by the process take.
Having a stack that is too small will result in a
out_of_stack error.
The default stack size is 256 words (1K Bytes) and should be more than enough for reasonable programs. Many processes will work perfectly fine with a lot less stack. You can find a good size by trial and error.
Use spawn-trap to spawn a child process and enable
trapping of exit conditions for that child. The form of a
spawn-trap expression is
(spawn-trap opt-name opt-stack-size closure arg1 .. argN).
If the child process is terminated because of an error, a message is
sent to the parent process of the form
(exit-error tid err-val). If the child process terminates
successfully a message of the form (exit-ok tid value) is
sent to the parent.
| Example | Result |
|
|
|
|
Use self to obtain the thread-id of the thread in which
self is evaluated. The form of a self
expression is (self). The thread id is of an integer
type.
| Example | Result |
|
|
Use wait to wait for a spawned process to finish. The
argument to wait should be a process id. The
wait blocks until the process with the given process id
finishes. When the process with with the given id finishes, the wait
function returns True.
Be careful to only wait for processes that actually exist and do finish. Otherwise you will wait forever.
To put a process to sleep, call yield. The argument to
yield is number indicating at least how many microseconds
the process should sleep.
| Example | Result |
|
|
'sleep' puts a thread to sleep and differs from 'yield' only in the argument. 'sleep' takes a floating point number indicating how long in seconds the thread should sleep at least.
| Example | Result |
|
|
Special form
atomic can be used to execute a LispBM one or more
expression without allowing the runtime system to switch process during
that time. atomic is similar to progn with the addition of
being uninterruptible.
| Example | Result |
|
|
The exit-ok function terminates the thread in a
"successful" way and returns a result specified by the programmer. The
form of an exit-ok expression is
(exit-ok value). If the process that calls
exit-ok was created using spawn-trap a message
of the form (exit-ok tid value) is be sent to the parent of
this process.
The exit-error function terminates the thread with an
error specified by the programmer. The form of an
exit-error expression is (exit-error err_val).
If the process that calls exit-error was created using
spawn-trap a message of the form
(exit-error tid err_val) is sent to the parent of this
process.
The kill function allows you to force terminate another
thread. It has the signature
(kill thread-id-expr val-expr), where
thread-id-expr is the thread that you want to terminate,
and val-expr is the final result the thread dies with.
| Example | Result |
|
|
The val-expr can be observed if the thread exit status
is captured using spawn-trap
| Example | Result |
|
|
The val-expr could be used to communicate to a thread
monitor that the thread it monitors has been intentionally but
externally killed.
Messages can be sent to a process by using send. The
form of a send expression is (send pid msg).
The message, msg, can be any LispBM value.
Special form
To receive a message use the recv command. A process
will block on a recv until there is a matching message in
the mailbox. The recv syntax is very similar to match.
| Example | Result |
|
|
Special form
Like recv, recv-to is used to
receive messages but recv-to takes an extra timeout
argument. It then receives a message containing the symbol
timeout after the timeout period ends.
The form of an recv-to expression is
(recv-to timeout-secs (pattern1 exp1) ... (patternN expN))
| Example | Result |
|
|
| Example | Result |
|
|
Lisp values can be "flattened" into an array representation. The flat representation of a value contains all information needed so that the value can be recreated, "unflattened", in another instance of the runtime system (for example running on another microcontroller).
Not all values can be flattened, custom types for example cannot.
Flat values are designed for recursive encoding and decoding each sub-value contains all information about its size either implicitly or explicitly (as is the case with arrays).
multibyte values are stored in network byte order (big endian).
Cons A cons cell is encoded into a byte 0x1 followed by the encoding of the car and then the cdr field of that cons cell.
| cons | car value | cdr value |
|---|---|---|
| 0x1 | M bytes | N bytes |
Symbol as value A symbol value can be flattened. Note that symbol values only make sense locally. A flattened symbol value will only make sense in the same runtime system instance that flattened it.
| symbol-value | value |
|---|---|
| 0x2 | 4 bytes on 32bit, 8 bytes on 64bit |
Symbol as string A symbol can be flattened as a string and thus make sense across runtime system instances.
| symbol-string | string |
|---|---|
| 0x3 | zero terminated C style string |
Byte Arrays Byte arrays can be flattened and the length is stored explicitly.
| byte array | size in bytes | data |
|---|---|---|
| 0xD | 4 bytes | size bytes |
The rest of the atomic types are flattened according to the following:
| type | flat-id | value |
|---|---|---|
| byte | 0x4 | 1 Byte |
| i28 | 0x5 | 4 Bytes |
| u28 | 0x6 | 4 Bytes |
| i32 | 0x7 | 4 Bytes |
| u32 | 0x8 | 4 Bytes |
| float | 0x9 | 4 Bytes |
| i64 | 0xA | 8 Bytes |
| u64 | 0xB | 8 Bytes |
| double | 0xC | 8 Bytes |
| i56 | 0xE | 8 Bytes |
| u56 | 0xF | 8 Bytes |
Note that some of the types are only present of 32Bit runtime systems and some only on 64 bit. i28 is present on 32 bit and i56 on 64 bit. likewise for u28 and u56.
When LispBM unflattens a i56 or u56 on a 32bit system it creates a i64 or u64 in its place.
Symbols as values, are not possible to transfer between runtime systems in general and is even more pointless between a 32 and 64 bit runtime system.
The flatten function takes a value as single argument
and returns the flat representation if successful. A flatten expression
has the form (flatten expr). Note that expr is
evaluated before the flattening. A flat value can be turned back into a
normal lisp value applying unflatten
| Example | Result |
|
|
|
|
|
|
|
|
A flat value is a byte-array containing an encoding of the value.
unflatten converts a flat value back into a lisp value.
Te form of an unflatten expression is
(unflatten flat-value)
| Example | Result |
|
|
|
|
|
|
|
|
LispBM macros are created using the macro keyword. A
macro is quite similar to lambda in
lispBM except that arguments are passed in unevaluated. Together with
the code-splicing capabilities given by quasiquotation, this provides a powerful
code-generation tool.
A macro application is run through the interpreter two times. Once to evaluate the body of the macro on the unevaluated arguments. The result of this first application should be a program. The resulting program then goes through the interpreter again to compute final values.
Given this repeated evaluation, macros are not a performance boost in lispbm. Macros are really a feature that should be used to invent new programming abstractions in cases where it is ok to pay a little for the overhead for benefits in expressivity.
Special form
The form of a macro expression is:
(macro args body)
| Example | Result |
|
|
|
|
|
|
"Call with current continuation" is called call-cc in
LBM. Call with current continuation saves the "current continuation",
which encodes what the evaluator will do next, into an object in the
language. This encoded continuation object behaves as a function taking
one argument.
The call-cc should be given a function, f,
as the single argument. This function, f, should also take
a single argument, the continuation. At any point in the body of
f the continuation can be applied to a value, in essense
replacing the entire call-cc with that value. All
side-effecting operations operations up until the application of the
continuation will take effect.
From within a call-cc application it is possible to bind
the continuation to a global variable which will allow some pretty
arbitrary control flow.
The example below creates a macro for a progn facility
that allows returning at an arbitrary point.
(define do (macro (body)
`(call-cc (lambda (return) (progn ,@body)))))The example using do below makes use of
print which is not a built-in feature of lispBM. There are
just to many different ways a programmer may want to implement
print on an microcontroller. Use the lispBM extensions
framework to implement your own version of print
(do ((print 10)
(return 't)
(print 20)))In the example above only "10" will be printed. Below is an example that conditionally returns.
(define f (lambda (x)
(do ((print "hello world")
(if (= x 1)
(return 't)
nil)
(print "Gizmo!")))))Special form
The form of a call-cc expression is
(call-cc f), where f is a function taking a continuation k.
In code most uses of call-cc will have the form
(call-cc (lambda (k) expr )). When using
call-cc the expr above is allowed to bind k to
a global variable.
Special form
call-cc-unsafe is similar to call-cc in
form. (call-cc-unsafe f) and in code usually as
(call-cc-unsafe (lambda (k) expr)). When using
call-cc-unsafe you must NOT let the k leak out of the scope
created by the enclosing call-cc-unsafe! That is, if
k is used at all, it must be within expr.
Binding k (directly or indirectly) to a global is a
violation of the trust I am putting in you.
If an error occurs while evaluating a program, the process that runs that program is killed. The result of the killed process is set to an error symbol indicating what went wrong.
If the process was created using spawn (or equivalently,
started by a issuing a command in the repl), the process dies and an
error message is presented over the registered printing callback
(dependent on how LispBM is integrated into your system). The
ctx_done_callback is also called and performs other
integration dependent tasks related to the shutting down of a
process.
If the process was created using spawn-trap, in addition
to the above, a message is sent to the parent process (the process that
executed the spawn-trap) containing information about the process that
struck an error. See spawn-trap. The parent
process can now choose to restart the process that crashed or to take
some other action.
Another way to catch errors is to use trap which works
similar to spawn-trap but it does not spawn a thread.
trap takes one argument which is an expressions. The
expression is evaluated and if it fails (trap expr) returns
an object representing the error. For more information on
trap, see trap.
The read_error symbol is returned if the reader cannot
parse the input code. Read errors are most likely caused by
syntactically incorrect input programs.
The type_error symbol is returned by built-in functions
or extensions if the values passed in are of incompatible types.
The eval_error symbol is returned if evaluation could
not proceed to evaluate the expression. This could be because the
expression is malformed.
Evaluation error happens on programs that may be syntactically correct (LispBM has a very low bar for what is considered syntactically correct), but semantically nonsensical.
The out_of_memory symbol is returned if the heap is full
and running the garbage collector was not able to free any memory
up.
The program you have written requires more memory.
The fatal_error symbol is returned in cases where the
LispBM runtime system cannot proceed. Something is corrupt and it is not
safe to continue.
The out_of_stack symbol is returned if the evaluator
runs out of continuation stack (this is its runtime-stack). You are most
likely writing a non-tail-recursive function that is exhausting all the
resources.
The division_by_zero symbol is returned when dividing by
zero.
The variable_not_bound symbol is returned when
evaluating a variable (symbol) that is neighter bound nor special
(built-in function).
Flash memory can be used to store data and functions that are
constant. Things can be moved to flash explicitly using the
move-to-flash function or as part of the reading procedure.
To move things automatically to flash during reading, there are
@directives.
@const-start opens a block of code where each global
definition is moved to constant memory (flash) automatically. This can
be used only together with the incremental reader (such as
read-eval-program).
A @const-start opened block should be closed with a
@const-end. Constant blocks cannot be nested.
@const-start
(defun f (x) (+ x 1))
@const-end
(+ (f 1) 2)@const-end closes an block opened by
@const-start.
Special form
A value can be moved to flash storage to save space on the normal
evaluation heap or lbm memory. A move-to-flash expression
is of the form (move-to-flash sym opt-sym1 ... opt-symN).
The symbols sym, opt-sym1 ... opt-symN should
be globally bound to the values you want moved to flash. After the value
has been moved, the environment binding is updated to point into flash
memory. CAUTION This function should be used carefully.
Ideally a value should be moved to flash immediately after it is created
so there is no chance that other references to original value
exists.
| Example | Result |
|
|
|
|
Convert any numerical value to a byte. If the input is not a number the output of this function will be 0.
| Example | Result |
|
|
|
|
|
|
Convert a value of any numerical type to an integer. The resulting integer is a 28bit value on 32bit platforms and 56 bits on 64 bit platforms. If the input is not a number the output of this function will be 0.
| Example | Result |
|
|
|
|
|
|
Convert a value of any numerical type to an unsigned integer. The resulting integer is a 28bit value on 32bit platforms and 56 bits on 64 bit platforms. If the input is not a number the output of this function will be 0.
| Example | Result |
|
|
|
|
|
|
Convert any numerical value to a 32bit int. If the input is not a number the output of this function will be 0.
| Example | Result |
|
|
|
|
|
|
Convert any numerical value to a 32bit unsigned int.
| Example | Result |
|
|
|
|
|
|
Convert any numerical value to a single precision floating point value. If the input is not a number the output of this function will be 0.
| Example | Result |
|
|
|
|
|
|
Convert any numerical value to a 64bit int. If the input is not a number the output of this function will be 0.
| Example | Result |
|
|
|
|
|
|
Convert any numerical value to a 64bit unsigned int. If the input is not a number the output of this function will be 0.
| Example | Result |
|
|
|
|
|
|
Convert any numerical value to a double precision floating point value. If the input is not a number the output of this function will be 0.
| Example | Result |
|
|
|
|
|
|
This document was generated by LispBM version 0.34.2