Skip to content

Latest commit

 

History

History
363 lines (217 loc) · 12.9 KB

INTRODUCTION.md

File metadata and controls

363 lines (217 loc) · 12.9 KB

Brief slisp Introduction

slisp is a typical lisp implementation, complete with support for floating-point numbers, integers, strings, characters, lambdas, packages, and functions. The compiler generates standalone binaries, which run in a self-contained way. There is also an interpreter (which has a REPL), and both of these benefit from a runtime stop&copy garbage-collection process.

Primitive Types

  • Comments are begun with ";" and continue until the end of the line.
    • There are no block comments.
  • We support integers and floating point numbers for mathematical operations.
  • Integers may be written in any base the golang strconv.ParseInt function supports:
    • (print 3)
    • (print 0xff)
    • (print 0b10101010)
  • Floating point numbers are only supported literally, in base10. (For example (print 3.4))
  • We don't have a boolean type, but nil (or the empty list) is false.
    • Anything else is true, and we have a t symbol for when you want to show that explicitly.
  • Strings are encoded literally, and escaped characters are honored:
    • (print "I say \"Hello, world\".\n") has embedded quotes and a trailing newline, as you would expect.
  • Character-literals are specified with a #\ prefix:
    • (print #\*)
  • Lists are written using parenthesis to group them:
    • (print (list 1 2 3))
  • The only native data structures we support is a list.
    • But alists and plists are implemented in our standard library, and are documented below.

We don't have "symbols" exposed to the language, but if you prefix a variable with ":" it will become visually distinct, and this is useful when working with alists, or plists. Internally that is actually translated to a stringified version of the variable name (So (print :name) becomes (print "name") - that might seem weird but it works for alist/plist usage, etc.)

Aliases

The (alias! ..) special form allows remapping a function:

(defun bar () (print "bar"))
(defun foo () (print "foo"))
(alias! "foo" "bar")

After that calls to (foo) will be rerouted to (bar). In our compiler we accept strings for the old/new functions, in our interpreter we accept bare references (and strings too for compatibility):

(alias! foo bar)

Bindings

To start a new scope with local variables use let:

(let ((foo "bar")
      (baz  "bart"))
  (println "foo is " foo)
  (println "baz is " baz))

To update the contents of a bound variable use set!:

(set! foo "bar")

So:

(let ((foo 23))
  (println foo)           ; prints 23.
  (set! foo (* 3 foo))
  (println foo))           ; prints 69.

Note that bindings within the let statements can refer to previous bindings, and so this is also valid:

(let ((x 3)
      (y (* x x)))
   (print y))

(i.e. let is actually let*.)

DO

The function do allows any number of expressions to be evaluated, and is useful if you want to run multiple expressions in inside one of the branches of an if expression, for example.

Any time you want to run multiple expressions but only one is permitted use do:

  (do
    (println "I'm the first expression")
    (println "I'm the second expression")
    (println "Multiple expressions can happen here.."))

Our defun, lambda, and let expressions allow an unlimited number of expressions to be executed within their bodies. Our if expression only allows a single expression to be executed, but using do you can run more.

IF

if is a standard of lisp, and we support it:

(if 1
  (println "This is executed")
 (println "This is not"))

The return value of the expression is the return value of the last executed expression.

If you want to run multiple expressions in either the "true" or "false" branch use do as seen above.

(if (not nil)
  (do
    (print "I'm true")
    (print "Multiple expressions can happen here..")
    ))

As noted you can use do to execute multiple blocks, but a simpler alternative is to use when and unless. These are macros which allow running a block of code when a condition is true, or false. The difference here is that you cannot have both a TRUE and FALSE block at the same time, only one or the other:

; When the condition is true, run the expressions.
(when (< i 10)
  (println "This is an expression")
  (println "This is an expression")
  (println "This is an expression")
  )

; When the condition is false, run the expressions.
(unless nil
  (println "This is an expression")
  (println "This is an expression")
  (println "This is an expression")
  )

Functions

To define a function use defun:

(defun fact (n)
  (if (<= n 1) 1 (* n (fact (- n 1)))))

Optionally you may write some help/usage information in your definition:

(defun fact (n)
  "Return the factorial of the given number."
  (if (<= n 1) 1 (* n (fact (- n 1)))))

In the compiler the help information does nothing useful, but inside inception you can use the help function to view it:

> (help print)
     Arguments: (&xs)
     Summary  : Print everything we're given.
     This function accepts variadic arguments, and maps against a local lambda to
     do the right thing for each entry in the given list.

Here's another simple function:

(defun square (x)
   "Square the given argument."
   (* x x))

A function may be defined with the sole/last argument having an &-prefix, which means this is a function which will accept a variable number of arguments. When such functions are called any extra parameters are converted into a list and available in that way. For example:

;; Accept any number of arguments
(defun foo (&args)
   (println args))

(foo)                  ; Prints: <nil>
(foo "Hello" "World")  ; Prints: ("Hello" "World")
(foo 1 2 3)            ; Prints: (1 2 3)

You can see this demonstrated in the print function, in our stdlib.lisp file.

NOTE: There must be a function named main, as that is the entry-point to the compiled program. This function can be defined either like so:

(defun main() ... )

Or like this, if you wish to receive the command-line arguments, supplied as a list:

(defun main(args) ... )

Global Variables

A global variable may be defined via defvar:

; Create a global variable
(defvar version 0.5)

A global variable may be declared as constant, which will cause errors when attempts are made to modify it, for this use defconst:

; Create a global variable which may not be modified
(defconst pi 3.14159)

Lambdas

Lambdas are functions which can be passed around, and we implement closures to allow counters and adders to be created, etc.

Here's an example of applying a function to each entry in a list:

 ; Create a scope with a list "n" containing numbers 1-20.
 (let ((n (nat 20)))

    ;; Print the results of squaring every item in that list.
    (println (map (lambda (x) (* x x)) n))
 )

Lists

Lists are internally created as cons-pairs, and you can create such a thing manually like so:

; Manually create the list "1 2 3"
(cons 1 (cons 2 (cons 3 nil)))

But using the list function allows that to be done more neatly:

; The same thing
(list 1 2 3)

(For the common case of creating lists of numbers see the nat, seq and range functions in our PRIMITIVES.md list.)

Association Lists (alist)

We only have lists as our main data-type, but using a list we can create something that is hash-like in behavior. An association list is one way of implementing that, it is a list of individual key-value lists, which can be used to store data.

This is hash-like because keys are unique, if you set the value of a key :name twice the second update removes the previous value.

Imagine you wanted to store details about a person you might use something like this to represent their details:

( (name "Bob") (age 42) (location Europe) )

Here's how you might use the functions:

(let ((a (alist:new)))
   (set! a (alist:set :name  "Steve"))
   (set! a (alist:set :email "steve@example.com"))
   (set! a (alist:set :hair  "Red"))

   ;; Do stuff
   (println "Person name " (alist:get a :name)))

See test/alist.lisp for an example of use.

Property Lists (plist)

A property-list, or plist, is a similar way of using a list to store details in a hash-like manner.

Because if you set the key ":foo" you remove any previous value.

Compared to an alist the list is flat, so an example might look like this:

 ( name "Bob" age 42 location "Europe" )

Here's how you might use the functions:

(let ((p (plist:new)))
   (set! p (plist:set :name  "Steve"))
   (set! p (plist:set :email "steve@example.com"))
   (set! p (plist:set :hair  "Red"))

   ;; Do stuff

   (println "Person name " (plist:get p :name)))

See test/plist.lisp for an example of use.

Requiring Packages

You'll note that plist and alist functionality we documented above is implemented in a pair of files within our packages/ directory:

By convention every package will prefix its functions, and variables, with the package name. This is why we have alist:new and plist:new. This is partly to avoid naming collisions, and partly to make it explicit these are things that were included externally.

Usually you'd need to run (require alist) to load such an external package, however these two libraries are included by default via our standard library.

You do need to (require arg-parser) if you wish to use that as that package is not loaded by default. Similarly you must (require maths) if you wish to load our enhanced mathematical primitives which allow arbitrary argument counts (e.g. To permit (+ 1 2 3 4) instead of the two arguments our default + routine permits).

The behaviour of our (require ..) primitive varies depending on the implementation:

  • Within the compiler this is a compile-time behaviour, akin to the C preprocessor's #include directive.
    • It literally inserts the code at the current position in the source.
  • Within our interpreter this can be called at any time, and includes and immediately evaluates the included code.
    • The (packages) function can be used to introspect the names of available embedded packages.

In all cases the process of resolving a (require) statement is the same:

  • Given (require FOO)
    • Look to see if the file foo.lisp was included in our binary. If so evaluate it.
    • Otherwise add ".lisp" suffix to FOO unless there is already a suffix of some kind.
    • Look for it on every directory within the current directory and the directories referenced on the LISP_PATH environmental variable. (Which is :-separated.)
    • If not found continue, but perhaps log a message.

Looping

We support the while expression to run loops:

(let ((i 0))
  (while (< i 10)
    (println i)
    (set! i (+ i 1))))

You can see this demonstrated in the brainfuck.lisp program.

File inclusion

We have the ability to include other files, literally, via (require name), by default this will look for name.lisp contained within our compiler itself, otherwise from within the current directory.

You can specify a list of directories to search with the LISP_PATH environmental variable. For example:

 export LISP_PATH=/usr/share/slisp:lib/

Then (require foo) will attempt to load, in order:

  • The embedded file-system created when the compiler is built.
    • This will contain packages/*.lisp.
  • ./foo.lisp
  • /usr/share/slisp/foo.lisp
  • ./lib/foo.lisp

REPL Introspection

The REPL has support for showing function names which match a given pattern, along with retrievng help-text from specific functions.

Here we see we have no arg-parser functions:

 > (functions "arg-parser")
 <nil>

But load the package and now they are visible:

 > (require arg-parser)
 <nil>
 > (functions "arg-parser")
 (arg-parser:expand-short-flags arg-parser:files arg-parser:flags-from-arg arg-parser:flags-helper arg-parser:new)

All available functions may be returned by (functions) otherwise only those which contain the given string are returned. (This is literal substring matching, rather than regular expression matches - we don't have support for regular expressions.)

The square function we demonstrated above had a help-string, and here's how to view it:

 > (defun square (x) "Square the given argument." (* x x))
 (symbol square)
 > (help square)
 Arguments: (x)
 Summary  : Square the given argument.
 <nil>

See Also

  • README.md
    • The main project introduction.
  • PRIMITIVES.md
    • The list of built-in functions, whether implemented in Golang or slisp itself.