From 87688657e874f30e2f6ed5c3aefb7a2941be12b3 Mon Sep 17 00:00:00 2001 From: Ethan Burns Date: Wed, 13 Oct 2021 22:25:34 -0400 Subject: [PATCH] Add a generalized basic interpreter library. It takes a custom basic-like language description defined by a set of commands, binary and unary operators. The execution of each command and operator is defined by a Go function. The interpreter itself handles the parsing and execution loop. --- plugins/catbasic/basic/basic.go | 846 ++++++++++++++++++++++++++ plugins/catbasic/basic/interp/main.go | 29 + plugins/catbasic/basic/lang/lang.go | 187 ++++++ 3 files changed, 1062 insertions(+) create mode 100644 plugins/catbasic/basic/basic.go create mode 100644 plugins/catbasic/basic/interp/main.go create mode 100644 plugins/catbasic/basic/lang/lang.go diff --git a/plugins/catbasic/basic/basic.go b/plugins/catbasic/basic/basic.go new file mode 100644 index 0000000..ecf0730 --- /dev/null +++ b/plugins/catbasic/basic/basic.go @@ -0,0 +1,846 @@ +// Package basic implements a very simple, embedded BASIC interpreter. +// The interpreter is extendable by defining custom +// statements, unary and binary operators, and functions. +package basic + +import ( + "bufio" + "errors" + "fmt" + "io" + "math/big" + "reflect" + "strconv" + "strings" + "sync" + "unicode" + "unicode/utf8" +) + +type Lang struct { + Cmds []CmdDef + BinOps [][]OpDef + UnOps []OpDef +} + +type CmdDef struct { + // Name is the name of the command. + // The name is formed by space-delimited keywords + // that define all keywords in the command. + // The first keyword in the Name begins all statements for this command. + // Subsequent keywords appear between arguments of the command + // as defined by the arguments of the Exec function. + // By convention, keywords are all CAPITAL. + Name string + + // Doc describes the command. + Doc string + + // Eval must be a func type. + // + // The first parameter type must be *Interp. + // The following parameters must be of the following types: + // Value, indicating the argument must be an expression; + // []Value, indicating the argument must be an expression list. + // + // The number of parameters, besides the leading *Interp, + // must be equal to or one grater than + // the number of keywords in the command Name. + // + // There must be no return or the return must be an error. + Eval interface{} +} + +var argTypes = map[reflect.Type]argType{ + reflect.TypeOf([]Value{}).Elem(): { + Name: "Expr", + Parse: func(interp *Interp, input *string) (node, error) { + return parseExpr(interp, input) + }, + }, + reflect.TypeOf([]Value{}): { + Name: "ExprList", + Parse: func(interp *Interp, input *string) (node, error) { + return parseExprList(interp, input) + }, + }, + reflect.TypeOf(Variable{}): { + Name: "ExprList", + Parse: func(interp *Interp, input *string) (node, error) { + return parseVar(input) + }, + }, +} + +type OpDef struct { + Op string + Doc string + + // TODO: comment + // binop: + // func with 2 arguments either both Number, both String, or both Value, + // returns a Value or a Value and error. + // unop: + // func with 1 argument either Number, String, or Value, + // returns a Value or a Value and error. + Eval interface{} +} + +type Interp struct { + cmds []cmd + bins [][]binop + uns []unop + + mu sync.Mutex + end bool + pos Pos + immediate line + prog []line + vars map[string]*Value + stack []interface{} +} + +type Pos struct { + line int + stmt int +} + +type cmd struct { + kwds []string + argTypes []argType + eval reflect.Value +} + +type argType struct { + Name string + Parse func(*Interp, *string) (node, error) +} + +type binop struct { + op string + ltype, rtype reflect.Type + eval reflect.Value +} + +type unop struct { + op string + typ reflect.Type + eval reflect.Value +} + +type line struct { + num int + src string + stmts []stmt +} + +type stmt struct { + cmd *cmd + nodes []node +} + +type node interface { + // eval returns either Value, []Value, Variable, or []Variable. + eval(*Interp) (interface{}, error) +} + +type Value interface { + String() string + eval(*Interp) (interface{}, error) +} + +type String string + +func (s String) String() string { return string(s) } + +type Number big.Float + +// MakeBool returns a Number representing the current boolean. +func MakeBool(b bool) Number { + if b { + return Number(*big.NewFloat(1)) + } + return Number(*big.NewFloat(0)) +} + +func (n Number) String() string { return (*big.Float)(&n).String() } +func (n Number) Float() *big.Float { return (*big.Float)(&n) } + +func (n Number) Bool() bool { + var zero big.Float + return n.Float().Cmp(&zero) != 0 +} + +type Variable struct { + Name string +} + +type exprList []node + +type binNode struct { + op *binop + left, right node +} + +type unNode struct { + op *unop + arg node +} + +type readVar struct { + Variable +} + +func New(lang Lang) (*Interp, error) { + interp := Interp{ + vars: make(map[string]*Value), + } + for _, cmdDef := range lang.Cmds { + cmd, err := newCmd(cmdDef) + if err != nil { + return nil, err + } + interp.cmds = append(interp.cmds, *cmd) + } + for _, prec := range lang.BinOps { + var ops []binop + for _, binDef := range prec { + binop, err := newBin(binDef) + if err != nil { + return nil, err + } + ops = append(ops, *binop) + } + interp.bins = append(interp.bins, ops) + } + for _, unDef := range lang.UnOps { + unop, err := newUn(unDef) + if err != nil { + return nil, err + } + interp.uns = append(interp.uns, *unop) + } + return &interp, nil +} + +func newCmd(def CmdDef) (*cmd, error) { + var cmd cmd + cmd.eval = reflect.ValueOf(def.Eval) + if cmd.kwds = strings.Fields(def.Name); len(cmd.kwds) == 0 { + return nil, fmt.Errorf("empty command name") + } + t := reflect.TypeOf(def.Eval) + if t.Kind() != reflect.Func { + return nil, fmt.Errorf("%s: bad eval type %s, want Func", def.Name, t) + } + if t.NumIn() == 0 { + return nil, fmt.Errorf("%s: bad eval arity: 0", def.Name) + } + if n := t.NumIn(); n < len(cmd.kwds) || n > len(cmd.kwds)+1 { + return nil, fmt.Errorf("%s: bad eval arity: %d args, %d keywords", + def.Name, n, len(cmd.kwds)) + } + if t.NumOut() > 1 || + (t.NumOut() == 1 && t.Out(0) != reflect.TypeOf([]error{}).Elem()) { + return nil, fmt.Errorf("%s: bad eval return", def.Name) + } + want := reflect.TypeOf(Exec{}) + if t.In(0) != want { + return nil, fmt.Errorf("%s: bad eval parm 0 type: %s, want %s", + def.Name, t.In(0), want) + } + for i := 1; i < t.NumIn(); i++ { + at, ok := argTypes[t.In(i)] + if !ok { + return nil, fmt.Errorf("%s: bad eval parm type: %s", + def.Name, t.In(i)) + } + cmd.argTypes = append(cmd.argTypes, at) + } + + return &cmd, nil +} + +func newBin(def OpDef) (*binop, error) { + op := binop{ + op: def.Op, + eval: reflect.ValueOf(def.Eval), + } + t := reflect.TypeOf(def.Eval) + if t.Kind() != reflect.Func { + return nil, fmt.Errorf("%s: bad binary op eval type %s, want Func", def.Op, t) + } + if t.NumIn() != 2 { + return nil, fmt.Errorf("%s: bad binary op eval arity: %d", def.Op, t.NumIn()) + } + if t.NumOut() != 1 && t.NumOut() != 2 { + return nil, fmt.Errorf("%s: bad binary op eval return arity: %d, want 1 or 2", + def.Op, t.NumOut()) + } + if t.Out(0) != reflect.TypeOf([]Value{}).Elem() { + return nil, fmt.Errorf("%s: bad binary op eval return type: %s, want Value", + def.Op, t.Out(0)) + } + if t.NumOut() == 2 && t.Out(1) != reflect.TypeOf([]error{}).Elem() { + return nil, fmt.Errorf("%s: bad binary op eval return type: %s, want error", + def.Op, t.Out(1)) + } + op.ltype, op.rtype = t.In(0), t.In(1) + var number Number + if op.ltype != reflect.TypeOf(number) && + op.ltype != reflect.TypeOf(String("")) && + op.ltype != reflect.TypeOf([]Value{}).Elem() { + return nil, fmt.Errorf("%s: bad binary op eval parm type: %s, "+ + "want Number, String, or Value", + def.Op, op.ltype) + } + if op.rtype != op.ltype { + return nil, fmt.Errorf("%s: bad binary op eval parm type: %s, want %s", + def.Op, op.rtype, op.ltype) + } + return &op, nil +} + +func newUn(def OpDef) (*unop, error) { + op := unop{ + op: def.Op, + eval: reflect.ValueOf(def.Eval), + } + t := reflect.TypeOf(def.Eval) + if t.Kind() != reflect.Func { + return nil, fmt.Errorf("%s: bad binary op eval type %s, want Func", def.Op, t) + } + if t.NumIn() != 1 { + return nil, fmt.Errorf("%s: bad unary op eval arity: %d", def.Op, t.NumIn()) + } + if t.NumOut() != 1 && t.NumOut() != 2 { + return nil, fmt.Errorf("%s: bad unary op eval return arity: %d, want 1 or 2", + def.Op, t.NumOut()) + } + if t.Out(0) != reflect.TypeOf([]Value{}).Elem() { + return nil, fmt.Errorf("%s: bad unary op eval return type: %s, want Value", + def.Op, t.Out(0)) + } + if t.NumOut() == 2 && t.Out(1) != reflect.TypeOf([]error{}).Elem() { + return nil, fmt.Errorf("%s: bad unary op eval return type: %s, want error", + def.Op, t.Out(1)) + } + op.typ = t.In(0) + var number Number + if op.typ != reflect.TypeOf(number) && + op.typ != reflect.TypeOf(String("")) && + op.typ != reflect.TypeOf([]Value{}).Elem() { + return nil, fmt.Errorf("%s: bad binary op eval parm type: %s, "+ + "want Number, String, or Value", + def.Op, op.typ) + } + return &op, nil +} + +func (interp *Interp) Read(r io.Reader) error { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line, err := parseLine(interp, scanner.Text()) + if err != nil { + return err + } + if line.num >= 0 { + addLine(interp, line) + continue + } + interp.immediate = line + interp.pos.line = -1 + interp.pos.stmt = 0 + if err := run(interp); err != nil { + return err + } + } + return scanner.Err() +} + +func addLine(interp *Interp, line line) { + panic("unimplemented") +} + +func run(interp *Interp) error { + interp.mu.Lock() + defer interp.mu.Unlock() + + interp.end = false + interp.stack = interp.stack[:0] + + for !interp.end { + + var line *line + if i := interp.pos.line; i < 0 { + line = &interp.immediate + } else if i >= len(interp.prog) { + break + } else { + line = &interp.prog[interp.pos.line] + } + if interp.pos.stmt >= len(line.stmts) { + if interp.pos.line < 0 { + break + } + interp.pos.line++ + interp.pos.stmt = 0 + continue + } + stmt := line.stmts[interp.pos.stmt] + interp.pos.stmt++ + if err := runStmt(interp, stmt); err != nil { + return err + } + } + return nil +} + +func runStmt(interp *Interp, stmt stmt) error { + args := []reflect.Value{reflect.ValueOf(Exec{interp})} + for _, node := range stmt.nodes { + arg, err := node.eval(interp) + if err != nil { + return err + } + args = append(args, reflect.ValueOf(arg)) + } + if err := stmt.cmd.eval.Call(args); len(err) > 0 { + if i := err[0].Interface(); i != nil { + return i.(error) + } + } + return nil +} + +func (s String) eval(*Interp) (interface{}, error) { return s, nil } + +func (n Number) eval(*Interp) (interface{}, error) { return n, nil } + +func (v Variable) eval(*Interp) (interface{}, error) { return v, nil } + +func (l exprList) eval(interp *Interp) (interface{}, error) { + var vals []Value + for _, n := range l { + v, err := n.eval(interp) + if err != nil { + return nil, err + } + vals = append(vals, v.(Value)) + } + return vals, nil +} + +func (b binNode) eval(interp *Interp) (interface{}, error) { + l, err := b.left.eval(interp) + if err != nil { + return nil, err + } + if t := reflect.TypeOf(l); !t.AssignableTo(b.op.ltype) { + return nil, fmt.Errorf("%s got type %s, want %s", b.op.op, t, b.op.ltype) + } + r, err := b.right.eval(interp) + if err != nil { + return nil, err + } + if t := reflect.TypeOf(r); !t.AssignableTo(b.op.rtype) { + return nil, fmt.Errorf("%s got type %s, want %s", b.op.op, t, b.op.rtype) + } + res := b.op.eval.Call([]reflect.Value{reflect.ValueOf(l), reflect.ValueOf(r)}) + if len(res) == 2 { + return res[0].Interface().(Value), res[1].Interface().(error) + } + return res[0].Interface().(Value), nil +} + +func (u unNode) eval(interp *Interp) (interface{}, error) { + a, err := u.arg.eval(interp) + if err != nil { + return nil, err + } + if t := reflect.TypeOf(a); !t.AssignableTo(u.op.typ) { + return nil, fmt.Errorf("%s got type %s, want %s", u.op.op, t, u.op.typ) + } + res := u.op.eval.Call([]reflect.Value{reflect.ValueOf(a)}) + if len(res) == 2 { + return res[0].Interface().(Value), res[1].Interface().(error) + } + return res[0].Interface().(Value), nil +} + +func (r readVar) eval(interp *Interp) (interface{}, error) { + return *getVar(interp, r.Variable), nil +} + +func getVar(interp *Interp, v Variable) *Value { + val, ok := interp.vars[v.Name] + if !ok { + val = new(Value) + r, _ := utf8.DecodeLastRuneInString(v.Name) + if r == '$' { + *val = String("") + } else { + *val = Number(big.Float{}) + } + interp.vars[v.Name] = val + } + return val +} + +func parseLine(interp *Interp, input string) (line line, err error) { + line.src = input + if line.num, err = parseLineNum(&input); err != nil { + return line, err + } + line.stmts, err = parseStmts(interp, &input) + return line, err +} + +// returns -1, nil if there is no number. +func parseLineNum(input *string) (int, error) { + i := digits(*input) + if i == 0 { + return -1, nil + } + n, err := strconv.Atoi((*input)[:i]) + if err != nil { + return 0, errors.New("bad line number: " + err.Error()) + } + *input = (*input)[:i] + return n, nil +} + +func parseStmts(interp *Interp, input *string) (stmts []stmt, err error) { + for { + consumeSpace(input) + if *input == "" { + break + } + stmt, err := parseStmt(interp, input) + if err != nil { + return nil, err + } + stmts = append(stmts, stmt) + if len(*input) == 0 { + break + } + consumeSpace(input) + if r, w := utf8.DecodeRuneInString(*input); r == ':' { + *input = (*input)[w:] + continue + } + return nil, fmt.Errorf("expected end-of-statement, got [%s]", *input) + } + return stmts, nil +} + +func parseStmt(interp *Interp, input *string) (stmt, error) { + k := parseKeyword(input) + if k == "" { + return stmt{}, fmt.Errorf("expected statement: [%s]", *input) + } + for i := range interp.cmds { + cmd := &interp.cmds[i] + if cmd.kwds[0] != k { + continue + } + nodes, err := parseArgs(interp, cmd, input) + if err != nil { + return stmt{}, err + } + return stmt{cmd: cmd, nodes: nodes}, nil + } + return stmt{}, errors.New(k + " command not found") +} + +func parseKeyword(input *string) string { + consumeSpace(input) + var i int + for i < len(*input) { + r, w := utf8.DecodeRuneInString((*input)[i:]) + if unicode.IsSpace(r) || r == ':' { + break + } + i += w + } + k := (*input)[:i] + *input = (*input)[i:] + return k +} + +func parseArgs(interp *Interp, cmd *cmd, input *string) ([]node, error) { + var nodes []node + for i, at := range cmd.argTypes { + node, err := at.Parse(interp, input) + if err != nil { + return nil, err + } + nodes = append(nodes, node) + if i+1 < len(cmd.kwds) { + want := cmd.kwds[i+1] + if k := parseKeyword(input); k != want { + return nil, fmt.Errorf("expected %s, got [%s]", want, k) + } + } + } + return nodes, nil +} + +func parseExprList(interp *Interp, input *string) (exprList, error) { + var vals exprList + for { + v, err := parseExpr(interp, input) + if err != nil { + return nil, err + } + vals = append(vals, v) + r, w := utf8.DecodeRuneInString(*input) + if r != ',' { + break + } + *input = (*input)[w:] + } + return vals, nil +} + +func parseExpr(interp *Interp, input *string) (node, error) { + return parseBinOp(interp, 0, input) +} + +func parseBinOp(interp *Interp, prec int, input *string) (n node, err error) { + if prec >= len(interp.bins) { + return parseFactor(interp, input) + } + if n, err = parseBinOp(interp, prec+1, input); err != nil { + return nil, err + } + for { + consumeSpace(input) + var op *binop + for i := range interp.bins[prec] { + if strings.HasPrefix(*input, interp.bins[prec][i].op) { + op = &interp.bins[prec][i] + break + } + } + if op == nil { + break + } + *input = (*input)[len(op.op):] + r, err := parseBinOp(interp, prec+1, input) + if err != nil { + return nil, err + } + n = binNode{op: op, left: n, right: r} + } + return n, nil +} + +func parseFactor(interp *Interp, input *string) (node, error) { + consumeSpace(input) + for _, op := range interp.uns { + if !strings.HasPrefix(*input, op.op) { + continue + } + *input = (*input)[len(op.op):] + arg, err := parseFactor(interp, input) + if err != nil { + return nil, err + } + return unNode{op: &op, arg: arg}, nil + } + switch r, _ := utf8.DecodeRuneInString(*input); { + case r == '+' || r == '-' || '0' <= r && r <= '9': + return parseNumber(input) + case r == '"': + return parseString(input) + case r == '(': + return parseSubExpr(interp, input) + case unicode.IsLetter(r): + v, err := parseVar(input) + return readVar{Variable: v}, err + default: + // TODO: Var and Call + return nil, fmt.Errorf("bad expression [%s]", *input) + } +} + +func parseSubExpr(interp *Interp, input *string) (node, error) { + *input = (*input)[1:] // consume ( + expr, err := parseExpr(interp, input) + if err != nil { + return nil, err + } + consumeSpace(input) + r, w := utf8.DecodeRuneInString(*input) + if r != ')' { + return nil, errors.New("unterminated (") + } + *input = (*input)[w:] + return expr, nil +} + +func parseVar(input *string) (Variable, error) { + consumeSpace(input) + i := 0 + for { + r, w := utf8.DecodeRuneInString((*input)[i:]) + if unicode.IsLetter(r) || unicode.IsNumber(r) { + i += w + continue + } + if i == 0 { + return Variable{}, fmt.Errorf("expected variable, got [%s]", *input) + } + if r == '$' { + i += w + } + break + } + v := Variable{Name: (*input)[:i]} + *input = (*input)[i:] + return v, nil +} + +func parseNumber(input *string) (Number, error) { + consumeSpace(input) + var i int + if r, w := utf8.DecodeRuneInString((*input)[i:]); r == '+' || r == '-' { + i += w + } + n := digits((*input)[i:]) + if n == 0 { + return Number{}, fmt.Errorf("expected digits, got [%s]", *input) + } + i += n + if r, w := utf8.DecodeRuneInString((*input)[i:]); r == '.' { + i += w + n := digits((*input)[i:]) + if n == 0 { + return Number{}, errors.New("expected digits") + } + i += n + } + if r, w := utf8.DecodeRuneInString((*input)[i:]); r == 'e' || r == 'E' { + i += w + n := digits((*input)[i:]) + if n == 0 { + return Number{}, errors.New("expected digits") + } + i += n + } + var f big.Float + if _, _, err := f.Parse((*input)[:i], 10); err != nil { + panic(err.Error()) + } + *input = (*input)[i:] + return Number(f), nil +} + +func parseString(input *string) (String, error) { + consumeSpace(input) + if len(*input) < 2 || (*input)[0] != '"' { + return "", errors.New("missing opening \"") + } + *input = (*input)[1:] // remove " + var s strings.Builder + var esc bool + for { + if len(*input) == 0 { + return "", errors.New("missing closing \"") + } + r, w := utf8.DecodeRuneInString(*input) + *input = (*input)[w:] + switch { + case !esc && r == '\\': + esc = true + continue + case !esc && r == '"': + return String(s.String()), nil + case esc && r == 'n': + r = '\n' + case esc && r == 't': + r = '\t' + } + esc = false + s.WriteRune(r) + } +} + +func digits(input string) int { + var i int + for { + r, w := utf8.DecodeRuneInString(input[i:]) + if r < '0' || '9' < r { + return i + } + i += w + } +} + +func consumeSpace(input *string) { + for len(*input) > 0 { + r, w := utf8.DecodeRuneInString(*input) + if !unicode.IsSpace(r) { + break + } + *input = (*input)[w:] + } +} + +// Exec is a currently executing Interp. +// It is used as an argument to command evaluation functions, +// and is not intended to be stored outside of the execution of a command. +type Exec struct { + *Interp +} + +// Var returns the value of a variable. +func (exec Exec) Var(v Variable) Value { + return *getVar(exec.Interp, v) +} + +// SetVar sets the Value of a Variable. +func (exec Exec) SetVar(v Variable, val Value) { + *getVar(exec.Interp, v) = val +} + +// End terminates the program after the current statement. +func (exec Exec) End() { + exec.end = true +} + +// Pos returns the program position immediately after the current statement. +func (exec Exec) Pos() Pos { + return exec.pos +} + +// SetPos sets the position to continue execution after the current statement. +func (exec Exec) SetPos(pos Pos) { + exec.pos = pos +} + +// TopFrame returns the top frame of the control stack. +// If the control stack is empty, nil is returned. +func (exec Exec) TopFrame() interface{} { + if len(exec.stack) == 0 { + return nil + } + return exec.stack[len(exec.stack)-1] +} + +// PopFrame removes and returns the top frame of the control stack. +// If the control stack is empty, nil is returned. +func (exec Exec) PopFrame() interface{} { + if len(exec.stack) == 0 { + return nil + } + f := exec.stack[len(exec.stack)-1] + exec.stack = exec.stack[:len(exec.stack)-1] + return f +} + +// PushFrame pushes a new frame onto the control stack. +func (exec Exec) PushFrame(frame interface{}) { + exec.stack = append(exec.stack, frame) +} diff --git a/plugins/catbasic/basic/interp/main.go b/plugins/catbasic/basic/interp/main.go new file mode 100644 index 0000000..b1e9951 --- /dev/null +++ b/plugins/catbasic/basic/interp/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/velour/catbase/plugins/catbasic/basic" + "github.com/velour/catbase/plugins/catbasic/basic/lang" +) + +const source = ` +FOR I = 0 TO 5: PRINT "I:", I: NEXT I +` + +func main() { + interp, err := basic.New(lang.Default) + if err != nil { + die(err) + } + if err := interp.Read(strings.NewReader(source)); err != nil { + die(err) + } +} + +func die(err error) { + fmt.Println(err.Error()) + os.Exit(1) +} diff --git a/plugins/catbasic/basic/lang/lang.go b/plugins/catbasic/basic/lang/lang.go new file mode 100644 index 0000000..25910b2 --- /dev/null +++ b/plugins/catbasic/basic/lang/lang.go @@ -0,0 +1,187 @@ +package lang + +import ( + "errors" + "fmt" + "math/big" + "reflect" + "strings" + + "github.com/velour/catbase/plugins/catbasic/basic" +) + +var Default = basic.Lang{ + Cmds: []basic.CmdDef{ + { + Name: "END", + Doc: "ends execution of the program", + Eval: endCmd, + }, + { + Name: "PRINT", + Doc: "prints each value, separated by a space, then a newline", + Eval: printCmd, + }, + { + Name: "LET =", + Doc: "assigns a value to a variable", + Eval: letCmd, + }, + { + Name: "FOR = TO", + Doc: "begins a for loop. The loop the variable is assigned and execution continues at the following statement. A subsequent NEXT statement with a variable matching the FOR variable will increment the variable; if the value is less than the TO value, then executinon will continue from the statement following the FOR statement, otherwise from the statement following the NEXT statement", + Eval: forCmd, + }, + { + Name: "NEXT", + Doc: "closes the body of a FOR loop. See FOR for details", + Eval: nextCmd, + }, + }, + BinOps: [][]basic.OpDef{ + { + {Op: "OR", Eval: logic(func(a, b bool) bool { return a || b })}, + }, + { + {Op: "AND", Eval: logic(func(a, b bool) bool { return a && b })}, + }, + { + {Op: "<", Eval: rel("<", func(c int) bool { return c < 0 })}, + {Op: "<=", Eval: rel("<=", func(c int) bool { return c <= 0 })}, + {Op: ">", Eval: rel(">", func(c int) bool { return c > 0 })}, + {Op: ">=", Eval: rel(">=", func(c int) bool { return c >= 0 })}, + {Op: "=", Eval: rel("=", func(c int) bool { return c == 0 })}, + {Op: "<>", Eval: rel("<>", func(c int) bool { return c != 0 })}, + }, + { + {Op: "+", Eval: add}, + {Op: "-", Eval: arithmetic("-", (*big.Float).Sub)}, + }, + { + {Op: "*", Eval: arithmetic("-", (*big.Float).Mul)}, + {Op: "/", Eval: arithmetic("-", (*big.Float).Quo)}, + }, + }, + UnOps: []basic.OpDef{ + {Op: "NOT", Eval: not}, + }, +} + +func endCmd(exec basic.Exec) { + exec.End() +} + +func printCmd(exec basic.Exec, values []basic.Value) { + for i, val := range values { + if i > 0 { + fmt.Print(" ") + } + fmt.Print(val.String()) + } + fmt.Print("\n") +} + +func letCmd(exec basic.Exec, vr basic.Variable, val basic.Value) error { + if t := reflect.TypeOf(exec.Var(vr)); t != reflect.TypeOf(val) { + return fmt.Errorf("cannot assign %T to a variable of type %T", val, t) + } + exec.SetVar(vr, val) + return nil +} + +type forFrame struct { + name string + v basic.Variable + to basic.Number + pos basic.Pos +} + +func forCmd(exec basic.Exec, v basic.Variable, from, to basic.Value) error { + if _, ok := exec.Var(v).(basic.Number); !ok { + return fmt.Errorf("FOR variable %s is a %T, not a Number", v.Name, exec.Var(v)) + } + f, ok := from.(basic.Number) + if !ok { + return fmt.Errorf("FOR start value is a %T, not a Number", from) + } + t, ok := to.(basic.Number) + if !ok { + return fmt.Errorf("FOR end value is a %T, not a Number", to) + } + exec.SetVar(v, f) + exec.PushFrame(forFrame{ + name: v.Name, + v: v, + to: t, + pos: exec.Pos(), + }) + return nil +} + +func nextCmd(exec basic.Exec, v basic.Variable) error { + top, ok := exec.TopFrame().(forFrame) + if !ok { + return errors.New("NEXT called not within a FOR loop") + } + if v.Name != top.name { + return fmt.Errorf("interleaved FOR loops: %s and %s", v.Name, top.name) + } + f := exec.Var(top.v).(basic.Number).Float() + exec.SetVar(top.v, basic.Number(*f.Add(f, big.NewFloat(1)))) + if f.Cmp(top.to.Float()) >= 0 { + exec.PopFrame() + } else { + exec.SetPos(top.pos) + } + return nil +} + +func logic(f func(a, b bool) bool) func(basic.Number, basic.Number) basic.Value { + return func(a, b basic.Number) basic.Value { + return basic.MakeBool(f(a.Bool(), b.Bool())) + } +} + +func rel(n string, ok func(int) bool) func(basic.Value, basic.Value) (basic.Value, error) { + return func(l, r basic.Value) (basic.Value, error) { + var c int + switch l := l.(type) { + case basic.String: + if r, ok := r.(basic.String); !ok { + return nil, fmt.Errorf("String %s expects String, got %T", n, r) + } else { + c = strings.Compare(string(l), string(r)) + } + case basic.Number: + if r, ok := r.(basic.Number); !ok { + return nil, fmt.Errorf("Number %s expects Numbe, got %T", n, r) + } else { + c = l.Float().Cmp(r.Float()) + } + default: + return nil, fmt.Errorf("%s expects String or Number, got %T", n, l) + } + return basic.MakeBool(ok(c)), nil + } +} + +func arithmetic(n string, op func(z, a, b *big.Float) *big.Float) func(l, r basic.Number) basic.Value { + return func(l, r basic.Number) basic.Value { + var z big.Float + op(&z, l.Float(), r.Float()) + return basic.Number(z) + } +} + +func add(l, r basic.Value) basic.Value { + switch l := l.(type) { + case basic.String: + return basic.String(l + r.(basic.String)) + case basic.Number: + return arithmetic("+", (*big.Float).Add)(l, r.(basic.Number)) + default: + panic("impossible") + } +} + +func not(b basic.Number) basic.Value { return basic.MakeBool(!b.Bool()) }