diff --git a/engine/builtin_test.go b/engine/builtin_test.go index d982ac4..b8a39e1 100644 --- a/engine/builtin_test.go +++ b/engine/builtin_test.go @@ -5,9 +5,6 @@ import ( "context" "errors" "fmt" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - orderedmap "github.com/wk8/go-ordered-map/v2" "io" "math" "os" @@ -16,6 +13,10 @@ import ( "strings" "testing" "unicode/utf8" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + orderedmap "github.com/wk8/go-ordered-map/v2" ) type procedurePair orderedmap.Pair[procedureIndicator, procedure] @@ -2709,8 +2710,8 @@ func TestAssertz(t *testing.T) { args: []Term{NewAtom("a")}, }, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("a")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("a")}, + {opcode: OpExit}, }, }, { @@ -2723,8 +2724,8 @@ func TestAssertz(t *testing.T) { args: []Term{NewAtom("b")}, }, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("b")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("b")}, + {opcode: OpExit}, }, }, }}, vm.procedures.GetPair(procedureIndicator{ @@ -2839,8 +2840,8 @@ func TestAsserta(t *testing.T) { args: []Term{NewAtom("b")}, }, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("b")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("b")}, + {opcode: OpExit}, }, }, { @@ -2850,8 +2851,8 @@ func TestAsserta(t *testing.T) { args: []Term{NewAtom("a")}, }, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("a")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("a")}, + {opcode: OpExit}, }, }, }}, vm.procedures.GetPair(procedureIndicator{name: NewAtom("foo"), arity: 1}).Value) @@ -2902,11 +2903,11 @@ func TestAsserta(t *testing.T) { }, }, bytecode: bytecode{ - {opcode: opEnter}, - {opcode: opPutConst, operand: NewAtom("a")}, - {opcode: opCall, operand: procedureIndicator{name: NewAtom("p"), arity: 1}}, - {opcode: opCut}, - {opcode: opExit}, + {opcode: OpEnter}, + {opcode: OpPutConst, operand: NewAtom("a")}, + {opcode: OpCall, operand: procedureIndicator{name: NewAtom("p"), arity: 1}}, + {opcode: OpCut}, + {opcode: OpExit}, }, }, { @@ -2922,10 +2923,10 @@ func TestAsserta(t *testing.T) { }, }, bytecode: bytecode{ - {opcode: opEnter}, - {opcode: opPutConst, operand: NewAtom("b")}, - {opcode: opCall, operand: procedureIndicator{name: NewAtom("p"), arity: 1}}, - {opcode: opExit}, + {opcode: OpEnter}, + {opcode: OpPutConst, operand: NewAtom("b")}, + {opcode: OpCall, operand: procedureIndicator{name: NewAtom("p"), arity: 1}}, + {opcode: OpExit}, }, }, }}, vm.procedures.GetPair(procedureIndicator{name: NewAtom("foo"), arity: 0}).Value) diff --git a/engine/clause.go b/engine/clause.go index 51e7e8f..e28916f 100644 --- a/engine/clause.go +++ b/engine/clause.go @@ -71,7 +71,7 @@ func compileClause(head Term, body Term, env *Env) (clause, error) { return c, typeError(validTypeCallable, body, env) } } - c.bytecode = append(c.bytecode, instruction{opcode: opExit}) + c.bytecode = append(c.bytecode, instruction{opcode: OpExit}) return c, nil } @@ -88,7 +88,7 @@ func (c *clause) compileHead(head Term, env *Env) { } func (c *clause) compileBody(body Term, env *Env) error { - c.bytecode = append(c.bytecode, instruction{opcode: opEnter}) + c.bytecode = append(c.bytecode, instruction{opcode: OpEnter}) iter := seqIterator{Seq: body, Env: env} for iter.Next() { if err := c.compilePred(iter.Current(), env); err != nil { @@ -107,16 +107,16 @@ func (c *clause) compilePred(p Term, env *Env) error { case Atom: switch p { case atomCut: - c.bytecode = append(c.bytecode, instruction{opcode: opCut}) + c.bytecode = append(c.bytecode, instruction{opcode: OpCut}) return nil } - c.bytecode = append(c.bytecode, instruction{opcode: opCall, operand: procedureIndicator{name: p, arity: 0}}) + c.bytecode = append(c.bytecode, instruction{opcode: OpCall, operand: procedureIndicator{name: p, arity: 0}}) return nil case Compound: for i := 0; i < p.Arity(); i++ { c.compileBodyArg(p.Arg(i), env) } - c.bytecode = append(c.bytecode, instruction{opcode: opCall, operand: procedureIndicator{name: p.Functor(), arity: Integer(p.Arity())}}) + c.bytecode = append(c.bytecode, instruction{opcode: OpCall, operand: procedureIndicator{name: p.Functor(), arity: Integer(p.Arity())}}) return nil default: return errNotCallable @@ -126,67 +126,67 @@ func (c *clause) compilePred(p Term, env *Env) error { func (c *clause) compileHeadArg(a Term, env *Env) { switch a := env.Resolve(a).(type) { case Variable: - c.bytecode = append(c.bytecode, instruction{opcode: opGetVar, operand: c.varOffset(a)}) + c.bytecode = append(c.bytecode, instruction{opcode: OpGetVar, operand: c.varOffset(a)}) case charList, codeList: // Treat them as if they're atomic. - c.bytecode = append(c.bytecode, instruction{opcode: opGetConst, operand: a}) + c.bytecode = append(c.bytecode, instruction{opcode: OpGetConst, operand: a}) case list: - c.bytecode = append(c.bytecode, instruction{opcode: opGetList, operand: Integer(len(a))}) + c.bytecode = append(c.bytecode, instruction{opcode: OpGetList, operand: Integer(len(a))}) for _, arg := range a { c.compileHeadArg(arg, env) } - c.bytecode = append(c.bytecode, instruction{opcode: opPop}) + c.bytecode = append(c.bytecode, instruction{opcode: OpPop}) case *partial: prefix := a.Compound.(list) - c.bytecode = append(c.bytecode, instruction{opcode: opGetPartial, operand: Integer(len(prefix))}) + c.bytecode = append(c.bytecode, instruction{opcode: OpGetPartial, operand: Integer(len(prefix))}) c.compileHeadArg(*a.tail, env) for _, arg := range prefix { c.compileHeadArg(arg, env) } - c.bytecode = append(c.bytecode, instruction{opcode: opPop}) + c.bytecode = append(c.bytecode, instruction{opcode: OpPop}) case Compound: - c.bytecode = append(c.bytecode, instruction{opcode: opGetFunctor, operand: procedureIndicator{name: a.Functor(), arity: Integer(a.Arity())}}) + c.bytecode = append(c.bytecode, instruction{opcode: OpGetFunctor, operand: procedureIndicator{name: a.Functor(), arity: Integer(a.Arity())}}) for i := 0; i < a.Arity(); i++ { c.compileHeadArg(a.Arg(i), env) } - c.bytecode = append(c.bytecode, instruction{opcode: opPop}) + c.bytecode = append(c.bytecode, instruction{opcode: OpPop}) default: - c.bytecode = append(c.bytecode, instruction{opcode: opGetConst, operand: a}) + c.bytecode = append(c.bytecode, instruction{opcode: OpGetConst, operand: a}) } } func (c *clause) compileBodyArg(a Term, env *Env) { switch a := env.Resolve(a).(type) { case Variable: - c.bytecode = append(c.bytecode, instruction{opcode: opPutVar, operand: c.varOffset(a)}) + c.bytecode = append(c.bytecode, instruction{opcode: OpPutVar, operand: c.varOffset(a)}) case charList, codeList: // Treat them as if they're atomic. - c.bytecode = append(c.bytecode, instruction{opcode: opPutConst, operand: a}) + c.bytecode = append(c.bytecode, instruction{opcode: OpPutConst, operand: a}) case list: - c.bytecode = append(c.bytecode, instruction{opcode: opPutList, operand: Integer(len(a))}) + c.bytecode = append(c.bytecode, instruction{opcode: OpPutList, operand: Integer(len(a))}) for _, arg := range a { c.compileBodyArg(arg, env) } - c.bytecode = append(c.bytecode, instruction{opcode: opPop}) + c.bytecode = append(c.bytecode, instruction{opcode: OpPop}) case *partial: var l int iter := ListIterator{List: a.Compound} for iter.Next() { l++ } - c.bytecode = append(c.bytecode, instruction{opcode: opPutPartial, operand: Integer(l)}) + c.bytecode = append(c.bytecode, instruction{opcode: OpPutPartial, operand: Integer(l)}) c.compileBodyArg(*a.tail, env) iter = ListIterator{List: a.Compound} for iter.Next() { c.compileBodyArg(iter.Current(), env) } - c.bytecode = append(c.bytecode, instruction{opcode: opPop}) + c.bytecode = append(c.bytecode, instruction{opcode: OpPop}) case Compound: - c.bytecode = append(c.bytecode, instruction{opcode: opPutFunctor, operand: procedureIndicator{name: a.Functor(), arity: Integer(a.Arity())}}) + c.bytecode = append(c.bytecode, instruction{opcode: OpPutFunctor, operand: procedureIndicator{name: a.Functor(), arity: Integer(a.Arity())}}) for i := 0; i < a.Arity(); i++ { c.compileBodyArg(a.Arg(i), env) } - c.bytecode = append(c.bytecode, instruction{opcode: opPop}) + c.bytecode = append(c.bytecode, instruction{opcode: OpPop}) default: - c.bytecode = append(c.bytecode, instruction{opcode: opPutConst, operand: a}) + c.bytecode = append(c.bytecode, instruction{opcode: OpPutConst, operand: a}) } } diff --git a/engine/text_test.go b/engine/text_test.go index 5f2da3b..0fa5139 100644 --- a/engine/text_test.go +++ b/engine/text_test.go @@ -4,11 +4,12 @@ import ( "context" "embed" "errors" - orderedmap "github.com/wk8/go-ordered-map/v2" "io" "io/fs" "testing" + orderedmap "github.com/wk8/go-ordered-map/v2" + "github.com/stretchr/testify/assert" ) @@ -44,8 +45,8 @@ foo(a). pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("a")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("a")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("a")}, + {opcode: OpExit}, }, }, }, @@ -62,8 +63,8 @@ foo(a). pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("c")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("c")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("c")}, + {opcode: OpExit}, }, }, }, @@ -82,16 +83,16 @@ foo(b). pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("a")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("a")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("a")}, + {opcode: OpExit}, }, }, { pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("b")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("b")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("b")}, + {opcode: OpExit}, }, }, }, @@ -111,8 +112,8 @@ bar(X, "abc", [a, b], [a, b|Y], f(a)) :- X, !, foo(X, "abc", [a, b], [a, b|Y], f pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("c")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("c")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("c")}, + {opcode: OpExit}, }, }, }, @@ -126,9 +127,9 @@ bar(X, "abc", [a, b], [a, b|Y], f(a)) :- X, !, foo(X, "abc", [a, b], [a, b|Y], f pi: procedureIndicator{name: NewAtom("bar"), arity: 0}, raw: atomIf.Apply(NewAtom("bar"), atomTrue), bytecode: bytecode{ - {opcode: opEnter}, - {opcode: opCall, operand: procedureIndicator{name: atomTrue, arity: 0}}, - {opcode: opExit}, + {opcode: OpEnter}, + {opcode: OpCall, operand: procedureIndicator{name: atomTrue, arity: 0}}, + {opcode: OpExit}, }, }, }, @@ -150,40 +151,40 @@ bar(X, "abc", [a, b], [a, b|Y], f(a)) :- X, !, foo(X, "abc", [a, b], [a, b|Y], f ), vars: []Variable{lastVariable() + 1, lastVariable() + 2}, bytecode: bytecode{ - {opcode: opGetVar, operand: Integer(0)}, - {opcode: opGetConst, operand: charList("abc")}, - {opcode: opGetList, operand: Integer(2)}, - {opcode: opGetConst, operand: NewAtom("a")}, - {opcode: opGetConst, operand: NewAtom("b")}, - {opcode: opPop}, - {opcode: opGetPartial, operand: Integer(2)}, - {opcode: opGetVar, operand: Integer(1)}, - {opcode: opGetConst, operand: NewAtom("a")}, - {opcode: opGetConst, operand: NewAtom("b")}, - {opcode: opPop}, - {opcode: opGetFunctor, operand: procedureIndicator{name: NewAtom("f"), arity: 1}}, - {opcode: opGetConst, operand: NewAtom("a")}, - {opcode: opPop}, - {opcode: opEnter}, - {opcode: opPutVar, operand: Integer(0)}, - {opcode: opCall, operand: procedureIndicator{name: atomCall, arity: 1}}, - {opcode: opCut}, - {opcode: opPutVar, operand: Integer(0)}, - {opcode: opPutConst, operand: charList("abc")}, - {opcode: opPutList, operand: Integer(2)}, - {opcode: opPutConst, operand: NewAtom("a")}, - {opcode: opPutConst, operand: NewAtom("b")}, - {opcode: opPop}, - {opcode: opPutPartial, operand: Integer(2)}, - {opcode: opPutVar, operand: Integer(1)}, - {opcode: opPutConst, operand: NewAtom("a")}, - {opcode: opPutConst, operand: NewAtom("b")}, - {opcode: opPop}, - {opcode: opPutFunctor, operand: procedureIndicator{name: NewAtom("f"), arity: 1}}, - {opcode: opPutConst, operand: NewAtom("a")}, - {opcode: opPop}, - {opcode: opCall, operand: procedureIndicator{name: NewAtom("foo"), arity: 5}}, - {opcode: opExit}, + {opcode: OpGetVar, operand: Integer(0)}, + {opcode: OpGetConst, operand: charList("abc")}, + {opcode: OpGetList, operand: Integer(2)}, + {opcode: OpGetConst, operand: NewAtom("a")}, + {opcode: OpGetConst, operand: NewAtom("b")}, + {opcode: OpPop}, + {opcode: OpGetPartial, operand: Integer(2)}, + {opcode: OpGetVar, operand: Integer(1)}, + {opcode: OpGetConst, operand: NewAtom("a")}, + {opcode: OpGetConst, operand: NewAtom("b")}, + {opcode: OpPop}, + {opcode: OpGetFunctor, operand: procedureIndicator{name: NewAtom("f"), arity: 1}}, + {opcode: OpGetConst, operand: NewAtom("a")}, + {opcode: OpPop}, + {opcode: OpEnter}, + {opcode: OpPutVar, operand: Integer(0)}, + {opcode: OpCall, operand: procedureIndicator{name: atomCall, arity: 1}}, + {opcode: OpCut}, + {opcode: OpPutVar, operand: Integer(0)}, + {opcode: OpPutConst, operand: charList("abc")}, + {opcode: OpPutList, operand: Integer(2)}, + {opcode: OpPutConst, operand: NewAtom("a")}, + {opcode: OpPutConst, operand: NewAtom("b")}, + {opcode: OpPop}, + {opcode: OpPutPartial, operand: Integer(2)}, + {opcode: OpPutVar, operand: Integer(1)}, + {opcode: OpPutConst, operand: NewAtom("a")}, + {opcode: OpPutConst, operand: NewAtom("b")}, + {opcode: OpPop}, + {opcode: OpPutFunctor, operand: procedureIndicator{name: NewAtom("f"), arity: 1}}, + {opcode: OpPutConst, operand: NewAtom("a")}, + {opcode: OpPop}, + {opcode: OpCall, operand: procedureIndicator{name: NewAtom("foo"), arity: 5}}, + {opcode: OpExit}, }, }, }, @@ -205,16 +206,16 @@ foo(b). pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("a")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("a")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("a")}, + {opcode: OpExit}, }, }, { pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("b")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("b")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("b")}, + {opcode: OpExit}, }, }, }, @@ -235,24 +236,24 @@ foo(b). pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("c")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("c")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("c")}, + {opcode: OpExit}, }, }, { pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("a")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("a")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("a")}, + {opcode: OpExit}, }, }, { pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("b")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("b")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("b")}, + {opcode: OpExit}, }, }, }, @@ -274,16 +275,16 @@ foo(b). pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("a")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("a")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("a")}, + {opcode: OpExit}, }, }, { pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("b")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("b")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("b")}, + {opcode: OpExit}, }, }, }, @@ -297,8 +298,8 @@ foo(b). pi: procedureIndicator{name: NewAtom("bar"), arity: 1}, raw: &compound{functor: NewAtom("bar"), args: []Term{NewAtom("a")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("a")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("a")}, + {opcode: OpExit}, }, }, }, @@ -317,8 +318,8 @@ foo(b). pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("c")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("c")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("c")}, + {opcode: OpExit}, }, }, }, @@ -332,7 +333,7 @@ foo(b). pi: procedureIndicator{name: NewAtom("foo"), arity: 0}, raw: NewAtom("foo"), bytecode: bytecode{ - {opcode: opExit}, + {opcode: OpExit}, }, }, }, @@ -351,8 +352,8 @@ foo(b). pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("c")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("c")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("c")}, + {opcode: OpExit}, }, }, }, @@ -366,7 +367,7 @@ foo(b). pi: procedureIndicator{name: NewAtom("foo"), arity: 0}, raw: NewAtom("foo"), bytecode: bytecode{ - {opcode: opExit}, + {opcode: OpExit}, }, }, }, @@ -385,8 +386,8 @@ foo(b). pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("c")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("c")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("c")}, + {opcode: OpExit}, }, }, }, @@ -405,8 +406,8 @@ foo(b). pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("c")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("c")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("c")}, + {opcode: OpExit}, }, }, }, @@ -500,8 +501,8 @@ bar(b). pi: procedureIndicator{name: NewAtom("foo"), arity: 1}, raw: &compound{functor: NewAtom("foo"), args: []Term{NewAtom("c")}}, bytecode: bytecode{ - {opcode: opGetConst, operand: NewAtom("c")}, - {opcode: opExit}, + {opcode: OpGetConst, operand: NewAtom("c")}, + {opcode: OpExit}, }, }, }, diff --git a/engine/vm.go b/engine/vm.go index acfae9d..f7d6e51 100644 --- a/engine/vm.go +++ b/engine/vm.go @@ -3,40 +3,99 @@ package engine import ( "context" "fmt" - orderedmap "github.com/wk8/go-ordered-map/v2" "io" "io/fs" "strings" + + orderedmap "github.com/wk8/go-ordered-map/v2" ) +// HookFunc is a type for a hook function that is triggered before the VM executes a specific instruction. +// If the hook function returns an error, the VM halts execution and returns the error. +type HookFunc func(opcode Opcode, operand Term, env *Env) error + +// DebugHookFn is a function that returns a hook function that prints the executed instruction. +func DebugHookFn(w io.Writer) HookFunc { + return func(opcode Opcode, operand Term, _ *Env) error { + _, _ = io.WriteString(w, opcode.String()) + + if operand != nil { + _, _ = io.WriteString(w, "(") + _ = operand.WriteTerm(w, &defaultWriteOptions, nil) + _, _ = io.WriteString(w, ")") + } + _, _ = io.WriteString(w, "\n") + + return nil + } +} + +// CompositeHook returns a hook function that chains multiple hooks together. +// The hooks are executed sequentially, and if any hook returns an error, the execution stops. +func CompositeHook(fs ...HookFunc) HookFunc { + return func(opcode Opcode, operand Term, env *Env) error { + for _, f := range fs { + if err := f(opcode, operand, env); err != nil { + return err + } + } + return nil + } +} + type bytecode []instruction type instruction struct { - opcode opcode + opcode Opcode operand Term } -type opcode byte +type Opcode byte const ( - opEnter opcode = iota - opCall - opExit - opGetConst - opPutConst - opGetVar - opPutVar - opGetFunctor - opPutFunctor - opPop - - opCut - opGetList - opPutList - opGetPartial - opPutPartial + OpEnter Opcode = iota + OpCall + OpExit + OpGetConst + OpPutConst + OpGetVar + OpPutVar + OpGetFunctor + OpPutFunctor + OpPop + + OpCut + OpGetList + OpPutList + OpGetPartial + OpPutPartial ) +func (op Opcode) String() string { + opcodeStrings := [...]string{ + OpEnter: "enter", + OpCall: "call", + OpExit: "exit", + OpGetConst: "get_const", + OpPutConst: "put_const", + OpGetVar: "get_var", + OpPutVar: "put_var", + OpGetFunctor: "get_functor", + OpPutFunctor: "put_functor", + OpPop: "pop", + OpCut: "cut", + OpGetList: "get_list", + OpPutList: "put_list", + OpGetPartial: "get_partial", + OpPutPartial: "put_partial", + } + + if int(op) < 0 || int(op) >= len(opcodeStrings) { + return fmt.Sprintf("(%d)", op) + } + return opcodeStrings[op] +} + // Success is a continuation that leads to true. func Success(*Env) *Promise { return Bool(true) @@ -73,6 +132,9 @@ type VM struct { // Limits maxVariables uint64 + // Hook + hook HookFunc + // Misc debug bool } @@ -181,20 +243,26 @@ func (vm *VM) exec(pc bytecode, vars []Variable, cont Cont, args []Term, astack ) for ok { op, pc = pc[0], pc[1:] + if vm.hook != nil { + if err := vm.hook(op.opcode, op.operand, env); err != nil { + return Error(err) + } + } + switch opcode, operand := op.opcode, op.operand; opcode { - case opGetConst: + case OpGetConst: arg, args = args[0], args[1:] env, ok = env.Unify(arg, operand) - case opPutConst: + case OpPutConst: args = append(args, operand) - case opGetVar: + case OpGetVar: v := vars[operand.(Integer)] arg, args = args[0], args[1:] env, ok = env.Unify(arg, v) - case opPutVar: + case OpPutVar: v := vars[operand.(Integer)] args = append(args, v) - case opGetFunctor: + case OpGetFunctor: pi := operand.(procedureIndicator) arg, astack = env.Resolve(args[0]), append(astack, args[1:]) args = make([]Term, int(pi.arity)) @@ -202,29 +270,29 @@ func (vm *VM) exec(pc bytecode, vars []Variable, cont Cont, args []Term, astack args[i] = NewVariable() } env, ok = env.Unify(arg, pi.name.Apply(args...)) - case opPutFunctor: + case OpPutFunctor: pi := operand.(procedureIndicator) vs := make([]Term, int(pi.arity)) arg = pi.name.Apply(vs...) args = append(args, arg) astack = append(astack, args) args = vs[:0] - case opPop: + case OpPop: args, astack = astack[len(astack)-1], astack[:len(astack)-1] - case opEnter: + case OpEnter: break - case opCall: + case OpCall: pi := operand.(procedureIndicator) return vm.Arrive(pi.name, args, func(env *Env) *Promise { return vm.exec(pc, vars, cont, nil, nil, env, cutParent) }, env) - case opExit: + case OpExit: return cont(env) - case opCut: + case OpCut: return cut(cutParent, func(context.Context) *Promise { return vm.exec(pc, vars, cont, args, astack, env, cutParent) }) - case opGetList: + case OpGetList: l := operand.(Integer) arg, astack = args[0], append(astack, args[1:]) args = make([]Term, int(l)) @@ -232,14 +300,14 @@ func (vm *VM) exec(pc bytecode, vars []Variable, cont Cont, args []Term, astack args[i] = NewVariable() } env, ok = env.Unify(arg, list(args)) - case opPutList: + case OpPutList: l := operand.(Integer) vs := make([]Term, int(l)) arg = list(vs) args = append(args, arg) astack = append(astack, args) args = vs[:0] - case opGetPartial: + case OpGetPartial: l := operand.(Integer) arg, astack = args[0], append(astack, args[1:]) args = make([]Term, int(l+1)) @@ -247,7 +315,7 @@ func (vm *VM) exec(pc bytecode, vars []Variable, cont Cont, args []Term, astack args[i] = NewVariable() } env, ok = env.Unify(arg, PartialList(args[0], args[1:]...)) - case opPutPartial: + case OpPutPartial: l := operand.(Integer) vs := make([]Term, int(l+1)) arg = &partial{ @@ -286,6 +354,16 @@ func (vm *VM) SetMaxVariables(n uint64) { maxVariables = n } +// InstallHook sets the given hook function in the VM. +func (vm *VM) InstallHook(f HookFunc) { + vm.hook = f +} + +// ClearHook removes the installed hook function from the VM. +func (vm *VM) ClearHook() { + vm.hook = nil +} + // ResetEnv is used to reset all global variable func (vm *VM) ResetEnv() { varCounter.count = 0 diff --git a/engine/vm_test.go b/engine/vm_test.go index 285da4d..52d883c 100644 --- a/engine/vm_test.go +++ b/engine/vm_test.go @@ -1,6 +1,7 @@ package engine import ( + "bytes" "context" "os" "testing" @@ -328,3 +329,19 @@ func TestVM_ResetEnv(t *testing.T) { assert.Equal(t, uint64(20), maxVariables) }) } + +func TestVM_DebugHook(t *testing.T) { + var vm VM + vm.Register0(NewAtom("foo"), func(_ *VM, k Cont, env *Env) *Promise { + return k(env) + }) + + buf := &bytes.Buffer{} + vm.InstallHook(DebugHookFn(buf)) + + var env Env + ok, err := Call(&vm, NewAtom("foo"), Success, &env).Force(context.Background()) + assert.NoError(t, err) + assert.True(t, ok) + assert.Equal(t, "enter\ncall(foo/0)\nexit\n", buf.String()) +} diff --git a/interpreter_test.go b/interpreter_test.go index 3472493..aeeeb54 100644 --- a/interpreter_test.go +++ b/interpreter_test.go @@ -5,13 +5,14 @@ import ( "context" "errors" "fmt" - "github.com/ichiban/prolog/engine" - "github.com/stretchr/testify/assert" "io" "os" "regexp" "testing" "time" + + "github.com/ichiban/prolog/engine" + "github.com/stretchr/testify/assert" ) func TestNew(t *testing.T) { @@ -1184,6 +1185,61 @@ next(N) :- retract(count(X)), N is X + 1, asserta(count(N)). }) } +func TestInterpreter_Bombing(t *testing.T) { + const callLimit = 25 + limitHooker := func(nbCall *int) engine.HookFunc { + return func(opcode engine.Opcode, operand engine.Term, env *engine.Env) error { + if opcode == engine.OpCall { + *nbCall++ + if *nbCall > callLimit { + return engine.ResourceError(engine.NewAtom("calls"), env) + } + } + return nil + } + } + + t.Run("💣 recursion of death", func(t *testing.T) { + nbCalls := 0 + t.Run("create vm", func(t *testing.T) { + i := New(nil, nil) + assert.NotNil(t, i) + i.InstallHook(limitHooker(&nbCalls)) + + t.Run("execute program", func(t *testing.T) { + assert.NoError(t, i.Exec("recursionOfDeath :- recursionOfDeath.")) + + t.Run("💥", func(t *testing.T) { + sol := i.QuerySolutionContext(context.Background(), `recursionOfDeath.`) + + assert.Nil(t, sol.sols) + assert.EqualError(t, sol.Err(), "error(resource_error(calls),recursionOfDeath/0)") + }) + }) + }) + }) + + t.Run("💣 backtrack of death", func(t *testing.T) { + nbCalls := 0 + t.Run("create vm", func(t *testing.T) { + i := New(nil, nil) + assert.NotNil(t, i) + i.InstallHook(limitHooker(&nbCalls)) + + t.Run("execute program", func(t *testing.T) { + assert.NoError(t, i.Exec("backtrackOfDeath :- repeat, fail.")) + + t.Run("💥", func(t *testing.T) { + sol := i.QuerySolutionContext(context.Background(), `backtrackOfDeath.`) + + assert.Nil(t, sol.sols) + assert.EqualError(t, sol.Err(), "error(resource_error(calls),\\+ /1)") + }) + }) + }) + }) +} + func TestInterpreter_QuerySolution(t *testing.T) { var i Interpreter assert.NoError(t, i.Exec(`