1/*
2 * gomacro - A Go interpreter with Lisp-like macros
3 *
4 * Copyright (C) 2017-2019 Massimiliano Ghilardi
5 *
6 *     This Source Code Form is subject to the terms of the Mozilla Public
7 *     License, v. 2.0. If a copy of the MPL was not distributed with this
8 *     file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 *
10 *
11 * macroexpand.go
12 *
13 *  Created on Jun 09, 2017
14 *      Author Massimiliano Ghilardi
15 */
16
17package fast
18
19import (
20	"go/ast"
21	r "reflect"
22
23	. "github.com/cosmos72/gomacro/ast2"
24	. "github.com/cosmos72/gomacro/base"
25	mt "github.com/cosmos72/gomacro/token"
26)
27
28// MacroExpandNodeCodewalk traverses the whole AST tree using pre-order traversal,
29// and replaces each node with the result of MacroExpandNode(node).
30// It implements the macroexpansion phase
31func (c *Comp) MacroExpandNodeCodewalk(in ast.Node) (out ast.Node, anythingExpanded bool) {
32	if in == nil {
33		return nil, false
34	}
35	var form Ast = ToAst(in)
36	form, anythingExpanded = c.MacroExpandCodewalk(form)
37	out = ToNode(form)
38	// if !anythingExpanded {
39	//    c.Debugf("MacroExpand1() nothing to expand: %v <%v>", out, r.TypeOf(out))
40	//}
41	return out, anythingExpanded
42}
43
44// MacroExpandCodewalk traverses the whole AST tree using pre-order traversal,
45// and replaces each node with the result of MacroExpand(node).
46// It implements the macroexpansion phase
47func (c *Comp) MacroExpandCodewalk(in Ast) (out Ast, anythingExpanded bool) {
48	return c.macroExpandCodewalk(in, 0)
49}
50
51func (c *Comp) macroExpandCodewalk(in Ast, quasiquoteDepth int) (out Ast, anythingExpanded bool) {
52	if in == nil || in.Size() == 0 {
53		return in, false
54	}
55	debug := c.Options&OptDebugMacroExpand != 0
56	if quasiquoteDepth <= 0 {
57		if debug {
58			c.Debugf("MacroExpandCodewalk: qq = %d, macroexpanding %v", quasiquoteDepth, in.Interface())
59		}
60		in, anythingExpanded = c.MacroExpand(in)
61	}
62	if in != nil {
63		in = UnwrapTrivialAst(in)
64	}
65	if in == nil {
66		return in, anythingExpanded
67	}
68	saved := in
69
70	if expr, ok := in.(UnaryExpr); ok {
71		op := expr.X.Op
72		switch op {
73		case mt.MACRO:
74			break
75		case mt.QUOTE:
76			// QUOTE prevents macroexpansion only if found outside any QUASIQUOTE
77			if quasiquoteDepth == 0 {
78				return saved, anythingExpanded
79			}
80		case mt.QUASIQUOTE:
81			// extract the body of QUASIQUOTE
82			quasiquoteDepth++
83		case mt.UNQUOTE, mt.UNQUOTE_SPLICE:
84			// extract the body of UNQUOTE or UNQUOTE_SPLICE
85			quasiquoteDepth--
86		default:
87			goto Recurse
88		}
89		inChild := UnwrapTrivialAst(in.Get(0).Get(1))
90		outChild, expanded := c.macroExpandCodewalk(inChild, quasiquoteDepth)
91		if op == mt.MACRO {
92			return outChild, expanded
93		}
94		out := in
95		if expanded {
96			out = MakeQuote2(expr, outChild.(AstWithNode))
97		}
98		return out, expanded
99	}
100Recurse:
101	if in == nil {
102		return saved, anythingExpanded
103	}
104	if debug {
105		c.Debugf("MacroExpandCodewalk: qq = %d, recursing on %v", quasiquoteDepth, in)
106	}
107	out = in.New()
108	n := in.Size()
109	if outSlice, appendable := out.(AstWithSlice); appendable {
110		// New() returns zero-length slice... resize it
111		for outSlice.Size() < n {
112			outSlice = outSlice.Append(nil)
113		}
114		out = outSlice
115	}
116	for i := 0; i < n; i++ {
117		child := UnwrapTrivialAst(in.Get(i))
118		if child != nil {
119			expanded := false
120			if child.Size() != 0 {
121				child, expanded = c.macroExpandCodewalk(child, quasiquoteDepth)
122			}
123			if expanded {
124				anythingExpanded = true
125			}
126		}
127		out.Set(i, child)
128	}
129	if debug {
130		c.Debugf("MacroExpandCodewalk: qq = %d, expanded to %v", quasiquoteDepth, out)
131	}
132	return out, anythingExpanded
133}
134
135// MacroExpandNode repeatedly invokes MacroExpandNode1
136// as long as the node represents a macro call.
137// it returns the resulting node.
138func (c *Comp) MacroExpandNode(in ast.Node) (out ast.Node, everExpanded bool) {
139	if in == nil {
140		return nil, false
141	}
142	inAst := ToAst(in)
143	outAst, everExpanded := c.MacroExpand(inAst)
144	out = ToNode(outAst)
145	// if !everExpanded {
146	//    c.Debugf("MacroExpand1() not a macro: %v <%v>", out, r.TypeOf(out))
147	//}
148	return out, everExpanded
149}
150
151// MacroExpand repeatedly invokes MacroExpand
152// as long as the node represents a macro call.
153// it returns the resulting node.
154func (c *Comp) MacroExpand(form Ast) (out Ast, everExpanded bool) {
155	var expanded bool
156	for {
157		form, expanded = c.MacroExpand1(form)
158		if !expanded {
159			return form, everExpanded
160		}
161		everExpanded = true
162	}
163}
164
165// if node represents a macro call, MacroExpandNode1 executes it
166// and returns the resulting node.
167// Otherwise returns the node argument unchanged
168func (c *Comp) MacroExpandNode1(in ast.Node) (out ast.Node, expanded bool) {
169	if in == nil {
170		return nil, false
171	}
172	var form Ast = ToAst(in)
173	form, expanded = c.MacroExpand1(form)
174	out = ToNode(form)
175	// if !expanded {
176	//    c.Debugf("MacroExpandNode1: not a macro: %v <%v>", out, r.TypeOf(out))
177	//}
178	return out, expanded
179}
180
181func (c *Comp) extractMacroCall(form Ast) Macro {
182	form = UnwrapTrivialAst(form)
183	switch form := form.(type) {
184	case Ident:
185		sym := c.TryResolve(form.X.Name)
186		if sym != nil && sym.Bind.Desc.Class() == ConstBind && sym.Type != nil && sym.Type.Kind() == r.Struct {
187			switch value := sym.Value.(type) {
188			case Macro:
189				if c.Options&OptDebugMacroExpand != 0 {
190					c.Debugf("MacroExpand1: found macro: %v", form.X.Name)
191				}
192				return value
193			}
194		}
195	}
196	return Macro{}
197}
198
199// if node represents a macro call, MacroExpandNode1 executes it
200// and returns the resulting node.
201// Otherwise returns the node argument unchanged
202func (c *Comp) MacroExpand1(in Ast) (out Ast, expanded bool) {
203	if in == nil {
204		return nil, false
205	}
206	// unwrap trivial nodes: DeclStmt, ParenExpr, ExprStmt
207	in = UnwrapTrivialAstKeepBlocks(in)
208	ins, ok := in.(AstWithSlice)
209	if !ok {
210		return in, false
211	}
212	debug := c.Options&OptDebugMacroExpand != 0
213	if debug {
214		c.Debugf("MacroExpand1: found list: %v", ins.Interface())
215	}
216	n := ins.Size()
217	outs := ins.New().(AstWithSlice)
218
219	// since macro calls are sequences of statements,
220	// we must scan the whole list,
221	// consume it as needed by the macros we find,
222	// and build a new list accumulating the results of macroexpansion
223	for i := 0; i < n; i++ {
224		elt := ins.Get(i)
225		macro := c.extractMacroCall(elt)
226		if macro.closure == nil {
227			outs = outs.Append(elt)
228			continue
229		}
230		argn := macro.argNum
231		leftn := n - i - 1
232		var args []r.Value
233		if argn > leftn {
234			args := make([]r.Value, leftn+1) // include the macro itself
235			for j := 0; j <= leftn; j++ {
236				args[j] = r.ValueOf(ins.Get(i + j).Interface())
237			}
238			c.Errorf("not enough arguments for macroexpansion of %v: expecting %d, found %d", args, macro.argNum, leftn)
239			return in, false
240		}
241		if debug {
242			c.Debugf("MacroExpand1: found macro call %v at %d-th position of %v", elt.Interface(), i, ins.Interface())
243		}
244		// wrap each ast.Node into a reflect.Value
245		args = make([]r.Value, argn)
246		for j := 0; j < argn; j++ {
247			args[j] = r.ValueOf(ToNode(ins.Get(i + j + 1)))
248		}
249		// invoke the macro
250		results := macro.closure(args)
251		if debug {
252			c.Debugf("MacroExpand1: macro expanded to: %v", results)
253		}
254		var out Ast
255		switch len(results) {
256		default:
257			args = append([]r.Value{r.ValueOf(elt.Interface())}, args...)
258			c.Warnf("macroexpansion returned %d values, using only the first one: %v %v returned %v",
259				len(results), args, results)
260			fallthrough
261		case 1:
262			any := results[0].Interface()
263			if any != nil {
264				out = anyToAst(any, "macroexpansion")
265				break
266			}
267			fallthrough
268		case 0:
269			// do not insert nil nodes... they would wreak havok, convert them to the identifier nil
270			out = Ident{&ast.Ident{Name: "nil"}}
271		}
272		outs = outs.Append(out)
273		i += argn
274		expanded = true
275	}
276	if !expanded {
277		return in, false
278	}
279	if outs.Size() == 0 {
280		return EmptyStmt{&ast.EmptyStmt{}}, true
281	}
282	return UnwrapTrivialAst(outs), true
283}
284