Skip to content

Commit

Permalink
Merge pull request #10 from axone-protocol/feat/delay-seq
Browse files Browse the repository at this point in the history
✨ add lazy sequence support for delayed promises
  • Loading branch information
ccamel authored Sep 30, 2024
2 parents 7819528 + 7eaf42d commit a7e37dc
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 32 deletions.
9 changes: 4 additions & 5 deletions engine/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import (
"bytes"
"context"
"errors"
orderedmap "github.com/wk8/go-ordered-map/v2"
"io"
"io/fs"
"os"
"sort"
"strings"
"unicode"
"unicode/utf8"

orderedmap "github.com/wk8/go-ordered-map/v2"
)

// Repeat repeats the continuation until it succeeds.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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:
Expand Down
1 change: 0 additions & 1 deletion engine/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions engine/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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()
Expand Down
80 changes: 63 additions & 17 deletions engine/promise.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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).
Expand All @@ -46,30 +61,34 @@ 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,
}
}

// 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,
}
}
Expand All @@ -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 {
Expand All @@ -106,20 +125,33 @@ 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
}

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) {
Expand All @@ -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 {
Expand Down
41 changes: 40 additions & 1 deletion engine/promise_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
})
}
4 changes: 2 additions & 2 deletions engine/term.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -23,7 +24,6 @@ type WriteOptions struct {
_ops *operators
priority Integer
visited map[termID]struct{}
prefixMinus bool
left, right operator
maxDepth Integer
}
Expand Down
5 changes: 3 additions & 2 deletions engine/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down

0 comments on commit a7e37dc

Please sign in to comment.