diff --git a/x/logic/keeper/interpreter.go b/x/logic/keeper/interpreter.go index 641cbd71..37d3f4ce 100644 --- a/x/logic/keeper/interpreter.go +++ b/x/logic/keeper/interpreter.go @@ -3,10 +3,8 @@ package keeper import ( "context" "math" - "strings" "github.com/ichiban/prolog" - "github.com/ichiban/prolog/engine" "github.com/samber/lo" errorsmod "cosmossdk.io/errors" @@ -73,41 +71,7 @@ func (k Keeper) execute(ctx context.Context, program, query string) (*types.Quer // queryInterpreter executes the given query on the given interpreter and returns the answer. func (k Keeper) queryInterpreter(ctx context.Context, i *prolog.Interpreter, query string, limit sdkmath.Uint) (*types.Answer, error) { - p := engine.NewParser(&i.VM, strings.NewReader(query)) - t, err := p.Term() - if err != nil { - return nil, err - } - - var env *engine.Env - count := sdkmath.ZeroUint() - envs := make([]*engine.Env, 0, limit.Uint64()) - _, callErr := engine.Call(&i.VM, t, func(env *engine.Env) *engine.Promise { - if count.LT(limit) { - envs = append(envs, env) - } - count = count.Incr() - return engine.Bool(count.GT(limit)) - }, env).Force(ctx) - - answerErr := lo.IfF(callErr != nil, func() string { - return callErr.Error() - }).Else("") - success := len(envs) > 0 - hasMore := count.GT(limit) - vars := parsedVarsToVars(p.Vars) - results, err := envsToResults(envs, p.Vars, i) - if err != nil { - return nil, err - } - - return &types.Answer{ - Success: success, - Error: answerErr, - HasMore: hasMore, - Variables: vars, - Results: results, - }, nil + return util.QueryInterpreter(ctx, i, query, limit) } // newInterpreter creates a new interpreter properly configured. @@ -193,30 +157,3 @@ func nonNilNorZeroOrDefaultUint64(v *sdkmath.Uint, defaultValue uint64) uint64 { return v.Uint64() } - -func parsedVarsToVars(vars []engine.ParsedVariable) []string { - return lo.Map(vars, func(v engine.ParsedVariable, _ int) string { - return v.Name.String() - }) -} - -func envsToResults(envs []*engine.Env, vars []engine.ParsedVariable, i *prolog.Interpreter) ([]types.Result, error) { - results := make([]types.Result, 0, len(envs)) - for _, rEnv := range envs { - substitutions := make([]types.Substitution, 0, len(vars)) - for _, v := range vars { - var expression prolog.TermString - err := expression.Scan(&i.VM, v.Variable, rEnv) - if err != nil { - return nil, err - } - substitution := types.Substitution{ - Variable: v.Name.String(), - Expression: string(expression), - } - substitutions = append(substitutions, substitution) - } - results = append(results, types.Result{Substitutions: substitutions}) - } - return results, nil -} diff --git a/x/logic/predicate/address.go b/x/logic/predicate/address.go index 4f9a30e3..d8f7066a 100644 --- a/x/logic/predicate/address.go +++ b/x/logic/predicate/address.go @@ -35,7 +35,7 @@ import ( // [base64]: https://fr.wikipedia.org/wiki/Base64 func Bech32Address(_ *engine.VM, address, bech32 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { forwardConverter := func(value []engine.Term, _ engine.Term, env *engine.Env) ([]engine.Term, error) { - hrpTerm, dataTerm, err := prolog.AssertPair(env, value[0]) + hrpTerm, dataTerm, err := prolog.AssertPair(value[0], env) if err != nil { return nil, err } @@ -43,7 +43,7 @@ func Bech32Address(_ *engine.VM, address, bech32 engine.Term, cont engine.Cont, if err != nil { return nil, err } - hrp, err := prolog.AssertAtom(env, hrpTerm) + hrp, err := prolog.AssertAtom(hrpTerm, env) if err != nil { return nil, err } @@ -56,7 +56,7 @@ func Bech32Address(_ *engine.VM, address, bech32 engine.Term, cont engine.Cont, return []engine.Term{engine.NewAtom(b)}, nil } backwardConverter := func(value []engine.Term, _ engine.Term, env *engine.Env) ([]engine.Term, error) { - b, err := prolog.AssertAtom(env, value[0]) + b, err := prolog.AssertAtom(value[0], env) if err != nil { return nil, err } diff --git a/x/logic/predicate/address_test.go b/x/logic/predicate/address_test.go index 3d44ca47..5c624dfd 100644 --- a/x/logic/predicate/address_test.go +++ b/x/logic/predicate/address_test.go @@ -39,6 +39,12 @@ func TestBech32(t *testing.T) { }}, wantSuccess: true, }, + { + program: `okp4_addr(Addr) :- bech32_address(-('okp4', _), Addr).`, + query: `okp4_addr('okp41p8u47en82gmzfm259y6z93r9qe63l25dfwwng6').`, + wantResult: []testutil.TermResults{{}}, + wantSuccess: true, + }, { query: `bech32_address(Address, 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`, wantResult: []testutil.TermResults{{ diff --git a/x/logic/predicate/crypto.go b/x/logic/predicate/crypto.go index d907ee52..0d277fa1 100644 --- a/x/logic/predicate/crypto.go +++ b/x/logic/predicate/crypto.go @@ -167,7 +167,7 @@ func xVerify(key, data, sig, options engine.Term, defaultAlgo util.KeyAlg, if err != nil { return engine.Error(err) } - typeAtom, err := prolog.AssertAtom(env, typeTerm) + typeAtom, err := prolog.AssertAtom(typeTerm, env) if err != nil { return engine.Error(err) } @@ -211,7 +211,7 @@ func termToBytes(term, options, defaultEncoding engine.Term, env *engine.Env) ([ if err != nil { return nil, err } - encodingAtom, err := prolog.AssertAtom(env, encodingTerm) + encodingAtom, err := prolog.AssertAtom(encodingTerm, env) if err != nil { return nil, err } diff --git a/x/logic/predicate/did.go b/x/logic/predicate/did.go index 6dc95ef6..25659e85 100644 --- a/x/logic/predicate/did.go +++ b/x/logic/predicate/did.go @@ -87,7 +87,7 @@ func DIDComponents(vm *engine.VM, did, components engine.Term, cont engine.Cont, switch segment := env.Resolve(t2.Arg(i)).(type) { case engine.Variable: default: - atom, err := prolog.AssertAtom(env, segment) + atom, err := prolog.AssertAtom(segment, env) if err != nil { return engine.Error(err) } diff --git a/x/logic/predicate/did_test.go b/x/logic/predicate/did_test.go index a6f3b3a0..6f349a1c 100644 --- a/x/logic/predicate/did_test.go +++ b/x/logic/predicate/did_test.go @@ -34,6 +34,11 @@ func TestDID(t *testing.T) { query: `did_components('did:example:123456',did_components(X,Y,_,_,_)).`, wantResult: []testutil.TermResults{{"X": "example", "Y": "'123456'"}}, }, + { + program: `is_did_key(DID) :- did_components(DID, Components), Components = did_components('key',_,_,_,_).`, + query: `is_did_key('did:key:123456').`, + wantResult: []testutil.TermResults{{}}, + }, { query: `did_components('did:example:123456',did_components(X,Y,Z,_,_)).`, wantResult: []testutil.TermResults{{"X": "example", "Y": "'123456'", "Z": "_1"}}, diff --git a/x/logic/predicate/file.go b/x/logic/predicate/file.go index d7c96f44..145ff7df 100644 --- a/x/logic/predicate/file.go +++ b/x/logic/predicate/file.go @@ -150,11 +150,11 @@ func Open(vm *engine.VM, sourceSink, mode, stream, options engine.Term, k engine s := engine.NewInputTextStream(f) if prolog.IsGround(options, env) { - _, err = prolog.AssertList(env, options) + _, err = prolog.AssertList(options, env) switch { case err != nil: return engine.Error(err) - case !prolog.IsEmptyList(options): + case !prolog.IsEmptyList(options, env): return engine.Error(engine.DomainError(prolog.ValidEmptyList(), options, env)) } } diff --git a/x/logic/predicate/string.go b/x/logic/predicate/string.go index 035dab16..97f6f775 100644 --- a/x/logic/predicate/string.go +++ b/x/logic/predicate/string.go @@ -106,7 +106,7 @@ func ReadString(vm *engine.VM, stream, length, result engine.Term, cont engine.C func StringBytes( _ *engine.VM, str, bts, encodingTerm engine.Term, cont engine.Cont, env *engine.Env, ) *engine.Promise { - encoding, err := prolog.AssertAtom(env, encodingTerm) + encoding, err := prolog.AssertAtom(encodingTerm, env) if err != nil { return engine.Error(err) } diff --git a/x/logic/predicate/uri.go b/x/logic/predicate/uri.go index 68d7e0a6..e4e1848b 100644 --- a/x/logic/predicate/uri.go +++ b/x/logic/predicate/uri.go @@ -31,11 +31,11 @@ import ( // // [RFC 3986]: https://datatracker.ietf.org/doc/html/rfc3986#section-2.1 func URIEncoded(_ *engine.VM, component, decoded, encoded engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - _, err := prolog.AssertIsGround(env, component) + _, err := prolog.AssertIsGround(component, env) if err != nil { return engine.Error(err) } - uriComponent, err := prolog.AssertURIComponent(env, component) + uriComponent, err := prolog.AssertURIComponent(component, env) if err != nil { return engine.Error(err) } diff --git a/x/logic/prolog/assert.go b/x/logic/prolog/assert.go index 9b66c9c6..9fe44c78 100644 --- a/x/logic/prolog/assert.go +++ b/x/logic/prolog/assert.go @@ -30,58 +30,23 @@ func PredicateMatches(this string) func(string) bool { } // IsList returns true if the given term is a list. -func IsList(term engine.Term) bool { - switch v := term.(type) { - case engine.Compound: - return v.Functor() == AtomDot && v.Arity() == 2 - case engine.Atom: - return v == AtomEmptyList - } - - return false +func IsList(term engine.Term, env *engine.Env) bool { + _, err := AssertList(term, env) + return err == nil } // IsEmptyList returns true if the given term is an empty list. -func IsEmptyList(term engine.Term) bool { - if v, ok := term.(engine.Atom); ok { +func IsEmptyList(term engine.Term, env *engine.Env) bool { + if v, ok := env.Resolve(term).(engine.Atom); ok { return v == AtomEmptyList } return false } -// IsVariable returns true if the given term is a variable. -func IsVariable(term engine.Term) bool { - _, ok := term.(engine.Variable) - return ok -} - -// IsAtom returns true if the given term is an atom. -func IsAtom(term engine.Term) bool { - _, ok := term.(engine.Atom) - return ok -} - -// IsCompound returns true if the given term is a compound. -func IsCompound(term engine.Term) bool { - _, ok := term.(engine.Compound) - return ok -} - // IsGround returns true if the given term holds no free variables. func IsGround(term engine.Term, env *engine.Env) bool { - switch term := env.Resolve(term).(type) { - case engine.Variable: - return false - case engine.Compound: - for i := 0; i < term.Arity(); i++ { - if !IsGround(term.Arg(i), env) { - return false - } - } - return true - default: - return true - } + _, err := AssertIsGround(term, env) + return err == nil } func AreGround(terms []engine.Term, env *engine.Env) bool { @@ -92,105 +57,117 @@ func AreGround(terms []engine.Term, env *engine.Env) bool { // AssertIsGround resolves a term and returns it if it is ground. // If the term is not ground, the function returns nil and the instantiation error. -func AssertIsGround(env *engine.Env, t engine.Term) (engine.Term, error) { - if IsGround(t, env) { - return t, nil +func AssertIsGround(term engine.Term, env *engine.Env) (engine.Term, error) { + switch term := env.Resolve(term).(type) { + case engine.Variable: + return nil, engine.InstantiationError(env) + case engine.Compound: + args := make([]engine.Term, term.Arity()) + for i := 0; i < term.Arity(); i++ { + arg, err := AssertIsGround(term.Arg(i), env) + if err != nil { + return nil, err + } + args[i] = arg + } + return term.Functor().Apply(args...), nil + default: + return term, nil } - return nil, engine.InstantiationError(env) } // AssertAtom resolves a term and attempts to convert it into an engine.Atom if possible. // If conversion fails, the function returns the empty atom and the error. -func AssertAtom(env *engine.Env, t engine.Term) (engine.Atom, error) { - _, err := AssertIsGround(env, t) - if err != nil { - return AtomEmpty, err - } - if t, ok := t.(engine.Atom); ok { - return t, nil +func AssertAtom(term engine.Term, env *engine.Env) (engine.Atom, error) { + switch term := env.Resolve(term).(type) { + case engine.Atom: + return term, nil + case engine.Variable: + return AtomEmpty, engine.InstantiationError(env) + default: + return AtomEmpty, engine.TypeError(AtomTypeAtom, term, env) } - return AtomEmpty, engine.TypeError(AtomTypeAtom, t, env) } // AssertCharacterCode resolves a term and attempts to convert it into a rune if possible. // If conversion fails, the function returns the zero value and the error. -func AssertCharacterCode(env *engine.Env, t engine.Term) (rune, error) { - _, err := AssertIsGround(env, t) - if err != nil { - return 0, err - } - - if t, ok := t.(engine.Integer); ok { - if t >= 0 && t <= utf8.MaxRune { - return rune(t), nil +func AssertCharacterCode(term engine.Term, env *engine.Env) (rune, error) { + switch term := env.Resolve(term).(type) { + case engine.Integer: + if term >= 0 && term <= utf8.MaxRune { + return rune(term), nil } + case engine.Variable: + return utf8.RuneError, engine.InstantiationError(env) } - return 0, engine.TypeError(AtomTypeCharacterCode, t, env) + return utf8.RuneError, engine.TypeError(AtomTypeCharacterCode, term, env) } // AssertCharacter resolves a term and attempts to convert it into an engine.Atom if possible. // If conversion fails, the function returns the empty atom and the error. -func AssertCharacter(env *engine.Env, t engine.Term) (rune, error) { - _, err := AssertIsGround(env, t) - if err != nil { - return utf8.RuneError, err - } - if t, ok := t.(engine.Atom); ok { - runes := []rune(t.String()) +func AssertCharacter(term engine.Term, env *engine.Env) (rune, error) { + switch term := env.Resolve(term).(type) { + case engine.Atom: + runes := []rune(term.String()) if len(runes) == 1 { return runes[0], nil } + case engine.Variable: + return utf8.RuneError, engine.InstantiationError(env) } - return utf8.RuneError, engine.TypeError(AtomTypeCharacter, t, env) + + return utf8.RuneError, engine.TypeError(AtomTypeCharacter, term, env) } // AssertByte resolves a term and attempts to convert it into a byte if possible. // If conversion fails, the function returns the zero value and the error. -func AssertByte(env *engine.Env, t engine.Term) (byte, error) { - _, err := AssertIsGround(env, t) - if err != nil { - return 0, err - } - if t, ok := t.(engine.Integer); ok { - if t >= 0 && t <= 255 { - return byte(t), nil +func AssertByte(term engine.Term, env *engine.Env) (byte, error) { + switch term := env.Resolve(term).(type) { + case engine.Integer: + if term >= 0 && term <= 255 { + return byte(term), nil } + case engine.Variable: + return 0, engine.InstantiationError(env) } - return 0, engine.TypeError(AtomTypeByte, t, env) + return 0, engine.TypeError(AtomTypeByte, term, env) } // AssertList resolves a term as a list and returns it as a engine.Compound. // If conversion fails, the function returns nil and the error. -func AssertList(env *engine.Env, t engine.Term) (engine.Term, error) { - _, err := AssertIsGround(env, t) - if err != nil { - return nil, err - } - if IsList(t) { - return t, nil +func AssertList(term engine.Term, env *engine.Env) (engine.Term, error) { + switch term := env.Resolve(term).(type) { + case engine.Compound: + if term.Functor() == AtomDot && term.Arity() == 2 { + return term, nil + } + case engine.Atom: + if term == AtomEmptyList { + return term, nil + } } - return nil, engine.TypeError(AtomTypeList, t, env) + return nil, engine.TypeError(AtomTypeList, term, env) } // AssertPair resolves a term as a pair and returns the pair components. // If conversion fails, the function returns nil and the error. -func AssertPair(env *engine.Env, t engine.Term) (engine.Term, engine.Term, error) { - _, err := AssertIsGround(env, t) +func AssertPair(term engine.Term, env *engine.Env) (engine.Term, engine.Term, error) { + term, err := AssertIsGround(term, env) if err != nil { return nil, nil, err } - if t, ok := t.(engine.Compound); ok && t.Functor() == AtomPair && t.Arity() == 2 { - return t.Arg(0), t.Arg(1), nil + if term, ok := term.(engine.Compound); ok && term.Functor() == AtomPair && term.Arity() == 2 { + return term.Arg(0), term.Arg(1), nil } - return nil, nil, engine.TypeError(AtomTypePair, t, env) + return nil, nil, engine.TypeError(AtomTypePair, term, env) } // AssertURIComponent resolves a term as a URI component and returns it as an URIComponent. -func AssertURIComponent(env *engine.Env, t engine.Term) (util.URIComponent, error) { - switch v := env.Resolve(t); v { +func AssertURIComponent(term engine.Term, env *engine.Env) (util.URIComponent, error) { + switch v := env.Resolve(term); v { case AtomQueryValue: return util.QueryValueComponent, nil case AtomFragment: @@ -200,6 +177,6 @@ func AssertURIComponent(env *engine.Env, t engine.Term) (util.URIComponent, erro case AtomSegment: return util.SegmentComponent, nil default: - return 0, engine.TypeError(AtomTypeURIComponent, t, env) + return 0, engine.TypeError(AtomTypeURIComponent, term, env) } } diff --git a/x/logic/prolog/assert_test.go b/x/logic/prolog/assert_test.go index 7119e7e6..310e42fb 100644 --- a/x/logic/prolog/assert_test.go +++ b/x/logic/prolog/assert_test.go @@ -9,6 +9,7 @@ import ( . "github.com/smartystreets/goconvey/convey" + "github.com/okp4/okp4d/x/logic/testutil" "github.com/okp4/okp4d/x/logic/util" ) @@ -183,12 +184,10 @@ func TestWhitelistBlacklistMatches(t *testing.T) { } func TestAreGround(t *testing.T) { - groundTerm := func(value string) engine.Term { - return engine.NewAtom(value) - } - nonGroundTerm := func() engine.Term { - return engine.NewVariable() - } + X := engine.NewVariable() + Y := engine.NewVariable() + foo := engine.NewAtom("foo") + fortyTwo := engine.Integer(42) Convey("Given a test cases", t, func() { cases := []struct { @@ -198,12 +197,22 @@ func TestAreGround(t *testing.T) { }{ { name: "all terms are ground", - terms: []engine.Term{groundTerm("a"), groundTerm("b")}, + terms: []engine.Term{X, foo, foo.Apply(X), fortyTwo, engine.List(X, fortyTwo)}, expected: true, }, { - name: "one term is not ground", - terms: []engine.Term{groundTerm("a"), nonGroundTerm()}, + name: "one term is a variable", + terms: []engine.Term{X, foo, Y, foo.Apply(X)}, + expected: false, + }, + { + name: "one term is a list containing a variable", + terms: []engine.Term{X, foo, engine.List(X, Y, foo), fortyTwo}, + expected: false, + }, + { + name: "one term is a compound containing a variable", + terms: []engine.Term{X, foo, foo.Apply(X, foo.Apply(X, Y, fortyTwo)), fortyTwo}, expected: false, }, { @@ -219,8 +228,7 @@ func TestAreGround(t *testing.T) { } Convey("and an environment", func() { - env := engine.NewEnv() - + env, _ := engine.NewEnv().Unify(X, engine.NewAtom("x")) for nc, tc := range cases { Convey( fmt.Sprintf("Given the test case %s (#%d)", tc.name, nc), func() { @@ -236,3 +244,73 @@ func TestAreGround(t *testing.T) { }) }) } + +func TestAssertIsGround(t *testing.T) { + X := engine.NewVariable() + Y := engine.NewVariable() + foo := engine.NewAtom("foo") + fortyTwo := engine.Integer(42) + + Convey("Given a test cases", t, func() { + cases := []struct { + name string + term engine.Term + expected error + }{ + { + name: "A variable unified", + term: X, + }, + { + name: "an atom", + term: foo, + }, + { + name: "an integer", + term: fortyTwo, + }, + { + name: "a grounded list", + term: engine.List(foo, X, fortyTwo), + }, + { + name: "a grounded compound", + term: foo.Apply(X, foo.Apply(foo, X, fortyTwo)), + }, + { + name: "a variable", + term: Y, + expected: engine.InstantiationError(engine.NewEnv()), + }, + { + name: "a list containing a variable", + term: engine.List(foo, X, Y, fortyTwo), + expected: engine.InstantiationError(engine.NewEnv()), + }, + { + name: "a compound containing a variable", + term: foo.Apply(X, foo.Apply(X, Y, fortyTwo)), + expected: engine.InstantiationError(engine.NewEnv()), + }, + } + + Convey("and an environment", func() { + env, _ := engine.NewEnv().Unify(X, engine.NewAtom("x")) + for nc, tc := range cases { + Convey( + fmt.Sprintf("Given the test case %s (#%d)", tc.name, nc), func() { + Convey("When the function AreGround() is called", func() { + result, err := AssertIsGround(tc.term, env) + Convey("Then it should return the expected output", func() { + if tc.expected == nil { + So(result, testutil.ShouldBeGrounded) + } else { + So(err, ShouldBeError, tc.expected) + } + }) + }) + }) + } + }) + }) +} diff --git a/x/logic/prolog/byte.go b/x/logic/prolog/byte.go index 6e40bc1a..08f52580 100644 --- a/x/logic/prolog/byte.go +++ b/x/logic/prolog/byte.go @@ -13,7 +13,7 @@ func ByteListTermToBytes(term engine.Term, env *engine.Env) ([]byte, error) { var bs []byte for iter.Next() { - b, err := AssertByte(env, iter.Current()) + b, err := AssertByte(iter.Current(), env) if err != nil { return nil, err } diff --git a/x/logic/prolog/hex.go b/x/logic/prolog/hex.go index 263ae6ba..5f7d65eb 100644 --- a/x/logic/prolog/hex.go +++ b/x/logic/prolog/hex.go @@ -8,7 +8,7 @@ import ( // TermHexToBytes try to convert an hexadecimal encoded atom to native golang []byte. func TermHexToBytes(term engine.Term, env *engine.Env) ([]byte, error) { - v, err := AssertAtom(env, term) + v, err := AssertAtom(term, env) if err != nil { return nil, err } diff --git a/x/logic/prolog/list.go b/x/logic/prolog/list.go index 1327fcda..5691ef17 100644 --- a/x/logic/prolog/list.go +++ b/x/logic/prolog/list.go @@ -13,7 +13,7 @@ func ListHead(list engine.Term, env *engine.Env) engine.Term { // ListIterator returns a list iterator. func ListIterator(list engine.Term, env *engine.Env) (engine.ListIterator, error) { - if !IsList(env.Resolve(list)) { + if !IsList(list, env) { return engine.ListIterator{}, engine.TypeError(AtomTypeList, list, env) } return engine.ListIterator{List: list, Env: env}, nil diff --git a/x/logic/prolog/option.go b/x/logic/prolog/option.go index 9bfcaffe..83442b99 100644 --- a/x/logic/prolog/option.go +++ b/x/logic/prolog/option.go @@ -11,7 +11,7 @@ import ( // If no option is found nil is returned. func GetOption(name engine.Atom, options engine.Term, env *engine.Env) (engine.Term, error) { extractOption := func(opt engine.Term) (engine.Term, error) { - switch v := opt.(type) { + switch v := env.Resolve(opt).(type) { case engine.Compound: if v.Functor() == name { if v.Arity() != 1 { @@ -31,13 +31,8 @@ func GetOption(name engine.Atom, options engine.Term, env *engine.Env) (engine.T return nil, engine.TypeError(AtomTypeOption, opt, env) } - resolvedTerm := env.Resolve(options) - if resolvedTerm == nil { - return nil, nil - } - - if IsList(resolvedTerm) { - iter, err := ListIterator(resolvedTerm, env) + if IsList(options, env) { + iter, err := ListIterator(options, env) if err != nil { return nil, err } @@ -56,7 +51,7 @@ func GetOption(name engine.Atom, options engine.Term, env *engine.Env) (engine.T } } - return extractOption(resolvedTerm) + return extractOption(options) } // GetOptionWithDefault returns the value of the first option with the given name in the given options, or the given @@ -81,7 +76,7 @@ func GetOptionAsAtomWithDefault( if err != nil { return AtomEmpty, err } - atom, err := AssertAtom(env, term) + atom, err := AssertAtom(term, env) if err != nil { return AtomEmpty, err } diff --git a/x/logic/prolog/text.go b/x/logic/prolog/text.go index 19a3dd6c..c13d9e75 100644 --- a/x/logic/prolog/text.go +++ b/x/logic/prolog/text.go @@ -8,12 +8,12 @@ import ( ) // AtomToString try to convert a given atom to a string. -func AtomToString(atom engine.Term, env *engine.Env) (string, error) { - v, err := AssertAtom(env, atom) +func AtomToString(term engine.Term, env *engine.Env) (string, error) { + atom, err := AssertAtom(term, env) if err != nil { return "", err } - return v.String(), nil + return atom.String(), nil } // listTermToString try to convert a given list to a string using the provided @@ -22,7 +22,7 @@ func AtomToString(atom engine.Term, env *engine.Env) (string, error) { // to return a rune. func listTermToString( term engine.Term, - converter func(*engine.Env, engine.Term) (rune, error), + converter func(engine.Term, *engine.Env) (rune, error), env *engine.Env, ) (string, error) { iter, err := ListIterator(term, env) @@ -32,7 +32,7 @@ func listTermToString( var sb strings.Builder for iter.Next() { - r, err := converter(env, iter.Current()) + r, err := converter(iter.Current(), env) if err != nil { return sb.String(), err } @@ -57,8 +57,8 @@ func CharacterCodeListTermToString(term engine.Term, env *engine.Env) (string, e // It's the same as CharacterCodeListTermToString, but expects the list to contain bytes. // It's equivalent to the prolog encoding 'octet'. func OctetListTermToString(term engine.Term, env *engine.Env) (string, error) { - return listTermToString(term, func(env *engine.Env, term engine.Term) (rune, error) { - b, err := AssertByte(env, term) + return listTermToString(term, func(term engine.Term, env *engine.Env) (rune, error) { + b, err := AssertByte(term, env) if err != nil { return utf8.RuneError, err } @@ -73,7 +73,7 @@ func TextTermToString(term engine.Term, env *engine.Env) (string, error) { case engine.Atom: return AtomToString(v, env) case engine.Compound: - if IsList(v) { + if IsList(v, env) { head := ListHead(v, env) if head == nil { return "", nil @@ -93,7 +93,7 @@ func TextTermToString(term engine.Term, env *engine.Env) (string, error) { } // StringToAtom converts a string to an atom. -func StringToAtom(s string) engine.Term { +func StringToAtom(s string) engine.Atom { return engine.NewAtom(s) } diff --git a/x/logic/testutil/logic.go b/x/logic/testutil/logic.go index f83aeb01..44f1ea6c 100644 --- a/x/logic/testutil/logic.go +++ b/x/logic/testutil/logic.go @@ -89,3 +89,35 @@ func ReindexUnknownVariables(s prolog.TermString) prolog.TermString { return fmt.Sprintf("_%d", index) })) } + +// ShouldBeGrounded is a goconvey assertion that asserts that the given term does not hold any +// uninstantiated variables. +func ShouldBeGrounded(actual any, expected ...any) string { + if len(expected) != 0 { + return fmt.Sprintf("This assertion requires exactly %d comparison values (you provided %d).", 0, len(expected)) + } + + var containsVariable func(engine.Term) bool + containsVariable = func(term engine.Term) bool { + switch t := term.(type) { + case engine.Variable: + return true + case engine.Compound: + for i := 0; i < t.Arity(); i++ { + if containsVariable(t.Arg(i)) { + return true + } + } + } + return false + } + if t, ok := actual.(engine.Term); ok { + if containsVariable(t) { + return "Expected term to NOT hold a free variable (but it was)." + } + + return "" + } + + return fmt.Sprintf("The argument to this assertion must be a term (you provided %v).", actual) +} diff --git a/x/logic/util/prolog.go b/x/logic/util/prolog.go new file mode 100644 index 00000000..f77e5d93 --- /dev/null +++ b/x/logic/util/prolog.go @@ -0,0 +1,82 @@ +package util + +import ( + "context" + "strings" + + "github.com/ichiban/prolog" + "github.com/ichiban/prolog/engine" + "github.com/samber/lo" + + sdkmath "cosmossdk.io/math" + + "github.com/okp4/okp4d/x/logic/types" +) + +// QueryInterpreter interprets a query and returns the solutions up to the given limit. +func QueryInterpreter( + ctx context.Context, i *prolog.Interpreter, query string, limit sdkmath.Uint, +) (*types.Answer, error) { + p := engine.NewParser(&i.VM, strings.NewReader(query)) + t, err := p.Term() + if err != nil { + return nil, err + } + + var env *engine.Env + count := sdkmath.ZeroUint() + envs := make([]*engine.Env, 0, limit.Uint64()) + _, callErr := engine.Call(&i.VM, t, func(env *engine.Env) *engine.Promise { + if count.LT(limit) { + envs = append(envs, env) + } + count = count.Incr() + return engine.Bool(count.GT(limit)) + }, env).Force(ctx) + + answerErr := lo.IfF(callErr != nil, func() string { + return callErr.Error() + }).Else("") + success := len(envs) > 0 + hasMore := count.GT(limit) + vars := parsedVarsToVars(p.Vars) + results, err := envsToResults(envs, p.Vars, i) + if err != nil { + return nil, err + } + + return &types.Answer{ + Success: success, + Error: answerErr, + HasMore: hasMore, + Variables: vars, + Results: results, + }, nil +} + +func parsedVarsToVars(vars []engine.ParsedVariable) []string { + return lo.Map(vars, func(v engine.ParsedVariable, _ int) string { + return v.Name.String() + }) +} + +func envsToResults(envs []*engine.Env, vars []engine.ParsedVariable, i *prolog.Interpreter) ([]types.Result, error) { + results := make([]types.Result, 0, len(envs)) + for _, rEnv := range envs { + substitutions := make([]types.Substitution, 0, len(vars)) + for _, v := range vars { + var expression prolog.TermString + err := expression.Scan(&i.VM, v.Variable, rEnv) + if err != nil { + return nil, err + } + substitution := types.Substitution{ + Variable: v.Name.String(), + Expression: string(expression), + } + substitutions = append(substitutions, substitution) + } + results = append(results, types.Result{Substitutions: substitutions}) + } + return results, nil +}