Overview

circuitpython_lisp_cycles.png
XKCD "Lisp Cycles" CCA Non-Commercial 2.5

A previous Adafruit Guide talks about using FORTH and LISP on SAMD51 based boards. The performance and memory provide the opportunity to get away from assembly language and C/C++ and use different styles of language. CircuitPython is a perfect example of this. But it does not have to stop there. 

A problem is the language implementations ran on the bare metal (the hardware), losing many of the advantages that and underlying language like CircuitPython can provide. Maybe the most important of these is the ability to treat part of the board's flash storage as a USB drive and place source code there simply by editing files directly on the drive or copying them to it (including dragging and dropping).

This guide introduces an implementation of a Lisp dialect very similar to Scheme with an interesting feature: it's written in Python 3. It's called is CircuitScheme. The implementation is based on Lispy.py written some time ago by Peter Norvig who wrote the books Artificial Intelligence: A Modern Approach and Paradigms of AI Programming: Case Studies in Common Lisp . He is currently Director of Research at Google. TL;DR he knows his stuff.

Norvig authored two articles that describe the background and implementation of Lispy:

(How to Write a (Lisp) Interpreter (in Python))

(An ((Even Better) Lisp) Interpreter (in Python))

These provide a description of Lispy and a good introduction to the techniques and considerations of implementing another programming language in Python. This guide will not replicate the content of those papers. You may read them, then come back and see how we can use this in CircuitPython.

Python is a a good language for implementing Lisp as it already has a lot of the capabilities that are needed. Being able to leverage Python's garbage collection is especially useful. Lispy goes even further than some implementations in that lists are implemented directly by Python lists.

So we have a Lisp to program in with all the advantages of the CircuitPython infrastructure. It also means that it's easy to add functionality such as support for hardware interaction. We'll cover that later in the guide.

The goal of this guide isn't to teach you Scheme; there are various resources online for that. https://schemers.org/ is a good central spot to find them. An excellent resource for learning Scheme is The Structure and Interpretation of Computer Programs which is a text that was used for first year computer science at MIT for many years. It was co-written by one of the creators of Scheme (and MIT professor), Gerald Sussman. MIT recorded the series of lectures from the course and has made them available, along with electronic versions of the book. In this guide, you will learn Scheme as a side effect of using it as the language used to explore concepts in computing.

Required Hardware

Lispy will run comfortably on any M4 Express board: Grand Central, Feather, or ItsyBitsy. Do not forget to also get a good USB cable to connect the board to your computer.

Adafruit Grand Central M4 Express featuring the SAMD51

PRODUCT ID: 4064
Are you ready? Really ready? Cause here comes the Adafruit Grand Central featuring the Microchip ATSAMD51. This dev board is so big, it's not...
$37.50
IN STOCK

Adafruit Feather M4 Express - Featuring ATSAMD51

PRODUCT ID: 3857
It's what you've been waiting for, the Feather M4 Express featuring ATSAMD51. This Feather is fast like a swift, smart like an owl, strong like a ox-bird (it's half ox,...
$22.95
IN STOCK

Adafruit ItsyBitsy M4 Express featuring ATSAMD51

PRODUCT ID: 3800
What's smaller than a Feather but larger than a Trinket? It's an Adafruit ItsyBitsy M4 Express featuring the Microchip ATSAMD51! Small,...
$14.95
IN STOCK

USB cable - USB A to Micro-B

PRODUCT ID: 592
This here is your standard A to micro-B USB cable, for USB 1.1 or 2.0. Perfect for connecting a PC to your Metro, Feather, Raspberry Pi or other dev-board or...
$2.95
IN STOCK
circuitpython_lisp.jpg
XKCD "Lisp" CCA Non-Commerical 2.5

So why Lisp? What's so special about it?

Simplicity

Lisp is one of those languages that is so simple it's almost nonsensically trivial. There is essentially two things to the syntax of Lisp:

  1. Atomic pieces of data; things that you will be familiar with, like numbers, strings, and symbols. Symbols are similar to identifiers in other languages. They can have a value bound to them. When a symbol is used, it's looked up in the environment and the bound value is used. If it's not found, an error is raised. Like Python identifiers, symbols can have different values, possibly of different types, over time. Additionally in CircuitScheme, atomic data can be any piece of Python data (e.g. an instance of a class).
  2. Lists of data. Like Python, items in a list can be any data including other lists. Also, like Python, items in a list do not have to be of the same type.

That's it. It gets more interesting when you consider that Lisp code is just Lisp data: there is no difference at all. Data can be evaluated. Atomic data simply evaluates to itself, while symbols get looked up resulting in the bound value.

Evaluating a list is where things get interesting. The first item in the list is expected to evaluate to a function. The rest of the list items are evaluated and used as arguments to that function.

Once more, that's all there is to it.  Well, almost. There are a few function-like things that are called special forms. They are often implemented in the interpreter, rather than as a function, and take their arguments verbatim, without evaluating them first.

Consider the if special form:

(if condition
  then-expression
  else-expression)

Based on what the condition evaluates to either the then or else expression should be evaluated. You can see how they can't be evaluated before being passed to if; we only want to evaluate one of then-expression or else-expression based on condition.  There are a handful of forms like this in any Lisp. In Norvig's original version of Lispy, there are 6 of these: quote, if, set!, define, lambda, and begin. The CircuitScheme adds cond to the list for convenience.

Macros

Another defining feature of most Lisp implementations is a very powerful macro system. This makes it possible to build language level capabilities that act like special forms.

For example, having a block of code evaluate when a condition of true is very common. With if we have to do something like:

(if condition
  (begin expression-1
         expression-2
         ...))

This can get unwieldy, and a when macro is often used:

(when condition
  expression-1
  expression-2
  ...)

This is a cleaner, more concise alternative. The when macro can be written as so:

(define-macro when (lambda args
  `(if ,(car args) (begin ,@(cdr args)))))

Even without being familiar with the macro syntax (i.e.: ` , ,@), it should be clear how when is being rewritten into the form using if and begin as shown above.

Recursion and Tail Call Optimization

One feature that is required of a proper scheme (but not of other Lisps) is tail call optimization. Before diving into this, let's consider recursion.

Writing iterative code should be familiar to everyone whose done any amount of Python programming.

while True:
    stuff

Is about as simple of an iteration as you can get. It just does the same thing over and over. That's all iteration is: doing the same thing multiple times. We see this when using iterables in Python. For instance:

def f(a_list):
    for i in a_list:

        print(i)

We say that we iterate over the list, printing each item.

There isn't a way to do this in CircuitScheme, although real Scheme has some iterative control structures. So how do we do this sort of thing in CircuitScheme (and this sort of thing is very common when programming).

Well, the other way to do repeated processing is to use recursion. Instead of doing the same thing for each item, the function calls itself to process the rest of the list. In Python this would be:

def f(a_list):
    if a_list:
        print(a_list[0])
        f(a_list[1:])

If a_list isn't empty, print the first item and call f again with the rest of a_list (i.e. from the second item on).

The CircuitScheme equivalent would be:

(define (f a-list)
  (if (not (null? a-list))
      (begin
        (display (car a-list))
        (newline)
        (f (cdr a-list)))))

Consider the two examples above as the list gets longer. Specifically think about what a function call does: it allocates some stack space to store enough information so that it can return properly. The call stack is typically allocated some finite amount of memory. Certainly in a microcontroller context that can be fairly small.

The iterative example above uses some stack space when f is called and a bit when print is called. When print returns, that stack space is reclaimed and used again by the next call to print. How much stack space we use is independent of the length of the list.

Now consider the recursive example. f is called which takes some stack space, as does the call to print but it is reclaimed when print returns. The embedded call to f takes more stack space. That leads to another call to f which takes more stack space. In fact, you can see that there is a call to f for each item in the list, so the amount of stack space used is proportional to the length of the list. A longer list will result in more stack space being used. Given that the stack has a finite amount of space, this puts a limit on how long of a list can be processed. Too long and we get a stack overflow error.

This would seem to be a bad thing. And it is, but there's a way to mitigate the problem. Tail Call Optimization. What's a tail call? That's the name for the style of recursion where the recursive call (when the function calls itself) is the last thing done before the function returns. We can see that that is indeed the case above. The last thing f does if the list wasn't empty is call itself with the rest of the list.

The neat thing about tail calls is that they can be changed quite easily into iteration. By doing so, excessive stack use may be avoided. Now the recursive code can process any size of list, just like the iterative one.

Scheme and, more importantly for us, CircuitScheme does this optimization within the evaluator.

Notably the folks that have controlled Python have steadfastly resisted supporting tail call optimization. To be fair, they encourage writing iterative code (just think about iterables, generators, list comprehensions, ...) Python is full of constructs that reinforce an iterative style of thinking. Which is fine, if not a little limiting and not always very mathematically elegant.

Non-tail Call Recursion

Since we have a special name for tail call recursion it's logical that there are other forms of recursion that are not tail-call. Generating Fibonacci numbers is a great example. Recall that a Fibonacci number is the sum of the two previous ones. I.e. fib(n) = fib(n-1) + fib(n-2). In Lispy, the obvious solution is:

(define (fib n)
  (if (<= n 1)
      n
      (+ (fib (- n 1)) (fib (- n 2)))))

Note that this does not involve tail-calls. Two recursive calls are made and the results added. The last thing done is the addition. Therefore this can not be optimized. We need to restructure it to use a tail-call. In essence we turn it inside out and accumulate the result as we make the recursive calls. When we reach the base-case (n being 0) the result is in hand and is returned, rippling back through the tail-calls. Note that this often requires a helper function (using a nested definition to hide it) in order to maintain the same API:

(define (fib n)
  (define (iter a b count)
    (if (<= count 0)
        a
        (iter b (+ a b) (- count 1))))
  (iter 0 1 n))

Now it does use a tail-call and can be optimized by the evaluator. This is a very powerful feature and it's worth making the effort to restructure the recursion to take advantage of it.

Getting Familiar

CircuitPython is a programming language based on Python, one of the fastest growing programming languages in the world. It is specifically designed to simplify experimenting and learning to code on low-cost microcontroller boards. Here are some guides which cover the basics:

Be sure you have the latest CircuitPython loaded onto your board per the second guide.

CircuitPython is easiest to use within the Mu Editor. If you haven't previously used Mu, this guide will get you started.

Download Library Files

Plug your Feather M4 Express board into your computer via a USB cable. Please be sure the cable is a good power+data cable so the computer can talk to the Feather board.

A new disk should appear in your computer's file explorer/finder called CIRCUITPY. This is the place we'll copy the code and code library. If you can only get a drive named CPLAYBOOT, load CircuitPython per the guide above.

Create a new directory on the CIRCUITPY drive named lib.

Download the latest CircuitPython driver package to your computer using the green button below. Match the library you get to the version of CircuitPython you are using. Save to your computer's hard drive where you can find it.

With your file explorer/finder, browse to the bundle and open it up. I advise simply copying all of the libraries to your CIRCUITPY /lib directory. You can interact with conceivably any of the libraries with the right wrapper file, so why limit yourself. The M4 boards have plenty of flash to hold it all.

Getting it

This guide isn't going to do a code walkthrough. Norvig's Lispy pages do that. You can get the code from GitHub, select Project Zip to get the entire project's files in one Zip file.

"""
Scheme Interpreter in CircuitPython
Based on Lispy.py (c) Peter Norvig, 2010; See http://norvig.com/lispy2.html

Adafruit invests time and resources providing this open source code.
Please support Adafruit and open source hardware by purchasing
products from Adafruit!

Written by Dave Astels for Adafruit Industries
Copyright (c) 2019 Adafruit Industries
Licensed under the MIT license.

All text above must be included in any redistribution.
"""

# Initially we'll avoid all pylint's complaints.
# Over time we'll bring it in line.

# pylint: disable=wrong-import-order,no-member,missing-docstring,invalid-name
# pylint: disable=redefined-builtin,multiple-statements,too-many-branches
# pylint: disable=too-many-return-statements,no-else-return,bad-whitespace
# pylint: disable=superfluous-parens,exec-used,wrong-import-position
# pylint: disable=unnecessary-lambda,multiple-imports
# pylint: disable=misplaced-comparison-constant,too-few-public-methods
# pylint: disable=dangerous-default-value,unnecessary-semicolon
# pylint: disable=broad-except,bad-continuation

################ Symbol, Procedure, classes

import re, sys
from io import StringIO
import gc


class Symbol(str): pass

def Sym(s, symbol_table={}):
    "Find or create unique Symbol entry for str s in symbol table."
    if s not in symbol_table: symbol_table[s] = Symbol(s)
    return symbol_table[s]

_quote, _if, _cond, _set, _define, _lambda, _begin, _definemacro, = map(Sym,
"quote   if   cond   set!  define   lambda   begin   define-macro".split())

_quasiquote, _unquote, _unquotesplicing = map(Sym,
"quasiquote   unquote   unquote-splicing".split())

class Procedure(object):
    "A user-defined Scheme procedure."
    def __init__(self, parms, exp, env):
        self.parms, self.exp, self.env = parms, exp, env
    def __call__(self, *args):
        return eval(self.exp, Env(self.parms, args, self.env))

################ parse, read, and user interaction

def parse(inport):
    "Parse a program: read and expand/error-check it."
    # Backwards compatibility: given a str, convert it to an InPort
    if isinstance(inport, str): inport = InPort(StringIO(inport))
    return expand(read(inport), toplevel=True)

eof_object = Symbol('#<eof-object>') # Note: uninterned; can't be read

class InPort(object):
    "An input port. Retains a line of chars."
    tokenizer = r""" *(,@|[('`,)]|"(?:\\.|[^\\"])*"|;.*|[^ ('"`,;)]*)(.*)"""
    def __init__(self, afile):
        self._file = afile; self.line = ''
    def next_token(self):
        "Return the next token, reading new text into line buffer if needed."
        while True:
            if self.line == '': self.line = self._file.readline()
            if self.line == '': return eof_object
            self.line = self.line.strip()
            m = re.match(InPort.tokenizer, self.line)
            token = m.group(1)
            self.line = m.group(2)
            if token != '' and not token.startswith(';'):
                return token

def readchar(inport):
    "Read the next character from an input port."
    if inport.line != '':
        ch, inport.line = inport.line[0], inport.line[1:]
        return ch
    else:
        return inport.file.read(1) or eof_object

def read(inport):
    "Read a Scheme expression from an input port."
    def read_ahead(token):
        if '(' == token:
            L = []
            while True:
                token = inport.next_token()
                if token == ')':
                    return L
                else: L.append(read_ahead(token))
        elif ')' == token: raise SyntaxError('unexpected )')
        elif token in quotes: return [quotes[token], read(inport)]
        elif token is eof_object: raise SyntaxError('unexpected EOF in list')
        else: return atom(token)
    # body of read:
    token1 = inport.next_token()
    return eof_object if token1 is eof_object else read_ahead(token1)

quotes = {"'":_quote, "`":_quasiquote, ",":_unquote, ",@":_unquotesplicing}

def atom(token):
    'Numbers become numbers; #t and #f are booleans; "..." string; otherwise Symbol.'
    if token == '#t': return True
    elif token == '#f': return False
    elif token[0] == '"': return token[1:-1]#.decode('string_escape')
    try: return int(token)
    except ValueError:
        try: return float(token)
        except ValueError:
            return Sym(token)

def to_string(x):
    "Convert a Python object back into a Lisp-readable string."
    if x is True: return "#t"
    elif x is False: return "#f"
    elif isa(x, Symbol): return str(x)
#    elif isa(x, str): return '"%s"' % x.encode('string_escape').replace('"',r'\"')
    elif isa(x, str): return '"%s"' % x.replace('"',r'\"')
    elif isa(x, list):
        return '('+' '.join(map(to_string, x))+')'
    else: return str(x)

def load(filename):
    "Eval every expression from a file."
    if not filename.endswith('.scm'):
        filename = filename + '.scm'
    inport = InPort(open(filename))
    while True:
        try:
            x = parse(inport)
            if x is eof_object: return
            eval(x)
        except Exception as e:
            sys.print_exception(e)

############ REPL history support

history_max_size = 40
history = []

def add_to_history(line):
    global history
    if line and (not history or history[0] != line):
        history = history[:history_max_size - 1]
        history.insert(0, line.strip())

def get_history(offset):
    if offset < 0 or offset >= len(history):
        return ''
    return history[offset]

def repl():
    "A prompt-read-eval-print loop."
    input = ''
    line = ''
    index = 0
    ctrl_c_seen = False

    while True:                         # for each line
        try:
            if input:
                prompt = '... '
            else:
                prompt = '==> '
            sys.stdout.write(prompt)
            index = 0
            line = ''
            history_offset = -1

            while True:                   # for each character
                ch = ord(sys.stdin.read(1))

                # if ch == 3:               # CTRL-C
                #     print('ctrl-c from ch == 3')
                #     if ctrl_c_seen:
                #         return
                #     ctrl_c_seen = True
                #     input = ''
                #     sys.stdout.write('\n')
                #     break

                ctrl_c_seen = False

                if 32 <= ch <= 126:           # printable character
                    line = line[:index] + chr(ch) + line[index:]
                    index += 1

                elif ch in {10, 13}:          # EOL - try to process
                    if input:
                        input = input + ' ' + line.strip()
                    else:
                        input = line.strip()
                    add_to_history(line.strip())
                    line = ''
                    try:
                        x = parse(input)
                        if x is eof_object:
                            raise SyntaxError('unexpected EOF in list')
                        val = eval(x)
                        if val is not None:
                            sys.stdout.write('\n{0}'.format(to_string(val)))
                        input = ''
                    except SyntaxError as e:
                        if str(e) != 'unexpected EOF in list':
                            sys.stdout.write('\n')
                            sys.stdout.write(str(e))
                            input = ''
                    sys.stdout.write('\n')
                    break

                #####################

                elif ch == 1:             # CTRL-A: start of line
                    index = 0

                elif ch == 5:             # CTRL-E: end of line
                    index = len(line)

                #####################

                elif ch == 2:             # CTRL-B: back a word
                    while index > 0 and line[index-1] == ' ':
                        index -= 1
                    while index > 0 and line[index-1] != ' ':
                        index -= 1

                elif ch == 6:             # CTRL-F: forward a word
                    while index < len(line) and line[index] == ' ':
                        index += 1
                    while index < len(line) and line[index] != ' ':
                        index += 1

                #####################

                elif ch == 4:             # CTRL-D: delete forward
                    if index < len(line):
                        line = line[:index] + line[index+1:]

                elif ch == 11:            # CTRL-K: clear to end of line
                    line = line[:index]

                elif ch in {8, 127}:     # backspace/DEL
                    if index > 0:
                        line = line[:index - 1] + line[index:]
                        index -= 1

                #####################

                elif ch == 20:            # CTRL-T: transpose characters
                    if index > 0 and index < len(line):
                        ch1 = line[index - 1]
                        ch2 = line[index]
                        line = line[:index - 1] + ch2 + ch1 + line[index + 1:]


                #####################

                elif ch == 27:            # ESC
                    next1, next2 = ord(sys.stdin.read(1)), ord(sys.stdin.read(1))
                    if next1 == 91:           # [
                        if next2 == 68:       # left arrow
                            if index > 0:
                                index -= 1
                            else:
                                sys.stdout.write('\x07')
                        elif next2 == 67:     # right arrow
                            if index < len(line):
                                index += 1
                            else:
                                sys.stdout.write('\x07')
                        elif next2 == 66:     # down arrow
                            if history_offset > -1:
                                history_offset -= 1
                                line = get_history(history_offset)
                                index = len(line)
                            else:
                                sys.stdout.write('\x07')
                        elif next2 == 65:     # up arrow
                            if history_offset < len(history) - 1:
                                history_offset += 1
                                line = get_history(history_offset)
                                index = len(line)
                            else:
                                sys.stdout.write('\x07')

                else:
                    print('Unknown character: {0}'.format(ch))

                # Update screen
                sys.stdout.write("\x1b[1000D") # Move all the way left
                sys.stdout.write("\x1b[0K")    # Clear the line
                sys.stdout.write(prompt)
                sys.stdout.write(line)
                sys.stdout.write("\x1b[1000D") # Move all the way left again
                sys.stdout.write("\x1b[{0}C".format(len(prompt) + index)) # Move cursor too index
                # sys.stdout.flush()
        except KeyboardInterrupt:
            if ctrl_c_seen:
                return
            ctrl_c_seen = True
            input = ''
            sys.stdout.write('\n')
        except Exception as e:
            sys.stdout.write('\n')
            sys.print_exception(e)
            sys.stdout.write('\n')
            input = ''

################ Environment class

class Env(object):
    "An environment: a dict of {'var':val} pairs, with an outer Env."
    def __init__(self, parms=(), args=(), outer=None):
        # Bind parm list to corresponding args, or single parm to list of args
        self.storage = {}
        self.outer = outer
        if isa(parms, Symbol):
            self.storage.update({str(parms):list(args)})
        else:
            if len(args) != len(parms):
                raise TypeError('expected %s, given %s, '
                                % (to_string(parms), to_string(args)))
            try:
                self.storage.update(zip([str(p) for p in parms],args))
            except TypeError as e:
                sys.print_exception(e)
    def find(self, var):
        "Find the innermost Env where var appears."
        if str(var) in self.storage: return self
        elif self.outer is None:
            raise LookupError(str(var))
        else: return self.outer.find(var)

def is_pair(x): return x != [] and isa(x, list)
def cons(x, y): return [x]+y

def callcc(proc):
    "Call proc with current continuation; escape only"
    ball = RuntimeWarning("Sorry, can't continue this continuation any longer.")
    def throw(retval): ball.retval = retval; raise ball
    try:
        return proc(throw)
    except RuntimeWarning as w:
        if w is ball: return ball.retval
        else: raise w

def mod_vars(mod):
    names = [i for i in dir(mod) if i[0] != '_']
    values = [getattr(mod, i) for i in names]
    return dict(zip(names, values))

def add_globals(self):
    "Add some Scheme standard procedures."
    import math, operator as op
    self.storage.update(mod_vars(math))
    # self.update(mod_vars(cmath))
    self.storage.update({
        '+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv, 'not':op.not_,
        '>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq,
        'equal?':op.eq, 'eq?':op.is_, 'length':len, 'cons':cons,
        'car':lambda x:x[0], 'cdr':lambda x:x[1:], 'append':op.add,
        'list':lambda *x:list(x), 'list?': lambda x:isa(x,list),
        'null?':lambda x:x==[], 'symbol?':lambda x: isa(x, Symbol),
        'boolean?':lambda x: isa(x, bool), 'pair?':is_pair,
        'port?': lambda x:isa(x,file), 'apply':lambda proc,l: proc(*l),
        'eval':lambda x: eval(expand(x)), 'load':lambda fn: load(fn), 'call/cc':callcc,
        'open-input-file':open,'close-input-port':lambda p: p.file.close(),
        'open-output-file':lambda f:open(f,'w'), 'close-output-port':lambda p: p.close(),
        'eof-object?':lambda x:x is eof_object, 'read-char':readchar,
        'read':read, 'write':lambda x,port=sys.stdout:port.write(to_string(x)),
        'display':lambda x,port=sys.stdout:port.write(x if isa(x,str) else to_string(x)),
        'newline':lambda port=sys.stdout:port.write("\n")})
    return self

isa = isinstance

global_env = add_globals(Env())

################ eval (tail recursive)

def eval(x, env=global_env):
    "Evaluate an expression in an environment."
    while True:
        if isa(x, Symbol):       # variable reference
            return env.find(str(x)).storage[str(x)]
        elif not isa(x, list):   # constant literal
            return x
        elif x[0] is _quote:     # (quote exp)
            (_, exp) = x
            return exp
        elif x[0] is _if:        # (if test conseq alt)
            (_, test, conseq, alt) = x
            x = (conseq if eval(test, env) else alt)
        elif x[0] is _cond:      # (cond (test code)...)
            for clause in x[1:]:
                if eval(clause[0], env):
                    for exp in clause[1:-1]:
                        eval(exp, env)
                    x = clause[-1]
                    break
        elif x[0] is _set:       # (set! var exp)
            (_, var, exp) = x
            env.find(var).storage[str(var)] = eval(exp, env)
            return None
        elif x[0] is _define:    # (define var exp)
            (_, var, exp) = x
            env.storage[str(var)] = eval(exp, env)
            return None
        elif x[0] is _lambda:    # (lambda (var*) exp)
            (_, vars, exp) = x
            return Procedure(vars, exp, env)
        elif x[0] is _begin:     # (begin exp+)
            for exp in x[1:-1]:
                eval(exp, env)
            x = x[-1]
        else:                    # (proc exp*)
            exps = [eval(exp, env) for exp in x]
            proc = exps.pop(0)
            if isa(proc, Procedure):
                x = proc.exp
                env = Env(proc.parms, exps, proc.env)
            else:
                return proc(*exps)

################ expand

def expand(x, toplevel=False):
    "Walk tree of x, making optimizations/fixes, and signaling SyntaxError."
    require(x, x!=[], "Empty list can't be expanded")                    # () => Error
    if not isa(x, list):                 # constant => unchanged
        return x
    elif x[0] is _quote:                 # (quote exp)
        require(x, len(x)==2)
        return x
    elif x[0] is _if:
        if len(x)==3: x = x + [None]     # (if t c) => (if t c None)
        require(x, len(x)==4)
        return list(map(expand, x))
    elif x[0] is _cond:
        require(x, len(x) > 1)
        for clause in x[1:]:
            require (clause, len(clause) >= 2)
        return list(map(expand, x))
    elif x[0] is _set:
        require(x, len(x)==3);
        var = x[1]                       # (set! non-var exp) => Error
        require(x, isa(var, Symbol), "can set! only a symbol")
        return [_set, var, expand(x[2])]
    elif x[0] is _define or x[0] is _definemacro:
        require(x, len(x)>=3)
        _def, v, body = x[0], x[1], x[2:]
        if isa(v, list) and v:           # (define (f args) body)
            f, args = v[0], v[1:]        #  => (define f (lambda (args) body))
            return expand([_def, f, [_lambda, args]+body])
        else:
            require(x, len(x)==3)        # (define non-var/list exp) => Error
            require(x, isa(v, Symbol), "can define only a symbol")
            exp = expand(x[2])
            if _def is _definemacro:
                require(x, toplevel, "define-macro only allowed at top level")
                proc = eval(exp)
                require(x, callable(proc), "macro must be a procedure")
                macro_table[v] = proc    # (define-macro v proc)
                return None              #  => None; add v:proc to macro_table
            return [_define, v, exp]
    elif x[0] is _begin:
        if len(x)==1: return None        # (begin) => None
        else: return [expand(xi, toplevel) for xi in x]
    elif x[0] is _lambda:                # (lambda (x) e1 e2)
        require(x, len(x)>=3)            #  => (lambda (x) (begin e1 e2))
        vars, body = x[1], x[2:]
        require(x, (isa(vars, list) and all(isa(v, Symbol) for v in vars))
                or isa(vars, Symbol), "illegal lambda argument list")
        exp = body[0] if len(body) == 1 else [_begin] + body
        return [_lambda, vars, expand(exp)]
    elif x[0] is _quasiquote:            # `x => expand_quasiquote(x)
        require(x, len(x)==2)
        return expand_quasiquote(x[1])
    elif isa(x[0], Symbol) and x[0] in macro_table:
        return expand(macro_table[x[0]](*x[1:]), toplevel) # (m arg...)
    else:                                #        => macroexpand if m isa macro
        return list(map(expand, x))            # (f arg...) => expand each

def require(x, predicate, msg="wrong length"):
    "Signal a syntax error if predicate is false."
    if not predicate: raise SyntaxError(to_string(x)+': '+msg)

_append, _cons, _let = map(Sym, "append cons let".split())

def expand_quasiquote(x):
    """Expand `x => 'x; `,x => x; `(,@x y) => (append x y) """
    if not is_pair(x):
        return [_quote, x]
    require(x, x[0] is not _unquotesplicing, "can't splice here")
    if x[0] is _unquote:
        require(x, len(x)==2)
        return x[1]
    elif is_pair(x[0]) and x[0][0] is _unquotesplicing:
        require(x[0], len(x[0])==2)
        return [_append, x[0][1], expand_quasiquote(x[1:])]
    else:
        return [_cons, expand_quasiquote(x[0]), expand_quasiquote(x[1:])]

def let(*args):
    args = list(args)
    x = cons(_let, args)
    require(x, len(args)>1)
    bindings, body = args[0], args[1:]
    require(x, all(isa(b, list) and len(b)==2 and isa(b[0], Symbol)
                   for b in bindings), "illegal binding list")
    vars, vals = zip(*bindings)
    return [[_lambda, list(vars)]+list(map(expand, body))] + list(map(expand, vals))

macro_table = {_let:let} ## More macros can go here

################ core builtins

eval(parse("""(begin

(define-macro and (lambda args
   (if (null? args) #t
       (if (= (length args) 1) (car args)
           `(if ,(car args) (and ,@(cdr args)) #f)))))

(define-macro or (lambda args
   (if (null? args) #f
       (if (= (length args) 1) (car args)
           `(if (not ,(car args)) (or ,@(cdr args)) #t)))))

(define-macro when (lambda args
  `(if ,(car args) (begin ,@(cdr args)))))

(define-macro unless (lambda args
  `(if (not ,(car args)) (begin ,@(cdr args)))))

;; More macros can also go here

)"""))


################ hardware builtins

import time
import board
import busio
from adafruit_bus_device.i2c_device import I2CDevice
import digitalio
import analogio

board_pins = mod_vars(board)

def get_board_pins():
    return list(board_pins.keys())

def get_pin(pin_name):
    try:
        return board_pins[pin_name]
    except KeyError:
        print('{0} is not a valid pin name'.format(pin_name))
        return None

def make_digital_pin(pin, direction, pull=None):
    p = digitalio.DigitalInOut(pin)
    p.direction = direction
    if direction is digitalio.Direction.INPUT:
        p.pull = pull
    return p

def make_analog_pin(pin, direction):
    if direction is digitalio.Direction.INPUT:
        p = analogio.AnalogIn(pin)
    else:
        p = analogio.AnalogOut(pin)
    return p

def set_pin_value(pin, value):
    pin.value = value

def get_pin_value(pin):
    return pin.value

def i2c_bus(scl, sda):
    return busio.I2C(scl, sda)

def i2c_device(i2c, address):
    return I2CDevice(i2c, address)



def execfile(f):
    exec(open(f).read())

def load_device(device_driver_name):
    try:
        execfile('./devices/{0}.py'.format(device_driver_name))
    except OSError:
        pass

    try:
        load('./devices/{0}.scm'.format(device_driver_name))
    except OSError:
        pass

global_env.storage.update({
    'load-device':load_device,
    'board-pins':get_board_pins,
    'board':get_pin,
    'digital-pin':make_digital_pin,
    'analog-pin':make_analog_pin,
    '**INPUT**':digitalio.Direction.INPUT,
    '**OUTPUT**':digitalio.Direction.OUTPUT,
    '**PULLUP**':digitalio.Pull.UP,
    '**PULLDOWN**':digitalio.Pull.DOWN,
    'pin-value':get_pin_value,
    'pin-value!':set_pin_value,

    'i2c':i2c_bus,

    'sleep':time.sleep
    })


if __name__ == '__main__':
    print('CircuitScheme version 1.0: {0} bytes free.\n'.format(gc.mem_free()))

    try:
        load("code")
    except OSError:
        pass

    repl()

Hardware Extensions

circuitpython_c64.jpg
C64 board by Christian Taube CCA-SA 2.5

Hardware support is done with a two level approach.

Builtin Support

Firstly, the most basic capabilities have been added to the CircuitScheme runtime by way of builtin functions: the board module to give access to pins, digital and analog I/O, I2C bus creation, and sleep.

Let's look at the Hello, World of hardware: blink. It shows setting up a digital output on D13, setting its value, and sleeping. Note it uses recursion. The let structure simply binds the symbol led to the digital output for use in the code within the let.

Download: file
(define (blink)
  (let ((led (digital-pin (board "D13") **OUTPUT**)))
    (define (loop val)
      (pin-value! led val)
      (sleep 0.5)
      (loop (not val)))
    (loop #t)))

The functions below are supplied. They work as you would expect. Later in the guide, we'll see some examples.

board-pins - get a list of the names of all pins on the board
board - given a pin name, return the corresponding pin object
digital-pin - create a DigitalInOut from a pin object
analog-pin - create an analog pin from a pin name and direction
**INPUT** - use to create a digital or analog input
**OUTPUT** - use to create a digital or analog output
**PULLUP** - set a pull up resistor on a digital input
**PULLDOWN**- set a pull down resistor on a digital input
pin-value - get the value (digital or analog) of a pin
pin-value! - set the value (digital or analog) of a pin
i2c - create an I2C bus object given SCL and SDA pins objects
sleep - delay some number of seconds

Wrapping CircuitPython Driver Libraries

The second approach provides a facility to dynamically load wrappers around Python device driver modules. This will generally just be a Python file, but can involve a file of CircuitScheme code as well. A .py and .scm file with the same basename can be loaded from the /devices subdirectory on CIRCUITPY. If either file is not found, it is ignored. The code implementing this is:

def execfile(f):
    exec(open(f).read())

def load_device(device_driver_name):
    try:
        execfile('./devices/{0}.py'.format(device_driver_name))
    except OSError:
        pass

    try:
        load('./devices/{0}.scm'.format(device_driver_name))
    except OSError:
        pass

The load_device function is bound to load-device in CircuitScheme, so to load support for a device (e.g. the Si7021 temperature and humidity sensor) you would use:

(load-device "si7021")

Then the functions provided can be used. Here is devices/si7021.py (there is no si7021.scm):

Download: file
import adafruit_si7021

def make_si7021(i2c_bus):
    return adafruit_si7021.SI7021(i2c_bus)

def si7021_relative_humidity(device):
    return device.relative_humidity

def si7021_temperature(device):
    return device.temperature

global_env.storage.update({
    'make-si7021':make_si7021,
    'si7021-relative-humidity':si7021_relative_humidity,
    'si7021-temperature':si7021_temperature
    })

This defines three functions that we want to make available: create an Si7021 interface object, read the humidity, and read the temperature. It then makes them available to CircuitScheme code by adding them to the global environment's dictionary: global_env.storage

Once loaded, it can be used:

Download: file
(load-device "si7021")
(define sensor (make-si7021 (i2c (board "SCL") (board "SDA"))))
(display "Temperature: ")
(display (si7021-temperature sensor))
(newline)
(display "   Humidity: ")
(display (si7021-relative-humidity sensor))
(newline)

Which results in:

CircuitScheme version 1.0: 185280 free bytes
==> (load "si7021-sample")
Temperature: 24.3539
Humidity: 42.3818
==>

Examples

The first example is the classic blink code. You can see the creation of a digital output on D13, setting its value and sleeping. Note the use of tail-call recursion to implement the repetition.

Download: file
    ;;; Blink the onboard LED

(define (blink)
  (let ((led (digital-pin (board "D13") **OUTPUT**)))
    (define (loop val)
      (pin-value! led val)
      (sleep 0.5)
      (loop (not val)))
    (loop #t)))
  

Next we'll add digital input. With a pushbutton between D12 and ground, this code turns the led on when the switch is pushed, off when released.

Download: file
;;; echo a switch on pin D12 onto the onboard LED

(define (echo)
  (let ((switch (digital-pin (board "D12") **INPUT** **PULLUP**))
        (led (digital-pin (board "D13") **OUTPUT**)))
    (define (loop)
      (pin-value! led (not (pin-value? switch)))
      (loop))
    (loop)))

Switching to analog, a potentiometer can be connected to A0 and set it up as an analog input. Then it can be read and the value displayed every half second.

Download: file
;;; Read an analog input every second and print result

(define (analog)
  (let ((input (analog-pin (board "A0") **INPUT**)))
    (define (loop)
      (display (pin-value input))
      (newline)
      (sleep 0.5)
      (loop))
    (loop)))

Flipping it around an LED can be connected to A1. The code below sets it up as an analog output and ramps the value up and down between minimum and maximum values. The LED gets brighter and dimmer. The value is also written to the console.

This code also uses the cond special form. It's similar to a switch statement in many languages. The cond contains a series of (condition code) clauses. The code associated with the first condition that evaluates to true is evaluated. FYI, true in CircuitScheme is #t and false is #f.

Download: file
;;; Ramp an analog output up and down

(define (analog)
  (let ((output (analog-pin (board "A1") **OUTPUT**)))
    (define (loop val delta)
      (let ((new-val (+ val delta)))
        (display new-val)
        (newline)
        (sleep 0.1)
        (cond ((<= new-val 0)
               (loop new-val (* -1 delta)))
              ((>= new-val 65535)
               (loop new-val (* -1 delta)))
              (#t (pin-value! output new-val)
                  (loop new-val delta)))))
    (loop 0 1000)))

Language Reference

CircuitScheme is a language from the Lisp family based on Lispy by Peter Norvig as described in (How to Write a (Lisp) Interpreter (in Python)) and (An ((Even Better) Lisp) Interpreter (in Python)). As such, it is very similar to Scheme and much of the text here is taken more or less verbatim from the MIT Scheme reference manual. 

Data Types

Booleans represent true and false. Boolean literals are #t and #f for true and false, respectively. The only thing that is considered to be logically false is #f. Everything else is logically true, including 0 and the empty list, which may surprise some.

Numbers are exactly as they are in Python.

Strings are any sequence of characters other than " enclosed by a pair of ", e.g. "string". If you need to have " in a string, use \".

Symbols are simple identifiers, e.g. function-name. Symbols follow the follow 4 simple rules:

  1. can only contain graphic characters (i.e. no control characters)
  2. can not contain any of the characters:();,"`&[]{}\
  3. can not begin with a number or single quote
  4. and not contain whitespace

Typically, - is used to separate words in a symbol, _ is used in special symbols (such as system use) to separate words and as a prefix and suffix. The characters ?, !, and * are typically used as the final character of a function name to denote:

? a predicate, e.g. null?

! a mutating function (changes the argument rather than returning a modified copy), e.g. set!

* a variant of the primary function, e.g. flatten (which does a one level flattening of a list) and flatten* (which is a recursive flatten)

Lists are the central data type in any Lisp. They are simply a non-homogeneous sequence of data items (as above) and/or other lists, surrounded by parentheses: (1 a #t (1 2 3) "hello").

Functions are user defined procedures. They are covered in detail later.

Macros are user defined syntactic extensions.

Additionally, any piece of CircuitPython data can be used in CircuitScheme, although they can only be created in Python code. See the Hardware Extension section for an example.

Special Forms

(lambda formals sexpr...)

A lambda expression evaluates to a procedure. The environment in effect when the lambda expression is evaluated is remembered as part of the procedure; it is called the closing environment. When the procedure is later called with some arguments, the closing environment is extended by binding the variables in the formal parameter list to the arguments according in order, left to right. The new environment created by this process is referred to as the invocation environment.

Once the invocation environment has been constructed, the sexprs in the body of the lambda expression are evaluated sequentially in that environment. This means that the region of the variables bound by the lambda expression is all of the sexprs in the body. The result of evaluating the last sexpr in the body is returned as the result of the procedure call.

formals, the formal parameter list, is often referred to as a lambda list.

Examples:

(lambda (x) (+ x x)) ⇒

((lambda (x) (+ x x)) 4) ⇒ 8

(define reverse-subtract
  (lambda (x y)
    (- y x)))
(reverse-subtract 7 10) ⇒ 3

(define foo
  (let ((x 4))
    (lambda (y) (+ x y))))
(foo 6) ⇒ 10

(let ((variable init)...) sexpr...)

The inits are evaluated in the current environment (in some unspecified order), the variables are then bound (in an environment extending the one the let is being evaluated in) to fresh locations holding the respective results, the sexprs are evaluated sequentially in the extended environment, and the value of the last sexpr is returned. Each binding of a variable has the sequence of sexpr as its region.

Note that the following are equivalent:

(let ((variable init)...) expression...)
((lambda (variable...) expression...) init...)

Some examples:

(let ((x 2)
      (y 3))

  (* x y)) ⇒ 6

(let ((x 2) (y 3))
  (let ((foo (lambda (z) (+ x y z)))
        (x 7))
    (foo 4))) ⇒ 9

Definitions

(define variable sexpr)
(define formals sexpr...)

Definitions may only occur at the top level of a program and at the beginning of a lambda body: that is,
the body of a lambda,let, or procedure define expression. A definition that occurs at the top level of a program is called a top-level definition, and a definition that occurs at the beginning of a body is called an internal definition.

The second form is used as a shorthand to define a procedure. We saw earlier how the result of lambda can be used as a variable value. The first item in formals is not a parameter but the name of the resulting procedure; thus formals cannot be empty.

Hence the following are identical.

(define inc (lambda (x) (+ x 1)))
(define (inc x) (+ x 1))

Using this form of define, a function that accepts a completely option set of arguments can be made:

A top-level definition,

(define variable sexpr)

has essentially the same effect as this assignment expression, if variable is bound. I.e. it binds a new value to the symbol.

(set! variable expression)

If variable is not bound, however, define binds variable to a new location in the current environment before performing the assignment (it is an error to perform aset! on an unbound variable).

(define add3(lambda (x) (+ x 3))) ⇒ unspecified
(add3 3) ⇒ 6

(define first car) ⇒ unspecified
(first '(1 2)) ⇒ 1

An internal definition is a definition that occurs at the beginning of a body (that is, the body of a  lambda, let, or procedure define expression), rather than at the top level of a program. The
variable defined by an internal definition is local to the body. That is, variable is bound rather than assigned, and the region of the binding is the entire body. For example,

(let ((x 5))
  (define foo (lambda (y) (bar x y)))
  (define bar (lambda (a b) (+ (* a b) a)))
  (foo (+ x 3))) ⇒ 45

Assignment

(set! variable expression)

expression is evaluated and the resulting value is stored in the location to which variable is bound. The value of the set! expression is unspecified.

variable must be bound either in some region enclosing the set! expression, or at the top level.

(define x 2) ⇒ unspecified
(+ x 1) ⇒ 3
(set! x 4) ⇒ unspecified
(+ x 1) ⇒ 5

Quoting

This section describes the expressions that are used to modify or prevent the evaluation of objects.

(quote datum)

This evaluates to datum which may be any external representation of a CircuitScheme object. Use quote to include literal constants in code.

(quote a) ⇒ a
(quote (+ 1 2)) ⇒ (+ 1 2)

(quote datum) may be abbreviated as 'datum. The two notations are equivalent in all respects.

'a ⇒ a
'(+ 1 2) ⇒ (+ 1 2)
'(quote a) ⇒ (quote a)
''a ⇒ (quote a)

Numeric constants, string constants, character constants, and boolean constants evaluate to themselves, so they don't need to be quoted.

'"abc" ⇒ "abc"
"abc" ⇒ "abc"
'145932 ⇒ 145932
145932 ⇒ 145932
'#t ⇒ #t
#t ⇒ #t

(quasiquote template)

Backquote or quasiquote expressions are useful for constructing a list structure when most, but not all of the desired structure is known in advance. If no commas appear within the template, the result of evaluating is equivalent to the result of evaluating 'template. If a comma appears within the template, however, the expression following the comma is evaluated (i.e. unquoted) and its result is inserted into the structure instead of the comma and the expression. If a comma appears followed immediately by an at-sign (@), then the following expression must evaluate to a list; the opening and closing parentheses of the list are then stripped away and the elements of the list are inserted in place of the comma at-sign expression sequence. quasiquote, comma, and comma-at are equivalent to `, ,, ,@, respectively.

`(list ,(+ 1 2) 4) ⇒ (list 3 4)

(let ((name 'a)) `(list ,name ',name)) ⇒ (list a 'a)

`(a ,(+ 1 2) ,@(map abs '(4 -5 6)) b) ⇒ (a 3 4 5 6 b)

`((foo ,(- 10 3)) ,@(cdr '(c)) . ,(car '(cons)))
⇒ ((foo 7) . cons)

`,(+ 2 3) ⇒ 5

Quasiquote forms may be nested. Substitutions are made only for unquoted components appearing
at the same nesting level as the outermost backquote. The nesting level increases by one inside each successive quasiquotation, and decreases by one inside each unquotation.

`(a `(b ,(+ 1 2) ,(foo ,(+ 1 3) d) e) f)
⇒ (a `(b ,(+ 1 2) ,(foo 4 d) e) f)

(let ((name1 'x)
      (name2 'y))
  `(a `(b ,,name1 ,',name2 d) e)) ⇒ (a `(b ,x ,'y d) e)

The above notations and (quasiquote template) are identical in all respects and is identical to .

(quasiquote (list (unquote (+ 1 2)) 4))
⇒ (list 3 4)

'(quasiquote (list (unquote (+ 1 2)) 4))
⇒ `(list ,(+ 1 2) 4)

Unpredictable behavior can result if any of the symbols quasiquote, unquote, or unquote-splicing appear in a template in ways otherwise than as described above.

Macros

(define-macro (formals) template)

Create a named macro:

formals is the same as in a procedure definition: a name followed by formal parameters,
if any. NOTE that the arguments to a macro invocation are not evaluated, but
are passed as is to the macro to do with as it wishes.

template the template expression that is processed when the macro is invoked. The result of evaluating the processed template expression becomes the value of the macro invocation. template is typically (even always) a quasiquoted expression using the formal parameter names for purposes of unquotiing in order to fill in the template.

(define-macro (double x)
  `(+ ,x ,x))

(double 5) ⇒ 10

Sequencing

The begin special form is used to evaluate expressions in a particular order.

(begin expression...)

The expressions are evaluated sequentially from left to right, and the value of the last expression is returned. This expression type is used to sequence side effects such as input and output. Keep in mind, begin does not create a nested lexical environment.

(define x 0)
(begin (set! x 5)
       (+ x 1)) ⇒ 6

(begin (display "4 plus 1 equals ")
       (display (+ 4 1)))
⇒ unspecified

It prints "4 plus 1 equals 5".

Often the explicit use of begin is unnecessary, because many special forms already support sequences of expressions (that is, they have an implicitbegin):

  • cond
  • define ;''procedure define'' only
  • lambda
  • let

Conditionals

The behavior of the conditional expressions is determined by whether objects are true or false. The conditional expressions count only #f as false. Everything else, including #t, lists, symbols, numbers, strings, and procedures count as true.

In the descriptions that follow, we say that an object has a true value or is true when the conditional expressions treat it as true, and we say that an object has a false value or is false when the conditional expressions treat it as false.

(cond clause...)

Each clause has this form: (predicate expression...) where predicate is any expression. The last clause may be an else clause, which has the form: (#t expression...)

A cond expression does the following:

  1. Evaluates the predicate expressions of successive clauses in order, until one of them evaluates to a true value.
  2. When a predicate evaluates to a true value, cond evaluates the expressions in the clause in left to right order, and uses the result of the last evaluation as the result of the entire cond expression.
  3. If all predicates evaluate to false values, and there is no else clause, the result of the conditional expression is unspecified; if there is an else clause, cond evaluates its expressions (left to right) and returns the value of the last one.

    (cond ((> 3 2) 'greater)
          ((< 3 2) 'less)) ⇒ greater
    (cond ((> 3 3) 'greater)
          ((< 3 3) 'less)
          (#t 'equal)) ⇒ equal

Normally, programs should not depend on the value of a cond expression that has no else clause. However, some prefer to write cond expressions in which at least one of the predicates is always true. In this style, the final clause is equivalent to an else clause.

(and expression...)

The expressions are evaluated from left to right, and the value of the first expression that evaluates to a false value is returned. Any remaining expressions are not evaluated. If all the expressions evaluate to true values, the value of the last expression is returned. If there are no expressions then #t is returned.

(and (= 2 2) (> 2 1)) ⇒ #t
(and (= 2 2) (< 2 1)) ⇒ #f
(and 1 2 'c '(f g)) ⇒ (f g)
(and) ⇒ #t

(or expression...)

The expressions are evaluated from left to right, and the value of the first expression that evaluates to a true value is returned. Any remaining expressions are not evaluated. If all expressions evaluate to false values, #f is returned. If there are no expressions then #f is returned.

(or (= 2 2) (> 2 1)) ⇒ #t
(or (= 2 2) (< 2 1)) ⇒ #t
(or #f #f #f) ⇒ #f
(or) ⇒ #f

(if predicate consequent [alternative])

predicate, consequent, and alternative are expressions. An if expression is evaluated as follows: first, predicate is evaluated. If it yields a true value, then consequent is evaluated and the result is returned. Otherwise alternative is evaluated and the result is returned. If predicate yields a false value and no alternative is specified, then the result of the expression is unspecified.

An if expression evaluates either consequent or alternative, never both. Programs should not depend on the value of an if expression that has no alternative.

(if (> 3 2) 'yes 'no) ⇒ yes
(if (> 2 3) 'yes 'no) ⇒ no
(if (> 3 2)
    (- 3 2)
    (+ 3 2)) ⇒ 1

(when predicate expression...)

when is a macro based on if.

If predicate evaluates to a true value, the sequence of expressions is evaluated and the result of the last one is the result of the when form, otherwise the result is undefined.

(when (> x 5)
  (write-line "greater")
  (+ x 2))

The above is equivalent to the following, but is simpler and clearer.

(if (> x 5)
  (begin (write-line "greater")
         (+ x 2)))

(unless predicate expression...)

unless is also a macro based on if.

If predicate evaluates to logically false, the sequence of expressions is evaluated and the result of the last one is the result of the unless form, otherwise the result is undefined.

(unless (> x 5)
  (write-line "greater")
  (+ x 2))

The above is equivalent to the following, but is much simpler and clearer.

(if (> x 5)
  ()
  (begin (write-line "greater")
         (+ x 2)))

Evaluation

(apply function list)

Apply the function that results from evaluating function to the argument list resulting
from evaluating list which, as indicated, must result in a list.

(apply + '(1 2)) ⇒ 3

(eval expression)

Evaluate expression in the current environment.

(eval '(+ 1 2)) ⇒ 3

Type tests

(boolean? object)

Returns whether object is a boolean.

(list? object)

Returns whether object is a list.

(null? object)

Returns whether object is an empty list.

(pair? object)

Returns whether object is a non-empty list. While CircuitScheme does not actually build its lists from pairs (see a scheme/lisp introduction for more on this), instead using Python lists directly, the name for the concept is still used.

(symbol? object)

Returns whether object is a symbol.

(port? object)

Returns whether object is a port, i.e. a file-like object.

Numerical Operations

(+ number1 number2)
(* number1 number2)

(- number1 number2)
(/ number1 number2)

These work as expected.

(not object)

Results in the logical negation of object. Keep in mind that anything other than #f is a true value.

(not #t) ⇒ #f
(not #f) ⇒ #t
(not 0) ⇒ #f

Operations and constants from CircuitPython's math module are directly available from CircuitScheme. E.g.

(sin 3.4) ⇒ -0.255541
pi ⇒ 3.14159

Numeric Comparisons

These work as expected.

(= number1 number2)
(< number1 number2)
(> number1 number2)
(<= number1 number2)

(>= number1 number2)

Equality

(equal? object1 object2)

This is equivalent to CircuitPython's eq operator.

(eq? object1 object2)

This is equivalent to CircuitPython's is operator.

List Operations

(list object...)

Returns a list of its arguments.

(list 'a (+ 3 4) 'c) ⇒ (a 7 c)
(list) ⇒ ()

These expressions are equivalent:

(list OBJ1 OBJ2 ... OBJN)
(cons OBJ1 (cons OBJ2 ... (cons OBJN '()) ...))

(cons obj1 list)

Returns a newly allocated pair whose car is obj1 and whose cdr is list (which must be a list).

(cons 'a '()) ⇒ (a)
(cons '(a) '(b c d)) ⇒ ((a) b c d)
(cons "a" '(b c)) ⇒ ("a" b c)

(append list1 list2)

Returns a list consisting of the elements of list1 followed by the elements of
list2.

(append '(x) '(y)) ⇒ (x y)
(append '(a) '(b c d)) ⇒ (a b c d)
(append '(a (b)) '((c))) ⇒ (a (b) (c))

(car list)

Returns the first item of list (i.e. list[0] in Python). Note that taking the car of the empty list results in an error.

(car '(a b c)) ⇒ a
(car '((a) b c d)) ⇒ (a)

(cdr list)

Returns the contents of the of list, other than the first item (i.e. list[1:] in Python). Note that taking the cdr of the empty list results in the empty list.

(cdr '(a b c d)) ⇒ (b c d)
(cdr '()) ⇒ ()

(length list)

Returns the length of list.

(length '(a b c)) ⇒ 3
(length '(a (b) (c d e))) ⇒ 3
(length '()) ⇒ 0

Input/Output

(open-input-file filename)

Takes a filename referring to an existing file and returns an input port capable of delivering characters from the file. Specifying a non-existent file results in an error. This is equivalent to open(filename) in CircuitPython.

(open-output-file filename)

Takes a filename referring to an output file to be created and returns an output port capable of writing characters to a new file by that name. This is equivalent to open(filename, 'w') in CircuitPython.

(close-input-port port)
(close-output-port port)

Closes port and returns an unspecified value. The associated file is also closed.

(write object [output-port])

Writes a representation of object to output-port (which defaults to the standard output stream (stdout), and returns an unspecified value. If object has a standard external representation, then the written representation generated by write shall be parsable by read into an equivalent object. Thus strings that appear in the written representation are enclosed in double-quotes, and within those strings backslash and double-quote are escaped by backslashes. write performs discretionary output flushing and returns the number of characters written.

(display object  [output-port])

Works as write does except that strings are written without surrounding quotes.

(newline [output-port])

Writes an end-of-line to output-port (defaults to stdout), and returns an unspecified value.

(read [input-port])

Converts external representations of CircuitScheme objects into the objects themselves. read returns the next object parsable from input-port (defaults to the standard input stream (stdin)), updating input-port to point to the first character past the end of the written representation of the object. If an end of file is encountered in the input before any characters are found that can begin an object, read returns an end-of-file object. The input-port remains open, and further attempts to read will also return an end-of-file object. If an end of file is encountered after the beginning of an object’s written representation, but the written representation is incomplete and therefore not parsable, an error is signaled.

(read-char [input-port])

Read a single character from input-port (defaulting to stdin).

(eof-object? object)

Returns #tif object is an end-of-file object; otherwise returns#f.

(load filename)

Reads and evaluates code from the file named by filename. If filename doesn't end in .scm it is appended.

Call-with-current-continuation (aka call/cc)

call/cc provides us with a capability comparable to Python's try/except mechanism. This section is cribbed from Norvig's description since it's so good.

Here are some, examples:

(call/cc (lambda (throw)
           
(+ 5 (* 10 (call/cc (lambda (escape)
                            
        (* 100 (escape 3))))))))
⇒ 35

(call/cc (lambda (throw)
           
(+ 5 (* 10 (call/cc (lambda (escape)
                                 (* 100 (throw 3))))))))
⇒ 3

In the first example, evaluating (escape 3)aborts the current calculation and returns 3 as the value of the enclosing call to call/cc. The result is the same as (+ 5 (* 10 3)) or 35.

In the second example, (throw 3) aborts up two levels, throwing the value of 3 back to the top level.

In general, call/cc takes a single argument, proc, which must be a procedure of one argument. proc is called, passing it a manufactured procedure which here is called throw. If throw is called with a single argument, then that argument is the value of the whole call to call/cc. If throw is not called, the value computed by proc is returned. 

Next Steps

circuitpython_steps.jpg
"Footprints at the Beach" Hansueli Krapf CC-BY-SA 3.0

CircuitScheme is quite a nice implementation of a small, but very Scheme-like language. It's a small subset of a full Scheme, but all the crucial parts are present and it's easy to add more. That, in itself, is one of the hallmarks of the Lisp family of languages: it's easy to extend the language in the language itself. This is a big reason for the evolution that Lisp underwent and the diversity of dialects that were created.

So one path forward is to extend the language to be more appropriate for microcontroller work, taking advantage of the CircuitPython substrate.

Another area of exploration is to improve the REPL. It's very basic at the moment, and more appropriate for its use as a file loader: the repl function gets used by load to essentially type in code from a file. It needs more editing capabilities.

UPDATE: CircutiScheme has an expanded REPL interface now. See this guide for details. TL;DR it gives you what you'd expect for REPL editing.

A related area is the addition of debugging capabilities.

Finally, more wrappers are needed to support more hardware. As we have seen, it's a fairly simple matter to create a wrapper that imports the CircuitPython device driver and exposes the functions required to let it be used from CircuitScheme.

In Closing

CircuitScheme is interesting not only because it's a Lisp running comfortably on SAMD51 boards, but because it does so within the CircuitPython environment, leveraging the benefits that provides.

This guide was first published on Feb 14, 2019. It was last updated on Feb 14, 2019.