1/*
2 * gomacro - A Go interpreter with Lisp-like macros
3 *
4 * Copyright (C) 2018-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 * cmd.go
12 *
13 *  Created on: Apr 20, 2018
14 *      Author: Massimiliano Ghilardi
15 */
16
17package fast
18
19import (
20	"errors"
21	"io"
22	"sort"
23	"strings"
24
25	"github.com/cosmos72/gomacro/base/paths"
26
27	"github.com/cosmos72/gomacro/base"
28	bstrings "github.com/cosmos72/gomacro/base/strings"
29)
30
31// ====================== Cmd ==============================
32
33// Cmd is an interpreter special command.
34//
35// The following Interp methods look for special commands and execute them:
36// Cmd, EvalFile, EvalReader, ParseEvalPrint, ReadParseEvalPrint, Repl, ReplStdin
37// note that Interp.Eval() does **not** look for special commands!
38//
39// Cmd.Name is the command name **without** the initial ':'
40//   it must be a valid Go identifier and must not be empty.
41//   Using a reserved Go keyword (const, for, func, if, package, return, switch, type, var...)
42//   or predefined identifier (bool, int, rune, true, false, nil...)
43//   is a bad idea because it interferes with gomacro preprocessor mode.
44//   Current limitation: Cmd.Name[0] must be ASCII.
45//
46// Cmd.Help is the help string that will be displayed by :help
47//   please look at current :help output and use the same layout if possible.
48//
49// Cmd.Func is the command implementation. it receives as arguments:
50//   - the current Interp object,
51//   - the (possibly multi-line) argument string typed by the user
52//     note: it will always have balanced amounts of {} [] () '' "" and ``
53//   - the current command options
54//
55// Cmd.Func can perform any action desired by the implementor,
56// including calls to Interp methods, and it must return:
57//   - a string to be subsequently evaluated by the interpreter.
58//     return the empty string if the command does not need any subsequent evaluation,
59//     or if it performed the evaluation by itself.
60//   - the updated command options.
61//     return the received 'opt' argument unless you need to update it.
62//
63// If Cmd.Func needs to print something, it's recommended to use
64//      g := &interp.Comp.Globals
65//      g.Fprintf(g.Stdout, FORMAT, ARGS...)
66//   instead of the various fmt.*Print* functions, in order to
67//   pretty-print interpreter-generated objects (g.Fprintf)
68//   and to honour configured redirections (g.Stdout)
69//
70// To register a new special command, use Commands.Add()
71// To unregister an existing special command, use Commands.Del()
72// To list existing special commands, use Commands.List()
73type Cmd struct {
74	Name string
75	Func func(interp *Interp, arg string, opt base.CmdOpt) (string, base.CmdOpt)
76	Help string
77}
78
79// if cmd.Name starts with prefix return 0;
80// else if cmd.Name < prefix return -1;
81// else return 1
82func (cmd *Cmd) Match(prefix string) int {
83	name := cmd.Name
84	if strings.HasPrefix(name, prefix) {
85		return 0
86	} else if name < prefix {
87		return -1
88	} else {
89		return 1
90	}
91}
92
93func (cmd *Cmd) ShowHelp(g *base.Globals) {
94	c := string(g.ReplCmdChar)
95
96	help := strings.Replace(cmd.Help, "%c", c, -1)
97	g.Fprintf(g.Stdout, "%s%s\n", c, help)
98}
99
100// ===================== Cmds ==============================
101
102type Cmds struct {
103	m map[byte][]Cmd
104}
105
106// search for a Cmd whose name starts with prefix.
107// return (zero value, io.EOF) if no match.
108// return (cmd, nil) if exactly one match.
109// return (zero value, list of match names) if more than one match
110func (cmds Cmds) Lookup(prefix string) (Cmd, error) {
111	if len(prefix) != 0 {
112		if vec, ok := cmds.m[prefix[0]]; ok {
113			i, err := prefixSearch(vec, prefix)
114			if err != nil {
115				return Cmd{}, err
116			}
117			return vec[i], nil
118		}
119	}
120	return Cmd{}, io.EOF
121}
122
123// prefix search: find all the Cmds whose name start with prefix.
124// if there are none, return 0 and io.EOF
125// if there is exactly one, return its index and nil.
126// if there is more than one, return 0 and an error listing the matching ones
127func prefixSearch(vec []Cmd, prefix string) (int, error) {
128	lo, _ := binarySearch(vec, prefix)
129	n := len(vec)
130	for ; lo < n; lo++ {
131		cmp := vec[lo].Match(prefix)
132		if cmp < 0 {
133			continue
134		} else if cmp == 0 {
135			break
136		} else {
137			return 0, io.EOF
138		}
139	}
140	if lo == n {
141		return 0, io.EOF
142	}
143	hi := lo + 1
144	for ; hi < n; hi++ {
145		if vec[hi].Match(prefix) > 0 {
146			break
147		}
148	}
149	if lo+1 == hi {
150		return lo, nil
151	}
152	names := make([]string, hi-lo)
153	for i := lo; i < hi; i++ {
154		names[i-lo] = vec[i].Name
155	}
156	return 0, errors.New(strings.Join(names, " "))
157}
158
159// plain binary search for exact Cmd name
160func binarySearch(vec []Cmd, exact string) (int, bool) {
161	lo, hi := 0, len(vec)-1
162	for lo <= hi {
163		mid := (lo + hi) / 2
164		name := vec[mid].Name
165		if name < exact {
166			lo = mid + 1
167		} else if name > exact {
168			hi = mid - 1
169		} else {
170			return mid, true
171		}
172	}
173	return lo, false
174}
175
176// return the list of currently registered special commands
177func (cmds Cmds) List() []Cmd {
178	var list []Cmd
179	for _, vec := range cmds.m {
180		for _, cmd := range vec {
181			list = append(list, cmd)
182		}
183	}
184	sortCmdList(list)
185	return list
186}
187
188// order Cmd list by name
189func sortCmdList(vec []Cmd) {
190	sort.Slice(vec, func(i, j int) bool {
191		return vec[i].Name < vec[j].Name
192	})
193}
194
195// register a new Cmd.
196// if cmd.Name is the empty string, do nothing and return false.
197// overwrites any existing Cmd with the same name
198func (cmds Cmds) Add(cmd Cmd) bool {
199	name := cmd.Name
200	if len(name) == 0 {
201		return false
202	}
203	c := name[0]
204	vec, _ := cmds.m[c]
205	if pos, ok := binarySearch(vec, name); ok {
206		vec[pos] = cmd
207	} else {
208		vec = append(vec, cmd)
209		sortCmdList(vec)
210		cmds.m[c] = vec
211	}
212	return true
213}
214
215// unregister an existing Cmd by name. return true if existed.
216// Use with care!
217func (cmds Cmds) Del(name string) bool {
218	if len(name) != 0 {
219		c := name[0]
220		if vec, ok := cmds.m[c]; ok {
221			if pos, ok := binarySearch(vec, name); ok {
222				vec = removeCmd(vec, pos)
223				if len(vec) == 0 {
224					delete(cmds.m, c)
225				} else {
226					cmds.m[c] = vec
227				}
228				return true
229			}
230		}
231	}
232	return false
233}
234
235// remove Cmd at index 'pos' from slice.
236// return updated slice.
237func removeCmd(vec []Cmd, pos int) []Cmd {
238	head := vec[:pos]
239	n := len(vec)
240	if pos == n-1 {
241		return head
242	}
243	tail := vec[pos+1:]
244	if pos == 0 {
245		return tail
246	}
247	headn, tailn := pos, len(tail)
248	if headn >= tailn {
249		copy(vec[headn:], tail)
250		vec = vec[:n-1]
251	} else {
252		copy(vec[1:], head)
253		vec = vec[1:]
254	}
255	return vec
256}
257
258func (cmds Cmds) ShowHelp(g *base.Globals) {
259	out := g.Stdout
260	g.Fprintf(out, "%s",
261		"// type Go code to execute it. example: func add(x, y int) int { return x + y }\n\n// interpreter commands:\n")
262
263	for _, cmd := range cmds.List() {
264		cmd.ShowHelp(g)
265	}
266	g.Fprintf(out, "%s", "// abbreviations are allowed if unambiguous.\n")
267}
268
269var Commands Cmds
270
271func init() {
272	Commands.m = map[byte][]Cmd{
273		'd': []Cmd{{"debug", (*Interp).cmdDebug, `debug EXPR        debug expression or statement interactively`}},
274		'e': []Cmd{{"env", (*Interp).cmdEnv, `env [NAME]        show available functions, variables and constants
275                   in current package, or from imported package NAME`}},
276		'h': []Cmd{{"help", (*Interp).cmdHelp, `help              show this help`}},
277		'i': []Cmd{{"inspect", (*Interp).cmdInspect, `inspect EXPR      inspect expression interactively`}},
278		'o': []Cmd{{"options", (*Interp).cmdOptions, `options [OPTS]    show or toggle interpreter options`}},
279		'p': []Cmd{{"package", (*Interp).cmdPackage, `package "PKGPATH" switch to package PKGPATH, importing it if possible`}},
280		'q': []Cmd{{"quit", (*Interp).cmdQuit, `quit              quit the interpreter`}},
281		'u': []Cmd{{"unload", (*Interp).cmdUnload, `unload "PKGPATH"  remove package PKGPATH from the list of known packages.
282                   later attempts to import it will trigger a recompile`}},
283		'w': []Cmd{{"write", (*Interp).cmdWrite, `write [FILE]      write collected declarations and/or statements to standard output or to FILE
284                   use %copt Declarations and/or %copt Statements to start collecting them`}},
285	}
286}
287
288// ==================== Interp =============================
289
290// execute one of the REPL commands starting with ':'
291// return any remainder string to be evaluated, and the options to evaluate it
292func (ir *Interp) Cmd(src string) (string, base.CmdOpt) {
293	g := &ir.Comp.Globals
294	var opt base.CmdOpt
295
296	trim := strings.TrimSpace(src)
297	n := len(trim)
298	if n > 0 && trim[0] == g.ReplCmdChar {
299		prefix, arg := bstrings.Split2(trim[1:], ' ') // skip g.ReplCmdChar
300		cmd, err := Commands.Lookup(prefix)
301		if err == nil {
302			src, opt = cmd.Func(ir, arg, opt)
303		} else if err == io.EOF {
304			// ":<something>"
305			// temporarily disable collection of declarations and statements,
306			// and temporarily disable macroexpandonly (i.e. re-enable eval)
307			opt |= base.CmdOptForceEval
308			src = " " + src[1:] // slower than src = src[1:], but gives accurate column positions in error messages
309		} else {
310			g.Warnf("ambiguous command %q matches: %s", prefix, err)
311			return "", opt
312		}
313	} else if g.Options&base.OptMacroExpandOnly == 0 && (trim == "package" || strings.HasPrefix(trim, "package ")) {
314		_, arg := bstrings.Split2(trim, ' ')
315		src, opt = ir.cmdPackage(arg, opt)
316	}
317	return src, opt
318}
319
320func (ir *Interp) cmdDebug(arg string, opt base.CmdOpt) (string, base.CmdOpt) {
321	g := &ir.Comp.Globals
322	if len(arg) == 0 {
323		g.Fprintf(g.Stdout, "// debug: missing argument\n")
324	} else {
325		g.Print(ir.Debug(arg))
326	}
327	return "", opt
328}
329
330func (ir *Interp) cmdEnv(arg string, opt base.CmdOpt) (string, base.CmdOpt) {
331	ir.ShowPackage(arg)
332	return "", opt
333}
334
335func (ir *Interp) cmdHelp(arg string, opt base.CmdOpt) (string, base.CmdOpt) {
336	Commands.ShowHelp(&ir.Comp.Globals)
337	return "", opt
338}
339
340func (ir *Interp) cmdInspect(arg string, opt base.CmdOpt) (string, base.CmdOpt) {
341	g := &ir.Comp.Globals
342	if len(arg) == 0 {
343		g.Fprintf(g.Stdout, "// inspect: missing argument\n")
344	} else {
345		ir.Inspect(arg)
346	}
347	return "", opt
348}
349
350func (ir *Interp) cmdOptions(arg string, opt base.CmdOpt) (string, base.CmdOpt) {
351	c := ir.Comp
352	g := &c.Globals
353
354	if len(arg) != 0 {
355		g.Options ^= base.ParseOptions(arg)
356
357		debugdepth := 0
358		if g.Options&base.OptDebugFromReflect != 0 {
359			debugdepth = 1
360		}
361		c.CompGlobals.Universe.DebugDepth = debugdepth
362
363	} else {
364		g.Fprintf(g.Stdout, "// current options: %v\n", g.Options)
365		g.Fprintf(g.Stdout, "// unset   options: %v\n", ^g.Options)
366	}
367	return "", opt
368}
369
370// change package. pkgpath can be empty or a package path WITH quotes
371// 'package NAME' where NAME is without quotes has no effect.
372func (ir *Interp) cmdPackage(path string, cmdopt base.CmdOpt) (string, base.CmdOpt) {
373	c := ir.Comp
374	g := &c.Globals
375	path = strings.TrimSpace(path)
376	n := len(path)
377	if len(path) == 0 {
378		g.Fprintf(g.Stdout, "// current package: %s %q\n", c.Name, c.Path)
379	} else if n > 2 && path[0] == '"' && path[n-1] == '"' {
380		path = path[1 : n-1]
381		ir.ChangePackage(paths.FileName(path), path)
382	} else if g.Options&base.OptShowPrompt != 0 {
383		g.Debugf(`package %s has no effect. To switch to a different package, use package "PACKAGE/FULL/PATH" - note the quotes`, path)
384	}
385	return "", cmdopt
386}
387
388func (ir *Interp) cmdQuit(_ string, opt base.CmdOpt) (string, base.CmdOpt) {
389	return "", opt | base.CmdOptQuit
390}
391
392// remove package 'path' from the list of known packages
393func (ir *Interp) cmdUnload(path string, opt base.CmdOpt) (string, base.CmdOpt) {
394	if len(path) != 0 {
395		ir.Comp.UnloadPackage(path)
396	}
397	return "", opt
398}
399
400func (ir *Interp) cmdWrite(filepath string, opt base.CmdOpt) (string, base.CmdOpt) {
401	g := &ir.Comp.Globals
402	if len(filepath) == 0 {
403		g.WriteDeclsToStream(g.Stdout)
404	} else {
405		g.WriteDeclsToFile(filepath)
406	}
407	return "", opt
408}
409