Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🧠 Logic: ⛓️ Implement read_string/3 predicate #381

Merged
merged 9 commits into from
Jun 23, 2023
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))

Check warning on line 73 in x/logic/predicate/string.go

View check run for this annotation

Codecov / codecov/patch

x/logic/predicate/string.go#L73

Added line #L73 was not covered by tests
}
totalLen += uint64(l)
_, err = builder.WriteRune(r)
if err != nil {
return engine.Error(fmt.Errorf("read_string/3: couldn't write string: %w", err))
}

Check warning on line 79 in x/logic/predicate/string.go

View check run for this annotation

Codecov / codecov/patch

x/logic/predicate/string.go#L78-L79

Added lines #L78 - L79 were not covered by tests
}

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
Loading