Skip to content

Commit

Permalink
Merge pull request #93 from skx/82-struct
Browse files Browse the repository at this point in the history
82 struct
  • Loading branch information
skx authored Nov 13, 2022
2 parents 508f7c2 + 4f2250e commit ebd8879
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 8 deletions.
61 changes: 60 additions & 1 deletion INTRODUCTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,63 @@

# Brief Yal Introduction

To set the contents of a variable use `set!`:
Yal is a typical toy lisp with support for numbers, strings, characters, hashes and structures.


## Primitive Types

Primitive types work as you would expect:

* Strings are just encoded literally, and escaped characters are honored:
* `(print "Hello, world\n")`
* Numbers can be written as integers in decimal, binary, or hex.
* Floating point numbers are also supported:
* `(print 3)`
* `(print 0xff)`
* `(print 0b1010)`
* `(print 3.4)`
* Characters are written with a `#\` prefix.
* `(print #\*)`


## Other Types

We support hashes, which are key/value pairs, written between `{` and `}` pairs:

```lisp
(print { name "Steve" age (- 2022 1976) } )
```

Functions exist for getting/setting fields by name, and for iterating over keys, values, or key/value pairs.

We also support structures, which are syntactical sugar for hashes, along with the autogeneration of some methods.

To define a "person" with three fields you'd write:

```lisp
(struct person name age address)
```

Once this `struct` has been defined it can be populated via the constructor:

```lisp
(person "Steve" "18" "123 Fake Street")
```

The structure's fields can be accessed, and updated:

```
; Define "me" as a person with fields
(set! me (person "Steve" "18" "123 Fake Street"))
; Change the adddress
(person.address me "999 Fake Lane")
```


## Variables

To set the contents of a variable use `set!` which we saw above:

(set! foo "bar")

Expand All @@ -28,6 +84,9 @@ form of `set!`:
;..
)


## Functions

To define a function use `set!` with `fn*`:

(set! fact (fn* (n)
Expand Down
34 changes: 33 additions & 1 deletion PRIMITIVES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* [Symbols](#symbols)
* [Special Forms](#special-forms)
* [Core Primitives](#core-primitives)
* [Structure Methods](#structure-methods)
* [Standard Library](#standard-library)
* [Type Checking](#type-checking)
* [Testing](#testing)
Expand All @@ -14,7 +15,8 @@
Here is a list of all the primitives which are available to yal users.

Note that you might need to consult the source of the standard-library, or
the help file, to see further details. This is just intended as a summary.
the help file, to see further details. This document is primarily intended
as a quick summary, and might lapse behind reality at times.


## Symbols
Expand Down Expand Up @@ -74,6 +76,8 @@ Special forms are things that are built into the core interpreter, and include:
* Read a form from the specified string.
* `set!`
* Set the value of a variable.
* `struct`
* Define a structure.
* `symbol`
* Create a new symbol from the given string.
* `try`
Expand Down Expand Up @@ -222,6 +226,34 @@ Things you'll find here include:



## Structure Methods

A structure is a minimal wrapper over a hash, but when a structure is
defined several methods are created. Assuming a person-structure has
been defined like so:

```lisp
(struct person name age address)
```

There is now a new structure, named `person` with three fields `name`, `age`, and `address` which can be instantiated.

To help operate upon this structure several methods have also been created:

* `(person "name" "age" "address")`
* Constructor method, which returns a new struct instance.
* If the number of arguments is less than the number of object-fields they will be left unset (i.e. nil).
* `(person? obj)`
* Returns true if the given object is an instance of the person struct.
* `(person.name obj [new-value])`
* Accessor/Mutator for the name-field in the given struct instance.
* `(person.age obj [new-value])`
* Accessor/Mutator for the age-field in the given struct instance.
* `(person.address obj [new-value])`
* Accessor/Mutator for the address-field in the given struct instance.



## Standard Library

The standard library consists of routines, and helpers, which are written in 100% yal itself.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

* [A brief introduction to using this lisp](INTRODUCTION.md).
* Getting started setting variables, defining functions, etc.
* This includes documentation on enhanced features such as
* Hashes.
* Structures.
* [A list of primitives we have implemented](PRIMITIVES.md).
* This describes the functions we support, whether implemented in lisp or golang.
* For example `(car)`, `(cdr)`, `(file:lines)`, `(shell)`, etc.
Expand Down
148 changes: 143 additions & 5 deletions eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,29 @@ type Eval struct {

// aliases contains any record of aliased functionality
aliases map[string]string

// structs contains a list of known structures.
//
// The key is the name of the structure, and the value is an
// array of the fields that structure possesses.
structs map[string][]string

// accessors contains struct field lookups
//
// The key is the name of the fake method, the value the name of
// the field to get/set
accessors map[string]string
}

// New constructs a new lisp interpreter.
func New(src string) *Eval {

// Create with a default context.
e := &Eval{
context: context.Background(),
symbols: make(map[string]primitive.Primitive),
context: context.Background(),
symbols: make(map[string]primitive.Primitive),
structs: make(map[string][]string),
accessors: make(map[string]string),
}

// Setup the default symbol-table entries
Expand Down Expand Up @@ -302,6 +316,8 @@ func (ev *Eval) eval(exp primitive.Primitive, e *env.Environment, expandMacro bo

ret.Set(x, val)
}

ret.SetStruct(obj.GetStruct())
return ret

// Numbers return themselves
Expand Down Expand Up @@ -641,6 +657,31 @@ func (ev *Eval) eval(exp primitive.Primitive, e *env.Environment, expandMacro bo
}
return ev.atom(listExp[1].ToString())

// (struct
case primitive.Symbol("struct"):
if len(listExp) <= 2 {
return primitive.ArityError()
}

// name of structure
name := listExp[1].ToString()

// the fields it contains
fields := []string{}

// convert the fields to strings
for _, field := range listExp[2:] {

f := field.ToString()

ev.accessors[name+"."+f] = f
fields = append(fields, f)
}

// save the structure as a known-thing
ev.structs[name] = fields
return primitive.Nil{}

// (env
case primitive.Symbol("env"):

Expand Down Expand Up @@ -792,16 +833,113 @@ func (ev *Eval) eval(exp primitive.Primitive, e *env.Environment, expandMacro bo
}

// Anything else is either a built-in function,
// or a user-function.
// a structure, or a user-function.
default:

// first item of the list - i.e. the thing
// we're gonna call.
thing := listExp[0]

// args supplied to this call
listArgs := listExp[1:]

// Is this a structure field access
access, okA := ev.accessors[thing.ToString()]
if okA {

if len(listArgs) == 1 || len(listArgs) == 2 {

obj := ev.eval(listArgs[0], e, expandMacro)
hsh, okH := obj.(primitive.Hash)
if okH {

if len(listArgs) == 1 {
return hsh.Get(access)
}

val := ev.eval(listArgs[1], e, expandMacro)
hsh.Set(access, val)
return primitive.Nil{}
}
}

}
// Is this a structure creation?
fields, ok := ev.structs[thing.ToString()]
if ok {

// ensure that we have some fields that
// match those we expect.
if len(listArgs) > len(fields) {
return primitive.ArityError()
}

// Create a hash to store the state
hash := primitive.NewHash()

// However mark this as a "struct",
// rather than a hash.
hash.SetStruct(thing.ToString())

// Set the fields, ensuring we evaluate them
for i, name := range fields {
if i < len(listArgs) {
hash.Set(name, ev.eval(listArgs[i], e, expandMacro))
} else {
hash.Set(name, primitive.Nil{})
}
}
return hash
}

// Is this a type-check on a struct?
if strings.HasSuffix(thing.ToString(), "?") {

// We're looking for a function-call
// that has a trailing "?", and one
// argument
if len(listExp) != 2 {
return primitive.ArityError()
}

// Get the thing that is being tested.
typeName := strings.TrimSuffix(thing.ToString(), "?")

// Does that represent a known-type?
_, ok2 := ev.structs[typeName]
if ok2 {

// OK a type-check on a known struct
//
// Note we evaluate the object
obj := ev.eval(listExp[1], e, expandMacro)

// is it a hash?
hsh, ok2 := obj.(primitive.Hash)
if !ok2 {
// nope - then not a struct
return primitive.Bool(false)
}

// is the struct-type the same as the type name?
if hsh.GetStruct() == typeName {
return primitive.Bool(true)
}
return primitive.Bool(false)
}

// just a method call with a trailing "?".
//
// could be "string?", etc, so we fall-through
}

// Find the thing we're gonna call.
procExp := ev.eval(listExp[0], e, expandMacro)
procExp := ev.eval(thing, e, expandMacro)

// Is it really a procedure we can call?
proc, ok := procExp.(*primitive.Procedure)
if !ok {
return primitive.Error(fmt.Sprintf("argument '%s' not a function", listExp[0].ToString()))
return primitive.Error(fmt.Sprintf("argument '%s' not a function", thing.ToString()))
}

// build up the arguments
Expand Down
17 changes: 17 additions & 0 deletions eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,14 @@ a
{"(cond false 7 (= 2 2) 8 \"else\" 9)", "8"},
{"(cond false 7 false 8 false 9)", "nil"},

// struct
{"(struct foo bar) (set! me (foo 3)) (foo.bar me)", "3"},
{"(struct foo bar) (set! me (foo 3)) (foo.bar me 32) (foo.bar me)", "32"},
{`(struct cat name age) (set! me (cat "meow" 3)) (cat.name me)`, "meow"},
{`(struct cat name age) (set! me (cat "meow")) (cat.name me)`, "meow"},
{`(struct cat name age) (set! me (cat "meow" 3)) (cat.age me)`, "3"},
{`(struct cat name age) (set! me (cat "meow")) (cat.age me)`, "nil"},

// maths
{"(+ 3 1)", "4"},
{"(- 3 1)", "2"},
Expand Down Expand Up @@ -322,6 +330,15 @@ a
{"(let* (a 3 b))", "ERROR{list for (len*) must have even length, got [a 3 b]}"},
{"(let* (a 3 3 b))", "ERROR{binding name is not a symbol, got 3}"},

{"(struct foo bar) (type (foo 3))", "struct-foo"},
{"(struct foo bar) (foo 3 3)", primitive.ArityError().ToString()},
{"(struct foo bar) (foo? nil)", "#f"},
{"(struct foo bar) (foo? (foo 3))", "#t"},
{"(struct a name) (struct b name) (a? (b 3))", "#f"},
{"(struct a name) (struct b name) (b? (b 3))", "#t"},

{"(struct)", primitive.ArityError().ToString()},
{"(do (struct foo bar ) (foo?))", primitive.ArityError().ToString()},
{"(error )", primitive.ArityError().ToString()},
{"(quote )", primitive.ArityError().ToString()},
{"(quasiquote )", primitive.ArityError().ToString()},
Expand Down
8 changes: 8 additions & 0 deletions lisp-tests.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,14 @@ If the name of the test is not unique then that will cause an error to be printe
(deftest binary:1 (list (dec2bin 3) "11"))
(deftest binary:2 (list (dec2bin 4) "100"))

;; structures
(deftest struct:1 (list (do (struct person name) (type (person "me")))
"struct-person"))
(deftest struct:2 (list (do (struct person name) (person? (person "me")))
true))
(deftest struct:3 (list (do (struct person name) (person.name (person "me")))
"me"))


;;
;; Define a function to run all the tests, by iterating over the hash.
Expand Down
Loading

0 comments on commit ebd8879

Please sign in to comment.