diff --git a/x/logic/interpreter/registry.go b/x/logic/interpreter/registry.go index c7dc50d5..a35c83fd 100644 --- a/x/logic/interpreter/registry.go +++ b/x/logic/interpreter/registry.go @@ -118,6 +118,7 @@ var registry = map[string]any{ "read_string/3": predicate.ReadString, "eddsa_verify/4": predicate.EDDSAVerify, "ecdsa_verify/4": predicate.ECDSAVerify, + "string_bytes/3": predicate.StringBytes, } // RegistryNames is the list of the predicate names in the Registry. diff --git a/x/logic/predicate/address.go b/x/logic/predicate/address.go index 7bc9f6fc..3318c141 100644 --- a/x/logic/predicate/address.go +++ b/x/logic/predicate/address.go @@ -1,9 +1,6 @@ package predicate import ( - "context" - "fmt" - "github.com/ichiban/prolog/engine" bech322 "github.com/cosmos/cosmos-sdk/types/bech32" @@ -21,7 +18,7 @@ import ( // bech32_address(+Address, +Bech32) // // where: -// - Address is a pair of the HRP (Human-Readable Part) which holds the address prefix and a list of integers +// - Address is a pair of the HRP (Human-Readable Part) which holds the address prefix and a list of numbers // ranging from 0 to 255 that represent the base64 encoded bech32 address string. // - Bech32 is an Atom or string representing the bech32 encoded string address // @@ -36,56 +33,41 @@ import ( // // [bech32]: https://docs.cosmos.network/main/build/spec/addresses/bech32#hrp-table // [base64]: https://fr.wikipedia.org/wiki/Base64 -func Bech32Address(vm *engine.VM, address, bech32 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - return engine.Delay(func(ctx context.Context) *engine.Promise { - switch b := env.Resolve(bech32).(type) { - case engine.Variable: - case engine.Atom: - h, a, err := bech322.DecodeAndConvert(b.String()) - if err != nil { - return engine.Error(fmt.Errorf("bech32_address/2: failed to decode Bech32: %w", err)) - } - pair := prolog.AtomPair.Apply(prolog.StringToTerm(h), prolog.BytesToCodepointListTermWithDefault(a, env)) - return engine.Unify(vm, address, pair, cont, env) - default: - return engine.Error(fmt.Errorf("bech32_address/2: invalid Bech32 type: %T, should be Atom or Variable", b)) +func Bech32Address(_ *engine.VM, address, bech32 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { + forwardConverter := func(value []engine.Term, options engine.Term, env *engine.Env) ([]engine.Term, error) { + hrpTerm, dataTerm, err := prolog.AssertPair(env, value[0]) + if err != nil { + return nil, err + } + data, err := prolog.ByteListTermToBytes(dataTerm, env) + if err != nil { + return nil, err + } + hrp, err := prolog.AssertAtom(env, hrpTerm) + if err != nil { + return nil, err } - switch addressPair := env.Resolve(address).(type) { - case engine.Compound: - bech32Decoded, err := addressPairToBech32(addressPair, env) - if err != nil { - return engine.Error(fmt.Errorf("bech32_address/2: %w", err)) - } - return engine.Unify(vm, bech32, prolog.StringToTerm(bech32Decoded), cont, env) - default: - return engine.Error(fmt.Errorf("bech32_address/2: invalid address type: %T, should be Compound (Hrp, Address)", addressPair)) + b, err := bech322.ConvertAndEncode(hrp.String(), data) + if err != nil { + return nil, prolog.WithError(engine.DomainError(prolog.ValidEncoding("bech32"), value[0], env), err, env) } - }) -} -func addressPairToBech32(addressPair engine.Compound, env *engine.Env) (string, error) { - if addressPair.Functor() != prolog.AtomPair || addressPair.Arity() != 2 { - return "", fmt.Errorf("address should be a Pair '-(Hrp, Address)'") + return []engine.Term{engine.NewAtom(b)}, nil } - - switch a := env.Resolve(addressPair.Arg(1)).(type) { - case engine.Compound: - data, err := prolog.StringTermToBytes(a, prolog.AtomEmpty, env) + backwardConverter := func(value []engine.Term, options engine.Term, env *engine.Env) ([]engine.Term, error) { + b, err := prolog.AssertAtom(env, value[0]) if err != nil { - return "", fmt.Errorf("failed to convert term to bytes list: %w", err) + return nil, err } - hrp, ok := env.Resolve(addressPair.Arg(0)).(engine.Atom) - if !ok { - return "", fmt.Errorf("HRP should be instantiated") - } - b, err := bech322.ConvertAndEncode(hrp.String(), data) + h, a, err := bech322.DecodeAndConvert(b.String()) if err != nil { - return "", fmt.Errorf("failed to convert base64 encoded address to bech32 string encoded: %w", err) + return nil, prolog.WithError(engine.DomainError(prolog.ValidEncoding("bech32"), value[0], env), err, env) } - - return b, nil - default: - return "", fmt.Errorf("address should be a Pair with a List of bytes in arity 2, given: %T", addressPair.Arg(1)) + var r engine.Term = engine.NewAtom(h) + pair := prolog.AtomPair.Apply(r, prolog.BytesToByteListTerm(a)) + return []engine.Term{pair}, nil } + return prolog.UnifyFunctionalPredicate( + []engine.Term{address}, []engine.Term{bech32}, prolog.AtomEmpty, forwardConverter, backwardConverter, cont, env) } diff --git a/x/logic/predicate/address_test.go b/x/logic/predicate/address_test.go index 2a59f576..9b6fa3c8 100644 --- a/x/logic/predicate/address_test.go +++ b/x/logic/predicate/address_test.go @@ -3,6 +3,7 @@ package predicate import ( "fmt" + "strings" "testing" "github.com/ichiban/prolog/engine" @@ -45,8 +46,8 @@ func TestBech32(t *testing.T) { wantSuccess: true, }, { - query: `bech32_address(-('okp4', [163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), foo(bar)).`, - wantError: fmt.Errorf("bech32_address/2: invalid Bech32 type: *engine.compound, should be Atom or Variable"), + query: `bech32_address(-('okp4', X), foo(bar)).`, + wantError: fmt.Errorf("error(type_error(atom,foo(bar)),bech32_address/2)"), wantSuccess: false, }, { @@ -83,33 +84,39 @@ func TestBech32(t *testing.T) { wantSuccess: true, }, { - query: `bech32_address(foo(Bar), Bech32).`, - wantError: fmt.Errorf("bech32_address/2: address should be a Pair '-(Hrp, Address)'"), + query: `bech32_address(foo(bar), Bech32).`, + wantError: fmt.Errorf("error(type_error(pair,foo(bar)),bech32_address/2)"), wantSuccess: false, }, { - query: `bech32_address(-('okp4', ['8956',167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32).`, - wantError: fmt.Errorf("bech32_address/2: failed to convert term to bytes list: error(domain_error(valid_character_code(8956),[8956,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]),bech32_address/2)"), + query: `bech32_address(-('okp4', ['163',167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32).`, + wantError: fmt.Errorf("error(type_error(byte,163),bech32_address/2)"), + wantSuccess: false, + }, + { + query: `bech32_address(-('okp4', [163,'x',23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32).`, + wantError: fmt.Errorf("error(type_error(byte,x),bech32_address/2)"), wantSuccess: false, }, { query: `bech32_address(-(Hrp, [163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32).`, - wantError: fmt.Errorf("bech32_address/2: HRP should be instantiated"), + wantError: fmt.Errorf("error(instantiation_error,bech32_address/2)"), wantSuccess: false, }, { query: `bech32_address(-('okp4', hey(2)), Bech32).`, - wantError: fmt.Errorf("bech32_address/2: failed to convert term to bytes list: error(type_error(character_code,hey(2)),bech32_address/2)"), + wantError: fmt.Errorf("error(type_error(list,hey(2)),bech32_address/2)"), wantSuccess: false, }, { - query: `bech32_address(-('okp4', 'foo'), Bech32).`, - wantError: fmt.Errorf("bech32_address/2: address should be a Pair with a List of bytes in arity 2, given: engine.Atom"), + query: `bech32_address(-('okp4', X), foo).`, + wantError: fmt.Errorf("error(domain_error(encoding(bech32),foo),[%s],bech32_address/2)", + strings.Join(strings.Split("decoding bech32 failed: invalid bech32 string length 3", ""), ",")), wantSuccess: false, }, { query: `bech32_address(Address, Bech32).`, - wantError: fmt.Errorf("bech32_address/2: invalid address type: engine.Variable, should be Compound (Hrp, Address)"), + wantError: fmt.Errorf("error(instantiation_error,bech32_address/2)"), wantSuccess: false, }, } @@ -125,13 +132,13 @@ func TestBech32(t *testing.T) { interpreter.Register2(engine.NewAtom("bech32_address"), Bech32Address) err := interpreter.Compile(ctx, tc.program) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) 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(err, ShouldEqual, nil) So(sols, ShouldNotBeNil) Convey("and the bindings should be as expected", func() { @@ -139,15 +146,15 @@ func TestBech32(t *testing.T) { for sols.Next() { m := types.TermResults{} err := sols.Scan(m) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) got = append(got, m) } if tc.wantError != nil { - So(sols.Err(), ShouldNotBeNil) + So(sols.Err(), ShouldNotEqual, nil) So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) } else { - So(sols.Err(), ShouldBeNil) + So(sols.Err(), ShouldEqual, nil) if tc.wantSuccess { So(len(got), ShouldBeGreaterThan, 0) diff --git a/x/logic/predicate/bank.go b/x/logic/predicate/bank.go index 06ebf6f9..8cccfc33 100644 --- a/x/logic/predicate/bank.go +++ b/x/logic/predicate/bank.go @@ -2,7 +2,6 @@ package predicate import ( "context" - "fmt" "github.com/ichiban/prolog/engine" @@ -10,7 +9,6 @@ import ( "github.com/okp4/okp4d/x/logic/prolog" "github.com/okp4/okp4d/x/logic/types" - "github.com/okp4/okp4d/x/logic/util" ) // BankBalances is a predicate which unifies the given terms with the list of balances (coins) of the given account. @@ -34,7 +32,7 @@ import ( // # Query the first balance of the given account by unifying the denomination and amount with the given terms. // - bank_balances('okp41ffd5wx65l407yvm478cxzlgygw07h79sq0m3fm', [-(D, A), _]). func BankBalances(vm *engine.VM, account, balances engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - return fetchBalances("bank_balances/2", + return fetchBalances( account, balances, vm, @@ -69,7 +67,7 @@ func BankBalances(vm *engine.VM, account, balances engine.Term, cont engine.Cont // # Query the first spendable balances of the given account by unifying the denomination and amount with the given terms. // - bank_spendable_balances('okp41ffd5wx65l407yvm478cxzlgygw07h79sq0m3fm', [-(D, A), _]). func BankSpendableBalances(vm *engine.VM, account, balances engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - return fetchBalances("bank_spendable_balances/2", + return fetchBalances( account, balances, vm, @@ -101,7 +99,7 @@ func BankSpendableBalances(vm *engine.VM, account, balances engine.Term, cont en // # Query the first locked balances of the given account by unifying the denomination and amount with the given terms. // - bank_locked_balances('okp41ffd5wx65l407yvm478cxzlgygw07h79sq0m3fm', [-(D, A), _]). func BankLockedBalances(vm *engine.VM, account, balances engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - return fetchBalances("bank_locked_balances/2", + return fetchBalances( account, balances, vm, @@ -116,15 +114,18 @@ func getBech32(env *engine.Env, account engine.Term) (sdk.AccAddress, error) { switch acc := env.Resolve(account).(type) { case engine.Variable: case engine.Atom: - return sdk.AccAddressFromBech32(acc.String()) + addr, err := sdk.AccAddressFromBech32(acc.String()) + if err != nil { + return nil, prolog.WithError(prolog.ResourceError(prolog.ResourceModule("bank"), env), err, env) + } + return addr, nil default: - return nil, fmt.Errorf("cannot unify account address with %T", acc) + return nil, engine.TypeError(prolog.AtomTypeAtom, account, env) } return sdk.AccAddress(nil), nil } func fetchBalances( - predicate string, account, balances engine.Term, vm *engine.VM, env *engine.Env, @@ -132,7 +133,7 @@ func fetchBalances( coinsFn func(ctx sdk.Context, bankKeeper types.BankKeeper, coins sdk.Coins, address sdk.AccAddress) sdk.Coins, ) *engine.Promise { return engine.Delay(func(ctx context.Context) *engine.Promise { - sdkContext, err := util.UnwrapSDKContext(ctx) + sdkContext, err := prolog.UnwrapSDKContext(ctx, env) if err != nil { return engine.Error(err) } @@ -140,7 +141,7 @@ func fetchBalances( bech32Addr, err := getBech32(env, account) if err != nil { - return engine.Error(fmt.Errorf("%s: %w", predicate, err)) + return engine.Error(err) } if bech32Addr != nil { @@ -154,7 +155,7 @@ func fetchBalances( address := balance.Address bech32Addr, err = sdk.AccAddressFromBech32(address) if err != nil { - return engine.Error(fmt.Errorf("%s: %w", predicate, err)) + return engine.Error(prolog.WithError(prolog.ResourceError(prolog.ResourceModule("bank"), env), err, env)) } coins := coinsFn(sdkContext, bankKeeper, balance.Coins, bech32Addr) diff --git a/x/logic/predicate/bank_test.go b/x/logic/predicate/bank_test.go index 7b87f96e..805d6be7 100644 --- a/x/logic/predicate/bank_test.go +++ b/x/logic/predicate/bank_test.go @@ -3,6 +3,7 @@ package predicate import ( "fmt" + "strings" "testing" "github.com/golang/mock/gomock" @@ -144,7 +145,8 @@ func TestBank(t *testing.T) { balances: []bank.Balance{}, query: `bank_balances('foo', X).`, wantResult: []types.TermResults{{"X": "[uknow-100]"}}, - wantError: fmt.Errorf("bank_balances/2: decoding bech32 failed: invalid bech32 string length 3"), + wantError: fmt.Errorf("error(resource_error(resource_module(bank)),[%s],unknown)", + strings.Join(strings.Split("decoding bech32 failed: invalid bech32 string length 3", ""), ",")), }, { balances: []bank.Balance{ @@ -271,7 +273,8 @@ func TestBank(t *testing.T) { spendableCoins: []bank.Balance{}, query: `bank_spendable_balances('foo', X).`, wantResult: []types.TermResults{{"X": "[uknow-100]"}}, - wantError: fmt.Errorf("bank_spendable_balances/2: decoding bech32 failed: invalid bech32 string length 3"), + wantError: fmt.Errorf("error(resource_error(resource_module(bank)),[%s],unknown)", + strings.Join(strings.Split("decoding bech32 failed: invalid bech32 string length 3", ""), ",")), }, { @@ -415,7 +418,8 @@ func TestBank(t *testing.T) { lockedCoins: []bank.Balance{}, query: `bank_locked_balances('foo', X).`, wantResult: []types.TermResults{{"X": "[uknow-100]"}}, - wantError: fmt.Errorf("bank_locked_balances/2: decoding bech32 failed: invalid bech32 string length 3"), + wantError: fmt.Errorf("error(resource_error(resource_module(bank)),[%s],unknown)", + strings.Join(strings.Split("decoding bech32 failed: invalid bech32 string length 3", ""), ",")), }, } for nc, tc := range cases { @@ -464,13 +468,13 @@ func TestBank(t *testing.T) { interpreter.Register2(engine.NewAtom("bank_locked_balances"), BankLockedBalances) err := interpreter.Compile(ctx, tc.program) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) 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(err, ShouldEqual, nil) So(sols, ShouldNotBeNil) Convey("and the bindings should be as expected", func() { @@ -478,15 +482,15 @@ func TestBank(t *testing.T) { for sols.Next() { m := types.TermResults{} err := sols.Scan(m) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) got = append(got, m) } if tc.wantError != nil { - So(sols.Err(), ShouldNotBeNil) + So(sols.Err(), ShouldNotEqual, nil) So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) } else { - So(sols.Err(), ShouldBeNil) + So(sols.Err(), ShouldEqual, nil) So(got, ShouldResemble, tc.wantResult) } }) diff --git a/x/logic/predicate/block.go b/x/logic/predicate/block.go index d51d9c31..c45a1add 100644 --- a/x/logic/predicate/block.go +++ b/x/logic/predicate/block.go @@ -2,11 +2,10 @@ package predicate import ( "context" - "fmt" "github.com/ichiban/prolog/engine" - "github.com/okp4/okp4d/x/logic/util" + "github.com/okp4/okp4d/x/logic/prolog" ) // BlockHeight is a predicate which unifies the given term with the current block height. @@ -25,9 +24,9 @@ import ( // - block_height(Height). func BlockHeight(vm *engine.VM, height engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { return engine.Delay(func(ctx context.Context) *engine.Promise { - sdkContext, err := util.UnwrapSDKContext(ctx) + sdkContext, err := prolog.UnwrapSDKContext(ctx, env) if err != nil { - return engine.Error(fmt.Errorf("block_height/1: %w", err)) + return engine.Error(err) } return engine.Unify(vm, height, engine.Integer(sdkContext.BlockHeight()), cont, env) @@ -49,9 +48,9 @@ func BlockHeight(vm *engine.VM, height engine.Term, cont engine.Cont, env *engin // - block_time(Time). func BlockTime(vm *engine.VM, time engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { return engine.Delay(func(ctx context.Context) *engine.Promise { - sdkContext, err := util.UnwrapSDKContext(ctx) + sdkContext, err := prolog.UnwrapSDKContext(ctx, env) if err != nil { - return engine.Error(fmt.Errorf("block_time/1: %w", err)) + return engine.Error(err) } return engine.Unify(vm, time, engine.Integer(sdkContext.BlockTime().Unix()), cont, env) diff --git a/x/logic/predicate/chain.go b/x/logic/predicate/chain.go index d13bf2e5..494d2067 100644 --- a/x/logic/predicate/chain.go +++ b/x/logic/predicate/chain.go @@ -2,11 +2,10 @@ package predicate import ( "context" - "fmt" "github.com/ichiban/prolog/engine" - "github.com/okp4/okp4d/x/logic/util" + "github.com/okp4/okp4d/x/logic/prolog" ) // ChainID is a predicate which unifies the given term with the current chain ID. The signature is: @@ -24,9 +23,9 @@ import ( // - chain_id(ID). func ChainID(vm *engine.VM, chainID engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { return engine.Delay(func(ctx context.Context) *engine.Promise { - sdkContext, err := util.UnwrapSDKContext(ctx) + sdkContext, err := prolog.UnwrapSDKContext(ctx, env) if err != nil { - return engine.Error(fmt.Errorf("chain_id/1: %w", err)) + return engine.Error(err) } return engine.Unify(vm, chainID, engine.NewAtom(sdkContext.ChainID()), cont, env) diff --git a/x/logic/predicate/crypto.go b/x/logic/predicate/crypto.go index 846000f2..5ad3b4b6 100644 --- a/x/logic/predicate/crypto.go +++ b/x/logic/predicate/crypto.go @@ -2,9 +2,8 @@ package predicate import ( "context" - "fmt" + "errors" "slices" - "strings" "github.com/ichiban/prolog/engine" @@ -29,6 +28,7 @@ import ( // For Format, the supported encodings are: // // - utf8 (default), the UTF-8 encoding represented as an atom. +// - text, the plain text encoding represented as an atom. // - hex, the hexadecimal encoding represented as an atom. // - octet, the raw byte encoding depicted as a list of integers ranging from 0 to 255. // @@ -54,32 +54,28 @@ import ( func CryptoDataHash( vm *engine.VM, data, hash, options engine.Term, cont engine.Cont, env *engine.Env, ) *engine.Promise { - functor := "crypto_data_hash/3" algorithmOpt := engine.NewAtom("algorithm") return engine.Delay(func(ctx context.Context) *engine.Promise { algorithmAtom, err := prolog.GetOptionAsAtomWithDefault(algorithmOpt, options, engine.NewAtom("sha256"), env) if err != nil { - return engine.Error(fmt.Errorf("%s: %w", functor, err)) + return engine.Error(err) } algorithm, err := util.ParseHashAlg(algorithmAtom.String()) if err != nil { - return engine.Error(fmt.Errorf("%s: invalid algorithm: %s. Possible values: %s", - functor, - algorithmAtom.String(), - util.HashAlgNames())) + return engine.Error(engine.TypeError(prolog.AtomTypeHashAlgorithm, algorithmAtom, env)) } decodedData, err := termToBytes(data, options, prolog.AtomUtf8, env) if err != nil { - return engine.Error(fmt.Errorf("%s: failed to decode data: %w", functor, err)) + return engine.Error(err) } result, err := util.Hash(algorithm, decodedData) if err != nil { - return engine.Error(fmt.Errorf("%s: failed to hash data: %w", functor, err)) + return engine.Error(prolog.SyntaxError(err, env)) } - return engine.Unify(vm, hash, prolog.BytesToCodepointListTermWithDefault(result, env), cont, env) + return engine.Unify(vm, hash, prolog.BytesToByteListTerm(result), cont, env) }) } @@ -104,6 +100,8 @@ func CryptoDataHash( // // - hex (default), the hexadecimal encoding represented as an atom. // - octet, the plain byte encoding depicted as a list of integers ranging from 0 to 255. +// - text, the plain text encoding represented as an atom. +// - utf8 (default), the UTF-8 encoding represented as an atom. // // For Alg, the supported algorithms are: // @@ -117,7 +115,7 @@ func CryptoDataHash( // # Verify a signature for binary data. // - eddsa_verify([127, ...], [56, 90, ..], [23, 56, ...], [encoding(octet), type(ed25519)]) func EDDSAVerify(_ *engine.VM, key, data, sig, options engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - return xVerify("eddsa_verify/4", key, data, sig, options, util.Ed25519, []util.KeyAlg{util.Ed25519}, cont, env) + return xVerify(key, data, sig, options, util.Ed25519, []util.KeyAlg{util.Ed25519}, cont, env) } // ECDSAVerify determines if a given signature is valid as per the ECDSA algorithm for the provided data, using the @@ -143,6 +141,8 @@ func EDDSAVerify(_ *engine.VM, key, data, sig, options engine.Term, cont engine. // // - hex (default), the hexadecimal encoding represented as an atom. // - octet, the plain byte encoding depicted as a list of integers ranging from 0 to 255. +// - text, the plain text encoding represented as an atom. +// - utf8 (default), the UTF-8 encoding represented as an atom. // // For Alg, the supported algorithms are: // @@ -157,59 +157,54 @@ func EDDSAVerify(_ *engine.VM, key, data, sig, options engine.Term, cont engine. // # Verify a signature for binary data using the ECDSA secp256k1 algorithm. // - ecdsa_verify([127, ...], [56, 90, ..], [23, 56, ...], [encoding(octet), type(secp256k1)]) func ECDSAVerify(_ *engine.VM, key, data, sig, options engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - return xVerify("ecdsa_verify/4", key, data, sig, options, util.Secp256r1, []util.KeyAlg{util.Secp256r1, util.Secp256k1}, cont, env) + return xVerify(key, data, sig, options, util.Secp256r1, []util.KeyAlg{util.Secp256r1, util.Secp256k1}, cont, env) } // xVerify return `true` if the Signature can be verified as the signature for Data, using the given PubKey for a // considered algorithm. // This is a generic predicate implementation that can be used to verify any signature. -func xVerify(functor string, key, data, sig, options engine.Term, defaultAlgo util.KeyAlg, +func xVerify(key, data, sig, options engine.Term, defaultAlgo util.KeyAlg, algos []util.KeyAlg, cont engine.Cont, env *engine.Env, ) *engine.Promise { typeOpt := engine.NewAtom("type") - return engine.Delay(func(ctx context.Context) *engine.Promise { - typeTerm, err := prolog.GetOptionWithDefault(typeOpt, options, engine.NewAtom(defaultAlgo.String()), env) - if err != nil { - return engine.Error(fmt.Errorf("%s: %w", functor, err)) - } - typeAtom, err := prolog.AssertAtom(env, typeTerm) - if err != nil { - return engine.Error(fmt.Errorf("%s: %w", functor, err)) - } + typeTerm, err := prolog.GetOptionWithDefault(typeOpt, options, engine.NewAtom(defaultAlgo.String()), env) + if err != nil { + return engine.Error(err) + } + typeAtom, err := prolog.AssertAtom(env, typeTerm) + if err != nil { + return engine.Error(err) + } - if idx := slices.IndexFunc(algos, func(a util.KeyAlg) bool { return a.String() == typeAtom.String() }); idx == -1 { - return engine.Error(fmt.Errorf("%s: invalid type: %s. Possible values: %s", - functor, - typeAtom.String(), - strings.Join(util.Map(algos, func(a util.KeyAlg) string { return a.String() }), ", "))) - } + if idx := slices.IndexFunc(algos, func(a util.KeyAlg) bool { return a.String() == typeAtom.String() }); idx == -1 { + return engine.Error(engine.TypeError(prolog.AtomTypeCryptographicAlgorithm, typeTerm, env)) + } - decodedKey, err := termToBytes(key, prolog.AtomEncoding.Apply(prolog.AtomOctet), prolog.AtomHex, env) - if err != nil { - return engine.Error(fmt.Errorf("%s: failed to decode public key: %w", functor, err)) - } + decodedKey, err := termToBytes(key, prolog.AtomEncoding.Apply(prolog.AtomOctet), prolog.AtomHex, env) + if err != nil { + return engine.Error(err) + } - decodedData, err := termToBytes(data, options, prolog.AtomHex, env) - if err != nil { - return engine.Error(fmt.Errorf("%s: failed to decode data: %w", functor, err)) - } + decodedData, err := termToBytes(data, options, prolog.AtomHex, env) + if err != nil { + return engine.Error(err) + } - decodedSignature, err := termToBytes(sig, prolog.AtomEncoding.Apply(prolog.AtomOctet), prolog.AtomHex, env) - if err != nil { - return engine.Error(fmt.Errorf("%s: failed to decode signature: %w", functor, err)) - } + decodedSignature, err := termToBytes(sig, prolog.AtomEncoding.Apply(prolog.AtomOctet), prolog.AtomHex, env) + if err != nil { + return engine.Error(err) + } - r, err := util.VerifySignature(util.KeyAlg(typeAtom.String()), decodedKey, decodedData, decodedSignature) - if err != nil { - return engine.Error(fmt.Errorf("%s: failed to verify signature: %w", functor, err)) - } + r, err := util.VerifySignature(util.KeyAlg(typeAtom.String()), decodedKey, decodedData, decodedSignature) + if err != nil { + return engine.Error(prolog.SyntaxError(err, env)) + } - if !r { - return engine.Bool(false) - } + if !r { + return engine.Bool(false) + } - return cont(env) - }) + return cont(env) } func termToBytes(term, options, defaultEncoding engine.Term, env *engine.Env) ([]byte, error) { @@ -225,9 +220,25 @@ func termToBytes(term, options, defaultEncoding engine.Term, env *engine.Env) ([ switch encodingAtom { case prolog.AtomHex: return prolog.TermHexToBytes(term, env) - case prolog.AtomOctet, prolog.AtomUtf8: - return prolog.StringTermToBytes(term, prolog.AtomEmpty, env) + case prolog.AtomOctet: + return prolog.ByteListTermToBytes(term, env) + case prolog.AtomUtf8, prolog.AtomText: + str, err := prolog.TextTermToString(term, env) + if err != nil { + return nil, err + } + bs, err := util.Encode(str, encodingAtom.String()) + if err != nil { + switch { + case errors.Is(err, util.ErrInvalidCharset): + return nil, engine.TypeError(prolog.AtomTypeCharset, encodingTerm, env) + default: + return nil, prolog.WithError( + engine.DomainError(prolog.ValidEncoding(encodingAtom.String()), term, env), err, env) + } + } + return bs, nil default: - return nil, fmt.Errorf("invalid encoding: %s. Possible values: hex, octet", encodingAtom.String()) + return nil, engine.DomainError(prolog.ValidEncoding(encodingAtom.String()), encodingTerm, env) } } diff --git a/x/logic/predicate/crypto_test.go b/x/logic/predicate/crypto_test.go index 89a45624..7367b0c7 100644 --- a/x/logic/predicate/crypto_test.go +++ b/x/logic/predicate/crypto_test.go @@ -3,6 +3,7 @@ package predicate import ( "fmt" + "strings" "testing" "github.com/ichiban/prolog/engine" @@ -77,7 +78,7 @@ func TestCryptoOperations(t *testing.T) { }, { query: ` crypto_data_hash('hello world', Hash, [algorithm(cheh)]).`, - wantError: fmt.Errorf("crypto_data_hash/3: invalid algorithm: cheh. Possible values: [md5 sha256 sha512]"), + wantError: fmt.Errorf("error(type_error(hash_algorithm,cheh),crypto_data_hash/3)"), wantSuccess: false, }, { @@ -110,13 +111,13 @@ func TestCryptoOperations(t *testing.T) { interpreter.Register2(engine.NewAtom("hex_bytes"), HexBytes) err := interpreter.Compile(ctx, tc.program) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) 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(err, ShouldEqual, nil) So(sols, ShouldNotBeNil) Convey("and the bindings should be as expected", func() { @@ -124,15 +125,15 @@ func TestCryptoOperations(t *testing.T) { for sols.Next() { m := types.TermResults{} err := sols.Scan(m) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) got = append(got, m) } if tc.wantError != nil { - So(sols.Err(), ShouldNotBeNil) + So(sols.Err(), ShouldNotEqual, nil) So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) } else { - So(sols.Err(), ShouldBeNil) + So(sols.Err(), ShouldEqual, nil) if tc.wantSuccess { So(len(got), ShouldBeGreaterThan, 0) @@ -178,90 +179,92 @@ func TestXVerify(t *testing.T) { }, { // All good with hex encoding program: `verify :- - hex_bytes('53167ac3fc4b720daa45b04fc73fe752578fa23a10048422d6904b7f4f7bba5a', PubKey), - hex_bytes('889bcfd331e8e43b5ebf430301dffb6ac9e2fce69f6227b43552fe3dc8cc1ee00c1cc53452a8712e9d5f80086dff8cf4999c1b93ed6c6e403c09334cb61ddd0b', Sig), - eddsa_verify(PubKey, '9b038f8ef6918cbb56040dfda401b56bb1ce79c472e7736e8677758c83367a9d', Sig, encoding(hex)).`, + hex_bytes('53167ac3fc4b720daa45b04fc73fe752578fa23a10048422d6904b7f4f7bba5a', PubKey), + hex_bytes('889bcfd331e8e43b5ebf430301dffb6ac9e2fce69f6227b43552fe3dc8cc1ee00c1cc53452a8712e9d5f80086dff8cf4999c1b93ed6c6e403c09334cb61ddd0b', Sig), + eddsa_verify(PubKey, '9b038f8ef6918cbb56040dfda401b56bb1ce79c472e7736e8677758c83367a9d', Sig, encoding(hex)).`, query: `verify.`, wantResult: []types.TermResults{{}}, wantSuccess: true, }, { // Wrong Msg program: `verify :- - hex_bytes('53167ac3fc4b720daa45b04fc73fe752578fa23a10048422d6904b7f4f7bba5a', PubKey), - hex_bytes('9b038f8ef6918cbb56040dfda401b56bb1ce79c472e7736e8677758c83367a9e', Msg), - hex_bytes('889bcfd331e8e43b5ebf430301dffb6ac9e2fce69f6227b43552fe3dc8cc1ee00c1cc53452a8712e9d5f80086dff8cf4999c1b93ed6c6e403c09334cb61ddd0b', Sig), - eddsa_verify(PubKey, Msg, Sig, encoding(octet)).`, + hex_bytes('53167ac3fc4b720daa45b04fc73fe752578fa23a10048422d6904b7f4f7bba5a', PubKey), + hex_bytes('9b038f8ef6918cbb56040dfda401b56bb1ce79c472e7736e8677758c83367a9e', Msg), + hex_bytes('889bcfd331e8e43b5ebf430301dffb6ac9e2fce69f6227b43552fe3dc8cc1ee00c1cc53452a8712e9d5f80086dff8cf4999c1b93ed6c6e403c09334cb61ddd0b', Sig), + eddsa_verify(PubKey, Msg, Sig, encoding(octet)).`, query: `verify.`, wantSuccess: false, }, { // Wrong public key program: `verify :- - hex_bytes('53167ac3fc4b720daa45b04fc73fe752578fa23a10048422d6904b7f4f7bba5b5b', PubKey), - hex_bytes('9b038f8ef6918cbb56040dfda401b56bb1ce79c472e7736e8677758c83367a9d', Msg), - hex_bytes('889bcfd331e8e43b5ebf430301dffb6ac9e2fce69f6227b43552fe3dc8cc1ee00c1cc53452a8712e9d5f80086dff8cf4999c1b93ed6c6e403c09334cb61ddd0b', Sig), - eddsa_verify(PubKey, Msg, Sig, encoding(octet)).`, + hex_bytes('53167ac3fc4b720daa45b04fc73fe752578fa23a10048422d6904b7f4f7bba5b5b', PubKey), + hex_bytes('9b038f8ef6918cbb56040dfda401b56bb1ce79c472e7736e8677758c83367a9d', Msg), + hex_bytes('889bcfd331e8e43b5ebf430301dffb6ac9e2fce69f6227b43552fe3dc8cc1ee00c1cc53452a8712e9d5f80086dff8cf4999c1b93ed6c6e403c09334cb61ddd0b', Sig), + eddsa_verify(PubKey, Msg, Sig, encoding(octet)).`, query: `verify.`, wantSuccess: false, - wantError: fmt.Errorf("eddsa_verify/4: failed to verify signature: ed25519: bad public key length: 33"), + wantError: fmt.Errorf("error(syntax_error([%s]),unknown)", + strings.Join(strings.Split("ed25519: bad public key length: 33", ""), ",")), }, { // Wrong signature program: `verify :- - hex_bytes('53167ac3fc4b720daa45b04fc73fe752578fa23a10048422d6904b7f4f7bba5a', PubKey), - hex_bytes('9b038f8ef6918cbb56040dfda401b56bb1ce79c472e7736e8677758c83367a9d', Msg), - hex_bytes('889bcfd331e8e43b5ebf430301dffb6ac9e2fce69f6227b43552fe3dc8cc1ee00c1cc53452a8712e9d5f80086dff', Sig), - eddsa_verify(PubKey, Msg, Sig, encoding(octet)).`, + hex_bytes('53167ac3fc4b720daa45b04fc73fe752578fa23a10048422d6904b7f4f7bba5a', PubKey), + hex_bytes('9b038f8ef6918cbb56040dfda401b56bb1ce79c472e7736e8677758c83367a9d', Msg), + hex_bytes('889bcfd331e8e43b5ebf430301dffb6ac9e2fce69f6227b43552fe3dc8cc1ee00c1cc53452a8712e9d5f80086dff', Sig), + eddsa_verify(PubKey, Msg, Sig, encoding(octet)).`, query: `verify.`, wantSuccess: false, }, { // Unsupported algo program: `verify :- - hex_bytes('53167ac3fc4b720daa45b04fc73fe752578fa23a10048422d6904b7f4f7bba5a', PubKey), - hex_bytes('9b038f8ef6918cbb56040dfda401b56bb1ce79c472e7736e8677758c83367a9d', Msg), - hex_bytes('889bcfd331e8e43b5ebf430301dffb6ac9e2fce69f6227b43552fe3dc8cc1ee00c1cc53452a8712e9d5f80086dff8cf4999c1b93ed6c6e403c09334cb61ddd0b', Sig), - eddsa_verify(PubKey, Msg, Sig, [encoding(octet), type(foo)]).`, + hex_bytes('53167ac3fc4b720daa45b04fc73fe752578fa23a10048422d6904b7f4f7bba5a', PubKey), + hex_bytes('9b038f8ef6918cbb56040dfda401b56bb1ce79c472e7736e8677758c83367a9d', Msg), + hex_bytes('889bcfd331e8e43b5ebf430301dffb6ac9e2fce69f6227b43552fe3dc8cc1ee00c1cc53452a8712e9d5f80086dff8cf4999c1b93ed6c6e403c09334cb61ddd0b', Sig), + eddsa_verify(PubKey, Msg, Sig, [encoding(octet), type(foo)]).`, query: `verify.`, wantSuccess: false, - wantError: fmt.Errorf("eddsa_verify/4: invalid type: foo. Possible values: ed25519"), + wantError: fmt.Errorf("error(type_error(cryptographic_algorithm,foo),eddsa_verify/4)"), }, // ECDSA - secp256r1 { // All good program: `verify :- - hex_bytes('0213c8426be471e55506f7ce4f7df557a42e310df09f92eb732ca3085e797cef9b', PubKey), - hex_bytes('e50c26e89f734b2ee12041ff27874c901891f74a0f0cf470333312a3034ce3be', Msg), - hex_bytes('30450220099e6f9dd218e0e304efa7a4224b0058a8e3aec73367ec239bee4ed8ed7d85db022100b504d3d0d2e879b04705c0e5a2b40b0521a5ab647ea207bd81134e1a4eb79e47', Sig), - ecdsa_verify(PubKey, Msg, Sig, [encoding(octet), type(secp256r1)]).`, + hex_bytes('0213c8426be471e55506f7ce4f7df557a42e310df09f92eb732ca3085e797cef9b', PubKey), + hex_bytes('e50c26e89f734b2ee12041ff27874c901891f74a0f0cf470333312a3034ce3be', Msg), + hex_bytes('30450220099e6f9dd218e0e304efa7a4224b0058a8e3aec73367ec239bee4ed8ed7d85db022100b504d3d0d2e879b04705c0e5a2b40b0521a5ab647ea207bd81134e1a4eb79e47', Sig), + ecdsa_verify(PubKey, Msg, Sig, [encoding(octet), type(secp256r1)]).`, query: `verify.`, wantResult: []types.TermResults{{}}, wantSuccess: true, }, { // Invalid secp signature program: `verify :- - hex_bytes('0213c8426be471e55506f7ce4f7df557', PubKey), - hex_bytes('9b038f8ef6918cbb56040dfda401b56bb1ce79c472e7736e8677758c83367a9d', Msg), - hex_bytes('889bcfd331e8e43b5ebf430301dffb6ac9e2fce69f6227b43552fe3dc8cc1ee00c1cc53452a8712e9d5f80086dff8cf4999c1b93ed6c6e403c09334cb61ddd0b', Sig), - ecdsa_verify(PubKey, Msg, Sig, encoding(octet)).`, + hex_bytes('0213c8426be471e55506f7ce4f7df557', PubKey), + hex_bytes('9b038f8ef6918cbb56040dfda401b56bb1ce79c472e7736e8677758c83367a9d', Msg), + hex_bytes('889bcfd331e8e43b5ebf430301dffb6ac9e2fce69f6227b43552fe3dc8cc1ee00c1cc53452a8712e9d5f80086dff8cf4999c1b93ed6c6e403c09334cb61ddd0b', Sig), + ecdsa_verify(PubKey, Msg, Sig, encoding(octet)).`, query: `verify.`, wantSuccess: false, - wantError: fmt.Errorf("ecdsa_verify/4: failed to verify signature: failed to parse compressed public key (first 10 bytes): 0213c8426be471e55506"), + wantError: fmt.Errorf("error(syntax_error([%s]),unknown)", + strings.Join(strings.Split("failed to parse compressed public key (first 10 bytes): 0213c8426be471e55506", ""), ",")), }, { // Unsupported algo program: `verify :- - hex_bytes('0213c8426be471e55506f7ce4f7df557a42e310df09f92eb732ca3085e797cef9b', PubKey), - hex_bytes('9b038f8ef6918cbb56040dfda401b56bb1ce79c472e7736e8677758c83367a9d', Msg), - hex_bytes('889bcfd331e8e43b5ebf430301dffb6ac9e2fce69f6227b43552fe3dc8cc1ee00c1cc53452a8712e9d5f80086dff8cf4999c1b93ed6c6e403c09334cb61ddd0b', Sig), - ecdsa_verify(PubKey, Msg, Sig, [encoding(octet), type(foo)]).`, + hex_bytes('0213c8426be471e55506f7ce4f7df557a42e310df09f92eb732ca3085e797cef9b', PubKey), + hex_bytes('9b038f8ef6918cbb56040dfda401b56bb1ce79c472e7736e8677758c83367a9d', Msg), + hex_bytes('889bcfd331e8e43b5ebf430301dffb6ac9e2fce69f6227b43552fe3dc8cc1ee00c1cc53452a8712e9d5f80086dff8cf4999c1b93ed6c6e403c09334cb61ddd0b', Sig), + ecdsa_verify(PubKey, Msg, Sig, [encoding(octet), type(foo)]).`, query: `verify.`, wantSuccess: false, - wantError: fmt.Errorf("ecdsa_verify/4: invalid type: foo. Possible values: secp256r1, secp256k1"), + wantError: fmt.Errorf("error(type_error(cryptographic_algorithm,foo),ecdsa_verify/4)"), }, { // Wrong msg program: `verify :- - hex_bytes('0213c8426be471e55506f7ce4f7df557a42e310df09f92eb732ca3085e797cef9b', PubKey), - hex_bytes('e50c26e89f734b2ee12041ff27874c901891f74a0f0cf470333312a3034ce3bf', Msg), - hex_bytes('30450220099e6f9dd218e0e304efa7a4224b0058a8e3aec73367ec239bee4ed8ed7d85db022100b504d3d0d2e879b04705c0e5a2b40b0521a5ab647ea207bd81134e1a4eb79e47', Sig), - ecdsa_verify(PubKey, Msg, Sig, encoding(octet)).`, + hex_bytes('0213c8426be471e55506f7ce4f7df557a42e310df09f92eb732ca3085e797cef9b', PubKey), + hex_bytes('e50c26e89f734b2ee12041ff27874c901891f74a0f0cf470333312a3034ce3bf', Msg), + hex_bytes('30450220099e6f9dd218e0e304efa7a4224b0058a8e3aec73367ec239bee4ed8ed7d85db022100b504d3d0d2e879b04705c0e5a2b40b0521a5ab647ea207bd81134e1a4eb79e47', Sig), + ecdsa_verify(PubKey, Msg, Sig, encoding(octet)).`, query: `verify.`, wantResult: []types.TermResults{{}}, wantSuccess: false, @@ -269,10 +272,10 @@ func TestXVerify(t *testing.T) { { // Wrong signature program: `verify :- - hex_bytes('0213c8426be471e55506f7ce4f7df557a42e310df09f92eb732ca3085e797cef9b', PubKey), - hex_bytes('e50c26e89f734b2ee12041ff27874c901891f74a0f0cf470333312a3034ce3be', Msg), - hex_bytes('30450220099e6f9dd218e0e304efa7a4224b0058a8e3aec73367ec239bee4ed8ed7d85db022100b504d3d0d2e879b04705c0e5a2b40b0521a5ab647ea207bd81134e1a4eb79e48', Sig), - ecdsa_verify(PubKey, Msg, Sig, encoding(octet)).`, + hex_bytes('0213c8426be471e55506f7ce4f7df557a42e310df09f92eb732ca3085e797cef9b', PubKey), + hex_bytes('e50c26e89f734b2ee12041ff27874c901891f74a0f0cf470333312a3034ce3be', Msg), + hex_bytes('30450220099e6f9dd218e0e304efa7a4224b0058a8e3aec73367ec239bee4ed8ed7d85db022100b504d3d0d2e879b04705c0e5a2b40b0521a5ab647ea207bd81134e1a4eb79e48', Sig), + ecdsa_verify(PubKey, Msg, Sig, encoding(octet)).`, query: `verify.`, wantResult: []types.TermResults{{}}, wantSuccess: false, @@ -281,10 +284,10 @@ func TestXVerify(t *testing.T) { { // All good program: `verify :- - hex_bytes('026b5450187ee9c63ba9e42cb6018d8469c903aca116178e223de76e49fe63b71c', PubKey), - hex_bytes('dece063885d3648078f903b6a3e8989f649dc3368cd9c8d69755ed9dcb6a0995', Msg), - hex_bytes('304402201448201bb4408549b0997f4b9ad9ed36f3cf8bb9c433fc7f3ba48c6b6e39476e022053f7d056f7ffeab9a79f3a36bc2ba969ddd530a3a1495d1ed7bba00039820223', Sig), - ecdsa_verify(PubKey, Msg, Sig, [encoding(octet), type(secp256k1)]).`, + hex_bytes('026b5450187ee9c63ba9e42cb6018d8469c903aca116178e223de76e49fe63b71c', PubKey), + hex_bytes('dece063885d3648078f903b6a3e8989f649dc3368cd9c8d69755ed9dcb6a0995', Msg), + hex_bytes('304402201448201bb4408549b0997f4b9ad9ed36f3cf8bb9c433fc7f3ba48c6b6e39476e022053f7d056f7ffeab9a79f3a36bc2ba969ddd530a3a1495d1ed7bba00039820223', Sig), + ecdsa_verify(PubKey, Msg, Sig, [encoding(octet), type(secp256k1)]).`, query: `verify.`, wantResult: []types.TermResults{{}}, wantSuccess: true, @@ -292,10 +295,10 @@ func TestXVerify(t *testing.T) { { // Wrong signature program: `verify :- - hex_bytes('026b5450187ee9c63ba9e42cb6018d8469c903aca116178e223de76e49fe63b71c', PubKey), - hex_bytes('dece063885d3648078f903b6a3e8989f649dc3368cd9c8d69755ed9dcb6a0996', Msg), - hex_bytes('304402201448201bb4408549b0997f4b9ad9ed36f3cf8bb9c433fc7f3ba48c6b6e39476e022053f7d056f7ffeab9a79f3a36bc2ba969ddd530a3a1495d1ed7bba00039820223', Sig), - ecdsa_verify(PubKey, Msg, Sig, [encoding(octet), type(secp256k1)]).`, + hex_bytes('026b5450187ee9c63ba9e42cb6018d8469c903aca116178e223de76e49fe63b71c', PubKey), + hex_bytes('dece063885d3648078f903b6a3e8989f649dc3368cd9c8d69755ed9dcb6a0996', Msg), + hex_bytes('304402201448201bb4408549b0997f4b9ad9ed36f3cf8bb9c433fc7f3ba48c6b6e39476e022053f7d056f7ffeab9a79f3a36bc2ba969ddd530a3a1495d1ed7bba00039820223', Sig), + ecdsa_verify(PubKey, Msg, Sig, [encoding(octet), type(secp256k1)]).`, query: `verify.`, wantResult: []types.TermResults{{}}, wantSuccess: false, @@ -321,7 +324,7 @@ func TestXVerify(t *testing.T) { sols, err := interpreter.QueryContext(ctx, tc.query) Convey("Then the error should be nil", func() { - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) So(sols, ShouldNotBeNil) Convey("and the bindings should be as expected", func() { @@ -329,14 +332,15 @@ func TestXVerify(t *testing.T) { for sols.Next() { m := types.TermResults{} err := sols.Scan(m) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) got = append(got, m) } if tc.wantError != nil { - So(sols.Err(), ShouldBeError, tc.wantError.Error()) + So(sols.Err(), ShouldNotEqual, nil) + So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) } else { - So(sols.Err(), ShouldBeNil) + So(sols.Err(), ShouldEqual, nil) if tc.wantSuccess { So(len(got), ShouldBeGreaterThan, 0) diff --git a/x/logic/predicate/did.go b/x/logic/predicate/did.go index a6268270..5340515f 100644 --- a/x/logic/predicate/did.go +++ b/x/logic/predicate/did.go @@ -1,7 +1,6 @@ package predicate import ( - "fmt" "net/url" "strings" @@ -49,28 +48,25 @@ func DIDComponents(vm *engine.VM, did, components engine.Term, cont engine.Cont, case engine.Atom: parsedDid, err := godid.ParseDIDURL(t1.String()) if err != nil { - return engine.Error(fmt.Errorf("did_components/2: %w", err)) + return engine.Error(prolog.WithError(engine.DomainError(prolog.ValidEncoding("did"), did, env), err, env)) } - terms, err := didToTerms(parsedDid) + terms, err := didToTerms(parsedDid, env) if err != nil { - return engine.Error(fmt.Errorf("did_components/2: %w", err)) + return engine.Error(err) } return engine.Unify(vm, components, AtomDID.Apply(terms...), cont, env) default: - return engine.Error(fmt.Errorf("did_components/2: cannot unify did with %T", t1)) + return engine.Error(engine.TypeError(prolog.AtomTypeAtom, did, env)) } switch t2 := env.Resolve(components).(type) { case engine.Variable: - return engine.Error(fmt.Errorf("did_components/2: at least one argument must be instantiated")) + return engine.Error(engine.InstantiationError(env)) case engine.Compound: - if t2.Functor() != AtomDID { - return engine.Error(fmt.Errorf("did_components/2: invalid functor %s. Expected %s", t2.Functor().String(), AtomDID.String())) - } - if t2.Arity() != 5 { - return engine.Error(fmt.Errorf("did_components/2: invalid arity %d. Expected 5", t2.Arity())) + if t2.Functor() != AtomDID || t2.Arity() != 5 { + return engine.Error(engine.DomainError(AtomDID, components, env)) } buf := strings.Builder{} @@ -102,13 +98,13 @@ func DIDComponents(vm *engine.VM, did, components engine.Term, cont engine.Cont, for i := 0; i < t2.Arity(); i++ { if err := processSegment(t2, uint8(i), processors[i], env); err != nil { - return engine.Error(fmt.Errorf("did_components/2: %w", err)) + return engine.Error(err) } } return engine.Unify(vm, did, engine.NewAtom(buf.String()), cont, env) default: - return engine.Error(fmt.Errorf("did_components/2: cannot unify did with %T", t2)) + return engine.Error(engine.TypeError(AtomDID, components, env)) } } @@ -120,7 +116,7 @@ func processSegment(segments engine.Compound, segmentNumber uint8, fn func(segme } segment, err := prolog.AssertAtom(env, segments.Arg(int(segmentNumber))) if err != nil { - return fmt.Errorf("failed to resolve atom at segment %d: %w", segmentNumber, err) + return err } fn(segment) @@ -131,16 +127,17 @@ func processSegment(segments engine.Compound, segmentNumber uint8, fn func(segme // didToTerms converts a DID to a "tuple" of terms (either an Atom or a Variable), // or returns an error if the conversion fails. // The returned atoms are url decoded. -func didToTerms(did *godid.DID) ([]engine.Term, error) { +func didToTerms(did *godid.DID, env *engine.Env) ([]engine.Term, error) { components := []string{did.Method, did.ID, did.Path, did.Query, did.Fragment} terms := make([]engine.Term, 0, len(components)) for _, component := range components { r, err := url.PathUnescape(component) if err != nil { - return nil, err + return nil, engine.DomainError(prolog.ValidEncoding("url_encoded"), engine.NewAtom(component), env) } - terms = append(terms, prolog.StringToTerm(r)) + var r2 engine.Term = engine.NewAtom(r) + terms = append(terms, r2) } return terms, nil diff --git a/x/logic/predicate/did_test.go b/x/logic/predicate/did_test.go index b1a35e67..3108478c 100644 --- a/x/logic/predicate/did_test.go +++ b/x/logic/predicate/did_test.go @@ -3,6 +3,7 @@ package predicate import ( "fmt" + "strings" "testing" "github.com/ichiban/prolog/engine" @@ -63,38 +64,38 @@ func TestDID(t *testing.T) { { query: `did_components(X,Y).`, wantResult: []types.TermResults{}, - wantError: fmt.Errorf("did_components/2: at least one argument must be instantiated"), + wantError: fmt.Errorf("error(instantiation_error,did_components/2)"), }, { query: `did_components('foo',X).`, wantResult: []types.TermResults{}, - wantError: fmt.Errorf("did_components/2: invalid DID: input length is less than 7"), + wantError: fmt.Errorf("error(domain_error(encoding(did),foo),[%s],did_components/2)", + strings.Join(strings.Split("invalid DID: input length is less than 7", ""), ",")), }, { query: `did_components(123,X).`, wantResult: []types.TermResults{}, - wantError: fmt.Errorf("did_components/2: cannot unify did with engine.Integer"), + wantError: fmt.Errorf("error(type_error(atom,123),did_components/2)"), }, { query: `did_components(X, 123).`, wantResult: []types.TermResults{}, - wantError: fmt.Errorf("did_components/2: cannot unify did with engine.Integer"), + wantError: fmt.Errorf("error(type_error(did,123),did_components/2)"), }, { query: `did_components(X,foo('bar')).`, wantResult: []types.TermResults{}, - wantError: fmt.Errorf("did_components/2: invalid functor foo. Expected did"), + wantError: fmt.Errorf("error(domain_error(did,foo(bar)),did_components/2)"), }, { query: `did_components(X,did('bar')).`, wantResult: []types.TermResults{}, - wantError: fmt.Errorf("did_components/2: invalid arity 1. Expected 5"), + wantError: fmt.Errorf("error(domain_error(did,did(bar)),did_components/2)"), }, { query: `did_components(X,did(example,'123456','path with/space',5,test)).`, wantResult: []types.TermResults{}, - wantError: fmt.Errorf( - "did_components/2: failed to resolve atom at segment 3: error(type_error(atom,5),did_components/2)"), + wantError: fmt.Errorf("error(type_error(atom,5),did_components/2)"), }, { query: `did_components('did:example:123456',foo(X)).`, @@ -113,13 +114,13 @@ func TestDID(t *testing.T) { interpreter.Register2(engine.NewAtom("did_components"), DIDComponents) err := interpreter.Compile(ctx, tc.program) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) 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(err, ShouldEqual, nil) So(sols, ShouldNotBeNil) Convey("and the bindings should be as expected", func() { @@ -127,12 +128,12 @@ func TestDID(t *testing.T) { for sols.Next() { m := types.TermResults{} err := sols.Scan(m) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) got = append(got, m) } if tc.wantError != nil { - So(sols.Err(), ShouldNotBeNil) + So(sols.Err(), ShouldNotEqual, nil) So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) } else { So(sols.Err(), ShouldBeNil) diff --git a/x/logic/predicate/encoding.go b/x/logic/predicate/encoding.go index e15f747a..1d57d4ec 100644 --- a/x/logic/predicate/encoding.go +++ b/x/logic/predicate/encoding.go @@ -3,7 +3,6 @@ package predicate import ( "context" "encoding/hex" - "fmt" "github.com/ichiban/prolog/engine" @@ -35,27 +34,30 @@ func HexBytes(vm *engine.VM, hexa, bts engine.Term, cont engine.Cont, env *engin result = make([]byte, hex.DecodedLen(len(src))) _, err := hex.Decode(result, src) if err != nil { - return engine.Error(fmt.Errorf("hex_bytes/2: failed decode hexadecimal %w", err)) + return engine.Error( + prolog.WithError( + engine.DomainError(prolog.ValidEncoding("hex"), hexa, env), err, env)) } default: - return engine.Error(fmt.Errorf("hex_bytes/2: invalid hex type: %T, should be Atom or Variable", h)) + return engine.Error(engine.TypeError(prolog.AtomTypeAtom, hexa, env)) } switch b := env.Resolve(bts).(type) { case engine.Variable: if result == nil { - return engine.Error(fmt.Errorf("hex_bytes/2: nil hexadecimal conversion in input")) + return engine.Error(engine.InstantiationError(env)) } - return engine.Unify(vm, bts, prolog.BytesToCodepointListTermWithDefault(result, env), cont, env) + return engine.Unify(vm, bts, prolog.BytesToByteListTerm(result), cont, env) case engine.Compound: - src, err := prolog.StringTermToBytes(b, prolog.AtomEmpty, env) + src, err := prolog.ByteListTermToBytes(b, env) if err != nil { - return engine.Error(fmt.Errorf("hex_bytes/2: %w", err)) + return engine.Error(err) } dst := hex.EncodeToString(src) - return engine.Unify(vm, hexa, prolog.StringToTerm(dst), cont, env) + var r engine.Term = engine.NewAtom(dst) + return engine.Unify(vm, hexa, r, cont, env) default: - return engine.Error(fmt.Errorf("hex_bytes/2: invalid hex type: %T, should be Variable or List", b)) + return engine.Error(engine.TypeError(prolog.AtomTypeText, bts, env)) } }) } diff --git a/x/logic/predicate/encoding_test.go b/x/logic/predicate/encoding_test.go index e00ecc34..7ee7e821 100644 --- a/x/logic/predicate/encoding_test.go +++ b/x/logic/predicate/encoding_test.go @@ -2,6 +2,7 @@ package predicate import ( "fmt" + "strings" "testing" "github.com/ichiban/prolog" @@ -56,7 +57,8 @@ func TestHexBytesPredicate(t *testing.T) { { query: `hex_bytes('fail', [44,38,180,107,104,255,198,143,249,155,69,60,29,48,65,52,19,66,45,112,100,131,191,160,249,138,94,136,98,102,231,174]).`, - wantError: fmt.Errorf("hex_bytes/2: failed decode hexadecimal encoding/hex: invalid byte: U+0069 'i'"), + wantError: fmt.Errorf("error(domain_error(encoding(hex),fail),[%s],hex_bytes/2)", + strings.Join(strings.Split("encoding/hex: invalid byte: U+0069 'i'", ""), ",")), wantSuccess: false, }, { @@ -68,7 +70,7 @@ func TestHexBytesPredicate(t *testing.T) { query: `hex_bytes('2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae', [345,38,'hey',107,104,255,198,143,249,155,69,60,29,48,65,52,19,66,45,112,100,131,191,160,249,138,94,136,98,102,231,174]).`, wantSuccess: false, - wantError: fmt.Errorf("hex_bytes/2: error(domain_error(valid_byte(345),[345,38,hey,107,104,255,198,143,249,155,69,60,29,48,65,52,19,66,45,112,100,131,191,160,249,138,94,136,98,102,231,174]),hex_bytes/2)"), //nolint:lll + wantError: fmt.Errorf("error(type_error(byte,345),hex_bytes/2)"), }, } for nc, tc := range cases { @@ -81,16 +83,15 @@ func TestHexBytesPredicate(t *testing.T) { Convey("and a vm", func() { interpreter := testutil.NewLightInterpreterMust(ctx) interpreter.Register2(engine.NewAtom("hex_bytes"), HexBytes) - interpreter.Register3(engine.NewAtom("string_bytes"), StringBytes) err := interpreter.Compile(ctx, tc.program) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) 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(err, ShouldEqual, nil) So(sols, ShouldNotBeNil) Convey("and the bindings should be as expected", func() { @@ -110,15 +111,15 @@ func checkSolutions(sols *prolog.Solutions, wantResult []types.TermResults, want for sols.Next() { m := types.TermResults{} err := sols.Scan(m) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) got = append(got, m) } if wantError != nil { - So(sols.Err(), ShouldNotBeNil) + So(sols.Err(), ShouldNotEqual, nil) So(sols.Err().Error(), ShouldEqual, wantError.Error()) } else { - So(sols.Err(), ShouldBeNil) + So(sols.Err(), ShouldEqual, nil) if wantSuccess { So(len(got), ShouldEqual, len(wantResult)) diff --git a/x/logic/predicate/file.go b/x/logic/predicate/file.go index 7102d43a..a4a344e0 100644 --- a/x/logic/predicate/file.go +++ b/x/logic/predicate/file.go @@ -2,12 +2,13 @@ package predicate import ( "context" - "fmt" "os" "reflect" "sort" "github.com/ichiban/prolog/engine" + + "github.com/okp4/okp4d/x/logic/prolog" ) // SourceFile is a predicate that unify the given term with the currently loaded source file. @@ -29,36 +30,35 @@ import ( func SourceFile(vm *engine.VM, file engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { loaded := getLoadedSources(vm) - inputFile, err := getFile(env, file) - if err != nil { - return engine.Error(fmt.Errorf("source_file/1: %w", err)) - } - - if inputFile != nil { - if _, ok := loaded[*inputFile]; ok { - return engine.Unify(vm, file, engine.NewAtom(*inputFile), cont, env) + switch file := env.Resolve(file).(type) { + case engine.Variable: + promises := make([]func(ctx context.Context) *engine.Promise, 0, len(loaded)) + sortedSource := sortLoadedSources(loaded) + for i := range sortedSource { + term := engine.NewAtom(sortedSource[i]) + promises = append( + promises, + func(ctx context.Context) *engine.Promise { + return engine.Unify( + vm, + file, + term, + cont, + env, + ) + }) } - return engine.Delay() - } - promises := make([]func(ctx context.Context) *engine.Promise, 0, len(loaded)) - sortedSource := sortLoadedSources(loaded) - for i := range sortedSource { - term := engine.NewAtom(sortedSource[i]) - promises = append( - promises, - func(ctx context.Context) *engine.Promise { - return engine.Unify( - vm, - file, - term, - cont, - env, - ) - }) + return engine.Delay(promises...) + case engine.Atom: + inputFile := file.String() + if _, ok := loaded[inputFile]; !ok { + return engine.Bool(false) + } + return cont(env) + default: + return engine.Error(engine.TypeError(prolog.AtomTypeAtom, file, env)) } - - return engine.Delay(promises...) } // ioMode describes what operations you can perform on the stream. @@ -98,28 +98,28 @@ func (m ioMode) Term() engine.Term { // that can be opened, such as a URI. The URI scheme determines the type of resource that is opened. // - Mode is an atom representing the mode of the stream (read, write, append). // - Stream is the stream to be opened. -// - Options is a list of options. +// - Options is a list of options. No options are currently defined, so the list should be empty. // // Examples: // // # Open a stream from a cosmwasm query. // # The Stream should be read as a string with a read_string/3 predicate, and then closed with the close/1 predicate. -// - open('cosmwasm:okp4-objectarium:okp412kgx?query=%7B%22object_data%22%3A%7B%...4dd539e3%22%7D%7D', 'read', Stream) +// - open('cosmwasm:okp4-objectarium:okp412kgx?query=%7B%22object_data%22%3A%7B%...4dd539e3%22%7D%7D', 'read', Stream, []) func Open(vm *engine.VM, sourceSink, mode, stream, options engine.Term, k engine.Cont, env *engine.Env) *engine.Promise { var name string switch s := env.Resolve(sourceSink).(type) { case engine.Variable: - return engine.Error(fmt.Errorf("open/4: source cannot be a variable")) + return engine.Error(engine.InstantiationError(env)) case engine.Atom: name = s.String() default: - return engine.Error(fmt.Errorf("open/4: invalid domain for source, should be an atom, got %T", s)) + return engine.Error(engine.TypeError(prolog.AtomTypeAtom, sourceSink, env)) } var streamMode ioMode switch m := env.Resolve(mode).(type) { case engine.Variable: - return engine.Error(fmt.Errorf("open/4: streamMode cannot be a variable")) + return engine.Error(engine.InstantiationError(env)) case engine.Atom: var ok bool streamMode, ok = map[engine.Atom]ioMode{ @@ -128,29 +128,35 @@ func Open(vm *engine.VM, sourceSink, mode, stream, options engine.Term, k engine atomAppend: ioModeAppend, }[m] if !ok { - return engine.Error(fmt.Errorf("open/4: invalid open mode (read | write | append)")) + return engine.Error(engine.TypeError(prolog.AtomTypeIOMode, mode, env)) } default: - return engine.Error(fmt.Errorf("open/4: invalid domain for open mode, should be an atom, got %T", m)) + return engine.Error(engine.TypeError(prolog.AtomTypeIOMode, mode, env)) } if _, ok := env.Resolve(stream).(engine.Variable); !ok { - return engine.Error(fmt.Errorf("open/4: stream can only be a variable, got %T", env.Resolve(stream))) + // TODO: replace InstantiationError with uninstantiation_error(+Culprit) once it's implemented by ichiban/prolog. + return engine.Error(engine.InstantiationError(env)) } if streamMode != ioModeRead { - return engine.Error(fmt.Errorf("open/4: only read mode is allowed here")) + return engine.Error(prolog.PermissionError(prolog.AtomOperationInput, prolog.AtomPermissionTypeStream, sourceSink, env)) } f, err := vm.FS.Open(name) if err != nil { - return engine.Error(fmt.Errorf("open/4: couldn't open stream: %w", err)) + return engine.Error(prolog.ExistenceError(prolog.AtomObjectTypeSourceSink, sourceSink, env)) } s := engine.NewInputTextStream(f) - iter := engine.ListIterator{List: options, Env: env} - for iter.Next() { - return engine.Error(fmt.Errorf("open/4: options is not allowed here")) + if prolog.IsGround(options, env) { + _, err = prolog.AssertList(env, options) + switch { + case err != nil: + return engine.Error(err) + case !prolog.IsEmptyList(options): + return engine.Error(engine.DomainError(prolog.ValidEmptyList(), options, env)) + } } return engine.Unify(vm, stream, s, k, env) @@ -177,16 +183,3 @@ func sortLoadedSources(sources map[string]struct{}) []string { return result } - -//nolint:nilnil -func getFile(env *engine.Env, term engine.Term) (*string, error) { - switch file := env.Resolve(term).(type) { - case engine.Variable: - case engine.Atom: - strFile := file.String() - return &strFile, nil - default: - return nil, fmt.Errorf("cannot unify file with %T", term) - } - return nil, nil -} diff --git a/x/logic/predicate/file_test.go b/x/logic/predicate/file_test.go index 79198399..247c483b 100644 --- a/x/logic/predicate/file_test.go +++ b/x/logic/predicate/file_test.go @@ -82,7 +82,7 @@ func TestSourceFile(t *testing.T) { interpreter: testutil.NewLightInterpreterMust, query: "source_file(foo(bar)).", wantResult: []types.TermResults{}, - wantError: fmt.Errorf("source_file/1: cannot unify file with *engine.compound"), + wantError: fmt.Errorf("error(type_error(atom,foo(bar)),source_file/1)"), }, { @@ -145,7 +145,7 @@ func TestSourceFile(t *testing.T) { sols, err := interpreter.QueryContext(ctx, tc.query) Convey("Then the error should be nil", func() { - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) So(sols, ShouldNotBeNil) Convey("and the bindings should be as expected", func() { @@ -153,16 +153,16 @@ func TestSourceFile(t *testing.T) { for sols.Next() { m := types.TermResults{} err := sols.Scan(m) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) got = append(got, m) } if tc.wantError != nil { - So(sols.Err(), ShouldNotBeNil) + So(sols.Err(), ShouldNotEqual, nil) So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) } else { - So(sols.Err(), ShouldBeNil) + So(sols.Err(), ShouldEqual, nil) if tc.wantSuccess { So(len(got), ShouldBeGreaterThan, 0) @@ -228,7 +228,7 @@ func TestOpen(t *testing.T) { }, program: "get_first_char(C) :- open(File, write, Stream, _), get_char(Stream, C).", query: `get_first_char(C).`, - wantError: fmt.Errorf("open/4: source cannot be a variable"), + wantError: fmt.Errorf("error(instantiation_error,open/4)"), wantSuccess: false, }, { @@ -237,7 +237,7 @@ func TestOpen(t *testing.T) { }, program: "get_first_char(C) :- open(34, write, Stream, _), get_char(Stream, C).", query: `get_first_char(C).`, - wantError: fmt.Errorf("open/4: invalid domain for source, should be an atom, got engine.Integer"), + wantError: fmt.Errorf("error(type_error(atom,34),open/4)"), wantSuccess: false, }, { @@ -246,7 +246,7 @@ func TestOpen(t *testing.T) { }, program: "get_first_char(C) :- open(file, write, stream, _), get_char(Stream, C).", query: `get_first_char(C).`, - wantError: fmt.Errorf("open/4: stream can only be a variable, got engine.Atom"), + wantError: fmt.Errorf("error(instantiation_error,open/4)"), wantSuccess: false, }, { @@ -255,7 +255,7 @@ func TestOpen(t *testing.T) { }, program: "get_first_char(C) :- open(file, 45, Stream, _), get_char(Stream, C).", query: `get_first_char(C).`, - wantError: fmt.Errorf("open/4: invalid domain for open mode, should be an atom, got engine.Integer"), + wantError: fmt.Errorf("error(type_error(io_mode,45),open/4)"), wantSuccess: false, }, { @@ -264,25 +264,25 @@ func TestOpen(t *testing.T) { }, program: "get_first_char(C) :- open(file, foo, Stream, _), get_char(Stream, C).", query: `get_first_char(C).`, - wantError: fmt.Errorf("open/4: invalid open mode (read | write | append)"), + wantError: fmt.Errorf("error(type_error(io_mode,foo),open/4)"), wantSuccess: false, }, { files: map[string][]byte{ "file": []byte("dumb(dumber)."), }, - program: "get_first_char(C) :- open(file, write, Stream, _), get_char(Stream, C).", + program: "get_first_char(C) :- open(my_file, write, Stream, _), get_char(Stream, C).", query: `get_first_char(C).`, - wantError: fmt.Errorf("open/4: only read mode is allowed here"), + wantError: fmt.Errorf("error(permission_error(input,stream,my_file),unknown)"), wantSuccess: false, }, { files: map[string][]byte{ "file": []byte("dumb(dumber)."), }, - program: "get_first_char(C) :- open(file, append, Stream, _), get_char(Stream, C).", + program: "get_first_char(C) :- open(my_file, append, Stream, _), get_char(Stream, C).", query: `get_first_char(C).`, - wantError: fmt.Errorf("open/4: only read mode is allowed here"), + wantError: fmt.Errorf("error(permission_error(input,stream,my_file),unknown)"), wantSuccess: false, }, { @@ -291,7 +291,7 @@ func TestOpen(t *testing.T) { }, program: "get_first_char(C) :- open(file2, read, Stream, _), get_char(Stream, C).", query: `get_first_char(C).`, - wantError: fmt.Errorf("open/4: couldn't open stream: read file2: path not found"), + wantError: fmt.Errorf("error(existence_error(source_sink,file2),unknown)"), wantSuccess: false, }, { @@ -300,7 +300,7 @@ func TestOpen(t *testing.T) { }, program: "get_first_char(C) :- open(file, read, Stream, [option1]), get_char(Stream, C).", query: `get_first_char(C).`, - wantError: fmt.Errorf("open/4: options is not allowed here"), + wantError: fmt.Errorf("error(domain_error(empty_list,[option1]),open/4)"), wantSuccess: false, }, } @@ -332,13 +332,13 @@ func TestOpen(t *testing.T) { interpreter.Register4(engine.NewAtom("open"), Open) err := interpreter.Compile(ctx, tc.program) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) 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(err, ShouldEqual, nil) So(sols, ShouldNotBeNil) Convey("and the bindings should be as expected", func() { @@ -346,15 +346,15 @@ func TestOpen(t *testing.T) { for sols.Next() { m := types.TermResults{} err := sols.Scan(m) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) got = append(got, m) } if tc.wantError != nil { - So(sols.Err(), ShouldNotBeNil) + So(sols.Err(), ShouldNotEqual, nil) So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) } else { - So(sols.Err(), ShouldBeNil) + So(sols.Err(), ShouldEqual, nil) if tc.wantSuccess { So(len(got), ShouldBeGreaterThan, 0) diff --git a/x/logic/predicate/json.go b/x/logic/predicate/json.go index 520a03cc..0aa869d8 100644 --- a/x/logic/predicate/json.go +++ b/x/logic/predicate/json.go @@ -40,63 +40,79 @@ func JSONProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engin switch t1 := env.Resolve(j).(type) { case engine.Variable: case engine.Atom: - terms, err := jsonStringToTerms(t1.String()) + terms, err := jsonStringToTerms(t1, env) if err != nil { - return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) + return engine.Error(err) } result = terms default: - return engine.Error(fmt.Errorf("json_prolog/2: cannot unify json with %T", t1)) + return engine.Error(engine.TypeError(prolog.AtomTypeAtom, j, env)) } switch t2 := env.Resolve(term).(type) { case engine.Variable: if result == nil { - return engine.Error(fmt.Errorf("json_prolog/2: could not unify two variable")) + return engine.Error(engine.InstantiationError(env)) } return engine.Unify(vm, term, result, cont, env) default: b, err := termsToJSON(t2, env) if err != nil { - return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) + return engine.Error(err) } b, err = sdk.SortJSON(b) if err != nil { - return engine.Error(fmt.Errorf("json_prolog/2: %w", err)) + return engine.Error( + prolog.WithError( + engine.DomainError(prolog.ValidEncoding("json"), term, env), err, env)) } - return engine.Unify(vm, j, prolog.StringToTerm(string(b)), cont, env) + var r engine.Term = engine.NewAtom(string(b)) + return engine.Unify(vm, j, r, cont, env) } }) } -func jsonStringToTerms(j string) (engine.Term, error) { +func jsonStringToTerms(j engine.Atom, env *engine.Env) (engine.Term, error) { var values any - decoder := json.NewDecoder(strings.NewReader(j)) + decoder := json.NewDecoder(strings.NewReader(j.String())) decoder.UseNumber() // unmarshal a number into an interface{} as a Number instead of as a float64 if err := decoder.Decode(&values); err != nil { - return nil, err + return nil, prolog.WithError( + engine.DomainError(prolog.ValidEncoding("json"), j, env), err, env) } - return jsonToTerms(values) + term, err := jsonToTerms(values) + if err != nil { + return nil, prolog.WithError( + engine.DomainError(prolog.ValidEncoding("json"), j, env), err, env) + } + + return term, nil } func termsToJSON(term engine.Term, env *engine.Env) ([]byte, error) { + asDomainError := func(bs []byte, err error) ([]byte, error) { + if err != nil { + return bs, prolog.WithError( + engine.DomainError(prolog.ValidEncoding("json"), term, env), err, env) + } + return bs, err + } switch t := term.(type) { case engine.Atom: - return json.Marshal(t.String()) + return asDomainError(json.Marshal(t.String())) case engine.Integer: - return json.Marshal(t) + return asDomainError(json.Marshal(t)) case engine.Compound: - switch t.Functor().String() { - case ".": // Represent an engine.List - if t.Arity() != 2 { - return nil, fmt.Errorf("wrong term arity for array, give %d, expected %d", t.Arity(), 2) + switch { + case t.Functor() == prolog.AtomDot: + iter, err := prolog.ListIterator(t, env) + if err != nil { + return nil, err } - iter := engine.ListIterator{List: t, Env: env} - elements := make([]json.RawMessage, 0) for iter.Next() { element, err := termsToJSON(env.Resolve(iter.Current()), env) @@ -105,8 +121,8 @@ func termsToJSON(term engine.Term, env *engine.Env) ([]byte, error) { } elements = append(elements, element) } - return json.Marshal(elements) - case prolog.AtomJSON.String(): + return asDomainError(json.Marshal(elements)) + case t.Functor() == prolog.AtomJSON: terms, err := prolog.ExtractJSONTerm(t, env) if err != nil { return nil, err @@ -120,30 +136,30 @@ func termsToJSON(term engine.Term, env *engine.Env) ([]byte, error) { } attributes[key] = raw } - return json.Marshal(attributes) - } - - switch { + return asDomainError(json.Marshal(attributes)) case prolog.JSONBool(true).Compare(t, env) == 0: - return json.Marshal(true) + return asDomainError(json.Marshal(true)) case prolog.JSONBool(false).Compare(t, env) == 0: - return json.Marshal(false) + return asDomainError(json.Marshal(false)) case prolog.JSONEmptyArray().Compare(t, env) == 0: - return json.Marshal([]json.RawMessage{}) + return asDomainError(json.Marshal([]json.RawMessage{})) case prolog.JSONNull().Compare(t, env) == 0: - return json.Marshal(nil) + return asDomainError(json.Marshal(nil)) + default: + // no-op } - - return nil, fmt.Errorf("invalid functor %s", t.Functor()) default: - return nil, fmt.Errorf("could not convert %s {%T} to json", t, t) + // no-op } + + return nil, engine.TypeError(prolog.AtomTypeJSON, term, env) } func jsonToTerms(value any) (engine.Term, error) { switch v := value.(type) { case string: - return prolog.StringToTerm(v), nil + var r engine.Term = engine.NewAtom(v) + return r, nil case json.Number: r, ok := math.NewIntFromString(string(v)) if !ok { @@ -185,6 +201,6 @@ func jsonToTerms(value any) (engine.Term, error) { } return engine.List(elements...), nil default: - return nil, fmt.Errorf("could not convert %s (%T) to a prolog term", v, v) + return nil, fmt.Errorf("unsupported type: %T", v) } } diff --git a/x/logic/predicate/json_test.go b/x/logic/predicate/json_test.go index 60109a5f..ebca37e9 100644 --- a/x/logic/predicate/json_test.go +++ b/x/logic/predicate/json_test.go @@ -3,6 +3,7 @@ package predicate import ( "fmt" + "strings" "testing" "github.com/ichiban/prolog/engine" @@ -34,13 +35,13 @@ func TestJsonProlog(t *testing.T) { description: "two variable", query: `json_prolog(Json, Term).`, wantSuccess: false, - wantError: fmt.Errorf("json_prolog/2: could not unify two variable"), + wantError: fmt.Errorf("error(instantiation_error,json_prolog/2)"), }, { description: "two variable", query: `json_prolog(ooo(r), Term).`, wantSuccess: false, - wantError: fmt.Errorf("json_prolog/2: cannot unify json with *engine.compound"), + wantError: fmt.Errorf("error(type_error(atom,ooo(r)),json_prolog/2)"), }, // ** JSON -> Prolog ** @@ -109,13 +110,15 @@ func TestJsonProlog(t *testing.T) { description: "convert large json number into prolog", query: `json_prolog('100000000000000000000', Term).`, wantSuccess: false, - wantError: fmt.Errorf("json_prolog/2: could not convert number '100000000000000000000' into integer term, overflow"), + wantError: fmt.Errorf("error(domain_error(encoding(json),100000000000000000000),[%s],json_prolog/2)", + strings.Join(strings.Split("could not convert number '100000000000000000000' into integer term, overflow", ""), ",")), }, { description: "decimal number not compatible yet", query: `json_prolog('10.4', Term).`, wantSuccess: false, - wantError: fmt.Errorf("json_prolog/2: could not convert number '10.4' into integer term, decimal number is not handled yet"), + wantError: fmt.Errorf("error(domain_error(encoding(json),10.4),[%s],json_prolog/2)", + strings.Join(strings.Split("could not convert number '10.4' into integer term, decimal number is not handled yet", ""), ",")), }, // ** JSON -> Prolog ** // Bool @@ -260,19 +263,19 @@ func TestJsonProlog(t *testing.T) { description: "invalid json term compound", query: `json_prolog(Json, foo([a-b])).`, wantSuccess: false, - wantError: fmt.Errorf("json_prolog/2: invalid functor foo"), + wantError: fmt.Errorf("error(type_error(json,foo([-(a,b)])),json_prolog/2)"), }, { description: "convert json term object from prolog with error inside", query: `json_prolog(Json, ['string with space',json('toto')]).`, wantSuccess: false, - wantError: fmt.Errorf("json_prolog/2: json compound should contains one list, give engine.Atom"), + wantError: fmt.Errorf("error(type_error(list,toto),json_prolog/2)"), }, { description: "convert json term object from prolog with error inside another object", query: `json_prolog(Json, ['string with space',json([key-json(error)])]).`, wantSuccess: false, - wantError: fmt.Errorf("json_prolog/2: json compound should contains one list, give engine.Atom"), + wantError: fmt.Errorf("error(type_error(list,error),json_prolog/2)"), }, // ** Prolog -> JSON ** // Number @@ -288,7 +291,7 @@ func TestJsonProlog(t *testing.T) { description: "decimal number not compatible yet", query: `json_prolog(Json, 10.4).`, wantSuccess: false, - wantError: fmt.Errorf("json_prolog/2: could not convert %%!s(engine.Float=10.4) {engine.Float} to json"), + wantError: fmt.Errorf("error(type_error(json,10.4),json_prolog/2)"), }, // ** Prolog -> Json ** // Array @@ -328,7 +331,7 @@ func TestJsonProlog(t *testing.T) { description: "convert json string array from prolog with error inside", query: `json_prolog(Json, ['string with space',hey('toto')]).`, wantSuccess: false, - wantError: fmt.Errorf("json_prolog/2: invalid functor hey"), + wantError: fmt.Errorf("error(type_error(json,hey(toto)),json_prolog/2)"), }, // ** Prolog -> JSON ** // Bool @@ -371,29 +374,29 @@ func TestJsonProlog(t *testing.T) { interpreter.Register2(engine.NewAtom("json_prolog"), JSONProlog) err := interpreter.Compile(ctx, tc.program) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) 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) + So(err, ShouldEqual, nil) + So(sols, ShouldNotEqual, nil) 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) + So(err, ShouldEqual, nil) got = append(got, m) } if tc.wantError != nil { - So(sols.Err(), ShouldNotBeNil) + So(sols.Err(), ShouldNotEqual, nil) So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) } else { - So(sols.Err(), ShouldBeNil) + So(sols.Err(), ShouldEqual, nil) if tc.wantSuccess { So(len(got), ShouldEqual, len(tc.wantResult)) @@ -481,7 +484,7 @@ func TestJsonPrologWithMoreComplexStructBidirectional(t *testing.T) { sols, err := interpreter.QueryContext(ctx, fmt.Sprintf("json_prolog(%s, Term).", tc.json)) Convey("Then the error should be nil", func() { - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) So(sols, ShouldNotBeNil) Convey("and the bindings should be as expected", func() { @@ -489,15 +492,15 @@ func TestJsonPrologWithMoreComplexStructBidirectional(t *testing.T) { for sols.Next() { m := types.TermResults{} err := sols.Scan(m) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) got = append(got, m) } if tc.wantError != nil { - So(sols.Err(), ShouldNotBeNil) + So(sols.Err(), ShouldNotEqual, nil) So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) } else { - So(sols.Err(), ShouldBeNil) + So(sols.Err(), ShouldEqual, nil) if tc.wantSuccess { So(len(got), ShouldBeGreaterThan, 0) @@ -520,7 +523,7 @@ func TestJsonPrologWithMoreComplexStructBidirectional(t *testing.T) { sols, err := interpreter.QueryContext(ctx, fmt.Sprintf("json_prolog(Json, %s).", tc.term)) Convey("Then the error should be nil", func() { - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) So(sols, ShouldNotBeNil) Convey("and the bindings should be as expected", func() { @@ -528,15 +531,15 @@ func TestJsonPrologWithMoreComplexStructBidirectional(t *testing.T) { for sols.Next() { m := types.TermResults{} err := sols.Scan(m) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) got = append(got, m) } if tc.wantError != nil { - So(sols.Err(), ShouldNotBeNil) + So(sols.Err(), ShouldNotEqual, nil) So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) } else { - So(sols.Err(), ShouldBeNil) + So(sols.Err(), ShouldEqual, nil) if tc.wantSuccess { So(len(got), ShouldBeGreaterThan, 0) @@ -560,14 +563,14 @@ func TestJsonPrologWithMoreComplexStructBidirectional(t *testing.T) { sols, err := interpreter.QueryContext(ctx, fmt.Sprintf("json_prolog(%s, %s).", tc.json, tc.term)) Convey("Then the error should be nil", func() { - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) So(sols, ShouldNotBeNil) Convey("and the bindings should be as expected", func() { So(sols.Next(), ShouldEqual, tc.wantSuccess) if tc.wantError != nil { - So(sols.Err(), ShouldNotBeNil) + So(sols.Err(), ShouldNotEqual, nil) So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) } }) diff --git a/x/logic/predicate/string.go b/x/logic/predicate/string.go index d1cbd741..3cff7f43 100644 --- a/x/logic/predicate/string.go +++ b/x/logic/predicate/string.go @@ -3,13 +3,13 @@ package predicate import ( "context" "errors" - "fmt" "io" "strings" "github.com/ichiban/prolog/engine" "github.com/okp4/okp4d/x/logic/prolog" + "github.com/okp4/okp4d/x/logic/util" ) // ReadString is a predicate that reads characters from the provided Stream and unifies them with String. @@ -45,11 +45,11 @@ func ReadString(vm *engine.VM, stream, length, result engine.Term, cont engine.C 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")) + return engine.Error(engine.InstantiationError(env)) case *engine.Stream: s = st default: - return engine.Error(fmt.Errorf("read_string/3: invalid domain for given stream")) + return engine.Error(engine.TypeError(prolog.AtomTypeStream, stream, env)) } var maxLength uint64 @@ -65,18 +65,19 @@ func ReadString(vm *engine.VM, stream, length, result engine.Term, cont engine.C if errors.Is(err, io.EOF) || totalLen >= maxLength { break } - return engine.Error(fmt.Errorf("read_string/3: couldn't read stream: %w", err)) + return engine.Error(prolog.SyntaxError(err, env)) } 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.Error(prolog.SyntaxError(err, env)) } } + var r engine.Term = engine.NewAtom(builder.String()) return engine.Unify( vm, prolog.Tuple(result, length), - prolog.Tuple(prolog.StringToTerm(builder.String()), engine.Integer(totalLen)), cont, env) + prolog.Tuple(r, engine.Integer(totalLen)), cont, env) }) } @@ -95,7 +96,6 @@ func ReadString(vm *engine.VM, stream, length, result engine.Term, cont engine.C // Encoding can be one of the following: // - 'text' considers the string as a sequence of Unicode characters. // - 'octet' considers the string as a sequence of bytes. -// - 'utf8' considers the string as a sequence of UTF-8 characters. // - '' considers the string as a sequence of characters in the given encoding. // // At least one of String or Bytes must be instantiated. @@ -108,44 +108,91 @@ func ReadString(vm *engine.VM, stream, length, result engine.Term, cont engine.C // # Convert a list of bytes to a string. // - string_bytes(String, [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100], octet). func StringBytes( - _ *engine.VM, str, bts, encoding engine.Term, cont engine.Cont, env *engine.Env, + _ *engine.VM, str, bts, encodingTerm engine.Term, cont engine.Cont, env *engine.Env, ) *engine.Promise { - encodingAtom, err := prolog.AssertAtom(env, encoding) + encoding, err := prolog.AssertAtom(env, encodingTerm) if err != nil { return engine.Error(err) } forwardConverter := func(value []engine.Term, options engine.Term, env *engine.Env) ([]engine.Term, error) { - bs, err := prolog.StringTermToBytes(value[0], encodingAtom, env) + str, err := prolog.TextTermToString(value[0], env) if err != nil { return nil, err } - result, err := prolog.BytesToCodepointListTerm(bs, prolog.AtomText, env) - if err != nil { - return nil, err + + switch encoding { + case prolog.AtomText: + return []engine.Term{prolog.StringToByteListTerm(str)}, nil + case prolog.AtomOctet: + term, err := prolog.StringToOctetListTerm(str, env) + if err != nil { + return nil, err + } + return []engine.Term{term}, nil + default: + bs, err := encode(value[0], str, encoding, env) + if err != nil { + return nil, err + } + + return []engine.Term{prolog.BytesToByteListTerm(bs)}, nil } - return []engine.Term{result}, nil } backwardConverter := func(value []engine.Term, options engine.Term, env *engine.Env) ([]engine.Term, error) { - if _, err := prolog.AssertList(env, value[0]); err != nil { - return nil, err - } - bs, err := prolog.StringTermToBytes(value[0], prolog.AtomText, env) - if err != nil { - return nil, err - } - result, err := prolog.BytesToAtomListTerm(bs, encodingAtom, env) - if err != nil { - return nil, err + var result string + switch encoding { + case prolog.AtomText: + bs, err := prolog.ByteListTermToBytes(value[0], env) + if err != nil { + return nil, err + } + result = string(bs) + case prolog.AtomOctet: + result, err = prolog.OctetListTermToString(value[0], env) + if err != nil { + return nil, err + } + default: + bs, err := prolog.ByteListTermToBytes(value[0], env) + if err != nil { + return nil, err + } + result, err = decode(value[0], bs, encoding, env) + if err != nil { + return nil, err + } } - return []engine.Term{result}, nil + return []engine.Term{prolog.StringToCharacterListTerm(result)}, nil } - ok, env, err := prolog.UnifyFunctional([]engine.Term{str}, []engine.Term{bts}, encoding, forwardConverter, backwardConverter, env) + return prolog.UnifyFunctionalPredicate( + []engine.Term{str}, []engine.Term{bts}, encoding, forwardConverter, backwardConverter, cont, env) +} + +func decode(value engine.Term, bs []byte, encoding engine.Atom, env *engine.Env) (string, error) { + str, err := util.Decode(bs, encoding.String()) if err != nil { - return engine.Error(err) + switch { + case errors.Is(err, util.ErrInvalidCharset): + return "", engine.TypeError(prolog.AtomTypeCharset, encoding, env) + default: + return "", prolog.WithError( + engine.DomainError(prolog.ValidEncoding(encoding.String()), value, env), err, env) + } } - if !ok { - return engine.Bool(false) + return str, nil +} + +func encode(value engine.Term, str string, encoding engine.Atom, env *engine.Env) ([]byte, error) { + bs, err := util.Encode(str, encoding.String()) + if err != nil { + switch { + case errors.Is(err, util.ErrInvalidCharset): + return nil, engine.TypeError(prolog.AtomTypeCharset, encoding, env) + default: + return nil, prolog.WithError( + engine.DomainError(prolog.ValidEncoding(encoding.String()), value, env), err, env) + } } - return cont(env) + return bs, nil } diff --git a/x/logic/predicate/string_test.go b/x/logic/predicate/string_test.go index 01222564..38a84c8d 100644 --- a/x/logic/predicate/string_test.go +++ b/x/logic/predicate/string_test.go @@ -128,13 +128,13 @@ func TestReadString(t *testing.T) { 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"), + wantError: fmt.Errorf("error(type_error(stream,foo),read_string/3)"), wantSuccess: false, }, { input: "Hello World!", query: `read_string(Stream, Len, data).`, - wantError: fmt.Errorf("read_string/3: stream cannot be a variable"), + wantError: fmt.Errorf("error(instantiation_error,read_string/3)"), wantSuccess: false, }, } @@ -152,13 +152,13 @@ func TestReadString(t *testing.T) { interpreter.SetUserInput(engine.NewInputTextStream(strings.NewReader(tc.input))) err := interpreter.Compile(ctx, tc.program) - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) 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(err, ShouldEqual, nil) So(sols, ShouldNotBeNil) Convey("and the bindings should be as expected", func() { @@ -171,10 +171,10 @@ func TestReadString(t *testing.T) { got = append(got, m) } if tc.wantError != nil { - So(sols.Err(), ShouldNotBeNil) + So(sols.Err(), ShouldNotEqual, nil) So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) } else { - So(sols.Err(), ShouldBeNil) + So(sols.Err(), ShouldEqual, nil) if tc.wantSuccess { So(len(got), ShouldBeGreaterThan, 0) @@ -261,16 +261,36 @@ func TestStringBytes(t *testing.T) { query: "test.", wantSuccess: true, }, + { + program: `test :- string_bytes('ù', B, text), B == [195, 185].`, + query: "test.", + wantSuccess: true, + }, + { + program: `test :- string_bytes(S, [195, 185], text), S == "ù".`, + query: "test.", + wantSuccess: true, + }, + { + program: `test :- string_bytes('ù', B, octet), B == [249].`, + query: "test.", + wantSuccess: true, + }, + { + program: `test :- string_bytes(S, [249], octet), S == "ù".`, + query: "test.", + wantSuccess: true, + }, // error cases { query: `string_bytes(_, [202,78,229,101,111,48], foo).`, wantSuccess: false, - wantError: fmt.Errorf("error(domain_error(valid_charset,foo),string_bytes/3)"), + wantError: fmt.Errorf("error(type_error(charset,foo),string_bytes/3)"), }, { query: `string_bytes(_, [202,78,400,101,111,48], latin2).`, wantSuccess: false, - wantError: fmt.Errorf("error(domain_error(valid_byte(400),[202,78,400,101,111,48]),string_bytes/3)"), + wantError: fmt.Errorf("error(type_error(byte,400),string_bytes/3)"), }, { query: `string_bytes([a, 97, p], B, latin2).`, @@ -295,7 +315,7 @@ func TestStringBytes(t *testing.T) { { query: `string_bytes(foo(bar), _, utf8).`, wantSuccess: false, - wantError: fmt.Errorf("error(type_error(character_code,foo(bar)),string_bytes/3)"), + wantError: fmt.Errorf("error(type_error(text,foo(bar)),string_bytes/3)"), }, { query: `string_bytes(_, foo(bar), utf8).`, diff --git a/x/logic/predicate/uri.go b/x/logic/predicate/uri.go index eced3453..32eb8b48 100644 --- a/x/logic/predicate/uri.go +++ b/x/logic/predicate/uri.go @@ -6,8 +6,6 @@ import ( "net/url" "github.com/ichiban/prolog/engine" - - "github.com/okp4/okp4d/x/logic/prolog" ) type Component string @@ -179,13 +177,15 @@ func URIEncoded(vm *engine.VM, component, decoded, encoded engine.Term, cont eng switch e := env.Resolve(encoded).(type) { case engine.Variable: - return engine.Unify(vm, encoded, prolog.StringToTerm(dec), cont, env) + var r engine.Term = engine.NewAtom(dec) + return engine.Unify(vm, encoded, r, cont, env) case engine.Atom: enc, err := comp.Unescape(e.String()) if err != nil { return engine.Error(fmt.Errorf("uri_encoded/3: %w", err)) } - return engine.Unify(vm, decoded, prolog.StringToTerm(enc), cont, env) + var r engine.Term = engine.NewAtom(enc) + return engine.Unify(vm, decoded, r, cont, env) default: return engine.Error(fmt.Errorf("uri_encoded/3: invalid encoded type: %T, should be Variable or Atom", e)) } diff --git a/x/logic/prolog/assert.go b/x/logic/prolog/assert.go index 2dcf2486..2c54d120 100644 --- a/x/logic/prolog/assert.go +++ b/x/logic/prolog/assert.go @@ -2,6 +2,7 @@ package prolog import ( "strings" + "unicode/utf8" "github.com/ichiban/prolog/engine" "github.com/samber/lo" @@ -64,14 +65,14 @@ func IsCompound(term engine.Term) bool { return ok } -// IsFullyInstantiated returns true if the given term is fully instantiated. -func IsFullyInstantiated(term engine.Term, env *engine.Env) bool { +// 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 !IsFullyInstantiated(term.Arg(i), env) { + if !IsGround(term.Arg(i), env) { return false } } @@ -81,47 +82,106 @@ func IsFullyInstantiated(term engine.Term, env *engine.Env) bool { } } -func AreFullyInstantiated(terms []engine.Term, env *engine.Env) bool { - _, ok := lo.Find(terms, func(t engine.Term) bool { - return IsFullyInstantiated(t, env) +func AreGround(terms []engine.Term, env *engine.Env) bool { + return lo.EveryBy(terms, func(t engine.Term) bool { + return IsGround(t, env) }) +} - return ok +// 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 + } + 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) { - if t, ok := env.Resolve(t).(engine.Atom); ok { + _, err := AssertIsGround(env, t) + if err != nil { + return AtomEmpty, err + } + if t, ok := t.(engine.Atom); ok { return t, nil } - return AtomEmpty, engine.TypeError(AtomAtom, t, env) + return AtomEmpty, engine.TypeError(AtomTypeAtom, t, env) } -// AssertCharacterCode resolves a term and attempts to convert it into an engine.Integer if possible. +// 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) (engine.Integer, error) { - if t, ok := env.Resolve(t).(engine.Integer); ok { - return t, nil +func AssertCharacterCode(env *engine.Env, t engine.Term) (rune, error) { + _, err := AssertIsGround(env, t) + if err != nil { + return 0, err } - return 0, engine.TypeError(AtomCharacterCode, t, env) + + if t, ok := t.(engine.Integer); ok { + if t >= 0 && t <= utf8.MaxRune { + return rune(t), nil + } + } + + return 0, engine.TypeError(AtomTypeCharacterCode, t, 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) (engine.Atom, error) { - if t, ok := env.Resolve(t).(engine.Atom); ok { - return t, nil +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()) + if len(runes) == 1 { + return runes[0], nil + } } - return AtomEmpty, engine.TypeError(AtomCharacter, t, env) + return utf8.RuneError, engine.TypeError(AtomTypeCharacter, t, 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 + } + } + return 0, engine.TypeError(AtomTypeByte, t, 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.Compound, error) { - if t, ok := env.Resolve(t).(engine.Compound); ok && IsList(t) { +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 } - return nil, engine.TypeError(AtomList, t, env) + return nil, engine.TypeError(AtomTypeList, t, 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) + 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 + } + + return nil, nil, engine.TypeError(AtomTypePair, t, env) } diff --git a/x/logic/prolog/assert_test.go b/x/logic/prolog/assert_test.go index 0ce42442..7119e7e6 100644 --- a/x/logic/prolog/assert_test.go +++ b/x/logic/prolog/assert_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/ichiban/prolog/engine" "github.com/samber/lo" . "github.com/smartystreets/goconvey/convey" @@ -180,3 +181,58 @@ 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() + } + + Convey("Given a test cases", t, func() { + cases := []struct { + name string + terms []engine.Term + expected bool + }{ + { + name: "all terms are ground", + terms: []engine.Term{groundTerm("a"), groundTerm("b")}, + expected: true, + }, + { + name: "one term is not ground", + terms: []engine.Term{groundTerm("a"), nonGroundTerm()}, + expected: false, + }, + { + name: "no terms", + terms: []engine.Term{}, + expected: true, + }, + { + name: "no terms (2)", + terms: []engine.Term{AtomEmptyList}, + expected: true, + }, + } + + Convey("and an environment", func() { + env := engine.NewEnv() + + 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 := AreGround(tc.terms, env) + + Convey("Then it should return the expected output", func() { + So(result, ShouldEqual, tc.expected) + }) + }) + }) + } + }) + }) +} diff --git a/x/logic/prolog/atom.go b/x/logic/prolog/atom.go index 24740523..c940759e 100644 --- a/x/logic/prolog/atom.go +++ b/x/logic/prolog/atom.go @@ -7,16 +7,6 @@ var ( AtomAs = engine.NewAtom("as") // AtomAt are terms with principal functor (@)/1 used to represent special values in json objects. AtomAt = engine.NewAtom("@") - // AtomAtom is the term used to indicate the atom atom. - AtomAtom = engine.NewAtom("atom") - // AtomCharacter is the term used to indicate the character type. - AtomCharacter = engine.NewAtom("character") - // AtomCharacterCode is the term used to indicate the character code type. - AtomCharacterCode = engine.NewAtom("character_code") - // AtomCharset is the term used to indicate the charset encoding type option. - AtomCharset = engine.NewAtom("charset") - // AtomCompound is the term used to indicate the atom compound. - AtomCompound = engine.NewAtom("compound") // AtomDot is the term used to represent the dot in a list. AtomDot = engine.NewAtom(".") // AtomEmpty is the term used to represent empty. @@ -35,8 +25,6 @@ var ( AtomHex = engine.NewAtom("hex") // AtomJSON are terms with principal functor json/1 used to represent json objects. AtomJSON = engine.NewAtom("json") - // AtomList is the term used to indicate the atom list. - AtomList = engine.NewAtom("list") // AtomNull is the term null. AtomNull = engine.NewAtom("null") // AtomOctet is the term used to indicate the byte encoding type option. diff --git a/x/logic/prolog/byte.go b/x/logic/prolog/byte.go new file mode 100644 index 00000000..6e40bc1a --- /dev/null +++ b/x/logic/prolog/byte.go @@ -0,0 +1,32 @@ +package prolog + +import ( + "github.com/ichiban/prolog/engine" +) + +// ByteListTermToBytes try to convert a given list of bytes into native golang []byte. +func ByteListTermToBytes(term engine.Term, env *engine.Env) ([]byte, error) { + iter, err := ListIterator(term, env) + if err != nil { + return nil, err + } + var bs []byte + + for iter.Next() { + b, err := AssertByte(env, iter.Current()) + if err != nil { + return nil, err + } + bs = append(bs, b) + } + return bs, nil +} + +// BytesToByteListTerm try to convert a given golang []byte into a list of bytes. +func BytesToByteListTerm(in []byte) engine.Term { + terms := make([]engine.Term, 0, len(in)) + for _, b := range in { + terms = append(terms, engine.Integer(b)) + } + return engine.List(terms...) +} diff --git a/x/logic/util/context.go b/x/logic/prolog/context.go similarity index 62% rename from x/logic/util/context.go rename to x/logic/prolog/context.go index eabc2fe5..ac2ad02b 100644 --- a/x/logic/util/context.go +++ b/x/logic/prolog/context.go @@ -1,15 +1,16 @@ -package util +package prolog import ( "context" - "fmt" + + "github.com/ichiban/prolog/engine" sdk "github.com/cosmos/cosmos-sdk/types" ) // UnwrapSDKContext retrieves a Context from a context.Context instance // attached with WrapSDKContext. -func UnwrapSDKContext(ctx context.Context) (sdk.Context, error) { +func UnwrapSDKContext(ctx context.Context, env *engine.Env) (sdk.Context, error) { if sdkCtx, ok := ctx.(sdk.Context); ok { return sdkCtx, nil } @@ -17,5 +18,5 @@ func UnwrapSDKContext(ctx context.Context) (sdk.Context, error) { return sdkCtx, nil } - return sdk.Context{}, fmt.Errorf("no sdk.Context found in context") + return sdk.Context{}, ResourceError(ResourceContext(), env) } diff --git a/x/logic/prolog/encoding.go b/x/logic/prolog/encoding.go deleted file mode 100644 index 5a2fb500..00000000 --- a/x/logic/prolog/encoding.go +++ /dev/null @@ -1,73 +0,0 @@ -package prolog - -import ( - "bytes" - "unicode/utf8" - - "github.com/ichiban/prolog/engine" - "golang.org/x/net/html/charset" -) - -// Decode converts a byte slice from a specified encoding. -// Decode function is the reverse of encode function. -func Decode(bs []byte, label engine.Atom, env *engine.Env) ([]byte, error) { - switch label { - case AtomEmpty, AtomText: - return bs, nil - case AtomOctet: - var buffer bytes.Buffer - for _, b := range bs { - buffer.WriteRune(rune(b)) - } - return buffer.Bytes(), nil - default: - encoding, _ := charset.Lookup(label.String()) - if encoding == nil { - return nil, engine.DomainError(ValidCharset(), label, env) - } - result, err := encoding.NewDecoder().Bytes(bs) - if err != nil { - culprit := BytesToCodepointListTermWithDefault(bs, env) - return nil, engine.DomainError(ValidEncoding(label.String(), err), culprit, env) - } - return result, nil - } -} - -// Encode converts a byte slice to a specified encoding. -// -// In case of: -// - empty encoding label or 'text', return the original bytes without modification. -// - 'octet', decode the bytes as unicode code points and return the resulting bytes. If a code point is greater than -// 0xff, an error is returned. -// - any other encoding label, convert the bytes to the specified encoding. -func Encode(bs []byte, label engine.Atom, env *engine.Env) ([]byte, error) { - switch label { - case AtomEmpty, AtomText: - return bs, nil - case AtomOctet: - result := make([]byte, 0, len(bs)) - for i := 0; i < len(bs); { - runeValue, width := utf8.DecodeRune(bs[i:]) - - if runeValue > 0xff { - culprit := BytesToCodepointListTermWithDefault(bs, env) - return nil, engine.DomainError(ValidByte(int64(runeValue)), culprit, env) - } - result = append(result, byte(runeValue)) - i += width - } - return result, nil - default: - encoding, _ := charset.Lookup(label.String()) - if encoding == nil { - return nil, engine.DomainError(ValidCharset(), label, env) - } - result, err := encoding.NewEncoder().Bytes(bs) - if err != nil { - culprit := BytesToCodepointListTermWithDefault(bs, env) - return nil, engine.DomainError(ValidEncoding(label.String(), err), culprit, env) - } - return result, nil - } -} diff --git a/x/logic/prolog/error.go b/x/logic/prolog/error.go index ea9c30d4..12d45e84 100644 --- a/x/logic/prolog/error.go +++ b/x/logic/prolog/error.go @@ -3,30 +3,144 @@ package prolog import "github.com/ichiban/prolog/engine" var ( - AtomDomainError = engine.NewAtom("domain_error") // AtomDomainError is the atom domain_error. - AtomValidByte = engine.NewAtom("valid_byte") // AtomValidByte is the atom valid_byte. - AtomValidCharset = engine.NewAtom("valid_charset") // AtomValidCharset is the atom valid_charset. - AtomValidCharacterCode = engine.NewAtom("valid_character_code") // AtomValidCharacterCode is the atom valid_character_code. - AtomValidEncoding = engine.NewAtom("valid_encoding") // AtomValidEncoding is the atom valid_encoding. - AtomValidHexDigit = engine.NewAtom("valid_hex_digit") // AtomValidHexDigit is the atom valid_hex_digit. + // AtomTypeAtom is the term used to represent the atom type. + AtomTypeAtom = engine.NewAtom("atom") + // AtomTypeByte is the term used to represent the byte type. + AtomTypeByte = engine.NewAtom("byte") + // AtomTypeCharacter is the term used to represent the character type. + // A character type is a single character (e.g. 'a') identified in the Unicode standard. + AtomTypeCharacter = engine.NewAtom("character") + // AtomTypeCharacterCode is the term used to represent the character code type. + // A character code type is a single character identified by its code point (a number) in the Unicode standard. + AtomTypeCharacterCode = engine.NewAtom("character_code") + // AtomTypeCharset is the term used to represent the charset type. + // A charset type is a set of characters identified by its name in the IANA standard. + AtomTypeCharset = engine.NewAtom("charset") + // AtomTypeCryptographicAlgorithm is the term used to represent the cryptographic algorithm type. + AtomTypeCryptographicAlgorithm = engine.NewAtom("cryptographic_algorithm") + // AtomTypeDID is the term used to represent the DID type. + // DID type is a compound with the name "did" and 5 arguments which are the components of the DID, in the form of + // did(Method, ID, Path, Query, Fragment). + AtomTypeDID = engine.NewAtom("did") + // AtomTypeHashAlgorithm is the term used to represent the hash algorithm type. + AtomTypeHashAlgorithm = engine.NewAtom("hash_algorithm") + // AtomTypeIOMode is the term used to represent the IO mode type. + // An IO mode specifies the direction of the IO operation represented as an atom. + // Possible values are: read, write, append. + AtomTypeIOMode = engine.NewAtom("io_mode") + // AtomTypeStream is the term used to represent the stream type. + AtomTypeStream = engine.NewAtom("stream") + // AtomTypeText is the term used to represent the text type. + // A text type is either an atom, a list of characters or a list of character codes. + AtomTypeText = AtomText + // AtomTypeList is the term used to represent the list type. + AtomTypeList = engine.NewAtom("list") + // AtomTypeNumber is the term used to represent the number type. + AtomTypeNumber = engine.NewAtom("number") + // AtomTypeOption is the term used to represent the option type. + // An option is a compound with the name of the option as functor and one term argument which is + // the value of the option. For instance: opt(v). + AtomTypeOption = engine.NewAtom("option") + // AtomTypePair is the term used to indicate the pair type. + AtomTypePair = engine.NewAtom("pair") + // AtomTypeJSON is the term used to indicate the json type. + AtomTypeJSON = AtomJSON ) -func ValidCharset() engine.Term { - return AtomValidCharset +var ( + // AtomValidEncoding is the atom denoting a valid encoding. + // The valid encoding atom is a compound with the name of the encoding which is a valid encoding with + // regard to the predicate where it is used. + // + // For instance: valid_encoding(utf8), valid_encoding(hex). + AtomValidEncoding = engine.NewAtom("encoding") + // AtomValidEmptyList is the atom denoting a valid empty list. + AtomValidEmptyList = engine.NewAtom("empty_list") +) + +func ValidEncoding(encoding string) engine.Term { + return AtomValidEncoding.Apply(engine.NewAtom(encoding)) } -func ValidEncoding(encoding string, cause error) engine.Term { - return AtomValidEncoding.Apply(engine.NewAtom(encoding), StringToStringTerm(cause.Error())) +func ValidEmptyList() engine.Term { + return AtomValidEmptyList } -func ValidByte(v int64) engine.Term { - return AtomValidByte.Apply(engine.Integer(v)) +// ResourceError creates a new resource error exception. +// TODO: to remove once engine.resourceError() is public. +func ResourceError(resource engine.Term, env *engine.Env) engine.Exception { + return engine.NewException( + AtomError.Apply( + engine.NewAtom("resource_error").Apply(resource), + engine.NewAtom("unknown")), env) } -func ValidCharacterCode(c string) engine.Term { - return AtomValidCharacterCode.Apply(engine.NewAtom(c)) +var ( + // AtomResourceContext is the atom denoting the "context" resource. + // The context resource is a contextual data that contains all information needed to + // process a request and produce a response with the blockchain. + AtomResourceContext = engine.NewAtom("resource_context") + // AtomResourceModule is the atom denoting the "module" resource. + // The module resource is the representation of the module with which the interaction is made. + // The module resource is denoted as a compound with the name of the module. + AtomResourceModule = engine.NewAtom("resource_module") +) + +func ResourceContext() engine.Term { + return AtomResourceContext +} + +func ResourceModule(module string) engine.Term { + return AtomResourceModule.Apply(engine.NewAtom(module)) +} + +// PermissionError creates a new permission error exception. +// TODO: to remove once engine.permissionError() is public. +func PermissionError(operation, permissionType, culprit engine.Term, env *engine.Env) engine.Exception { + return engine.NewException( + AtomError.Apply( + engine.NewAtom("permission_error").Apply( + operation, permissionType, culprit, + ), + engine.NewAtom("unknown")), env) +} + +var AtomOperationInput = engine.NewAtom("input") + +var AtomPermissionTypeStream = engine.NewAtom("stream") + +// ExistenceError creates a new existence error exception. +// TODO: to remove once engine.existenceError() is public. +func ExistenceError(objectType, culprit engine.Term, env *engine.Env) engine.Exception { + return engine.NewException( + AtomError.Apply( + engine.NewAtom("existence_error").Apply( + objectType, culprit, + ), + engine.NewAtom("unknown")), env) +} + +var AtomObjectTypeSourceSink = engine.NewAtom("source_sink") + +// UnexpectedError creates a new unexpected error exception. +// TODO: to remove once engine.syntaxError() is public. +func SyntaxError(err error, env *engine.Env) engine.Exception { + return engine.NewException( + AtomError.Apply( + engine.NewAtom("syntax_error").Apply(StringToCharacterListTerm(err.Error())), + engine.NewAtom("unknown")), env) } -func ValidHexDigit(d string) engine.Term { - return AtomValidHexDigit.Apply(engine.NewAtom(d)) +// WithError adds the error term to the exception term if possible. +// TODO: wait for ichiban/prolog to offer a better way to do this. +func WithError(exception engine.Exception, err error, env *engine.Env) engine.Exception { + if term, ok := exception.Term().(engine.Compound); ok { + if term.Functor() == AtomError && term.Arity() == 2 { + return engine.NewException(term.Functor().Apply( + term.Arg(0), + StringToCharacterListTerm(err.Error()), + term.Arg(1)), env) + } + } + return exception } diff --git a/x/logic/prolog/hex.go b/x/logic/prolog/hex.go new file mode 100644 index 00000000..263ae6ba --- /dev/null +++ b/x/logic/prolog/hex.go @@ -0,0 +1,21 @@ +package prolog + +import ( + "encoding/hex" + + "github.com/ichiban/prolog/engine" +) + +// 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) + if err != nil { + return nil, err + } + + result, err := hex.DecodeString(v.String()) + if err != nil { + err = WithError(engine.DomainError(ValidEncoding("hex"), term, env), err, env) + } + return result, err +} diff --git a/x/logic/prolog/hex_test.go b/x/logic/prolog/hex_test.go new file mode 100644 index 00000000..9d21f33c --- /dev/null +++ b/x/logic/prolog/hex_test.go @@ -0,0 +1,59 @@ +package prolog + +import ( + "fmt" + "testing" + + "github.com/ichiban/prolog/engine" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestTermHexToBytes(t *testing.T) { + Convey("Given a test cases", t, func() { + cases := []struct { + term engine.Term + result []byte + wantSuccess bool + wantError error + }{ + { // If no option, by default, given term is in hexadecimal format. + term: engine.NewAtom("486579202120596f752077616e7420746f20736565207468697320746578742c20776f6e64657266756c21"), + result: []byte{72, 101, 121, 32, 33, 32, 89, 111, 117, 32, 119, 97, 110, 116, 32, 116, 111, 32, 115, 101, 101, 32, 116, 104, 105, 115, 32, 116, 101, 120, 116, 44, 32, 119, 111, 110, 100, 101, 114, 102, 117, 108, 33}, //nolint:lll + wantSuccess: true, + }, + { + term: engine.NewAtom("foo").Apply(engine.NewAtom("bar")), + result: nil, + wantSuccess: false, + wantError: fmt.Errorf("error(type_error(atom,foo(bar)),root)"), + }, + } + for nc, tc := range cases { + Convey(fmt.Sprintf("Given the term #%d: %s", nc, tc.term), func() { + Convey("when converting hex term to bytes", func() { + env := engine.NewEnv() + result, err := TermHexToBytes(tc.term, env) + + if tc.wantSuccess { + Convey("then no error should be thrown", func() { + So(err, ShouldBeNil) + + Convey("and result should be as expected", func() { + So(result, ShouldResemble, tc.result) + }) + }) + } else { + Convey("then error should occurs", func() { + So(err, ShouldNotEqual, nil) + + Convey("and should be as expected", func() { + So(err.Error(), ShouldEqual, tc.wantError.Error()) + }) + }) + } + }) + }) + } + }) +} diff --git a/x/logic/prolog/json.go b/x/logic/prolog/json.go index 75a15789..f38242d0 100644 --- a/x/logic/prolog/json.go +++ b/x/logic/prolog/json.go @@ -1,8 +1,6 @@ package prolog import ( - "fmt" - "github.com/ichiban/prolog/engine" ) @@ -27,7 +25,7 @@ func JSONEmptyArray() engine.Term { return AtomAt.Apply(AtomEmptyArray) } -// ExtractJSONTerm is an utility function that would extract all attribute of a JSON object +// ExtractJSONTerm is a utility function that would extract all attribute of a JSON object // that is represented in prolog with the `json` atom. // // This function will ensure the json atom follow our json object representation in prolog. @@ -41,34 +39,27 @@ func JSONEmptyArray() engine.Term { // That give a JSON object: `{"foo": "bar"}` // Returns the map of all attributes with its term value. func ExtractJSONTerm(term engine.Compound, env *engine.Env) (map[string]engine.Term, error) { - if term.Functor() != AtomJSON { - return nil, fmt.Errorf("invalid functor %s. Expected %s", term.Functor().String(), AtomJSON.String()) - } else if term.Arity() != 1 { - return nil, fmt.Errorf("invalid compound arity : %d but expected %d", term.Arity(), 1) + if term.Functor() != AtomJSON || term.Arity() != 1 { + return nil, engine.TypeError(AtomTypeJSON, term, env) } - list := term.Arg(0) - switch l := env.Resolve(list).(type) { - case engine.Compound: - iter := engine.ListIterator{ - List: l, - Env: env, + iter, err := ListIterator(term.Arg(0), env) + if err != nil { + return nil, err + } + terms := make(map[string]engine.Term, 0) + for iter.Next() { + current := iter.Current() + pair, ok := current.(engine.Compound) + if !ok || pair.Functor() != AtomPair || pair.Arity() != 2 { + return nil, engine.TypeError(AtomTypePair, current, env) } - terms := make(map[string]engine.Term, 0) - for iter.Next() { - pair, ok := env.Resolve(iter.Current()).(engine.Compound) - if !ok || pair.Functor() != AtomPair || pair.Arity() != 2 { - return nil, fmt.Errorf("json attributes should be a pair") - } - key, ok := env.Resolve(pair.Arg(0)).(engine.Atom) - if !ok { - return nil, fmt.Errorf("first pair arg should be an atom") - } - terms[key.String()] = pair.Arg(1) + key, ok := pair.Arg(0).(engine.Atom) + if !ok { + return nil, engine.TypeError(AtomTypeAtom, pair.Arg(0), env) } - return terms, nil - default: - return nil, fmt.Errorf("json compound should contains one list, give %T", l) + terms[key.String()] = pair.Arg(1) } + return terms, nil } diff --git a/x/logic/prolog/json_test.go b/x/logic/prolog/json_test.go index d6bb750f..c8cbaf58 100644 --- a/x/logic/prolog/json_test.go +++ b/x/logic/prolog/json_test.go @@ -20,17 +20,17 @@ func TestExtractJsonTerm(t *testing.T) { { compound: engine.NewAtom("foo").Apply(engine.NewAtom("bar")).(engine.Compound), wantSuccess: false, - wantError: fmt.Errorf("invalid functor foo. Expected json"), + wantError: fmt.Errorf("error(type_error(json,foo(bar)),root)"), }, { compound: engine.NewAtom("json").Apply(engine.NewAtom("bar"), engine.NewAtom("foobar")).(engine.Compound), wantSuccess: false, - wantError: fmt.Errorf("invalid compound arity : 2 but expected 1"), + wantError: fmt.Errorf("error(type_error(json,json(bar,foobar)),root)"), }, { compound: engine.NewAtom("json").Apply(engine.NewAtom("bar")).(engine.Compound), wantSuccess: false, - wantError: fmt.Errorf("json compound should contains one list, give engine.Atom"), + wantError: fmt.Errorf("error(type_error(list,bar),root)"), }, { compound: AtomJSON.Apply(engine.List(AtomPair.Apply(engine.NewAtom("foo"), engine.NewAtom("bar")))).(engine.Compound), @@ -42,19 +42,19 @@ func TestExtractJsonTerm(t *testing.T) { { compound: AtomJSON.Apply(engine.List(engine.NewAtom("foo"), engine.NewAtom("bar"))).(engine.Compound), wantSuccess: false, - wantError: fmt.Errorf("json attributes should be a pair"), + wantError: fmt.Errorf("error(type_error(pair,foo),root)"), }, { compound: AtomJSON.Apply(engine.List(AtomPair.Apply(engine.Integer(10), engine.NewAtom("bar")))).(engine.Compound), wantSuccess: false, - wantError: fmt.Errorf("first pair arg should be an atom"), + wantError: fmt.Errorf("error(type_error(atom,10),root)"), }, } for nc, tc := range cases { Convey(fmt.Sprintf("Given the term compound #%d: %s", nc, tc.compound), func() { Convey("when extract json term", func() { - env := engine.Env{} - result, err := ExtractJSONTerm(tc.compound, &env) + env := engine.NewEnv() + result, err := ExtractJSONTerm(tc.compound, env) if tc.wantSuccess { Convey("then no error should be thrown", func() { @@ -67,10 +67,11 @@ func TestExtractJsonTerm(t *testing.T) { }) } else { Convey("then error should occurs", func() { - So(err, ShouldNotBeNil) + So(err, ShouldNotEqual, nil) + So(tc.wantError, ShouldNotBeNil) Convey("and should be as expected", func() { - So(err, ShouldResemble, tc.wantError) + So(err.Error(), ShouldEqual, tc.wantError.Error()) }) }) } diff --git a/x/logic/prolog/list.go b/x/logic/prolog/list.go index afb2a237..1327fcda 100644 --- a/x/logic/prolog/list.go +++ b/x/logic/prolog/list.go @@ -10,3 +10,11 @@ func ListHead(list engine.Term, env *engine.Env) engine.Term { } return iter.Current() } + +// ListIterator returns a list iterator. +func ListIterator(list engine.Term, env *engine.Env) (engine.ListIterator, error) { + if !IsList(env.Resolve(list)) { + 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 4e178d60..9bfcaffe 100644 --- a/x/logic/prolog/option.go +++ b/x/logic/prolog/option.go @@ -1,8 +1,6 @@ package prolog import ( - "fmt" - "github.com/ichiban/prolog/engine" ) @@ -12,35 +10,40 @@ import ( // The options are either a list of options or an option. // If no option is found nil is returned. func GetOption(name engine.Atom, options engine.Term, env *engine.Env) (engine.Term, error) { - extractOption := func(term engine.Term) (engine.Term, error) { - switch v := term.(type) { + extractOption := func(opt engine.Term) (engine.Term, error) { + switch v := opt.(type) { case engine.Compound: if v.Functor() == name { if v.Arity() != 1 { - return nil, fmt.Errorf("invalid arity for compound '%s': %d but expected 1", name, v.Arity()) + return nil, engine.TypeError(AtomTypeOption, opt, env) } return v.Arg(0), nil } return nil, nil + case engine.Atom: + if v == AtomEmptyList { + return nil, nil + } case nil: return nil, nil - default: - return nil, fmt.Errorf("invalid term '%s' - expected engine.Compound but got %T", term, v) } + return nil, engine.TypeError(AtomTypeOption, opt, env) } resolvedTerm := env.Resolve(options) - - if IsEmptyList(resolvedTerm) { + if resolvedTerm == nil { return nil, nil } if IsList(resolvedTerm) { - iter := engine.ListIterator{List: resolvedTerm, Env: env} + iter, err := ListIterator(resolvedTerm, env) + if err != nil { + return nil, err + } for iter.Next() { - opt := env.Resolve(iter.Current()) + opt := iter.Current() term, err := extractOption(opt) if err != nil { @@ -51,7 +54,6 @@ func GetOption(name engine.Atom, options engine.Term, env *engine.Env) (engine.T return term, nil } } - return nil, nil } return extractOption(resolvedTerm) diff --git a/x/logic/prolog/option_test.go b/x/logic/prolog/option_test.go index 9a22e133..ed8de0f7 100644 --- a/x/logic/prolog/option_test.go +++ b/x/logic/prolog/option_test.go @@ -121,7 +121,7 @@ func TestGetOption(t *testing.T) { option: engine.NewAtom("foo"), options: engine.NewAtom("foo"), wantResult: nil, - wantError: fmt.Errorf("invalid term 'foo' - expected engine.Compound but got engine.Atom"), + wantError: fmt.Errorf("error(type_error(option,foo),root)"), }, { option: engine.NewAtom("foo"), @@ -130,7 +130,7 @@ func TestGetOption(t *testing.T) { engine.NewAtom("hey"), engine.NewAtom("foo").Apply(engine.NewAtom("bar"))), wantResult: nil, - wantError: fmt.Errorf("invalid term 'hey' - expected engine.Compound but got engine.Atom"), + wantError: fmt.Errorf("error(type_error(option,hey),root)"), }, { option: engine.NewAtom("foo"), @@ -139,18 +139,18 @@ func TestGetOption(t *testing.T) { engine.NewAtom("hey").Apply(engine.NewAtom("hoo")), engine.NewAtom("foo").Apply(engine.NewAtom("bar1"), engine.NewAtom("bar2"))), wantResult: nil, - wantError: fmt.Errorf("invalid arity for compound 'foo': 2 but expected 1"), + wantError: fmt.Errorf("error(type_error(option,foo(bar1,bar2)),root)"), }, } for nc, tc := range cases { Convey(fmt.Sprintf("Given the term option #%d: %s", nc, tc.option), func() { Convey("when getting option", func() { - env := engine.Env{} - result, err := GetOption(tc.option, tc.options, &env) + env := engine.NewEnv() + result, err := GetOption(tc.option, tc.options, env) if tc.wantError == nil { Convey("then no error should be thrown", func() { - So(err, ShouldBeNil) + So(err, ShouldEqual, nil) Convey("and result should be as expected", func() { So(result, ShouldEqual, tc.wantResult) @@ -161,10 +161,10 @@ func TestGetOption(t *testing.T) { So(result, ShouldEqual, tc.wantResult) }) Convey("then error should occurs", func() { - So(err, ShouldNotBeNil) + So(err, ShouldNotEqual, nil) Convey("and should be as expected", func() { - So(err, ShouldBeError, tc.wantError) + So(err.Error(), ShouldEqual, tc.wantError.Error()) }) }) } diff --git a/x/logic/prolog/term.go b/x/logic/prolog/term.go deleted file mode 100644 index 2b0d59ff..00000000 --- a/x/logic/prolog/term.go +++ /dev/null @@ -1,173 +0,0 @@ -package prolog - -import ( - "encoding/hex" - "unicode/utf8" - - "github.com/ichiban/prolog/engine" -) - -// Tuple is a predicate which unifies the given term with a tuple of the given arity. -func Tuple(args ...engine.Term) engine.Term { - return engine.Atom(0).Apply(args...) -} - -// ListOfIntegers converts a list of integers to a term. -func ListOfIntegers(args ...int) engine.Term { - terms := make([]engine.Term, 0, len(args)) - for _, arg := range args { - terms = append(terms, engine.Integer(arg)) - } - return engine.List(terms...) -} - -// StringToTerm converts a string to a term. -// TODO: this function should be removed. -func StringToTerm(s string) engine.Term { - return engine.NewAtom(s) -} - -// StringToStringTerm converts a string to a term representing a list of characters. -func StringToStringTerm(s string) engine.Term { - terms := make([]engine.Term, 0, utf8.RuneCountInString(s)) - for _, c := range s { - terms = append(terms, engine.NewAtom(string(c))) - } - - return engine.List(terms...) -} - -// BytesToCodepointListTerm try to convert a given golang []byte into a list of codepoints. -func BytesToCodepointListTerm(in []byte, encoding engine.Atom, env *engine.Env) (engine.Term, error) { - out, err := Decode(in, encoding, env) - if err != nil { - return nil, err - } - - terms := make([]engine.Term, 0, len(out)) - for _, b := range out { - terms = append(terms, engine.Integer(b)) - } - return engine.List(terms...), nil -} - -// BytesToCodepointListTermWithDefault is like the BytesToCodepointListTerm function but with a default encoding. -// This function panics if the conversion fails, which can't happen with the default encoding. -func BytesToCodepointListTermWithDefault(in []byte, env *engine.Env) engine.Term { - term, err := BytesToCodepointListTerm(in, AtomEmpty, env) - if err != nil { - panic(err) - } - return term -} - -// BytesToAtomListTerm try to convert a given golang []byte into a list of atoms, one for each character. -func BytesToAtomListTerm(in []byte, encoding engine.Atom, env *engine.Env) (engine.Term, error) { - out, err := Decode(in, encoding, env) - if err != nil { - return nil, err - } - str := string(out) - terms := make([]engine.Term, 0, len(str)) - for _, c := range str { - terms = append(terms, engine.NewAtom(string(c))) - } - return engine.List(terms...), nil -} - -// StringTermToBytes try to convert a given string into native golang []byte. -// String is an instantiated term which represents text as an atom, string, list of character codes or list or characters. -// Encoding is the supported encoding type: -// - empty encoding or 'text', return the original bytes without modification. -// - 'octet', decode the bytes as unicode code points and return the resulting bytes. If a code point is greater than -// 0xff, an error is returned. -// - any other encoding label, convert the bytes to the specified encoding. -// -// The mapping from encoding labels to encodings is defined at https://encoding.spec.whatwg.org/. -func StringTermToBytes(str engine.Term, encoding engine.Atom, env *engine.Env) (bs []byte, err error) { - v := env.Resolve(str) - switch v := v.(type) { - case engine.Atom: - if bs, err = Encode([]byte(v.String()), encoding, env); err != nil { - return nil, err - } - return bs, nil - case engine.Compound: - if IsList(v) { - head := ListHead(v, env) - if head == nil { - return make([]byte, 0), nil - } - - switch head.(type) { - case engine.Atom: - if bs, err = characterListToBytes(v, env); err != nil { - return bs, err - } - case engine.Integer: - if bs, err = characterCodeListToBytes(v, env); err != nil { - return bs, err - } - default: - return nil, engine.TypeError(AtomCharacterCode, v, env) - } - return Encode(bs, encoding, env) - } - return nil, engine.TypeError(AtomCharacterCode, str, env) - default: - return nil, engine.TypeError(AtomText, str, env) - } -} - -func characterListToBytes(str engine.Compound, env *engine.Env) ([]byte, error) { - iter := engine.ListIterator{List: str, Env: env} - bs := make([]byte, 0) - - for iter.Next() { - e, err := AssertCharacter(env, iter.Current()) - if err != nil { - return bs, err - } - rs := []rune(e.String()) - if len(rs) != 1 { - return bs, engine.DomainError(ValidCharacterCode(e.String()), str, env) - } - - bs = append(bs, []byte(e.String())...) - } - return bs, nil -} - -func characterCodeListToBytes(str engine.Compound, env *engine.Env) ([]byte, error) { - iter := engine.ListIterator{List: str, Env: env} - bs := make([]byte, 0) - - for iter.Next() { - e, err := AssertCharacterCode(env, iter.Current()) - if err != nil { - return nil, err - } - if e < 0 || e > 255 { - return nil, engine.DomainError(ValidByte(int64(e)), str, env) - } - - bs = append(bs, byte(e)) - } - return bs, nil -} - -// 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) - if err != nil { - return nil, err - } - - src := []byte(v.String()) - result := make([]byte, hex.DecodedLen(len(src))) - _, err = hex.Decode(result, src) - if err != nil { - err = engine.DomainError(ValidEncoding("hex", err), term, env) - } - return result, err -} diff --git a/x/logic/prolog/term_test.go b/x/logic/prolog/term_test.go deleted file mode 100644 index b5c05076..00000000 --- a/x/logic/prolog/term_test.go +++ /dev/null @@ -1,193 +0,0 @@ -//nolint:lll -package prolog - -import ( - "fmt" - "testing" - - "github.com/ichiban/prolog/engine" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestTermHexToBytes(t *testing.T) { - Convey("Given a test cases", t, func() { - cases := []struct { - term engine.Term - result []byte - wantSuccess bool - wantError error - }{ - { // If no option, by default, given term is in hexadecimal format. - term: engine.NewAtom("486579202120596f752077616e7420746f20736565207468697320746578742c20776f6e64657266756c21"), - result: []byte{72, 101, 121, 32, 33, 32, 89, 111, 117, 32, 119, 97, 110, 116, 32, 116, 111, 32, 115, 101, 101, 32, 116, 104, 105, 115, 32, 116, 101, 120, 116, 44, 32, 119, 111, 110, 100, 101, 114, 102, 117, 108, 33}, - wantSuccess: true, - }, - { - term: engine.NewAtom("foo").Apply(engine.NewAtom("bar")), - result: nil, - wantSuccess: false, - wantError: fmt.Errorf("error(type_error(atom,foo(bar)),_2)"), - }, - } - for nc, tc := range cases { - Convey(fmt.Sprintf("Given the term #%d: %s", nc, tc.term), func() { - Convey("when converting hex term to bytes", func() { - env := engine.Env{} - result, err := TermHexToBytes(tc.term, &env) - - if tc.wantSuccess { - Convey("then no error should be thrown", func() { - So(err, ShouldBeNil) - - Convey("and result should be as expected", func() { - So(result, ShouldResemble, tc.result) - }) - }) - } else { - Convey("then error should occurs", func() { - So(err, ShouldNotEqual, nil) - - Convey("and should be as expected", func() { - So(err.Error(), ShouldEqual, tc.wantError.Error()) - }) - }) - } - }) - }) - } - }) -} - -func TestTermToBytes(t *testing.T) { - Convey("Given a test cases", t, func() { - cases := []struct { - term engine.Term - encoding string - result []byte - wantSuccess bool - wantError error - }{ - { - term: engine.NewAtom("foo"), - result: []byte{102, 111, 111}, - wantSuccess: true, - }, - { - term: engine.List(engine.Integer(72), engine.Integer(101), engine.Integer(121), engine.Integer(32), engine.Integer(33), engine.Integer(32), engine.Integer(89), engine.Integer(111), engine.Integer(117), engine.Integer(32), engine.Integer(119), engine.Integer(97), engine.Integer(110), engine.Integer(116), engine.Integer(32), engine.Integer(116), engine.Integer(111), engine.Integer(32), engine.Integer(115), engine.Integer(101), engine.Integer(101), engine.Integer(32), engine.Integer(116), engine.Integer(104), engine.Integer(105), engine.Integer(115), engine.Integer(32), engine.Integer(116), engine.Integer(101), engine.Integer(120), engine.Integer(116), engine.Integer(44), engine.Integer(32), engine.Integer(119), engine.Integer(111), engine.Integer(110), engine.Integer(100), engine.Integer(101), engine.Integer(114), engine.Integer(102), engine.Integer(117), engine.Integer(108), engine.Integer(33)), - result: []byte{72, 101, 121, 32, 33, 32, 89, 111, 117, 32, 119, 97, 110, 116, 32, 116, 111, 32, 115, 101, 101, 32, 116, 104, 105, 115, 32, 116, 101, 120, 116, 44, 32, 119, 111, 110, 100, 101, 114, 102, 117, 108, 33}, - wantSuccess: true, - }, - { - term: engine.List(engine.NewAtom("f"), engine.NewAtom("o"), engine.NewAtom("o")), - result: []byte{102, 111, 111}, - wantSuccess: true, - }, - { - term: engine.List(engine.NewAtom("ü")), - result: []byte{195, 188}, - wantSuccess: true, - }, - { - term: engine.List(engine.NewAtom("ü")), - encoding: "utf-8", - result: []byte{195, 188}, - wantSuccess: true, - }, - { - term: engine.List(engine.NewAtom("ü")), - encoding: "octet", - result: []byte{252}, - wantSuccess: true, - }, - { - term: engine.NewAtom("ツ"), - encoding: "utf8", - result: []byte{227, 131, 132}, - wantSuccess: true, - }, - { - term: engine.NewAtom("ツ"), - encoding: "text", - result: []byte{227, 131, 132}, - wantSuccess: true, - }, - { - term: engine.List(engine.NewAtom("ツ")), - encoding: "utf8", - result: []byte{227, 131, 132}, - wantSuccess: true, - }, - { - term: engine.List(engine.Integer(227), engine.Integer(131), engine.Integer(132)), - encoding: "utf8", - result: []byte{227, 131, 132}, - wantSuccess: true, - }, - { - term: engine.List(engine.NewAtom("ツ")), - encoding: "shift-jis", - result: []byte{131, 99}, - wantSuccess: true, - }, - { - term: engine.List(engine.Integer(227), engine.Integer(131), engine.Integer(132)), - encoding: "shift-jis", - result: []byte{131, 99}, - wantSuccess: true, - }, - { - term: engine.List(engine.NewAtom("ツ")), - encoding: "octet", - result: nil, - wantSuccess: false, - wantError: fmt.Errorf("error(domain_error(valid_byte(12484),[227,131,132]),_3)"), - }, - { - term: engine.NewAtom("foo").Apply(engine.NewAtom("bar")), - result: nil, - wantSuccess: false, - wantError: fmt.Errorf("error(type_error(character_code,foo(bar)),_4)"), - }, - { - term: engine.List(engine.NewAtom("f"), engine.NewAtom("oo")), - result: nil, - wantSuccess: false, - wantError: fmt.Errorf("error(domain_error(valid_character_code(oo),[f,oo]),_5)"), - }, - { - term: engine.NewAtom("foo"), - encoding: "foo", - result: nil, - wantSuccess: false, - wantError: fmt.Errorf("error(domain_error(valid_charset,foo),_6)"), - }, - } - for nc, tc := range cases { - Convey(fmt.Sprintf("Given the term #%d: %s", nc, tc.term), func() { - Convey("when converting string term to bytes", func() { - env := engine.Env{} - result, err := StringTermToBytes(tc.term, engine.NewAtom(tc.encoding), &env) - - if tc.wantSuccess { - Convey("then no error should be thrown", func() { - So(err, ShouldEqual, nil) - - Convey("and result should be as expected", func() { - So(result, ShouldResemble, tc.result) - }) - }) - } else { - Convey("then error should occurs", func() { - So(err, ShouldNotEqual, nil) - - Convey("and should be as expected", func() { - So(err.Error(), ShouldEqual, tc.wantError.Error()) - }) - }) - } - }) - }) - } - }) -} diff --git a/x/logic/prolog/text.go b/x/logic/prolog/text.go new file mode 100644 index 00000000..27590ad3 --- /dev/null +++ b/x/logic/prolog/text.go @@ -0,0 +1,138 @@ +package prolog + +import ( + "strings" + "unicode/utf8" + + "github.com/ichiban/prolog/engine" +) + +// 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) + if err != nil { + return "", err + } + return v.String(), nil +} + +// listTermToString try to convert a given list to a string using the provided +// converter function. +// The converter function is called for each element of the list and is expected +// to return a rune. +func listTermToString( + term engine.Term, + converter func(*engine.Env, engine.Term) (rune, error), + env *engine.Env) (string, error) { + iter, err := ListIterator(term, env) + if err != nil { + return "", err + } + var sb strings.Builder + + for iter.Next() { + r, err := converter(env, iter.Current()) + if err != nil { + return sb.String(), err + } + sb.WriteRune(r) + } + return sb.String(), nil +} + +// CharacterListTermToString try to convert a given list of characters to a string. +// Characters is a list of atoms, each representing a single character. +func CharacterListTermToString(term engine.Term, env *engine.Env) (string, error) { + return listTermToString(term, AssertCharacter, env) +} + +// CharacterCodeListTermToString try to convert a given list of character codes to a string. +// The character codes must be between 0 and 0x10ffff (i.e. a Rune). +func CharacterCodeListTermToString(term engine.Term, env *engine.Env) (string, error) { + return listTermToString(term, AssertCharacterCode, env) +} + +// OctetListTermToString try to convert a given list of bytes to a string. +// 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) + if err != nil { + return utf8.RuneError, err + } + return rune(b), nil + }, env) +} + +// TextTermToString try to convert a given Text term to a string. +// Text is an instantiated term which represents text as: an atom, a list of character codes, or list of characters. +func TextTermToString(term engine.Term, env *engine.Env) (string, error) { + switch v := env.Resolve(term).(type) { + case engine.Atom: + return AtomToString(v, env) + case engine.Compound: + if IsList(v) { + head := ListHead(v, env) + if head == nil { + return "", nil + } + + switch head.(type) { + case engine.Atom: + return CharacterListTermToString(v, env) + case engine.Integer: + return CharacterCodeListTermToString(v, env) + default: + return "", engine.TypeError(AtomTypeCharacterCode, v, env) + } + } + } + return "", engine.TypeError(AtomTypeText, term, env) +} + +// StringToAtom converts a string to an atom. +func StringToAtom(s string) engine.Term { + return engine.NewAtom(s) +} + +// StringToCharacterListTerm converts a string to a term representing a list of characters. +func StringToCharacterListTerm(s string) engine.Term { + terms := make([]engine.Term, 0, utf8.RuneCountInString(s)) + for _, r := range s { + terms = append(terms, engine.NewAtom(string(r))) + } + + return engine.List(terms...) +} + +// StringToCharacterCodeListTerm converts a string to a term representing a list of character codes. +func StringToCharacterCodeListTerm(s string) engine.Term { + terms := make([]engine.Term, 0, utf8.RuneCountInString(s)) + for _, r := range s { + terms = append(terms, engine.Integer(r)) + } + + return engine.List(terms...) +} + +// StringToOctetListTerm converts a string (utf8) to a term representing a list of bytes. +// This is the same as StringToCharacterCodeListTerm, but it returns an error when a rune is greater than 0xff. +// This is equivalent to the prolog encoding 'octet'. +func StringToOctetListTerm(s string, env *engine.Env) (engine.Term, error) { + terms := make([]engine.Term, 0, utf8.RuneCountInString(s)) + for _, r := range s { + if r > 0xff { + return nil, engine.TypeError(AtomTypeByte, engine.Integer(r), env) + } + terms = append(terms, engine.Integer(r)) + } + + return engine.List(terms...), nil +} + +// StringToByteListTerm converts a string (utf8) to a term representing a list of bytes. +// This is equivalent to the prolog encoding 'text'. +func StringToByteListTerm(s string) engine.Term { + return BytesToByteListTerm([]byte(s)) +} diff --git a/x/logic/prolog/tuple.go b/x/logic/prolog/tuple.go new file mode 100644 index 00000000..c7d41903 --- /dev/null +++ b/x/logic/prolog/tuple.go @@ -0,0 +1,8 @@ +package prolog + +import "github.com/ichiban/prolog/engine" + +// Tuple is a predicate which unifies the given term with a tuple of the given arity. +func Tuple(args ...engine.Term) engine.Term { + return engine.Atom(0).Apply(args...) +} diff --git a/x/logic/prolog/unify.go b/x/logic/prolog/unify.go index 0d471be9..244f8186 100644 --- a/x/logic/prolog/unify.go +++ b/x/logic/prolog/unify.go @@ -45,27 +45,55 @@ func UnifyFunctional( backwardConverter ConvertFunc, env *engine.Env, ) (bool, *engine.Env, error) { - isInFI, isOutFi := AreFullyInstantiated(in, env), AreFullyInstantiated(out, env) + isInFI, isOutFi := AreGround(in, env), AreGround(out, env) if !isInFI && !isOutFi { return false, env, engine.InstantiationError(env) } var err error from, to := in, out - if isInFI { + + switch { + case forwardConverter == nil && backwardConverter == nil: + // no-op + case isInFI && forwardConverter != nil: from, err = forwardConverter(in, options, env) if err != nil { return false, env, err } - } else { + case isOutFi && backwardConverter != nil: to, err = backwardConverter(out, options, env) if err != nil { return false, env, err } + default: + return false, env, engine.InstantiationError(env) } + env, result := env.Unify( Tuple(from...), Tuple(to...), ) + return result, env, nil } + +// UnifyFunctionalPredicate is the predicate version of UnifyFunctional returning a promise. +func UnifyFunctionalPredicate( + in, + out []engine.Term, + options engine.Term, + forwardConverter ConvertFunc, + backwardConverter ConvertFunc, + cont engine.Cont, + env *engine.Env, +) *engine.Promise { + ok, env, err := UnifyFunctional(in, out, options, forwardConverter, backwardConverter, env) + if err != nil { + return engine.Error(err) + } + if !ok { + return engine.Bool(false) + } + return cont(env) +} diff --git a/x/logic/util/encoding.go b/x/logic/util/encoding.go new file mode 100644 index 00000000..dc2b04f8 --- /dev/null +++ b/x/logic/util/encoding.go @@ -0,0 +1,32 @@ +package util + +import ( + "errors" + + "golang.org/x/net/html/charset" +) + +var ErrInvalidCharset = errors.New("invalid charset") + +// Decode converts a byte slice from a specified encoding to a string. +// Decode function is the reverse of encode function. +func Decode(bs []byte, label string) (string, error) { + encoding, _ := charset.Lookup(label) + if encoding == nil { + return "", ErrInvalidCharset + } + result, err := encoding.NewDecoder().Bytes(bs) + if err != nil { + return "", err + } + return string(result), nil +} + +// Encode converts a string to a slice of bytes in a specified encoding. +func Encode(str string, label string) ([]byte, error) { + encoding, _ := charset.Lookup(label) + if encoding == nil { + return nil, ErrInvalidCharset + } + return encoding.NewEncoder().Bytes([]byte(str)) +}