Skip to content

Commit

Permalink
Merge pull request #3 from skx/hash
Browse files Browse the repository at this point in the history
Hash
  • Loading branch information
skx authored Aug 1, 2022
2 parents b08561e + 214cf35 commit 63668f0
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 25 deletions.
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ Another trivial/toy Lisp implementation in Go.

Although this implementation is clearly derived from the [make a lisp](https://github.com/kanaka/mal/) series there are a couple of areas where we've implemented special/unusual things:

* Type checking for function parameters
* Via a `:type` suffix. For example `(lambda (a:string b:number) ..`.
* Access to command-line arguments, when used via a shebang.
* See [args.lisp](args.lisp) for an example.
* Introspection via the `(env)` function, which will return details of all variables/functions in the environment.
* Allowing dynamic invocation shown in [dynamic.lisp](dynamic.lisp) and other neat things.
* Optional parameters for functions.
* Any parameter which is prefixed by `&` is optional.
* If not specified then `nil` is assumed.
* Access to command-line arguments, when used via a shebang.
* See [args.lisp](args.lisp) for an example.
* Support for hashes as well as lists.
* A hash looks like this `{ :name "Steve" :location "Helsinki" }`
* `(print (type { :foo "bar" } ))` -> "hash"
* Type checking for function parameters.
* Via a `:type` suffix. For example `(lambda (a:string b:number) ..`.


Here's what the optional parameters look like in practice:
Expand Down Expand Up @@ -163,6 +168,9 @@ We have a reasonable number of functions implemented in our golang core:

* Support for strings, numbers, errors, lists, etc.
* `#t` is the true symbol, `#f` is false, though `true` and `false` are synonyms.
* Hash operations:
* Hashes are literals like this `{ :name "Steve" :location "Helsinki" }`
* Keys can be retrieved/updated via `get`, & `set`.
* List operations:
* `car`, `cdr`, `list`, & `sort`.
* Logical operations
Expand All @@ -176,7 +184,7 @@ We have a reasonable number of functions implemented in our golang core:
* Misc features
* `error`, `str`, `print`, & `type`
* Special forms
* `begin`, `cond`, `define`, `eval`, `if`, `lambda`, `let`, `read`, `set!`, `quote`,
* `begin`, `cond`, `define`, `env`, `eval`, `if`, `lambda`, `let`, `read`, `set!`, `quote`,
* Tail recursion optimization.

Building upon those primitives we have a larger standard-library of functions written in Lisp such as:
Expand Down
42 changes: 39 additions & 3 deletions builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"math"
"os"
"sort"
"time"
"strconv"
"strings"
"time"

"github.com/skx/yal/env"
"github.com/skx/yal/primitive"
Expand Down Expand Up @@ -61,9 +61,11 @@ func PopulateEnvironment(env *env.Environment) {

// core
env.Set("error", &primitive.Procedure{F: errorFn})
env.Set("get", &primitive.Procedure{F: getFn})
env.Set("getenv", &primitive.Procedure{F: getenvFn})
env.Set("now", &primitive.Procedure{F:nowFn})
env.Set("now", &primitive.Procedure{F: nowFn})
env.Set("print", &primitive.Procedure{F: printFn})
env.Set("set", &primitive.Procedure{F: setFn})
env.Set("sort", &primitive.Procedure{F: sortFn})
env.Set("sprintf", &primitive.Procedure{F: sprintfFn})

Expand Down Expand Up @@ -652,6 +654,41 @@ func orFn(args []primitive.Primitive) primitive.Primitive {
return primitive.Bool(false)
}

// getFn is the implementation of `(get hash key)`
func getFn(args []primitive.Primitive) primitive.Primitive {

// We need two arguments
if len(args) != 2 {
return primitive.Error("invalid argument count")
}

// First is a Hash
if _, ok := args[0].(primitive.Hash); !ok {
return primitive.Error("argument not a hash")
}

tmp := args[0].(primitive.Hash)
return tmp.Get(args[1].ToString())
}

// setFn is the implementation of `(set hash key val)`
func setFn(args []primitive.Primitive) primitive.Primitive {

// We need three arguments
if len(args) != 3 {
return primitive.Error("invalid argument count")
}

// First is a Hash
if _, ok := args[0].(primitive.Hash); !ok {
return primitive.Error("argument not a hash")
}

tmp := args[0].(primitive.Hash)
tmp.Set(args[1].ToString(), args[2])
return args[2]
}

// getenvFn is the implementation of `(getenv "PATH")`
func getenvFn(args []primitive.Primitive) primitive.Primitive {

Expand All @@ -670,7 +707,6 @@ func getenvFn(args []primitive.Primitive) primitive.Primitive {
return primitive.String(os.Getenv(string(str)))
}


// nowFn is the implementation of `(now)`
func nowFn(args []primitive.Primitive) primitive.Primitive {

Expand Down
109 changes: 107 additions & 2 deletions builtins/builtins_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package builtins

import (
"math"
"os"
"strings"
"testing"
"time"
"math"

"github.com/skx/yal/env"
"github.com/skx/yal/primitive"
Expand Down Expand Up @@ -1527,8 +1527,113 @@ func TestNow(t *testing.T) {
// Get the current time
tm := time.Now().Unix()

if math.Abs( float64(tm - int64(e)) ) > 10 {
if math.Abs(float64(tm-int64(e))) > 10 {
t.Fatalf("weird result; (now) != now - outside our bound of ten seconds inaccuracy")
}

}

func TestGet(t *testing.T) {

// no arguments
out := getFn([]primitive.Primitive{})

// Will lead to an error
e, ok := out.(primitive.Error)
if !ok {
t.Fatalf("expected error, got %v", out)
}
if !strings.Contains(string(e), "argument") {
t.Fatalf("got error, but wrong one")
}

// First argument must be a hash
out = getFn([]primitive.Primitive{
primitive.String("foo"),
primitive.String("foo"),
})

// Will lead to an error
e, ok = out.(primitive.Error)
if !ok {
t.Fatalf("expected error, got %v", out)
}
if !strings.Contains(string(e), "not a hash") {
t.Fatalf("got error, but wrong one %v", out)
}

// create a hash
h := primitive.Hash{}
h.Entries = make(map[string]primitive.Primitive)
h.Set("Name", primitive.String("STEVE"))

out2 := getFn([]primitive.Primitive{
h,
primitive.String("Name"),
})

// Will lead to a string
s, ok2 := out2.(primitive.String)
if !ok2 {
t.Fatalf("expected string, got %v", out2)
}
if !strings.Contains(string(s), "STEVE") {
t.Fatalf("got string, but wrong one %v", s)
}
}

func TestSet(t *testing.T) {

// no arguments
out := setFn([]primitive.Primitive{})

// Will lead to an error
e, ok := out.(primitive.Error)
if !ok {
t.Fatalf("expected error, got %v", out)
}
if !strings.Contains(string(e), "argument") {
t.Fatalf("got error, but wrong one")
}

// First argument must be a hash
out = setFn([]primitive.Primitive{
primitive.String("foo"),
primitive.String("foo"),
primitive.String("foo"),
})

// Will lead to an error
e, ok = out.(primitive.Error)
if !ok {
t.Fatalf("expected error, got %v", out)
}
if !strings.Contains(string(e), "not a hash") {
t.Fatalf("got error, but wrong one %v", out)
}

// create a hash
h := primitive.Hash{}
h.Entries = make(map[string]primitive.Primitive)

out2 := setFn([]primitive.Primitive{
h,
primitive.String("Name"),
primitive.String("Steve"),
})

// Will lead to a string
s, ok2 := out2.(primitive.String)
if !ok2 {
t.Fatalf("expected string, got %v", out2)
}
if !strings.Contains(string(s), "Steve") {
t.Fatalf("got string, but wrong one %v", s)
}

// Now ensure the hash value was set
v := h.Get("Name")
if v.ToString() != "Steve" {
t.Fatalf("The value wasn't set?")
}
}
10 changes: 5 additions & 5 deletions dynamic.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
;; Given the (string) name of a function to be called, and some
;; arguments .. call it.
;;
;; (env) returns lists of lists, so we can find the function with
;; (env) returns a lists of hashes, so we can find the function with
;; a given name via `filter`. Assuming only one response then we're
;; able to find it by name, and execute it.
;;
Expand All @@ -15,14 +15,14 @@
(fn nil)) ; fn is the function of the result

;; find the entry in the list with the right name
(set! out (filter (env) (lambda (x) (eq (car x) name))))
(set! out (filter (env) (lambda (x) (eq (get x :name) name))))

;; there should be only one matching entry
(if (= (length out) 1)
(begin
(set! nm (car (car out))) ;; nm == name
(set! fn (car (cdr (car out)))) ;; fn is the function to call
(if fn (fn args))))))) ;; if we got it, invoke it
(set! nm (get (car out) :name)) ;; nm == name
(set! fn (get (car out) :value)) ;; fn is the function to call
(if fn (fn args))))))) ;; if we got it, invoke it


;; Print a string
Expand Down
69 changes: 61 additions & 8 deletions eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,56 @@ func (ev *Eval) readExpression() (primitive.Primitive, error) {
ev.offset++

return list, nil
case ")":
case "{":
// Are we at the end of our program?
if ev.offset >= len(ev.toks) {
return nil, ErrEOF
}

// Create a hash, which we'll populate with items
// until we reach the matching ")" statement
hash := primitive.Hash{}
hash.Entries = make(map[string]primitive.Primitive)

// Loop until we hit the closing bracket
for ev.toks[ev.offset] != "}" {

// Read the sub-expressions, recursively.
key, err := ev.readExpression()
if err != nil {
return nil, err
}

// Check again we've not hit the end of the program
if ev.offset >= len(ev.toks) {
return nil, ErrEOF
}

// Read the sub-expressions, recursively.
val, err2 := ev.readExpression()
if err2 != nil {
return nil, err2
}

// Check again we've not hit the end of the program
if ev.offset >= len(ev.toks) {
return nil, ErrEOF
}

hash.Set(key.ToString(), val)
}

// We bump the current read-position one more here,
// which means we skip over the closing "}" character.
ev.offset++

return hash, nil
case ")", "}":
// We shouldn't ever hit this, because we skip over
// the closing ")" when we handle "(".
//
// If a program is malformed we'll see it though
return nil, errors.New("unexpected ')'")
return nil, errors.New("unexpected '" + token + "'")
default:

// Return just a single atom/primitive.
Expand Down Expand Up @@ -260,6 +304,10 @@ func (ev *Eval) eval(exp primitive.Primitive, e *env.Environment) primitive.Prim
case primitive.String:
return exp

// Hashes return themselves
case primitive.Hash:
return exp

// Nil returns itself
case primitive.Nil:
return exp
Expand Down Expand Up @@ -512,9 +560,12 @@ func (ev *Eval) eval(exp primitive.Primitive, e *env.Environment) primitive.Prim

v := val.(primitive.Primitive)

var tmp primitive.List
tmp = append(tmp, primitive.String(key))
tmp = append(tmp, v)
var tmp primitive.Hash
tmp.Entries = make(map[string]primitive.Primitive)

tmp.Set(":name", primitive.String(key))
tmp.Set(":value", v)

c = append(c, tmp)
}

Expand Down Expand Up @@ -616,9 +667,9 @@ func (ev *Eval) eval(exp primitive.Primitive, e *env.Environment) primitive.Prim
evalArgExp := ev.eval(argExp, e)

// Was it an error? Then abort
_, ok := evalArgExp.(primitive.Error)
x, ok := evalArgExp.(primitive.Error)
if ok {
return primitive.Error(fmt.Sprintf("error expanding argument %v for call to (%s ..)", argExp, listExp[0]))
return primitive.Error(fmt.Sprintf("error expanding argument %v for call to (%s ..): %s", argExp, listExp[0], x.ToString()))
}

// Otherwise append it to the list we'll supply
Expand Down Expand Up @@ -705,10 +756,12 @@ func (ev *Eval) eval(exp primitive.Primitive, e *env.Environment) primitive.Prim
}

// Since we're calling `type` we need to
// do some rewriting for those two unusual cases
// do some rewriting for the function-case,
// which has distinct types.
if typ == "function" {
valid["procedure(lisp)"] = true
valid["procedure(golang)"] = true
valid["macro"] = true
continue
}

Expand Down
Loading

0 comments on commit 63668f0

Please sign in to comment.