From f3de7f070e6f577e3315dedb173839e662ec92af Mon Sep 17 00:00:00 2001 From: ccamel Date: Mon, 23 Sep 2024 11:22:30 +0200 Subject: [PATCH 1/5] feat(vm): implement hook support for prolog execution control --- engine/builtin_test.go | 41 +++++------ engine/clause.go | 46 ++++++------- engine/text_test.go | 153 +++++++++++++++++++++-------------------- engine/vm.go | 147 ++++++++++++++++++++++++++++++--------- 4 files changed, 234 insertions(+), 153 deletions(-) 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..b797b5e 100644 --- a/engine/vm.go +++ b/engine/vm.go @@ -1,42 +1,102 @@ package engine import ( + "bytes" "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 + +// DebugHook is a hook function that prints the current instruction and its operand (if any). +func DebugHook(opcode Opcode, operand Term, env *Env) error { + var buf bytes.Buffer + + buf.WriteString(opcode.String()) + + if operand != nil { + buf.WriteRune('(') + _ = operand.WriteTerm(&buf, &defaultWriteOptions, nil) + buf.WriteRune(')') + } + fmt.Println(buf.String()) + + 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 +133,9 @@ type VM struct { // Limits maxVariables uint64 + // Hook + hook HookFunc + // Misc debug bool } @@ -181,20 +244,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 +271,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 +301,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 +316,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 +355,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 From 237ae8f542812f3677e6934538e7116daa8380f4 Mon Sep 17 00:00:00 2001 From: ccamel Date: Mon, 23 Sep 2024 11:24:45 +0200 Subject: [PATCH 2/5] test(vm): add test for Backtrack of Death scenario --- interpreter_test.go | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/interpreter_test.go b/interpreter_test.go index 3472493..fc7c1fa 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,42 @@ 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)") + }) + }) + }) + }) + +} + func TestInterpreter_QuerySolution(t *testing.T) { var i Interpreter assert.NoError(t, i.Exec(` From 7466eb628932fbbbe66727cf2b5cc390f4c7dad0 Mon Sep 17 00:00:00 2001 From: ccamel Date: Mon, 23 Sep 2024 11:25:20 +0200 Subject: [PATCH 3/5] test(vm): add test for Predicate Recursion of Death scenario --- interpreter_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/interpreter_test.go b/interpreter_test.go index fc7c1fa..aeeeb54 100644 --- a/interpreter_test.go +++ b/interpreter_test.go @@ -1219,6 +1219,25 @@ func TestInterpreter_Bombing(t *testing.T) { }) }) + 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) { From f43a8cd68bc0d0a2cc724a0646fb4bed1b35cb55 Mon Sep 17 00:00:00 2001 From: ccamel Date: Mon, 23 Sep 2024 16:03:02 +0200 Subject: [PATCH 4/5] refactor(vm): add configurability to DebugHook with custom output writer --- engine/vm.go | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/engine/vm.go b/engine/vm.go index b797b5e..f7d6e51 100644 --- a/engine/vm.go +++ b/engine/vm.go @@ -1,7 +1,6 @@ package engine import ( - "bytes" "context" "fmt" "io" @@ -15,20 +14,20 @@ import ( // 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 -// DebugHook is a hook function that prints the current instruction and its operand (if any). -func DebugHook(opcode Opcode, operand Term, env *Env) error { - var buf bytes.Buffer +// 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()) - buf.WriteString(opcode.String()) + if operand != nil { + _, _ = io.WriteString(w, "(") + _ = operand.WriteTerm(w, &defaultWriteOptions, nil) + _, _ = io.WriteString(w, ")") + } + _, _ = io.WriteString(w, "\n") - if operand != nil { - buf.WriteRune('(') - _ = operand.WriteTerm(&buf, &defaultWriteOptions, nil) - buf.WriteRune(')') + return nil } - fmt.Println(buf.String()) - - return nil } // CompositeHook returns a hook function that chains multiple hooks together. From c6a535a747a615dfe3bbcb78084c3971e5cc52ba Mon Sep 17 00:00:00 2001 From: ccamel Date: Mon, 23 Sep 2024 16:36:22 +0200 Subject: [PATCH 5/5] test(vm): put DebugHook into test --- engine/vm_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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()) +}