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.
This commit is contained in:
Sameer Rahmani 2020-12-04 21:08:48 +00:00
parent b3f621ece1
commit 31a4cfb765
9 changed files with 483 additions and 6 deletions

View File

@ -33,4 +33,5 @@ type IColl interface {
ISeq
ICountable
ToSlice() []IExpr
Cons(e IExpr) IExpr
}

View File

@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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")

View File

@ -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("<Fn: %s at %p", f.name, f)
if f.isMacro {
return fmt.Sprintf("<Macro: %s at %p>", f.name, f)
}
return fmt.Sprintf("<Fn: %s at %p>", 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("<Fn: %s at %p", f.name, f)
if f.isMacro {
return fmt.Sprintf("<Macro: %s at %p>", f.name, f)
}
return fmt.Sprintf("<Fn: %s at %p>", 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,
}
}

View File

@ -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,

100
bootstrap/pkg/core/macro.go Normal file
View File

@ -0,0 +1,100 @@
/*
Serene --- Yet an other Lisp
Copyright (c) 2020 Sameer Rahmani <lxsameer@gnu.org>
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 <http://www.gnu.org/licenses/>.
*/
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
}
}
}

View File

@ -0,0 +1,159 @@
/*
Serene --- Yet an other Lisp
Copyright (c) 2020 Sameer Rahmani <lxsameer@gnu.org>
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 <http://www.gnu.org/licenses/>.
*/
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)
}

View File

@ -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{},

View File

@ -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) {

View File

@ -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))
}