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
definsted ofdef!,tryinstead oftry*, etc. symbols atomis multithread- Tests executed using Go test library. Original implementation uses a
runtest.pyin 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.gothat 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
corelibrary moved tolib/core- Using chzyer/readline instead of C
readlinefor the mal REPL - Multiline REPL
- REPL history stored in
~/.lisp_history(instead of kanaka/mal's~/.mal-history) (let () A B C)returnsCas Clojureletinstead ofA, and evaluatesA,BandC(do)returns nil as Clojure instead of panickinghash-mapcreates maps or converts a Go object to a map if the marshaler is defined in Go for that objectreduce-kvaddedtake,take-last,drop,drop-last,subvecadded
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 fromatob-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[]bytevariables(sleep ms)sleepsmsmilliseconds- 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
maltestinterpreter (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 notnilnorfalse, 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 ammap;ksmust 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-encodewill encode a set to a JSON array update,update-inandassoc-insupported for hash maps and vectors- Go function
READ_WithPreambleworks likeREADbut supports placeholders to be filled on READ time (see ./placeholder_test.go for som samples) - Added support for
finallyinsidetry.finallyexpression is evaluated for side effects only.finallyis optional - Added
spew - Added
future, andfuture-*companion functions from Clojure type?returns the type name stringgo-error,unwrapandpanicmapping to Go'serrors.New/fmt.Errorf,Unwrapandpanicrespectivelygetenv,setenvandunsetenvfunctions 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 2scd cmd/lisp
go installlispUse Ctrl + D to exit Lisp REPL.
lisp helloworld.lispThis "lisp" implementation is licensed under the MPL 2.0 (Mozilla Public License 2.0). See LICENCE for more details.