Derived from kanaka/mal
Go implementation of a Lisp interpreter.
kanaka/mal
Lisp is Clojure inspired.
Keeping 100% backwards compatibility with kanaka/mal
.
There almost 100 implementations on almost 100 languages available on repository kanaka/mal.
This derived implementation is focused on embeddability in Go projects. See lisp main for an example on how to embed it in Go code.
Requires Go 1.18.
This implementation uses chzyer/readline instead of C implented readline or libedit, making this implementation pure Go.
Changes respect to kanaka/mal:
- Using
def
insted ofdef!
,try
instead oftry*
, etc. symbols atom
is multithread- Tests executed using Go test library. Original implementation uses a
runtest.py
in Python to keep all implementations compatible. But it makes the Go development less enjoyable. Tests files are the original ones, there is simply a newruntest_test.go
that substitutes the original Python script - Some tests are actually in lisp (mal), using the macros commented in Additions section (now only the test library itself). Well, actually not many at this moment, see "Test file specs" below
- Reader regexp's are removed and substituted by an ad-hoc scanner jig/scanner
core
library moved tolib/core
- Using chzyer/readline instead of C
readline
for the mal REPL - Multiline REPL
- REPL history stored in
~/.lisp_history
(instead of kanaka/mal's~/.mal-history
) (let () A B C)
returnsC
as Clojurelet
instead ofA
, and evaluatesA
,B
andC
(do)
returns nil as Clojure instead of panickinghash-map
creates maps or converts a Go object to a map if the marshaler is defined in Go for that objectreduce-kv
addedtake
,take-last
,drop
,drop-last
,subvec
added
To test the implementation use:
go test ./...
go test
actually validates the step*.mal
files.
There are some benchmarks as well:
go test -benchmem -benchtime 5s -bench '^.+$' github.com/jig/lisp
- Debugger: prefix program name with
--debug
. File to debug is the sole argument supported - Errors return line position and stack trace
(range a b)
returns a vector of integers froma
tob-1
(merge hm1 hm2)
returns the merge of two hash maps, second takes precedence(unbase64 string)
,(unbase64 byteString)
,(str2binary string)
,(binary2str byteString)
to deal with[]byte
variables(sleep ms)
sleepsms
milliseconds- Support of
¬
as string terminator to simplify JSON strings. Strings that start with{"
and end with"}
are printed using¬
, otherwise strings are printed as usual (with"
). To escape a¬
character in a¬
delimited string you must escape it by doubling it:¬Hello¬¬World!¬
would be printed asHello¬World
. This behaviour allows to not to have to escape"
nor\
characters (json-decode {} ¬{"key": "value"}¬)
to decode JSON to lisp hash map(json-encode obj)
JSON encodes either a lisp structure or a go. Example:(json-encode (json-decode {} ¬{"key":"value","key1": [{"a":"b","c":"d"},2,3]}¬))
. Note that lisp vectors (e.g.[1 2 3]
) and lisp lists (e.g.(list 1 2 3)
are both converted to JSON vectors always. Decoding a JSON vector is done on a lisp vector always though(hash-map-decode (new-go-object) ¬{"key": "value"}¬)
to decode hash map to a Go struct if that struct has the appropiate Go marshaler(context (do ...))
provides a Go context. Context contents depend on Go, and might be passed to specific functions context compatible- Test minimal library to be used with
maltest
interpreter (see ./cmd/maltest/ folder). See below test specs - Project compatible with GitHub CodeSpaces. Press
.
on your keyboard and you are ready to deploy a CodeSpace with mal in it (assert expr & optional-error)
asserts expression is notnil
norfalse
, otherwise it success returningnil
- Errors are decorated with line numbers
(rename-keys hm hmAlterKeys)
as in Clojure(get-in m ks)
to access nested values from am
map;ks
must be a vector of hash map keys(uuid)
returns an 128 bit rfc4122 random UUID(split string cutset)
returns a lisp Vector of the elements splitted by the cutset (see ./tests/stepH_strings for examples)- support of (hashed, unordered) sets. Only sets of strings or keywords supported. Use
#{}
for literal sets. Functions supported for sets:set
,set?
,conj
,get
,assoc
,dissoc
,contains?
,empty?
.meta
,with-meta
(see ./tests/stepA_mal and (see ./tests/stepA_mal for examples).json-encode
will encode a set to a JSON array update
,update-in
andassoc-in
supported for hash maps and vectors- Go function
READ_WithPreamble
works likeREAD
but supports placeholders to be filled on READ time (see ./placeholder_test.go for som samples) - Added support for
finally
insidetry
.finally
expression is evaluated for side effects only.finally
is optional - Added
spew
- Added
future
, andfuture-*
companion functions from Clojure type?
returns the type name stringgo-error
,unwrap
andpanic
mapping to Go'serrors.New/fmt.Errorf
,Unwrap
andpanic
respectivelygetenv
,setenv
andunsetenv
functions for environment variables
You execute lisp from Go code and get results from it back to Go. Example from ./example_test/example_test.go:
func ExampleEVAL() {
newEnv := env.NewEnv()
// Load required lisp libraries
for _, library := range []struct {
name string
load func(newEnv types.EnvType) error
}{
{"core mal", nscore.Load},
{"core mal with input", nscore.LoadInput},
{"command line args", nscore.LoadCmdLineArgs},
{"core mal extended", nscoreextended.Load},
{"assert", nsassert.Load},
} {
if err := library.load(newEnv); err != nil {
log.Fatalf("Library Load Error: %v", err)
}
}
// parse (READ) lisp code
ast, err := lisp.READ(`(+ 2 2)`, nil)
if err != nil {
log.Fatalf("READ error: %v", err)
}
// eval AST
result, err := lisp.EVAL(ast, newEnv, nil)
if err != nil {
log.Fatalf("EVAL error: %v", err)
}
// use result
if result.(int) != 4 {
log.Fatalf("Result check error: %v", err)
}
// optionally print resulting AST
fmt.Println(lisp.PRINT(result))
// Output: 4
}
You may generate lisp Go structures without having to parse lisp strings, by using Go L
notation.
var (
prn = S("prn")
str = S("str")
)
// (prn (str "hello" " " "world!"))
sampleCode := L(prn, L(str, "hello", " ", "world!"))
EVAL(sampleCode, newTestEnv(), nil)
See ./helloworldlnotationexample_test.go and ./lnotation/lnotation_test.go.
Execute the testfile with:
$ lisp --test .
And a minimal test example sample_test.mal
:
(test.suite "complete tests"
(assert-true "2 + 2 = 4 is true" (= 4 (+ 2 2)))
(assert-false "2 + 2 = 5 is false" (= 5 (+ 2 2)))
(assert-throws "0 / 0 throws an error" (/ 0 0)))
Some benchmark of the implementations:
$ go test -bench ".+" -benchtime 2s
cd cmd/lisp
go install
lisp
Use Ctrl + D to exit Lisp REPL.
lisp helloworld.lisp
This "lisp" implementation is licensed under the MPL 2.0 (Mozilla Public License 2.0). See LICENCE for more details.