Files
tablizer/vendor/github.com/glycerine/zygomys/zygo/repl.go
2024-05-14 12:10:58 +02:00

516 lines
11 KiB
Go

package zygo
import (
"bufio"
"bytes"
"encoding/gob"
"fmt"
"io"
"os"
"runtime/pprof"
"strconv"
"strings"
)
var precounts map[string]int
var postcounts map[string]int
func CountPreHook(env *Zlisp, name string, args []Sexp) {
precounts[name] += 1
}
func CountPostHook(env *Zlisp, name string, retval Sexp) {
postcounts[name] += 1
}
func getLine(reader *bufio.Reader) (string, error) {
line := make([]byte, 0)
for {
linepart, hasMore, err := reader.ReadLine()
if err != nil {
return "", err
}
line = append(line, linepart...)
if !hasMore {
break
}
}
return string(line), nil
}
// NB at the moment this doesn't track comment and strings state,
// so it will fail if unbalanced '(' are found in either.
func isBalanced(str string) bool {
parens := 0
squares := 0
for _, c := range str {
switch c {
case '(':
parens++
case ')':
parens--
case '[':
squares++
case ']':
squares--
}
}
return parens == 0 && squares == 0
}
var continuationPrompt = "... "
func (pr *Prompter) getExpressionOrig(reader *bufio.Reader) (readin string, err error) {
line, err := getLine(reader)
if err != nil {
return "", err
}
for !isBalanced(line) {
fmt.Printf(continuationPrompt)
nextline, err := getLine(reader)
if err != nil {
return "", err
}
line += "\n" + nextline
}
return line, nil
}
// liner reads Stdin only. If noLiner, then we read from reader.
func (pr *Prompter) getExpressionWithLiner(env *Zlisp, reader *bufio.Reader, noLiner bool) (readin string, xs []Sexp, err error) {
var line, nextline string
if noLiner {
fmt.Printf(pr.prompt)
line, err = getLine(reader)
} else {
line, err = pr.Getline(nil)
}
if err != nil {
return "", nil, err
}
err = UnexpectedEnd
var x []Sexp
// test parse, but don't load or generate bytecode
env.parser.ResetAddNewInput(bytes.NewBuffer([]byte(line + "\n")))
x, err = env.parser.ParseTokens()
//P("\n after ResetAddNewInput, err = %v. x = '%s'\n", err, SexpArray(x).SexpString())
if len(x) > 0 {
xs = append(xs, x...)
}
for err == ErrMoreInputNeeded || err == UnexpectedEnd || err == ResetRequested {
if noLiner {
fmt.Printf(continuationPrompt)
nextline, err = getLine(reader)
} else {
nextline, err = pr.Getline(&continuationPrompt)
}
if err != nil {
return "", nil, err
}
env.parser.NewInput(bytes.NewBuffer([]byte(nextline + "\n")))
x, err = env.parser.ParseTokens()
if len(x) > 0 {
for i := range x {
if x[i] == SexpEnd {
P("found an SexpEnd token, omitting it")
continue
}
xs = append(xs, x[i])
}
}
switch err {
case nil:
line += "\n" + nextline
Q("no problem parsing line '%s' into '%s', proceeding...\n", line, (&SexpArray{Val: x, Env: env}).SexpString(nil))
return line, xs, nil
case ResetRequested:
continue
case ErrMoreInputNeeded:
continue
default:
return "", nil, fmt.Errorf("Error on line %d: %v\n", env.parser.lexer.Linenum(), err)
}
}
return line, xs, nil
}
func processDumpCommand(env *Zlisp, args []string) {
if len(args) == 0 {
env.DumpEnvironment()
} else {
err := env.DumpFunctionByName(args[0])
if err != nil {
fmt.Println(err)
}
}
}
func Repl(env *Zlisp, cfg *ZlispConfig) {
var reader *bufio.Reader
if cfg.NoLiner {
// reader is used if one wishes to drop the liner library.
// Useful for not full terminal env, like under test.
reader = bufio.NewReader(os.Stdin)
}
if cfg.Trace {
// debug tracing
env.debugExec = true
}
if !cfg.Quiet {
if cfg.Sandboxed {
fmt.Printf("zygo [sandbox mode] version %s\n", Version())
} else {
fmt.Printf("zygo version %s\n", Version())
}
fmt.Printf("press tab (repeatedly) to get completion suggestions. Shift-tab goes back. Ctrl-d to exit.\n")
}
var pr *Prompter // can be nil if noLiner
if !cfg.NoLiner {
pr = NewPrompter(cfg.Prompt)
defer pr.Close()
} else {
pr = &Prompter{prompt: cfg.Prompt}
}
infixSym := env.MakeSymbol("infix")
for {
line, exprsInput, err := pr.getExpressionWithLiner(env, reader, cfg.NoLiner)
//Q("\n exprsInput(len=%d) = '%v'\n line = '%s'\n", len(exprsInput), (&SexpArray{Val: exprsInput}).SexpString(nil), line)
if err != nil {
fmt.Println(err)
if err == io.EOF {
os.Exit(0)
}
env.Clear()
continue
}
parts := strings.Split(strings.Trim(line, " "), " ")
//parts := strings.Split(line, " ")
if len(parts) == 0 {
continue
}
first := strings.Trim(parts[0], " ")
if first == ".quit" {
break
}
if first == ".cd" {
if len(parts) < 2 {
fmt.Printf("provide directory path to change to.\n")
continue
}
err := os.Chdir(parts[1])
if err != nil {
fmt.Printf("error: %s\n", err)
continue
}
pwd, err := os.Getwd()
if err == nil {
fmt.Printf("cur dir: %s\n", pwd)
} else {
fmt.Printf("error: %s\n", err)
}
continue
}
// allow & at the repl to take the address of an expression
if len(first) > 0 && first[0] == '&' {
//P("saw & at repl, first='%v', parts='%#v'. exprsInput = '%#v'", first, parts, exprsInput)
exprsInput = []Sexp{MakeList(exprsInput)}
}
// allow * at the repl to dereference a pointer and print
if len(first) > 0 && first[0] == '*' {
//P("saw * at repl, first='%v', parts='%#v'. exprsInput = '%#v'", first, parts, exprsInput)
exprsInput = []Sexp{MakeList(exprsInput)}
}
if first == ".dump" {
processDumpCommand(env, parts[1:])
continue
}
if first == ".gls" {
fmt.Printf("\nScopes:\n")
prev := env.showGlobalScope
env.showGlobalScope = true
err = env.ShowStackStackAndScopeStack()
env.showGlobalScope = prev
if err != nil {
fmt.Printf("%s\n", err)
}
continue
}
if first == ".ls" {
err := env.ShowStackStackAndScopeStack()
if err != nil {
fmt.Println(err)
}
continue
}
if first == ".verb" {
Verbose = !Verbose
fmt.Printf("verbose: %v.\n", Verbose)
continue
}
if first == ".debug" {
env.debugExec = true
fmt.Printf("instruction debugging on.\n")
continue
}
if first == ".undebug" {
env.debugExec = false
fmt.Printf("instruction debugging off.\n")
continue
}
var expr Sexp
n := len(exprsInput)
if n > 0 {
infixWrappedSexp := MakeList([]Sexp{infixSym, &SexpArray{Val: exprsInput, Env: env}})
expr, err = env.EvalExpressions([]Sexp{infixWrappedSexp})
} else {
line = env.ReplLineInfixWrap(line)
expr, err = env.EvalString(line + " ") // print standalone variables
}
switch err {
case nil:
case NoExpressionsFound:
env.Clear()
continue
default:
fmt.Print(env.GetStackTrace(err))
env.Clear()
continue
}
if expr != SexpNull {
// try to print strings more elegantly!
switch e := expr.(type) {
case *SexpStr:
if e.backtick {
fmt.Printf("`%s`\n", e.S)
} else {
fmt.Printf("%s\n", strconv.Quote(e.S))
}
default:
switch sym := expr.(type) {
case Selector:
Q("repl calling RHS() on Selector")
rhs, err := sym.RHS(env)
if err != nil {
Q("repl problem in call to RHS() on SexpSelector: '%v'", err)
fmt.Print(env.GetStackTrace(err))
env.Clear()
continue
} else {
Q("got back rhs of type %T", rhs)
fmt.Println(rhs.SexpString(nil))
continue
}
case *SexpSymbol:
if sym.isDot {
resolved, err := dotGetSetHelper(env, sym.name, nil)
if err != nil {
fmt.Print(env.GetStackTrace(err))
env.Clear()
continue
}
fmt.Println(resolved.SexpString(nil))
continue
}
}
fmt.Println(expr.SexpString(nil))
}
}
}
}
func runScript(env *Zlisp, fname string, cfg *ZlispConfig) {
file, err := os.Open(fname)
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
err = env.LoadFile(file)
if err != nil {
fmt.Println(err)
if cfg.ExitOnFailure {
os.Exit(-1)
}
return
}
_, err = env.Run()
if cfg.CountFuncCalls {
fmt.Println("Pre:")
for name, count := range precounts {
fmt.Printf("\t%s: %d\n", name, count)
}
fmt.Println("Post:")
for name, count := range postcounts {
fmt.Printf("\t%s: %d\n", name, count)
}
}
if err != nil {
fmt.Print(env.GetStackTrace(err))
if cfg.ExitOnFailure {
os.Exit(-1)
}
Repl(env, cfg)
}
}
func (env *Zlisp) StandardSetup() {
env.ImportBaseTypes()
env.ImportEval()
env.ImportTime()
env.ImportPackageBuilder()
env.ImportMsgpackMap()
defmap := `(defmac defmap [name] ^(defn ~name [& rest] (msgmap (quote ~name) rest)))`
_, err := env.EvalString(defmap)
panicOn(err)
// colonOp := `(defmac : [key hmap & def] ^(hget ~hmap (quote ~key) ~@def))`
// _, err = env.EvalString(colonOp)
// panicOn(err)
rangeMacro := `(defmac range [key value myhash & body]
^(let [n (len ~myhash)]
(for [(def i 0) (< i n) (def i (+ i 1))]
(begin
(mdef (quote ~key) (quote ~value) (hpair ~myhash i))
~@body))))`
_, err = env.EvalString(rangeMacro)
panicOn(err)
reqMacro := `(defmac req [a] ^(source (sym2str (quote ~a))))`
_, err = env.EvalString(reqMacro)
panicOn(err)
incrMacro := `(defmac ++ [a] ^(set ~a (+ ~a 1)))`
_, err = env.EvalString(incrMacro)
panicOn(err)
incrEqMacro := `(defmac += [a b] ^(set ~a (+ ~a ~b)))`
_, err = env.EvalString(incrEqMacro)
panicOn(err)
decrMacro := `(defmac -- [a] ^(set ~a (- ~a 1)))`
_, err = env.EvalString(decrMacro)
panicOn(err)
decrEqMacro := `(defmac -= [a b] ^(set ~a (- ~a ~b)))`
_, err = env.EvalString(decrEqMacro)
panicOn(err)
env.ImportChannels()
env.ImportGoroutines()
env.ImportRegex()
env.ImportRandom()
gob.Register(SexpHash{})
gob.Register(SexpArray{})
}
// like main() for a standalone repl, now in library
func ReplMain(cfg *ZlispConfig) {
var env *Zlisp
if cfg.LoadDemoStructs {
RegisterDemoStructs()
}
if cfg.Sandboxed {
env = NewZlispSandbox()
} else {
env = NewZlisp()
}
env.StandardSetup()
if cfg.LoadDemoStructs {
// avoid data conflicts by only loading these in demo mode.
env.ImportDemoData()
}
if cfg.CpuProfile != "" {
f, err := os.Create(cfg.CpuProfile)
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
err = pprof.StartCPUProfile(f)
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
defer pprof.StopCPUProfile()
}
precounts = make(map[string]int)
postcounts = make(map[string]int)
if cfg.CountFuncCalls {
env.AddPreHook(CountPreHook)
env.AddPostHook(CountPostHook)
}
if cfg.Command != "" {
_, err := env.EvalString(cfg.Command)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
os.Exit(0)
}
runRepl := true
args := cfg.Flags.Args()
if len(args) > 0 {
runRepl = false
runScript(env, args[0], cfg)
if cfg.AfterScriptDontExit {
runRepl = true
}
}
if runRepl {
Repl(env, cfg)
}
if cfg.MemProfile != "" {
f, err := os.Create(cfg.MemProfile)
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
defer f.Close()
err = pprof.Lookup("heap").WriteTo(f, 1)
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
}
}
func (env *Zlisp) ReplLineInfixWrap(line string) string {
return "{" + line + "}"
}