Skip to content

Commit

Permalink
Merge pull request #381 from okp4/feat/read_string
Browse files Browse the repository at this point in the history
🧠 Logic: ⛓️ Implement `read_string/3` predicate
  • Loading branch information
bdeneux committed Jun 23, 2023
2 parents c5fcda1 + aee8b8b commit d931d67
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 0 deletions.
1 change: 1 addition & 0 deletions x/logic/interpreter/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ var registry = map[string]any{
"source_file/1": predicate.SourceFile,
"json_prolog/2": predicate.JSONProlog,
"uri_encoded/3": predicate.URIEncoded,
"read_string/3": predicate.ReadString,
}

// RegistryNames is the list of the predicate names in the Registry.
Expand Down
84 changes: 84 additions & 0 deletions x/logic/predicate/string.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package predicate

import (
"context"
"errors"
"fmt"
"io"
"strings"

"github.com/ichiban/prolog/engine"

"github.com/okp4/okp4d/x/logic/util"
)

// ReadString is a predicate that will read a given Stream and unify it to String.
// Optionally give a max length of reading, when stream reach the given length, the reading is stop.
// If Length is unbound, Stream is read to the end and Length is unified with the number of characters read.
//
// read_string(+Stream, ?Length, -String) is det
//
// Where
// - `Stream`: represent a stream
// - `Length`: is the max length to read
// - `String`: represent the unified read stream as string
//
// Example:
//
// # Given a file `foo.txt` that contains `Hello World`:
// ```
// file_to_string(File, String, Length) :-
//
// open(File, read, In),
// read_string(In, Length, String),
// close(Stream).
//
// ```
//
// Result :
//
// ```
//
// ?- file_to_string('path/file/foo.txt', String, Length).
//
// String = 'Hello World'
// Length = 11
//
// ```.
func ReadString(vm *engine.VM, stream, length, result engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return engine.Delay(func(ctx context.Context) *engine.Promise {
var s *engine.Stream
switch st := env.Resolve(stream).(type) {
case engine.Variable:
return engine.Error(fmt.Errorf("read_string/3: stream cannot be a variable"))
case *engine.Stream:
s = st
default:
return engine.Error(fmt.Errorf("read_string/3: invalid domain for given stream"))
}

var maxLength uint64
if maxLen, ok := env.Resolve(length).(engine.Integer); ok {
maxLength = uint64(maxLen)
}

var builder strings.Builder
var totalLen uint64
for {
r, l, err := s.ReadRune()
if err != nil || (maxLength != 0 && totalLen >= maxLength) {
if errors.Is(err, io.EOF) || totalLen >= maxLength {
break
}
return engine.Error(fmt.Errorf("read_string/3: couldn't read stream: %w", err))
}
totalLen += uint64(l)
_, err = builder.WriteRune(r)
if err != nil {
return engine.Error(fmt.Errorf("read_string/3: couldn't write string: %w", err))
}
}

return engine.Unify(vm, Tuple(result, length), Tuple(util.StringToTerm(builder.String()), engine.Integer(totalLen)), cont, env)
})
}
199 changes: 199 additions & 0 deletions x/logic/predicate/string_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//nolint:gocognit
package predicate

import (
"fmt"
"strings"
"testing"

"github.com/ichiban/prolog/engine"

. "github.com/smartystreets/goconvey/convey"

tmdb "github.com/cometbft/cometbft-db"
"github.com/cometbft/cometbft/libs/log"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"

"github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/okp4/okp4d/x/logic/testutil"
"github.com/okp4/okp4d/x/logic/types"
)

func TestReadString(t *testing.T) {
Convey("Given a test cases", t, func() {
cases := []struct {
input string
program string
query string
wantResult []types.TermResults
wantError error
wantSuccess bool
}{
{
input: "foo",
program: "read_input(String) :- current_input(Stream), read_string(Stream, _, String).",
query: `read_input(String).`,
wantResult: []types.TermResults{{
"String": "foo",
}},
wantSuccess: true,
},
{
input: "foo bar",
program: "read_input(String) :- current_input(Stream), read_string(Stream, _, String).",
query: `read_input(String).`,
wantResult: []types.TermResults{{
"String": "'foo bar'",
}},
wantSuccess: true,
},
{
input: "foo bar",
program: "read_input(String, Len) :- current_input(Stream), read_string(Stream, Len, String).",
query: `read_input(String, Len).`,
wantResult: []types.TermResults{{
"String": "'foo bar'",
"Len": "7",
}},
wantSuccess: true,
},
{
input: "foo bar",
program: "read_input(String, Len) :- current_input(Stream), read_string(Stream, Len, String).",
query: `read_input(String, 3).`,
wantResult: []types.TermResults{{
"String": "foo",
}},
wantSuccess: true,
},
{
input: "foo bar",
program: "read_input(String, Len) :- current_input(Stream), read_string(Stream, Len, String).",
query: `read_input(String, 7).`,
wantResult: []types.TermResults{{
"String": "'foo bar'",
}},
wantSuccess: true,
},
{
input: "foo bar 🧙",
program: "read_input(String, Len) :- current_input(Stream), read_string(Stream, Len, String).",
query: `read_input(String, _).`,
wantResult: []types.TermResults{{
"String": "'foo bar 🧙'",
}},
wantSuccess: true,
},
{
input: "foo bar 🧙",
program: "read_input(String, Len) :- current_input(Stream), read_string(Stream, Len, String).",
query: `read_input(String, Len).`,
wantResult: []types.TermResults{{
"String": "'foo bar 🧙'",
"Len": "12",
}},
wantSuccess: true,
},
{
input: "🧙",
program: "read_input(String, Len) :- current_input(Stream), read_string(Stream, Len, String).",
query: `read_input(String, Len).`,
wantResult: []types.TermResults{{
"String": "'🧙'",
"Len": "4",
}},
wantSuccess: true,
},
{
input: "🧙",
program: "read_input(String, Len) :- current_input(Stream), read_string(Stream, Len, String).",
query: `read_input(String, 1).`,
wantResult: []types.TermResults{{
"String": "'🧙'",
}},
wantSuccess: false,
},
{
input: "Hello World!",
program: "read_input(String, Len) :- current_input(Stream), read_string(Stream, Len, String).",
query: `read_input(String, 15).`,
wantResult: []types.TermResults{{
"String": "'Hello World!'",
}},
wantSuccess: false,
},
{
input: "Hello World!",
program: "read_input(String, Len) :- current_input(Stream), read_string(foo, Len, String).",
query: `read_input(String, Len).`,
wantError: fmt.Errorf("read_string/3: invalid domain for given stream"),
wantSuccess: false,
},
{
input: "Hello World!",
query: `read_string(Stream, Len, data).`,
wantError: fmt.Errorf("read_string/3: stream cannot be a variable"),
wantSuccess: false,
},
}
for nc, tc := range cases {
Convey(fmt.Sprintf("Given the query #%d: %s", nc, tc.query), func() {
Convey("and a context", func() {
db := tmdb.NewMemDB()
stateStore := store.NewCommitMultiStore(db)
ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger())

Convey("and a vm", func() {
interpreter := testutil.NewComprehensiveInterpreterMust(ctx)
interpreter.Register3(engine.NewAtom("read_string"), ReadString)

interpreter.SetUserInput(engine.NewInputTextStream(strings.NewReader(tc.input)))

err := interpreter.Compile(ctx, tc.program)
So(err, ShouldBeNil)

Convey("When the predicate is called", func() {
sols, err := interpreter.QueryContext(ctx, tc.query)

Convey("Then the error should be nil", func() {
So(err, ShouldBeNil)
So(sols, ShouldNotBeNil)

Convey("and the bindings should be as expected", func() {
var got []types.TermResults
for sols.Next() {
m := types.TermResults{}
err := sols.Scan(m)
So(err, ShouldBeNil)

got = append(got, m)
}
if tc.wantError != nil {
So(sols.Err(), ShouldNotBeNil)
So(sols.Err().Error(), ShouldEqual, tc.wantError.Error())
} else {
So(sols.Err(), ShouldBeNil)

if tc.wantSuccess {
So(len(got), ShouldBeGreaterThan, 0)
So(len(got), ShouldEqual, len(tc.wantResult))
for iGot, resultGot := range got {
for varGot, termGot := range resultGot {
So(testutil.ReindexUnknownVariables(termGot), ShouldEqual, tc.wantResult[iGot][varGot])
}
}
} else {
So(len(got), ShouldEqual, 0)
}
}
})
})
})
})
})
})
}
})
}
1 change: 1 addition & 0 deletions x/logic/testutil/logic.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func NewComprehensiveInterpreterMust(ctx context.Context) (i *prolog.Interpreter
i.Register1(engine.NewAtom("consult"), engine.Consult)
i.Register3(engine.NewAtom("bagof"), engine.BagOf)
i.Register1(engine.NewAtom("current_output"), engine.CurrentOutput)
i.Register1(engine.NewAtom("current_input"), engine.CurrentInput)
i.Register2(engine.NewAtom("put_char"), engine.PutChar)
i.Register3(engine.NewAtom("write_term"), engine.WriteTerm)

Expand Down

0 comments on commit d931d67

Please sign in to comment.