Skip to content

Commit

Permalink
Merge pull request #580 from okp4/refactor/predicate-pre-condition
Browse files Browse the repository at this point in the history
🧠 ♻️ Refactor predicate execution pre-conditions
  • Loading branch information
ccamel authored Feb 29, 2024
2 parents 973b9da + 5a21900 commit 0ae60fa
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 97 deletions.
92 changes: 55 additions & 37 deletions x/logic/interpreter/instrument.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,105 +4,123 @@ import (
"github.com/ichiban/prolog/engine"
)

type Hook[T any] func() T
type Invariant func(env *engine.Env) error

// Instrument0 is a higher order function that given a 0arg-predicate and a hook returns a new predicate that calls the
// hook before calling the predicate.
func Instrument0[T any](hook Hook[T], p engine.Predicate0) engine.Predicate0 {
// Instrument0 is a higher order function that given a 0arg-predicate and an invariant returns a new predicate that calls the
// invariant before calling the predicate.
func Instrument0(invariant Invariant, p engine.Predicate0) engine.Predicate0 {
return func(vm *engine.VM, cont engine.Cont, env *engine.Env) *engine.Promise {
hook()
if err := invariant(env); err != nil {
return engine.Error(err)
}

return p(vm, cont, env)
}
}

// Instrument1 is a higher order function that given a 1arg-predicate and a hook returns a new predicate that calls the
// hook before calling the predicate.
func Instrument1[T any](hook Hook[T], p engine.Predicate1) engine.Predicate1 {
// Instrument1 is a higher order function that given a 1arg-predicate and an invariant returns a new predicate that calls the
// invariant before calling the predicate.
func Instrument1(invariant Invariant, p engine.Predicate1) engine.Predicate1 {
return func(vm *engine.VM, t1 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
hook()
if err := invariant(env); err != nil {
return engine.Error(err)
}

return p(vm, t1, cont, env)
}
}

// Instrument2 is a higher order function that given a 2args-predicate and a hook returns a new predicate that calls the
// hook before calling the predicate.
func Instrument2[T any](hook Hook[T], p engine.Predicate2) engine.Predicate2 {
// Instrument2 is a higher order function that given a 2args-predicate and an invariant returns a new predicate that calls the
// invariant before calling the predicate.
func Instrument2(invariant Invariant, p engine.Predicate2) engine.Predicate2 {
return func(vm *engine.VM, t1 engine.Term, t2 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
hook()
if err := invariant(env); err != nil {
return engine.Error(err)
}

return p(vm, t1, t2, cont, env)
}
}

// Instrument3 is a higher order function that given a 3args-predicate and a hook returns a new predicate that calls the
// hook before calling the predicate.
func Instrument3[T any](hook Hook[T], p engine.Predicate3) engine.Predicate3 {
// Instrument3 is a higher order function that given a 3args-predicate and an invariant returns a new predicate that calls the
// invariant before calling the predicate.
func Instrument3(invariant Invariant, p engine.Predicate3) engine.Predicate3 {
return func(vm *engine.VM, t1 engine.Term, t2 engine.Term, t3 engine.Term, cont engine.Cont,
env *engine.Env,
) *engine.Promise {
hook()
if err := invariant(env); err != nil {
return engine.Error(err)
}

return p(vm, t1, t2, t3, cont, env)
}
}

// Instrument4 is a higher order function that given a 4args-predicate and a hook returns a new predicate that calls the
// hook before calling the predicate.
// Instrument4 is a higher order function that given a 4args-predicate and an invariant returns a new predicate that calls the
// invariant before calling the predicate.
//
//nolint:lll
func Instrument4[T any](hook Hook[T], p engine.Predicate4) engine.Predicate4 {
func Instrument4(invariant Invariant, p engine.Predicate4) engine.Predicate4 {
return func(vm *engine.VM, t1 engine.Term, t2 engine.Term, t3 engine.Term, t4 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
hook()
if err := invariant(env); err != nil {
return engine.Error(err)
}

return p(vm, t1, t2, t3, t4, cont, env)
}
}

// Instrument5 is a higher order function that given a 5args-predicate and a hook returns a new predicate that calls the
// hook before calling the predicate.
// Instrument5 is a higher order function that given a 5args-predicate and an invariant returns a new predicate that calls the
// invariant before calling the predicate.
//
//nolint:lll
func Instrument5[T any](hook Hook[T], p engine.Predicate5) engine.Predicate5 {
func Instrument5(invariant Invariant, p engine.Predicate5) engine.Predicate5 {
return func(vm *engine.VM, t1 engine.Term, t2 engine.Term, t3 engine.Term, t4 engine.Term, t5 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
hook()
if err := invariant(env); err != nil {
return engine.Error(err)
}

return p(vm, t1, t2, t3, t4, t5, cont, env)
}
}

// Instrument6 is a higher order function that given a 6args-predicate and a hook returns a new predicate that calls the
// hook before calling the predicate.
// Instrument6 is a higher order function that given a 6args-predicate and an invariant returns a new predicate that calls the
// invariant before calling the predicate.
//
//nolint:lll
func Instrument6[T any](hook Hook[T], p engine.Predicate6) engine.Predicate6 {
func Instrument6(invariant Invariant, p engine.Predicate6) engine.Predicate6 {
return func(vm *engine.VM, t1 engine.Term, t2 engine.Term, t3 engine.Term, t4 engine.Term, t5 engine.Term, t6 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
hook()
if err := invariant(env); err != nil {
return engine.Error(err)
}

return p(vm, t1, t2, t3, t4, t5, t6, cont, env)
}
}

// Instrument7 is a higher order function that given a 7args-predicate and a hook returns a new predicate that calls the
// hook before calling the predicate.
// Instrument7 is a higher order function that given a 7args-predicate and an invariant returns a new predicate that calls the
// invariant before calling the predicate.
//
//nolint:lll
func Instrument7[T any](hook Hook[T], p engine.Predicate7) engine.Predicate7 {
func Instrument7(invariant Invariant, p engine.Predicate7) engine.Predicate7 {
return func(vm *engine.VM, t1 engine.Term, t2 engine.Term, t3 engine.Term, t4 engine.Term, t5 engine.Term, t6 engine.Term, t7 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
hook()
if err := invariant(env); err != nil {
return engine.Error(err)
}

return p(vm, t1, t2, t3, t4, t5, t6, t7, cont, env)
}
}

// Instrument8 is a higher order function that given a 8args-predicate and a hook returns a new predicate that calls the
// hook before calling the predicate.
// Instrument8 is a higher order function that given a 8args-predicate and an invariant returns a new predicate that calls the
// invariant before calling the predicate.
//
//nolint:lll
func Instrument8[T any](hook Hook[T], p engine.Predicate8) engine.Predicate8 {
func Instrument8(invariant Invariant, p engine.Predicate8) engine.Predicate8 {
return func(vm *engine.VM, t1 engine.Term, t2 engine.Term, t3 engine.Term, t4 engine.Term, t5 engine.Term, t6 engine.Term, t7 engine.Term, t8 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
hook()
if err := invariant(env); err != nil {
return engine.Error(err)
}

return p(vm, t1, t2, t3, t4, t5, t6, t7, t8, cont, env)
}
Expand Down
29 changes: 19 additions & 10 deletions x/logic/interpreter/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,38 @@ import (

"github.com/ichiban/prolog"
"github.com/ichiban/prolog/engine"

storetypes "cosmossdk.io/store/types"
)

// Predicates is a map of predicate names to their execution costs.
type Predicates map[string]uint64

// Option is a function that configures an Interpreter.
type Option func(*prolog.Interpreter) error

// WithPredicates configures the interpreter to register the specified predicates.
// The predicates names must be present in the registry, otherwise the function will return an error.
func WithPredicates(_ goctx.Context, predicates Predicates, meter storetypes.GasMeter) Option {
// See WithPredicate for more details.
func WithPredicates(ctx goctx.Context, predicates []string, hook Hook) Option {
return func(i *prolog.Interpreter) error {
for predicate, cost := range predicates {
if err := Register(i, predicate, cost, meter); err != nil {
return fmt.Errorf("error registering predicate '%s': %w", predicate, err)
for _, predicate := range predicates {
if err := WithPredicate(ctx, predicate, hook)(i); err != nil {
return err
}
}
return nil
}
}

// WithPredicate configures the interpreter to register the specified predicate with the specified hook.
// The hook is a function that is called before the predicate is executed and can be used to check some conditions,
// like the gas consumption or the permission to execute the predicate.
//
// The predicates names must be present in the registry, otherwise the function will return an error.
func WithPredicate(_ goctx.Context, predicate string, hook Hook) Option {
return func(i *prolog.Interpreter) error {
if err := Register(i, predicate, hook); err != nil {
return fmt.Errorf("error registering predicate '%s': %w", predicate, err)
}
return nil
}
}

// WithBootstrap configures the interpreter to compile the specified bootstrap script to serve as setup context.
// If compilation of the bootstrap script fails, the function will return an error.
func WithBootstrap(ctx goctx.Context, bootstrap string) Option {
Expand Down
30 changes: 13 additions & 17 deletions x/logic/interpreter/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import (
"github.com/ichiban/prolog"
"github.com/ichiban/prolog/engine"

storetypes "cosmossdk.io/store/types"

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

Expand Down Expand Up @@ -131,14 +129,16 @@ var RegistryNames = func() []string {
return names
}()

type Hook = func(functor string) func(env *engine.Env) error

// Register registers a well-known predicate in the interpreter with support for consumption measurement.
// name is the name of the predicate in the form of "atom/arity".
// cost is the cost of executing the predicate.
// meter is the gas meter object that is called when the predicate is called and which allows to count the cost of
// executing the predicate(ctx).
//
//nolint:lll
func Register(i *prolog.Interpreter, name string, cost uint64, meter storetypes.GasMeter) error {
func Register(i *prolog.Interpreter, name string, hook Hook) error {
if p, ok := registry[name]; ok {
parts := strings.Split(name, "/")
if len(parts) == 2 {
Expand All @@ -148,31 +148,27 @@ func Register(i *prolog.Interpreter, name string, cost uint64, meter storetypes.
return err
}

hook := func() storetypes.Gas {
meter.ConsumeGas(cost, fmt.Sprintf("predicate %s", name))

return meter.GasRemaining()
}
invariant := hook(name)

switch arity {
case 0:
i.Register0(atom, Instrument0(hook, p.(func(*engine.VM, engine.Cont, *engine.Env) *engine.Promise)))
i.Register0(atom, Instrument0(invariant, p.(func(*engine.VM, engine.Cont, *engine.Env) *engine.Promise)))
case 1:
i.Register1(atom, Instrument1(hook, p.(func(*engine.VM, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
i.Register1(atom, Instrument1(invariant, p.(func(*engine.VM, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
case 2:
i.Register2(atom, Instrument2(hook, p.(func(*engine.VM, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
i.Register2(atom, Instrument2(invariant, p.(func(*engine.VM, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
case 3:
i.Register3(atom, Instrument3(hook, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
i.Register3(atom, Instrument3(invariant, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
case 4:
i.Register4(atom, Instrument4(hook, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
i.Register4(atom, Instrument4(invariant, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
case 5:
i.Register5(atom, Instrument5(hook, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
i.Register5(atom, Instrument5(invariant, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
case 6:
i.Register6(atom, Instrument6(hook, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
i.Register6(atom, Instrument6(invariant, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
case 7:
i.Register7(atom, Instrument7(hook, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
i.Register7(atom, Instrument7(invariant, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
case 8:
i.Register8(atom, Instrument8(hook, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
i.Register8(atom, Instrument8(invariant, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise)))
default:
panic(fmt.Sprintf("unsupported arity: %s", name))
}
Expand Down
60 changes: 53 additions & 7 deletions x/logic/keeper/grpc_query_ask_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ func TestGRPCAsk(t *testing.T) {
emptySolution := types.Result{}
Convey("Given a test cases", t, func() {
cases := []struct {
program string
query string
limit int
expectedAnswer *types.Answer
expectedError string
program string
query string
limit int
predicateBlacklist []string
maxGas uint64
predicateCosts map[string]uint64
expectedAnswer *types.Answer
expectedError string
}{
{
program: "foo.",
Expand Down Expand Up @@ -93,15 +96,30 @@ func TestGRPCAsk(t *testing.T) {
},
},
{
program: "",
query: "block_height(X).",
query: "block_height(X).",
expectedAnswer: &types.Answer{
Variables: []string{"X"},
Results: []types.Result{{Substitutions: []types.Substitution{{
Variable: "X", Expression: "0",
}}}},
},
},
{
query: "block_height(X).",
maxGas: 1000,
expectedError: "out of gas: logic <ReadPerByte> (1018/1000): limit exceeded",
},
{
query: "block_height(X).",
maxGas: 3000,
predicateCosts: map[string]uint64{
"block_height/1": 10000,
},
expectedAnswer: &types.Answer{
Variables: []string{"X"},
Results: []types.Result{{Error: "error(resource_error(gas(block_height/1,12353,3000)),block_height/1)"}},
},
},
{
program: "father(bob, 'élodie').",
query: "father(bob, X).",
Expand Down Expand Up @@ -154,6 +172,16 @@ func TestGRPCAsk(t *testing.T) {
Results: []types.Result{{Error: "error(existence_error(procedure,father/3),root)"}},
},
},
{
program: "",
query: "block_height(X).",
predicateBlacklist: []string{"block_height/1"},
expectedAnswer: &types.Answer{
HasMore: false,
Variables: []string{"X"},
Results: []types.Result{{Error: "error(permission_error(execute,forbidden_predicate,block_height/1),block_height/1)"}},
},
},
{
program: "father°(bob, alice).",
query: "father(bob, X).",
Expand Down Expand Up @@ -270,6 +298,24 @@ foo(a4).
limit := sdkmath.NewUint(uint64(lo.If(tc.limit == 0, 1).Else(tc.limit)))
params := types.DefaultParams()
params.Limits.MaxResultCount = &limit
if tc.predicateBlacklist != nil {
params.Interpreter.PredicatesFilter.Blacklist = tc.predicateBlacklist
}
if tc.maxGas != 0 {
maxGas := sdkmath.NewUint(tc.maxGas)
params.Limits.MaxGas = &maxGas
}
if tc.predicateCosts != nil {
predicateCosts := make([]types.PredicateCost, 0, len(tc.predicateCosts))
for predicate, cost := range tc.predicateCosts {
cost := sdkmath.NewUint(cost)
predicateCosts = append(predicateCosts, types.PredicateCost{
Predicate: predicate,
Cost: &cost,
})
}
params.GasPolicy.PredicateCosts = predicateCosts
}
err := logicKeeper.SetParams(testCtx.Ctx, params)

So(err, ShouldBeNil)
Expand Down
Loading

0 comments on commit 0ae60fa

Please sign in to comment.