Browse Source

Add picol mini TCL interpreter.

--unset 2 years ago
parent
commit
e2f2c794e4
7 changed files with 681 additions and 0 deletions
  1. 20 0
      LICENSE
  2. 45 0
      cmd/pin/main.go
  3. 3 0
      go.mod
  4. 20 0
      picol/LICENSE
  5. 205 0
      picol/commands.go
  6. 250 0
      picol/parser.go
  7. 138 0
      picol/picol.go

+ 20 - 0
LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2021 Beoran <beoran@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 45 - 0
cmd/pin/main.go

@@ -0,0 +1,45 @@
+package main
+
+import (
+	"bufio"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"src.eruta.nl/beoran/bdjncl/picol"
+)
+
+var fname = flag.String("f", "", "file name")
+
+func CommandPuts(i *picol.Interp, argv []string, pd interface{}) (string, error) {
+	if len(argv) != 2 {
+		return "", fmt.Errorf("Wrong number of args for %s %s", argv[0], argv)
+	}
+	fmt.Println(argv[1])
+	return "", nil
+}
+
+func main() {
+	flag.Parse()
+	interp := picol.InitInterp()
+	interp.RegisterCoreCommands()
+	interp.RegisterCommand("puts", CommandPuts, nil)
+
+	buf, err := ioutil.ReadFile(*fname)
+	if err == nil {
+		result, err := interp.Eval(string(buf))
+		if err != nil {
+			fmt.Println("ERRROR", result, err)
+		}
+	} else {
+		for {
+			fmt.Print("picol> ")
+			scanner := bufio.NewReader(os.Stdin)
+			clibuf, _ := scanner.ReadString('\n')
+			result, err := interp.Eval(clibuf[:len(clibuf)-1])
+			if len(result) != 0 {
+				fmt.Println("ERRROR", result, err)
+			}
+		}
+	}
+}

+ 3 - 0
go.mod

@@ -0,0 +1,3 @@
+module src.eruta.nl/beoran/bdjncl
+
+go 1.16

+ 20 - 0
picol/LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Lain dono <lain.dono@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 205 - 0
picol/commands.go

@@ -0,0 +1,205 @@
+package picol
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+func ArityErr(i *Interp, name string, argv []string) error {
+	return fmt.Errorf("Wrong number of args for %s %s", name, argv)
+}
+
+func CommandMath(i *Interp, argv []string, pd interface{}) (string, error) {
+	if len(argv) != 3 {
+		return "", ArityErr(i, argv[0], argv)
+	}
+	a, _ := strconv.Atoi(argv[1])
+	b, _ := strconv.Atoi(argv[2])
+	var c int
+	switch {
+	case argv[0] == "+":
+		c = a + b
+	case argv[0] == "-":
+		c = a - b
+	case argv[0] == "*":
+		c = a * b
+	case argv[0] == "/":
+		c = a / b
+	case argv[0] == ">":
+		if a > b {
+			c = 1
+		}
+	case argv[0] == ">=":
+		if a >= b {
+			c = 1
+		}
+	case argv[0] == "<":
+		if a < b {
+			c = 1
+		}
+	case argv[0] == "<=":
+		if a <= b {
+			c = 1
+		}
+	case argv[0] == "==":
+		if a == b {
+			c = 1
+		}
+	case argv[0] == "!=":
+		if a != b {
+			c = 1
+		}
+	default: // FIXME I hate warnings
+		c = 0
+	}
+	return fmt.Sprintf("%d", c), nil
+}
+
+func CommandSet(i *Interp, argv []string, pd interface{}) (string, error) {
+	if len(argv) != 3 {
+		return "", ArityErr(i, argv[0], argv)
+	}
+	i.SetVar(argv[1], argv[2])
+	return argv[2], nil
+}
+
+func CommandUnset(i *Interp, argv []string, pd interface{}) (string, error) {
+	if len(argv) != 2 {
+		return "", ArityErr(i, argv[0], argv)
+	}
+	i.UnsetVar(argv[1])
+	return "", nil
+}
+
+func CommandIf(i *Interp, argv []string, pd interface{}) (string, error) {
+	if len(argv) != 3 && len(argv) != 5 {
+		return "", ArityErr(i, argv[0], argv)
+	}
+
+	result, err := i.Eval(argv[1])
+	if err != nil {
+		return "", err
+	}
+
+	if r, _ := strconv.Atoi(result); r != 0 {
+		return i.Eval(argv[2])
+	} else if len(argv) == 5 {
+		return i.Eval(argv[4])
+	}
+
+	return result, nil
+}
+
+func CommandWhile(i *Interp, argv []string, pd interface{}) (string, error) {
+	if len(argv) != 3 {
+		return "", ArityErr(i, argv[0], argv)
+	}
+
+	for {
+		result, err := i.Eval(argv[1])
+		if err != nil {
+			return "", err
+		}
+		if r, _ := strconv.Atoi(result); r != 0 {
+			result, err := i.Eval(argv[2])
+			switch err {
+			case PICOL_CONTINUE, nil:
+				//pass
+			case PICOL_BREAK:
+				return result, nil
+			default:
+				return result, err
+			}
+		} else {
+			return result, nil
+		}
+	}
+}
+
+func CommandRetCodes(i *Interp, argv []string, pd interface{}) (string, error) {
+	if len(argv) != 1 {
+		return "", ArityErr(i, argv[0], argv)
+	}
+	switch argv[0] {
+	case "break":
+		return "", PICOL_BREAK
+	case "continue":
+		return "", PICOL_CONTINUE
+	}
+	return "", nil
+}
+
+func CommandCallProc(i *Interp, argv []string, pd interface{}) (string, error) {
+	var x []string
+
+	if pd, ok := pd.([]string); ok {
+		x = pd
+	} else {
+		return "", nil
+	}
+
+	i.callframe = &CallFrame{vars: make(map[string]Var), parent: i.callframe}
+	defer func() { i.callframe = i.callframe.parent }() // remove the called proc callframe
+
+	arity := 0
+	for _, arg := range strings.Split(x[0], " ") {
+		if len(arg) == 0 {
+			continue
+		}
+		arity++
+		i.SetVar(arg, argv[arity])
+	}
+
+	if arity != len(argv)-1 {
+		return "", fmt.Errorf("Proc '%s' called with wrong arg num", argv[0])
+	}
+
+	body := x[1]
+	result, err := i.Eval(body)
+	if err == PICOL_RETURN {
+		err = nil
+	}
+	return result, err
+}
+
+func CommandProc(i *Interp, argv []string, pd interface{}) (string, error) {
+	if len(argv) != 4 {
+		return "", ArityErr(i, argv[0], argv)
+	}
+	return "", i.RegisterCommand(argv[1], CommandCallProc, []string{argv[2], argv[3]})
+}
+
+func CommandReturn(i *Interp, argv []string, pd interface{}) (string, error) {
+	if len(argv) != 1 && len(argv) != 2 {
+		return "", ArityErr(i, argv[0], argv)
+	}
+	var r string
+	if len(argv) == 2 {
+		r = argv[1]
+	}
+	return r, PICOL_RETURN
+}
+
+func CommandError(i *Interp, argv []string, pd interface{}) (string, error) {
+	if len(argv) != 1 && len(argv) != 2 {
+		return "", ArityErr(i, argv[0], argv)
+	}
+	return "", fmt.Errorf(argv[1])
+}
+
+func (i *Interp) RegisterCoreCommands() {
+	name := [...]string{"+", "-", "*", "/", ">", ">=", "<", "<=", "==", "!="}
+	for _, n := range name {
+		i.RegisterCommand(n, CommandMath, nil)
+	}
+	i.RegisterCommand("set", CommandSet, nil)
+	i.RegisterCommand("unset", CommandUnset, nil)
+	i.RegisterCommand("if", CommandIf, nil)
+	i.RegisterCommand("while", CommandWhile, nil)
+	i.RegisterCommand("break", CommandRetCodes, nil)
+	i.RegisterCommand("continue", CommandRetCodes, nil)
+	i.RegisterCommand("proc", CommandProc, nil)
+	i.RegisterCommand("return", CommandReturn, nil)
+	i.RegisterCommand("error", CommandError, nil)
+}

+ 250 - 0
picol/parser.go

@@ -0,0 +1,250 @@
+package picol
+
+import (
+	"unicode"
+	"unicode/utf8"
+)
+
+const (
+	PT_ESC = iota
+	PT_STR
+	PT_CMD
+	PT_VAR
+	PT_SEP
+	PT_EOL
+	PT_EOF
+)
+
+type Parser struct {
+	text              string
+	p, start, end, ln int
+	insidequote       int
+	Type              int
+}
+
+func InitParser(text string) *Parser {
+	return &Parser{text, 0, 0, 0, len(text), 0, PT_EOL}
+}
+
+func (p *Parser) next() {
+	_, w := utf8.DecodeRuneInString(p.text[p.p:])
+	p.p += w
+	p.ln -= w
+}
+
+func (p *Parser) current() rune {
+	r, _ := utf8.DecodeRuneInString(p.text[p.p:])
+	return r
+}
+
+func (p *Parser) token() (t string) {
+	defer recover()
+	return p.text[p.start:p.end]
+}
+
+func (p *Parser) parseSep() string {
+	p.start = p.p
+	for ; p.p < len(p.text); p.next() {
+		if !unicode.IsSpace(p.current()) {
+			break
+		}
+	}
+	p.end = p.p
+	p.Type = PT_SEP
+	return p.token()
+}
+
+func (p *Parser) parseEol() string {
+	p.start = p.p
+
+	for ; p.p < len(p.text); p.next() {
+		if p.current() == ';' || unicode.IsSpace(p.current()) {
+			// pass
+		} else {
+			break
+		}
+	}
+
+	p.end = p.p
+	p.Type = PT_EOL
+	return p.token()
+}
+
+func (p *Parser) parseCommand() string {
+	level, blevel := 1, 0
+	p.next() // skip
+	p.start = p.p
+Loop:
+	for {
+		switch {
+		case p.ln == 0:
+			break Loop
+		case p.current() == '[' && blevel == 0:
+			level++
+		case p.current() == ']' && blevel == 0:
+			level--
+			if level == 0 {
+				break Loop
+			}
+		case p.current() == '\\':
+			p.next()
+		case p.current() == '{':
+			blevel++
+		case p.current() == '}' && blevel != 0:
+			blevel--
+		}
+		p.next()
+	}
+	p.end = p.p
+	p.Type = PT_CMD
+	if p.p < len(p.text) && p.current() == ']' {
+		p.next()
+	}
+	return p.token()
+}
+
+func (p *Parser) parseVar() string {
+	p.next() // skip the $
+	p.start = p.p
+
+	if p.current() == '{' {
+		p.Type = PT_VAR
+		return p.parseBrace()
+	}
+
+	for p.p < len(p.text) {
+		c := p.current()
+		if unicode.IsLetter(c) || ('0' <= c && c <= '9') || c == '_' {
+			p.next()
+			continue
+		}
+		break
+	}
+
+	if p.start == p.p { // It's just a single char string "$"
+		p.start = p.p - 1
+		p.end = p.p
+		p.Type = PT_STR
+	} else {
+		p.end = p.p
+		p.Type = PT_VAR
+	}
+	return p.token()
+}
+
+func (p *Parser) parseBrace() string {
+	level := 1
+	p.next() // skip
+	p.start = p.p
+
+Loop:
+	for p.p < len(p.text) {
+		c := p.current()
+		switch {
+		case p.ln >= 2 && c == '\\':
+			p.next()
+		case p.ln == 0 || c == '}':
+			level--
+			if level == 0 || p.ln == 0 {
+				break Loop
+			}
+		case c == '{':
+			level++
+		}
+		p.next()
+	}
+	p.end = p.p
+	if p.ln != 0 { // Skip final closed brace
+		p.next()
+	}
+	return p.token()
+}
+
+func (p *Parser) parseString() string {
+	newword := p.Type == PT_SEP || p.Type == PT_EOL || p.Type == PT_STR
+
+	if c := p.current(); newword && c == '{' {
+		p.Type = PT_STR
+		return p.parseBrace()
+	} else if newword && c == '"' {
+		p.insidequote = 1
+		p.next() // skip
+	}
+
+	p.start = p.p
+
+Loop:
+	for ; p.ln != 0; p.next() {
+		switch p.current() {
+		case '\\':
+			if p.ln >= 2 {
+				p.next()
+			}
+		case '$', '[':
+			break Loop
+		case '"':
+			if p.insidequote != 0 {
+				p.end = p.p
+				p.Type = PT_ESC
+				p.next()
+				p.insidequote = 0
+				return p.token()
+			}
+		}
+		if p.current() == ';' || unicode.IsSpace(p.current()) {
+			if p.insidequote == 0 {
+				break Loop
+			}
+		}
+	}
+
+	p.end = p.p
+	p.Type = PT_ESC
+	return p.token()
+}
+
+func (p *Parser) parseComment() string {
+	for p.ln != 0 && p.current() != '\n' {
+		p.next()
+	}
+	return p.token()
+}
+
+func (p *Parser) GetToken() string {
+	for {
+		if p.ln == 0 {
+			if p.Type != PT_EOL && p.Type != PT_EOF {
+				p.Type = PT_EOL
+			} else {
+				p.Type = PT_EOF
+			}
+			return p.token()
+		}
+
+		switch p.current() {
+		case ' ', '\t', '\r':
+			if p.insidequote != 0 {
+				return p.parseString()
+			}
+			return p.parseSep()
+		case '\n', ';':
+			if p.insidequote != 0 {
+				return p.parseString()
+			}
+			return p.parseEol()
+		case '[':
+			return p.parseCommand()
+		case '$':
+			return p.parseVar()
+		case '#':
+			if p.Type == PT_EOL {
+				p.parseComment()
+				continue
+			}
+			return p.parseString()
+		default:
+			return p.parseString()
+		}
+	}
+	return p.token() /* unreached */
+}

+ 138 - 0
picol/picol.go

@@ -0,0 +1,138 @@
+package picol
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+)
+
+var (
+	PICOL_RETURN   = errors.New("RETURN")
+	PICOL_BREAK    = errors.New("BREAK")
+	PICOL_CONTINUE = errors.New("CONTINUE")
+)
+
+type Var string
+type CmdFunc func(i *Interp, argv []string, privdata interface{}) (string, error)
+type Cmd struct {
+	fn       CmdFunc
+	privdata interface{}
+}
+type CallFrame struct {
+	vars   map[string]Var
+	parent *CallFrame
+}
+type Interp struct {
+	level     int
+	callframe *CallFrame
+	commands  map[string]Cmd
+}
+
+func InitInterp() *Interp {
+	return &Interp{
+		level:     0,
+		callframe: &CallFrame{vars: make(map[string]Var)},
+		commands:  make(map[string]Cmd),
+	}
+}
+
+func (i *Interp) Var(name string) (Var, bool) {
+	for frame := i.callframe; frame != nil; frame = frame.parent {
+		v, ok := frame.vars[name]
+		if ok {
+			return v, ok
+		}
+	}
+	return "", false
+}
+func (i *Interp) SetVar(name, val string) {
+	i.callframe.vars[name] = Var(val)
+}
+
+func (i *Interp) UnsetVar(name string) {
+	delete(i.callframe.vars, name)
+}
+
+func (i *Interp) Command(name string) *Cmd {
+	v, ok := i.commands[name]
+	if !ok {
+		return nil
+	}
+	return &v
+}
+
+func (i *Interp) RegisterCommand(name string, fn CmdFunc, privdata interface{}) error {
+	c := i.Command(name)
+	if c != nil {
+		return fmt.Errorf("Command '%s' already defined", name)
+	}
+
+	i.commands[name] = Cmd{fn, privdata}
+	return nil
+}
+
+/* EVAL! */
+func (i *Interp) Eval(t string) (string, error) {
+	p := InitParser(t)
+	var result string
+	var err error
+
+	argv := []string{}
+
+	for {
+		prevtype := p.Type
+		// XXX
+		t = p.GetToken()
+		if p.Type == PT_EOF {
+			break
+		}
+
+		switch p.Type {
+		case PT_VAR:
+			v, ok := i.Var(t)
+			if !ok {
+				return "", fmt.Errorf("No such variable '%s'", t)
+			}
+			t = string(v)
+		case PT_CMD:
+			result, err = i.Eval(t)
+			if err != nil {
+				return result, err
+			} else {
+				t = result
+			}
+		case PT_ESC:
+			// XXX: escape handling missing!
+		case PT_SEP:
+			prevtype = p.Type
+			continue
+		}
+
+		// We have a complete command + args. Call it!
+		if p.Type == PT_EOL {
+			prevtype = p.Type
+			if len(argv) != 0 {
+				c := i.Command(argv[0])
+				if c == nil {
+					return "", fmt.Errorf("No such command '%s'", argv[0])
+				}
+				result, err = c.fn(i, argv, c.privdata)
+				if err != nil {
+					return result, err
+				}
+			}
+			// Prepare for the next command
+			argv = []string{}
+			continue
+		}
+
+		// We have a new token, append to the previous or as new arg?
+		if prevtype == PT_SEP || prevtype == PT_EOL {
+			argv = append(argv, t)
+		} else { // Interpolation
+			argv[len(argv)-1] = strings.Join([]string{argv[len(argv)-1], t}, "")
+		}
+		prevtype = p.Type
+	}
+	return result, nil
+}