diff options
Diffstat (limited to 'interp')
| -rw-r--r-- | interp/dump_test.go | 49 | ||||
| -rw-r--r-- | interp/interpreter.go | 55 | ||||
| -rw-r--r-- | interp/interpreter_test.go | 263 | ||||
| -rw-r--r-- | interp/repl.go | 35 |
4 files changed, 402 insertions, 0 deletions
diff --git a/interp/dump_test.go b/interp/dump_test.go new file mode 100644 index 0000000..fbaa778 --- /dev/null +++ b/interp/dump_test.go @@ -0,0 +1,49 @@ +package interp_test + +import ( + "testing" + + "github.com/mvertes/parscan/interp" + "github.com/mvertes/parscan/lang/golang" +) + +func TestDump(t *testing.T) { + initProgram := "var a int = 2+1; a" + intp := interp.NewInterpreter(golang.GoSpec) + r, e := intp.Eval(initProgram) + t.Log(r, e) + if e != nil { + t.Fatal(e) + } + + r, e = intp.Eval("a = 100") + t.Log(r, e) + if e != nil { + t.Fatal(e) + } + + d := intp.Dump() + t.Log(d) + + intp = interp.NewInterpreter(golang.GoSpec) + r, e = intp.Eval(initProgram) + t.Log(r, e) + if e != nil { + t.Fatal(e) + } + + e = intp.ApplyDump(d) + if e != nil { + t.Fatal(e) + } + + r, e = intp.Eval("a = a + 1;a") + t.Log(r, e) + if e != nil { + t.Fatal(e) + } + + if r.Interface() != int(101) { + t.Fatalf("unexpected result: %v", r) + } +} diff --git a/interp/interpreter.go b/interp/interpreter.go new file mode 100644 index 0000000..8d372c3 --- /dev/null +++ b/interp/interpreter.go @@ -0,0 +1,55 @@ +// Package interp implements an interpreter. +package interp + +import ( + "reflect" + + "github.com/mvertes/parscan/comp" + "github.com/mvertes/parscan/lang" + "github.com/mvertes/parscan/vm" +) + +const debug = true + +// Interp represents the state of an interpreter. +type Interp struct { + *comp.Compiler + *vm.Machine +} + +// NewInterpreter returns a new interpreter. +func NewInterpreter(s *lang.Spec) *Interp { + return &Interp{comp.NewCompiler(s), &vm.Machine{}} +} + +// Eval evaluates code string and return the last produced value if any, or an error. +func (i *Interp) Eval(src string) (res reflect.Value, err error) { + codeOffset := len(i.Code) + dataOffset := 0 + if codeOffset > 0 { + // All data must be copied to the VM the first time only (re-entrance). + dataOffset = len(i.Data) + } + i.PopExit() // Remove last exit from previous run (re-entrance). + + t, err := i.Parse(src) + if err != nil { + return res, err + } + if err = i.Generate(t); err != nil { + return res, err + } + i.Push(i.Data[dataOffset:]...) + i.PushCode(i.Code[codeOffset:]...) + if s, ok := i.Symbols["main"]; ok { + i.PushCode(vm.Instruction{Op: vm.Calli, Arg: []int{int(i.Data[s.Index].Int())}}) + } + i.PushCode(vm.Instruction{Op: vm.Exit}) + i.SetIP(max(codeOffset, i.Entry)) + if debug { + i.PrintData() + i.PrintCode() + } + err = i.Run() + return i.Top().Value, err +} diff --git a/interp/interpreter_test.go b/interp/interpreter_test.go new file mode 100644 index 0000000..21e6274 --- /dev/null +++ b/interp/interpreter_test.go @@ -0,0 +1,263 @@ +package interp_test + +import ( + "fmt" + "log" + "testing" + + "github.com/mvertes/parscan/interp" + "github.com/mvertes/parscan/lang/golang" +) + +type etest struct { + src, res, err string + skip bool +} + +func init() { + log.SetFlags(log.Lshortfile) +} + +func gen(test etest) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + if test.skip { + t.Skip() + } + intp := interp.NewInterpreter(golang.GoSpec) + errStr := "" + r, e := intp.Eval(test.src) + t.Log(r, e) + if e != nil { + errStr = e.Error() + } + if errStr != test.err { + t.Errorf("got error %#v, want error %#v", errStr, test.err) + } + if res := fmt.Sprintf("%v", r); test.err == "" && res != test.res { + t.Errorf("got %#v, want %#v", res, test.res) + } + } +} + +func run(t *testing.T, tests []etest) { + for _, test := range tests { + t.Run("", gen(test)) + } +} + +func TestExpr(t *testing.T) { + run(t, []etest{ + {src: "", res: "<invalid reflect.Value>"}, + {src: "1+2", res: "3"}, + {src: "1+", err: "block not terminated"}, + {src: "a := 1 + 2; b := 0; a + 1", res: "4"}, + {src: "1+(2+3)", res: "6"}, + {src: "(1+2)+3", res: "6"}, + {src: "(6+(1+2)+3)+5", res: "17"}, + {src: "(6+(1+2+3)+5", err: "1:1: block not terminated"}, + {src: "a := 2; a = 3; a", res: "3"}, + {src: "2 * 3 + 1 == 7", res: "true"}, + {src: "7 == 2 * 3 + 1", res: "true"}, + {src: "1 + 3 * 2 == 2 * 3 + 1", res: "true"}, + {src: "a := 1 + 3 * 2 == 2 * 3 + 1; a", res: "true"}, + {src: "-2", res: "-2"}, + {src: "-2 + 5", res: "3"}, + {src: "5 + -2", res: "3"}, + {src: "!false", res: "true"}, + {src: `a := "hello"`, res: "hello"}, + }) +} + +func TestLogical(t *testing.T) { + run(t, []etest{ + {src: "true && false", res: "false"}, + {src: "true && true", res: "true"}, + {src: "true && true && false", res: "false"}, + {src: "false || true && true", res: "true"}, + {src: "2 < 3 && 1 > 2 || 3 == 3", res: "true"}, + {src: "2 > 3 && 1 > 2 || 3 == 3", res: "true"}, + {src: "2 > 3 || 2 == 1+1 && 3>0", res: "true"}, + {src: "2 > 3 || 2 == 1+1 && 3>4 || 1<2", res: "true"}, + {src: "a := 1+1 < 3 && 4 == 2+2; a", res: "true"}, + {src: "a := 1+1 < 3 || 3 == 2+2; a", res: "true"}, + }) +} + +func TestFunc(t *testing.T) { + run(t, []etest{ + {src: "func f() int {return 2}; a := f(); a", res: "2"}, + {src: "func f() int {return 2}; f()", res: "2"}, + {src: "func f(a int) int {return a+2}; f(3)", res: "5"}, + {src: "func f(a int) int {if a < 4 {a = 5}; return a}; f(3)", res: "5"}, + {src: "func f(a int) int {return a+2}; 7 - f(3)", res: "2"}, + {src: "func f(a int) int {return a+2}; f(5) - f(3)", res: "2"}, + {src: "func f(a int) int {return a+2}; f(3) - 2", res: "3"}, + {src: "func f(a, b, c int) int {return a+b-c} ; f(7, 1, 3)", res: "5"}, + {src: "var a int; func f() {a = a+2}; f(); a", res: "2"}, + {src: "var f = func(a int) int {return a+3}; f(2)", res: "5"}, + }) +} + +func TestIf(t *testing.T) { + run(t, []etest{ + {src: "a := 0; if a == 0 { a = 2 } else { a = 1 }; a", res: "2"}, + {src: "a := 0; if a == 1 { a = 2 } else { a = 1 }; a", res: "1"}, + {src: "a := 0; if a == 1 { a = 2 } else if a == 0 { a = 3 } else { a = 1 }; a", res: "3"}, + {src: "a := 0; if a == 1 { a = 2 } else if a == 2 { a = 3 } else { a = 1 }; a", res: "1"}, + {src: "a := 1; if a > 0 && a < 2 { a = 3 }; a", res: "3"}, + {src: "a := 1; if a < 0 || a < 2 { a = 3 }; a", res: "3"}, + }) +} + +func TestFor(t *testing.T) { + run(t, []etest{ + {src: "a := 0; for i := 0; i < 3; i = i+1 {a = a+i}; a", res: "3"}, + {src: "func f() int {a := 0; for i := 0; i < 3; i = i+1 {a = a+i}; return a}; f()", res: "3"}, + {src: "a := 0; for {a = a+1; if a == 3 {break}}; a", res: "3"}, + {src: "func f() int {a := 0; for {a = a+1; if a == 3 {break}}; return a}; f()", res: "3"}, + {src: "func f() int {a := 0; for {a = a+1; if a < 3 {continue}; break}; return a}; f()", res: "3"}, + }) +} + +func TestGoto(t *testing.T) { + run(t, []etest{ + {src: ` +func f(a int) int { + a = a+1 + goto end + a = a+1 +end: + return a +} +f(3)`, res: "4"}, + }) +} + +func TestSwitch(t *testing.T) { + src0 := `func f(a int) int { + switch a { + default: a = 0 + case 1,2: a = a+1 + case 3: a = a+2; break; a = 3 + case 4: a = 10 + } + return a +} +` + src1 := `func f(a int) int { + switch { + case a < 3: return 2 + case a < 5: return 5 + default: a = 0 + } + return a +} +` + run(t, []etest{ + {src: src0 + "f(1)", res: "2"}, + {src: src0 + "f(2)", res: "3"}, + {src: src0 + "f(3)", res: "5"}, + {src: src0 + "f(4)", res: "10"}, + {src: src0 + "f(5)", res: "0"}, + + {src: src1 + "f(1)", res: "2"}, + {src: src1 + "f(4)", res: "5"}, + {src: src1 + "f(6)", res: "0"}, + }) +} + +func TestConst(t *testing.T) { + src0 := `const ( + a = iota + b + c +) +` + run(t, []etest{ + {src: "const a = 1+2; a", res: "3"}, + {src: "const a, b = 1, 2; a+b", res: "3"}, + {src: "const huge = 1 << 100; const four = huge >> 98; four", res: "4"}, + + {src: src0 + "c", res: "2"}, + }) +} + +func TestArray(t *testing.T) { + run(t, []etest{ + {src: "type T []int; var t T; t", res: "[]"}, + {src: "type T [3]int; var t T; t", res: "[0 0 0]"}, + {src: "type T [3]int; var t T; t[1] = 2; t", res: "[0 2 0]"}, + }) +} + +func TestPointer(t *testing.T) { + run(t, []etest{ + {src: "var a *int; a", res: "<nil>"}, + {src: "var a int; var b *int = &a; *b", res: "0"}, + {src: "var a int = 2; var b *int = &a; *b", res: "2"}, + }) +} + +func TestStruct(t *testing.T) { + run(t, []etest{ + {src: "type T struct {a string; b, c int}; var t T; t", res: "{ 0 0}"}, + {src: "type T struct {a int}; var t T; t.a", res: "0"}, + {src: "type T struct {a int}; var t T; t.a = 1; t.a", res: "1"}, + }) +} + +func TestType(t *testing.T) { + src0 := `type ( + I int + S string +) +` + run(t, []etest{ + {src: "type t int; var a t = 1; a", res: "1"}, + {src: "type t = int; var a t = 1; a", res: "1"}, + {src: src0 + `var s S = "xx"; s`, res: "xx"}, + }) +} + +func TestVar(t *testing.T) { + run(t, []etest{ + {src: "var a int; a", res: "0"}, + {src: "var a, b, c int; a", res: "0"}, + {src: "var a, b, c int; a + b", res: "0"}, + {src: "var a, b, c int; a + b + c", res: "0"}, + {src: "var a int = 2+1; a", res: "3"}, + {src: "var a, b int = 2, 5; a+b", res: "7"}, + {src: "var x = 5; x", res: "5"}, + {src: "var a = 1; func f() int { var a, b int = 3, 4; return a+b}; a+f()", res: "8"}, + {src: `var a = "hello"; a`, res: "hello"}, + {src: `var ( + a, b int = 4+1, 3 + c = 8 +); a+b+c`, res: "16"}, + }) +} + +func TestImport(t *testing.T) { + src0 := `import ( + "fmt" +) +` + run(t, []etest{ + {src: "fmt.Println(4)", err: "symbol not found: fmt"}, + {src: `import "xxx"`, err: "package not found: xxx"}, + {src: `import "fmt"; fmt.Println(4)`, res: "<nil>"}, + {src: src0 + "fmt.Println(4)", res: "<nil>"}, + {src: `func main() {import "fmt"; fmt.Println("hello")}`, err: "unexpected import"}, + {src: `import m "fmt"; m.Println(4)`, res: "<nil>"}, + {src: `import . "fmt"; Println(4)`, res: "<nil>"}, + }) +} + +func TestComposite(t *testing.T) { + run(t, []etest{ + {src: "type T struct{}; t := T{}; t", res: "{}"}, + {src: "t := struct{}{}; t", res: "{}"}, + // {src: `type T struct{N int; S string}; t := T{2, "foo"}`, res: `{2 foo}`}, + }) +} diff --git a/interp/repl.go b/interp/repl.go new file mode 100644 index 0000000..b8a5338 --- /dev/null +++ b/interp/repl.go @@ -0,0 +1,35 @@ +package interp + +import ( + "bufio" + "errors" + "fmt" + "io" + + "github.com/mvertes/parscan/scanner" +) + +// Repl executes an interactive line oriented Read Eval Print Loop (REPL). +func (i *Interp) Repl(in io.Reader) (err error) { + liner := bufio.NewScanner(in) + text, prompt := "", "> " + fmt.Print(prompt) + for liner.Scan() { + text += liner.Text() + res, err := i.Eval(text + "\n") + switch { + case err == nil: + if res.IsValid() { + fmt.Println(": ", res) + } + text, prompt = "", "> " + case errors.Is(err, scanner.ErrBlock): + prompt = ">> " + default: + fmt.Println("Error:", err) + text, prompt = "", "> " + } + fmt.Print(prompt) + } + return err +} |
