Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

82 struct #93

Merged
merged 12 commits into from
Nov 13, 2022
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