From 0e5c4f512e83783de5a2a049ee44d5a6ec406239 Mon Sep 17 00:00:00 2001 From: ccamel Date: Thu, 26 Sep 2024 20:30:05 +0200 Subject: [PATCH 1/3] style: resolve linting issues across the codebase --- engine/builtin.go | 9 ++++----- engine/lexer.go | 1 - engine/parser.go | 4 ++-- engine/term.go | 4 ++-- engine/text.go | 5 +++-- interpreter.go | 4 ++-- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/engine/builtin.go b/engine/builtin.go index 8df0193d..6653641a 100644 --- a/engine/builtin.go +++ b/engine/builtin.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "errors" - orderedmap "github.com/wk8/go-ordered-map/v2" "io" "io/fs" "os" @@ -12,6 +11,8 @@ import ( "strings" "unicode" "unicode/utf8" + + orderedmap "github.com/wk8/go-ordered-map/v2" ) // Repeat repeats the continuation until it succeeds. @@ -1268,7 +1269,7 @@ func Open(vm *VM, sourceSink, mode, stream, options Term, k Cont, env *Env) *Pro case err == nil: if s.mode == ioModeRead { s.source = f - s.initRead() + _ = s.initRead() } else { s.sink = f } @@ -2426,9 +2427,7 @@ func StreamProperty(vm *VM, stream, property Term, k Cont, env *Env) *Promise { streams := make([]*Stream, 0, len(vm.streams.elems)) switch s := env.Resolve(stream).(type) { case Variable: - for _, v := range vm.streams.elems { - streams = append(streams, v) - } + streams = append(streams, vm.streams.elems...) case *Stream: streams = append(streams, s) default: diff --git a/engine/lexer.go b/engine/lexer.go index 3bdf705c..4d9b8876 100644 --- a/engine/lexer.go +++ b/engine/lexer.go @@ -737,7 +737,6 @@ func (l *Lexer) fraction() (Token, error) { return Token{}, err case isDecimalDigitChar(r): l.backup() - break default: l.backup() if sign != 0 { diff --git a/engine/parser.go b/engine/parser.go index 1a8fd3c8..8c5714a5 100644 --- a/engine/parser.go +++ b/engine/parser.go @@ -3,13 +3,14 @@ package engine import ( "errors" "fmt" - orderedmap "github.com/wk8/go-ordered-map/v2" "io" "math/big" "reflect" "regexp" "strconv" "strings" + + orderedmap "github.com/wk8/go-ordered-map/v2" ) var ( @@ -530,7 +531,6 @@ func (p *Parser) term0(maxPriority Integer) (Term, error) { return CodeList(unDoubleQuote(t.val)), nil default: p.backup() - break } default: p.backup() diff --git a/engine/term.go b/engine/term.go index f36b0758..4f165ae5 100644 --- a/engine/term.go +++ b/engine/term.go @@ -2,9 +2,10 @@ package engine import ( "fmt" - orderedmap "github.com/wk8/go-ordered-map/v2" "io" "strings" + + orderedmap "github.com/wk8/go-ordered-map/v2" ) // Term is a prolog term. @@ -23,7 +24,6 @@ type WriteOptions struct { _ops *operators priority Integer visited map[termID]struct{} - prefixMinus bool left, right operator maxDepth Integer } diff --git a/engine/text.go b/engine/text.go index 8af1f7f9..3e48128c 100644 --- a/engine/text.go +++ b/engine/text.go @@ -3,9 +3,10 @@ package engine import ( "context" "fmt" - orderedmap "github.com/wk8/go-ordered-map/v2" "io/fs" "strings" + + orderedmap "github.com/wk8/go-ordered-map/v2" ) // discontiguousError is an error that the user-defined predicate is defined by clauses which are not consecutive read-terms. @@ -110,7 +111,7 @@ func (vm *VM) compile(ctx context.Context, text *text, s string, args ...interfa } continue case procedureIndicator{name: atomIf, arity: 2}: // Rule - pi, arg, err = piArg(arg(0), nil) + pi, _, err = piArg(arg(0), nil) if err != nil { return err } diff --git a/interpreter.go b/interpreter.go index 65ff5c40..4f7c1639 100644 --- a/interpreter.go +++ b/interpreter.go @@ -4,11 +4,12 @@ import ( "context" _ "embed" // for go:embed "errors" - "github.com/ichiban/prolog/engine" "io" "io/fs" "os" "strings" + + "github.com/ichiban/prolog/engine" ) //go:embed bootstrap.pl @@ -17,7 +18,6 @@ var bootstrap string // Interpreter is a Prolog interpreter. type Interpreter struct { engine.VM - loaded map[string]struct{} } // NewEmpty creates a new Prolog interpreter without any predicates/operators defined. From d14ae3bacee5b446bb6142fa4046cb0a4a6ebfd3 Mon Sep 17 00:00:00 2001 From: ccamel Date: Thu, 26 Sep 2024 20:32:09 +0200 Subject: [PATCH 2/3] feat(engine): add support for lazy delayed executions --- engine/promise.go | 80 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/engine/promise.go b/engine/promise.go index f3a94b55..b92774f2 100644 --- a/engine/promise.go +++ b/engine/promise.go @@ -10,10 +10,17 @@ var ( falsePromise = &Promise{ok: false} ) +// PromiseFunc defines the type of a function that returns a promise. +type PromiseFunc = func(context.Context) *Promise + +// NextFunc defines the type of a function that returns the next PromiseFunc in a sequence, +// along with a boolean indicating whether the returned value is valid. +type NextFunc = func() (PromiseFunc, bool) + // Promise is a delayed execution that results in (bool, error). The zero value for Promise is equivalent to Bool(false). type Promise struct { // delayed execution with multiple choices - delayed []func(context.Context) *Promise + delayed *NextFunc // final result ok bool @@ -26,8 +33,16 @@ type Promise struct { } // Delay delays an execution of k. -func Delay(k ...func(context.Context) *Promise) *Promise { - return &Promise{delayed: k} +// Should be used with reasonable quantity of k, otherwise prefer DelaySeq. +func Delay(k ...PromiseFunc) *Promise { + return DelaySeq(makeNextFunc(k...)) +} + +// DelaySeq delays an execution of a sequence of promises. +func DelaySeq(next NextFunc) *Promise { + return &Promise{ + delayed: &next, + } } // Bool returns a promise that simply returns (ok, nil). @@ -46,20 +61,22 @@ func Error(err error) *Promise { var dummyCutParent Promise // cut returns a promise that once the execution reaches it, it eliminates other possible choices. -func cut(parent *Promise, k func(context.Context) *Promise) *Promise { +func cut(parent *Promise, k PromiseFunc) *Promise { if parent == nil { parent = &dummyCutParent } + next := makeNextFunc(k) return &Promise{ - delayed: []func(context.Context) *Promise{k}, + delayed: &next, cutParent: parent, } } // repeat returns a promise that repeats k. -func repeat(k func(context.Context) *Promise) *Promise { +func repeat(k PromiseFunc) *Promise { + next := makeNextFunc(k) return &Promise{ - delayed: []func(context.Context) *Promise{k}, + delayed: &next, repeat: true, } } @@ -67,9 +84,11 @@ func repeat(k func(context.Context) *Promise) *Promise { // catch returns a promise with a recovering function. // Once a promise results in error, the error goes through ancestor promises looking for a recovering function that // returns a non-nil promise to continue on. -func catch(recover func(error) *Promise, k func(context.Context) *Promise) *Promise { +func catch(recover func(error) *Promise, k PromiseFunc) *Promise { + next := makeNextFunc(k) + return &Promise{ - delayed: []func(context.Context) *Promise{k}, + delayed: &next, recover: recover, } } @@ -84,7 +103,7 @@ func (p *Promise) Force(ctx context.Context) (ok bool, err error) { default: p := stack.pop() - if len(p.delayed) == 0 { + if p.delayed == nil { switch { case p.err != nil: if err := stack.recover(p.err); err != nil { @@ -106,7 +125,11 @@ func (p *Promise) Force(ctx context.Context) (ok bool, err error) { // Try the child promises from left to right. q := p.child(ctx) - stack = append(stack, p, q) + if q == nil { + stack = append(stack, p) + } else { + stack = append(stack, p, q) + } } } return false, nil @@ -114,12 +137,21 @@ func (p *Promise) Force(ctx context.Context) (ok bool, err error) { func (p *Promise) child(ctx context.Context) (promise *Promise) { defer ensurePromise(&promise) - defer func() { - if !p.repeat { - p.delayed, p.delayed[0] = p.delayed[1:], nil - } - }() - return p.delayed[0](ctx) + + promiseFn, ok := (*p.delayed)() + if !ok { + p.delayed = nil + return nil + } + + promise = promiseFn(ctx) + + if p.repeat { + nextFunc := makeNextFunc(promiseFn) + p.delayed = &nextFunc + } + + return } func ensurePromise(p **Promise) { @@ -137,6 +169,20 @@ func panicError(r interface{}) error { } } +// makeNextFunc creates a NextFunc that iterates over a list of PromiseFunc. +// It returns the next PromiseFunc in the list and a boolean indicating if a valid function was returned. +// Once all PromiseFuncs are consumed, the boolean will be false. +func makeNextFunc(k ...PromiseFunc) NextFunc { + return func() (PromiseFunc, bool) { + if len(k) == 0 { + return nil, false + } + f := k[0] + k = k[1:] + return f, true + } +} + type promiseStack []*Promise func (s *promiseStack) pop() *Promise { From 7eaf42d2f0c8a9d146e855905e3f79991ad97a34 Mon Sep 17 00:00:00 2001 From: ccamel Date: Fri, 27 Sep 2024 10:56:19 +0200 Subject: [PATCH 3/3] test(engine): put DelaySeq under test --- engine/promise_test.go | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/engine/promise_test.go b/engine/promise_test.go index 2c18bf7b..4592ef2e 100644 --- a/engine/promise_test.go +++ b/engine/promise_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestPromise_Force(t *testing.T) { +func TestPromise_ForceWithDelayedExecutions(t *testing.T) { var res []int k := Delay(func(context.Context) *Promise { res = append(res, 1) @@ -73,3 +73,42 @@ func TestPromise_Force(t *testing.T) { assert.Equal(t, 10, count) }) } + +func TestPromise_ForceWithDelayedSequenceExecutions(t *testing.T) { + var res []int + k := DelaySeq( + func() NextFunc { + i := 0 + return func() (PromiseFunc, bool) { + defer func() { i++ }() + + v := i + res = append(res, v) + return func(ctx context.Context) *Promise { + return Bool(v%((v%4)+1) == 0) + }, i < 11 + } + }()) + + t.Run("ok", func(t *testing.T) { + cases := []struct { + wantOk bool + wantRes []int + }{ + {wantOk: true, wantRes: []int{0}}, + {wantOk: true, wantRes: []int{1, 2, 3, 4}}, + {wantOk: true, wantRes: []int{5, 6}}, + {wantOk: true, wantRes: []int{7, 8}}, + {wantOk: false, wantRes: []int{9, 10, 11}}, + {wantOk: false, wantRes: nil}, + } + + for _, tc := range cases { + res = nil + ok, err := k.Force(context.Background()) + assert.NoError(t, err) + assert.Equal(t, tc.wantOk, ok) + assert.Equal(t, tc.wantRes, res) + } + }) +}