From 31a4cfb7652215a5b360fc0fc5bce75a84579b4d Mon Sep 17 00:00:00 2001 From: Sameer Rahmani Date: Fri, 4 Dec 2020 21:08:48 +0000 Subject: [PATCH] Add support for macros via defmacro Added the support for normal macro (in compare to reader macros and other types) expansion with out quasiquote support. The quasiquote will be added as a recursive macro itself. --- bootstrap/pkg/core/coll.go | 1 + bootstrap/pkg/core/eval.go | 128 +++++++++++++++++++++++++ bootstrap/pkg/core/function.go | 26 +++-- bootstrap/pkg/core/list.go | 20 ++++ bootstrap/pkg/core/macro.go | 100 +++++++++++++++++++ bootstrap/pkg/core/quasiquote.go | 159 +++++++++++++++++++++++++++++++ bootstrap/pkg/core/runtime.go | 6 ++ bootstrap/pkg/core/sforms.go | 45 +++++++++ bootstrap/pkg/core/types.go | 4 + 9 files changed, 483 insertions(+), 6 deletions(-) create mode 100644 bootstrap/pkg/core/macro.go create mode 100644 bootstrap/pkg/core/quasiquote.go diff --git a/bootstrap/pkg/core/coll.go b/bootstrap/pkg/core/coll.go index 640fa65..04c6027 100644 --- a/bootstrap/pkg/core/coll.go +++ b/bootstrap/pkg/core/coll.go @@ -33,4 +33,5 @@ type IColl interface { ISeq ICountable ToSlice() []IExpr + Cons(e IExpr) IExpr } diff --git a/bootstrap/pkg/core/eval.go b/bootstrap/pkg/core/eval.go index f4115e7..49b3df2 100644 --- a/bootstrap/pkg/core/eval.go +++ b/bootstrap/pkg/core/eval.go @@ -19,6 +19,8 @@ along with this program. If not, see . package core import ( + "fmt" + "serene-lang.org/bootstrap/pkg/ast" ) @@ -126,6 +128,21 @@ tco: break tco // return ret, err } + // Expand macroes that exists in the given array of expression `forms`. + // Since this implementation of Serene is an interpreter, the line + // between compile time and runtime is unclear (afterall every thing + // is happening in runtime). So we need to expand macroes before evaluating + // other forms. In the future we might want to cache the expanded AST + // as a cache and some sort of a bytecode for faster evaluation. + forms, err = macroexpand(rt, scope, forms) + if err != nil { + return nil, err + } + + if forms.GetType() != ast.List { + return evalForm(rt, scope, forms) + } + list := forms.(*List) // Empty list evaluates to itself @@ -164,6 +181,63 @@ tco: } return list.Rest().First(), nil + // case "quasiquote-expand": + // return quasiquote(list.Rest().First()), nil + + // // For `quasiquote` evaluation rules, check out the documentation on + // // the `quasiquote` function in `quasiquote.go` + // case "quasiquote": + // expressions = quasiquote(list.Rest().First()) + // continue tco // Loop over to execute the new expressions + + // TODO: Implement `list` in serene itself when we have destructuring available + // Creates a new list form it's arguments. + case "list": + return evalForm(rt, scope, list.Rest().(*List)) + + // TODO: Implement `concat` in serene itself when we have protocols available + // Concats all the collections together. + case "concat": + evaledForms, err := evalForm(rt, scope, list.Rest().(*List)) + + if err != nil { + return nil, err + } + + lists := evaledForms.(*List).ToSlice() + + result := []IExpr{} + for _, lst := range lists { + if lst.GetType() != ast.List { + return nil, MakeErrorFor(rt, lst, fmt.Sprintf("don't know how to concat '%s'", lst.String())) + } + + result = append(result, lst.(*List).ToSlice()...) + } + return MakeList(result), nil + + // TODO: Implement `list` in serene itself when we have destructuring available + // Calls the `Cons` function on the second argument to cons the first arg to it. + // In terms of a list, cons adds the first argument to as the new head of the list + // given in the second argument. + case "cons": + if list.Count() != 3 { + return nil, MakeErrorFor(rt, list, "'cons' needs exactly 3 arguments") + } + + evaledForms, err := evalForm(rt, scope, list.Rest().(*List)) + + if err != nil { + return nil, err + } + coll, ok := evaledForms.(*List).Rest().First().(IColl) + + if !ok { + return nil, MakeErrorFor(rt, list, "second arg of 'cons' has to be a collection") + } + + return coll.Cons(evaledForms.(*List).First()), nil + // `def` evaluation rules // * The first argument has to be a symbol. // * The second argument has to be evaluated and be used as @@ -174,6 +248,32 @@ tco: ret, err = Def(rt, scope, list.Rest().(*List)) break tco // return + // `defmacro` evaluation rules: + // * The first argument has to be a symbol + // * The second argument has to be a list of argument for the macro + // * The rest of the arguments will form a block that acts as the + // body of the macro. + case "defmacro": + ret, err = DefMacro(rt, scope, list.Rest().(*List)) + break tco // return + + // `macroexpand` evaluation rules: + // * It has to have only one argument + // * It WILL evaluate the only argument and tries to expand it + // as a macro and returns the expanded forms. + case "macroexpand": + if list.Count() != 2 { + return nil, MakeErrorFor(rt, list, "'macroexpand' needs exactly one argument.") + } + evaledForm, e := evalForm(rt, scope, list.Rest().(*List)) + + if e != nil { + return nil, e + } + + ret, err = macroexpand(rt, scope, evaledForm.(*List).First()) + break tco // return + // `fn` evaluation rules: // * It needs at least a collection of arguments // * Defines an anonymous function. @@ -217,6 +317,34 @@ tco: expressions = MakeBlock(list.Rest().(*List).ToSlice()) continue tco // Loop over to execute the new expressions + // `eval` evaluation rules: + // * It only takes on arguments. + // * The argument has to be a form. For example if we pass a string + // to it as an argument that contains some expressions it will + // evaluate the string as string which will result to the same + // string. So IT DOES NOT READ the argument. + // * It will evaluate the given form as the argument and return + // the result. + case "eval": + if list.Count() != 2 { + return nil, MakeErrorFor(rt, list, "'eval' needs exactly 1 arguments") + } + form, err := evalForm(rt, scope, list.Rest().(*List)) + if err != nil { + return nil, err + } + + return EvalForms(rt, scope, form) + + // `let` evaluation rules: + // Let's assume the following: + // L = (let (A B C D) BODY) + // * Create a new scope which has the current scope as the parent + // * Evaluate the bindings by evaluating `B` and bind it to the name `A` + // in the scope. + // * Repeat the prev step for expr D and name C + // * Eval the block `BODY` using the created scope and return the result + // which is the result of the last expre in `BODY` case "let": if list.Count() < 2 { return nil, MakeError(rt, "'let' needs at list 1 aruments") diff --git a/bootstrap/pkg/core/function.go b/bootstrap/pkg/core/function.go index f4c8c37..1523624 100644 --- a/bootstrap/pkg/core/function.go +++ b/bootstrap/pkg/core/function.go @@ -44,15 +44,24 @@ type Function struct { params IColl // A reference to the body block of the function - body *Block + body *Block + isMacro bool } func (f *Function) GetType() ast.NodeType { return ast.Fn } +func (f *Function) IsMacro() bool { + return f.isMacro +} + func (f *Function) String() string { - return fmt.Sprintf("", f.name, f) + } + + return fmt.Sprintf("", f.name, f) } func (f *Function) GetName() string { @@ -69,7 +78,11 @@ func (f *Function) GetParams() IColl { } func (f *Function) ToDebugStr() string { - return fmt.Sprintf("", f.name, f) + } + + return fmt.Sprintf("", f.name, f) } func (f *Function) GetBody() *Block { @@ -80,9 +93,10 @@ func (f *Function) GetBody() *Block { // the given `scope`. func MakeFunction(scope IScope, params IColl, body *Block) *Function { return &Function{ - scope: scope, - params: params, - body: body, + scope: scope, + params: params, + body: body, + isMacro: false, } } diff --git a/bootstrap/pkg/core/list.go b/bootstrap/pkg/core/list.go index 838d4ac..0957775 100644 --- a/bootstrap/pkg/core/list.go +++ b/bootstrap/pkg/core/list.go @@ -89,8 +89,28 @@ func (l *List) ToSlice() []IExpr { return l.exprs } +func (l *List) Cons(e IExpr) IExpr { + elems := l.ToSlice() + return MakeList(append([]IExpr{e}, elems...)) +} + // END: IColl --- +func (l *List) AppendToList(e IExpr) *List { + l.exprs = append(l.exprs, e) + return l +} + +func ListStartsWith(l *List, sym string) bool { + if l.Count() > 0 { + firstElem := l.First() + if firstElem.GetType() == ast.Symbol { + return firstElem.(*Symbol).GetName() == sym + } + } + return false +} + func MakeList(elements []IExpr) *List { return &List{ exprs: elements, diff --git a/bootstrap/pkg/core/macro.go b/bootstrap/pkg/core/macro.go new file mode 100644 index 0000000..c75ad0e --- /dev/null +++ b/bootstrap/pkg/core/macro.go @@ -0,0 +1,100 @@ +/* + Serene --- Yet an other Lisp + +Copyright (c) 2020 Sameer Rahmani + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package core + +import "serene-lang.org/bootstrap/pkg/ast" + +// Serene macros are in fact functions with the `isMacro` flag set to true. +// We have only normal macro implementation in bootstrap version of serene in +// compare to reader macros and evaluator macros and and other types. + +// MakeMacro creates a macro with the given `params` and `body` in +// the given `scope`. +func MakeMacro(scope IScope, name string, params IColl, body *Block) *Function { + return &Function{ + name: name, + scope: scope, + params: params, + body: body, + isMacro: true, + } +} + +// isMacroCall looks up the given `form` in the given `scope` if it is a symbol. +// If there is a value associated with the symbol in the scope, it will be checked +// to be a macro. +func isMacroCall(rt *Runtime, scope IScope, form IExpr) (*Function, bool) { + if form.GetType() == ast.List { + list := form.(*List) + if list.Count() == 0 { + return nil, false + } + + first := list.First() + var macro IExpr = nil + + if first.GetType() == ast.Symbol { + binding := scope.Lookup(first.(*Symbol).GetName()) + if binding != nil && binding.Public { + macro = binding.Value + } + } + if macro != nil { + if macro.GetType() == ast.Fn && macro.(*Function).IsMacro() { + return macro.(*Function), true + } + + } + } + return nil, false +} + +// applyMacro works very similar to how we evaluate function calls the only difference +// is that we don't evaluate the arguments and create the bindings in the scope of the +// body directly as they are. It's Lisp Macroes after all. +func applyMacro(rt *Runtime, macro *Function, args *List) (IExpr, IError) { + mscope, e := MakeFnScope(rt, macro.GetScope(), macro.GetParams(), args) + + if e != nil { + return nil, e + } + + return EvalForms(rt, mscope, macro.GetBody()) +} + +// macroexpand expands the given `form` as a macro and returns the resulted +// expression +func macroexpand(rt *Runtime, scope IScope, form IExpr) (IExpr, IError) { + var macro *Function + var e IError + ok := false + + for { + macro, ok = isMacroCall(rt, scope, form) + if !ok { + return form, nil + } + + form, e = applyMacro(rt, macro, form.(IColl).Rest().(*List)) + + if e != nil { + return nil, e + } + } +} diff --git a/bootstrap/pkg/core/quasiquote.go b/bootstrap/pkg/core/quasiquote.go new file mode 100644 index 0000000..fd023f8 --- /dev/null +++ b/bootstrap/pkg/core/quasiquote.go @@ -0,0 +1,159 @@ +/* + Serene --- Yet an other Lisp + +Copyright (c) 2020 Sameer Rahmani + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package core + +import "serene-lang.org/bootstrap/pkg/ast" + +// func qqLoop(xs []IExpr) IExpr { +// acc := MakeEmptyList() +// for i := len(xs) - 1; 0 <= i; i -= 1 { +// elem := xs[i] +// switch elem.GetType() { +// case ast.List: +// if ListStartsWith(elem.(*List), "unquote-splicing") { +// acc = MakeList([]IExpr{ +// MakeSymbol(MakeNodeFromExpr(elem), "concat"), +// elem.(*List).Rest().First(), +// acc}) +// continue +// } +// default: +// } +// acc = MakeList([]IExpr{ +// MakeSymbol(MakeNodeFromExpr(elem), "cons"), +// quasiquote(elem), +// acc}) +// } +// return acc +// } + +// func quasiquote(e IExpr) IExpr { +// switch e.GetType() { +// case ast.Symbol: +// return MakeList([]IExpr{ +// MakeSymbol(MakeNodeFromExpr(e), "quote"), e}) +// case ast.List: +// list := e.(*List) +// if ListStartsWith(list, "unquote") { +// return list.Rest().First() +// } +// if ListStartsWith(list, "quasiquote") { +// return quasiquote(qqLoop(list.ToSlice())) +// } + +// return qqLoop(list.ToSlice()) +// default: +// return e +// } +// } +const qqQUOTE string = "*quote*" + +func isSymbolEqual(e IExpr, name string) bool { + if e.GetType() == ast.Symbol && e.(*Symbol).GetName() == name { + return true + } + return false +} +func isQuasiQuote(e IExpr) bool { + return isSymbolEqual(e, "quasiquote") +} + +func isUnquote(e IExpr) bool { + return isSymbolEqual(e, "unquote") +} + +func isUnquoteSplicing(e IExpr) bool { + return isSymbolEqual(e, "unquote-splicing") +} + +func qqSimplify(e IExpr) (IExpr, IError) { + return e, nil +} + +func qqProcess(rt *Runtime, e IExpr) (IExpr, IError) { + switch e.GetType() { + + // Example: `x => (*quote* x) => (quote x) + case ast.Symbol: + return MakeList([]IExpr{ + MakeSymbol(MakeNodeFromExpr(e), qqQUOTE), + e, + }), nil + + case ast.List: + list := e.(*List) + first := list.First() + + // Example: ``... reads as (quasiquote (quasiquote ...)) and this if will check + // for the second `quasiquote` + if isQuasiQuote(first) { + result, err := qqCompletelyProcess(rt, list.Rest().First()) + + if err != nil { + return nil, err + } + + return qqProcess(rt, result) + } + + // Example: `~x reads as (quasiquote (unquote x)) + if isUnquote(first) { + return list.Rest().First(), nil + } + // ??? + if isUnquoteSplicing(first) { + return nil, MakeErrorFor(rt, first, "'unquote-splicing' is not allowed out of a collection.") + } + + // p := list + // q := MakeEmptyList() + // for { + // p = p.Rest().(*List) + // } + + } + + return e, nil +} + +func qqRemoveQQFunctions(e IExpr) (IExpr, IError) { + return e, nil +} + +func qqCompletelyProcess(rt *Runtime, e IExpr) (IExpr, IError) { + rawResult, err := qqProcess(rt, e) + + if err != nil { + return nil, err + } + + if rt.IsQQSimplificationEnabled() { + rawResult, err = qqSimplify(rawResult) + + if err != nil { + return nil, err + } + } + + return qqRemoveQQFunctions(rawResult) +} + +func quasiquote(rt *Runtime, e IExpr) (IExpr, IError) { + return qqCompletelyProcess(rt, e) +} diff --git a/bootstrap/pkg/core/runtime.go b/bootstrap/pkg/core/runtime.go index 169a136..27c1a6f 100644 --- a/bootstrap/pkg/core/runtime.go +++ b/bootstrap/pkg/core/runtime.go @@ -63,6 +63,12 @@ func (r *Runtime) CreateNS(name string, source string, setAsCurrent bool) { r.namespaces[name] = ns } +func (r *Runtime) IsQQSimplificationEnabled() bool { + // TODO: read the value of this flag from the arguments of serene + // and set the default to true + return false +} + func MakeRuntime(debug bool) *Runtime { return &Runtime{ namespaces: map[string]Namespace{}, diff --git a/bootstrap/pkg/core/sforms.go b/bootstrap/pkg/core/sforms.go index 1ebf705..fac3930 100644 --- a/bootstrap/pkg/core/sforms.go +++ b/bootstrap/pkg/core/sforms.go @@ -54,6 +54,51 @@ func Def(rt *Runtime, scope IScope, args *List) (IExpr, IError) { return nil, MakeError(rt, "'def' form need at least 2 arguments") } +// Def defines a macro in the current namespace. The first +// arguments in `args` has to be a symbol ( none ns qualified ) and +// the rest of params should be the body of the macro. Unlike other +// expressions in Serene `defmacro` DOES NOT evaluate its arguments. +// That is what makes macros great +func DefMacro(rt *Runtime, scope IScope, args *List) (IExpr, IError) { + + // TODO: Add support for docstrings and meta + + switch args.Count() { + case 3: + name := args.First() + + if name.GetType() != ast.Symbol { + return nil, MakeError(rt, "the first argument of 'defmacro' has to be a symbol") + } + + sym := name.(*Symbol) + + var params IColl + body := MakeEmptyBlock() + + arguments := args.Rest().First() + + // TODO: Add vector in here + // Or any other icoll + if arguments.GetType() == ast.List { + params = arguments.(IColl) + } + + if args.Count() > 2 { + body.SetContent(args.Rest().Rest().(*List).ToSlice()) + } + + macro := MakeMacro(scope, sym.GetName(), params, body) + + ns := rt.CurrentNS() + ns.DefineGlobal(sym.GetName(), macro, true) + + return macro, nil + } + + return nil, MakeError(rt, "'defmacro' form need at least 2 arguments") +} + // Fn defines a function inside the given scope `scope` with the given `args`. // `args` contains the arugment list, docstring and body of the function. func Fn(rt *Runtime, scope IScope, args *List) (IExpr, IError) { diff --git a/bootstrap/pkg/core/types.go b/bootstrap/pkg/core/types.go index c0a91e8..8862a2c 100644 --- a/bootstrap/pkg/core/types.go +++ b/bootstrap/pkg/core/types.go @@ -65,6 +65,10 @@ func MakeNodeFromLocation(loc ast.Location) Node { } } +func MakeNodeFromExpr(e IExpr) Node { + return MakeNodeFromLocation(e.GetLocation()) +} + func MakeNode(input *[]string, start int, end int) Node { return MakeNodeFromLocation(ast.MakeLocation(input, start, end)) }