Skip to content

Latest commit

 

History

History
192 lines (146 loc) · 7.69 KB

README.md

File metadata and controls

192 lines (146 loc) · 7.69 KB

lisp

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

Changes respect to kanaka/mal:

  • Using def insted of def!, try instead of try*, 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 new runtest_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 to lib/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) returns C as Clojure let instead of A, and evaluates A, B and C
  • (do) returns nil as Clojure instead of panicking
  • hash-map creates maps or converts a Go object to a map if the marshaler is defined in Go for that object
  • reduce-kv added
  • take, 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

Additions

  • 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 from a to b-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) sleeps ms 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 as Hello¬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 not nil nor false, otherwise it success returning nil
  • Errors are decorated with line numbers
  • (rename-keys hm hmAlterKeys) as in Clojure
  • (get-in m ks) to access nested values from a m 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 and assoc-in supported for hash maps and vectors
  • Go function READ_WithPreamble works like READ but supports placeholders to be filled on READ time (see ./placeholder_test.go for som samples)
  • Added support for finally inside try. finally expression is evaluated for side effects only. finally is optional
  • Added spew
  • Added future, and future-* companion functions from Clojure
  • type? returns the type name string
  • go-error, unwrap and panic mapping to Go's errors.New/fmt.Errorf, Unwrap and panic respectively
  • getenv, setenv and unsetenv functions for environment variables

Embed Lisp in Go code

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
}

L notation

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.

Test file specs

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

Install

cd cmd/lisp
go install

Execute REPL

lisp

Use Ctrl + D to exit Lisp REPL.

Execute lisp program

lisp helloworld.lisp

Licence

This "lisp" implementation is licensed under the MPL 2.0 (Mozilla Public License 2.0). See LICENCE for more details.