Skip to content

Commit

Permalink
Merge pull request #311 from okp4/feat/source-file-predicate
Browse files Browse the repository at this point in the history
🧠 Logic: 🗂️ source_file/1 predicate
  • Loading branch information
amimart authored Mar 17, 2023
2 parents 7679f63 + f08bb26 commit e89fcf2
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 4 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ mock: ## Generate all the mocks (for tests)
@echo "${COLOR_CYAN} 🧱 Generating all the mocks${COLOR_RESET}"
@go install github.com/golang/mock/[email protected]
@mockgen -source=x/logic/types/expected_keepers.go -package testutil -destination x/logic/testutil/expected_keepers_mocks.go
@mockgen -source=x/logic/fs/fs.go -package testutil -destination x/logic/testutil/fs_mocks.go

## Release:
.PHONY: release-assets
Expand Down
3 changes: 1 addition & 2 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
Expand Down Expand Up @@ -1017,7 +1016,7 @@ func (app *App) SimulationManager() *module.SimulationManager {

// provideFS is used to provide the virtual file system used for the logic module
// to load external file.
func (app *App) provideFS(ctx context.Context) fs.FS {
func (app *App) provideFS(ctx context.Context) logicfs.FS {
wasmHandler := logicfs.NewWasmHandler(app.WasmKeeper)
return logicfs.NewVirtualFS(ctx, []logicfs.URIHandler{wasmHandler})
}
6 changes: 5 additions & 1 deletion x/logic/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import (
"io/fs"
)

type FS interface {
fs.FS
}

// VirtualFS is the custom virtual file system used into the blockchain.
// It will hold a list of handler that can resolve file URI and return the corresponding binary file.
type VirtualFS struct {
ctx goctx.Context
router Router
}

var _ fs.FS = (*VirtualFS)(nil)
var _ FS = (*VirtualFS)(nil)

// NewVirtualFS return a new VirtualFS object that will handle all virtual file on the interpreter.
// File can be provided from different sources like CosmWasm cw-storage smart contract.
Expand Down
1 change: 1 addition & 0 deletions x/logic/interpreter/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ var Registry = map[string]RegistryEntry{
"sha_hash/2": {predicate.SHAHash, 1},
"hex_bytes/2": {predicate.HexBytes, 1},
"bech32_address/2": {predicate.Bech32Address, 1},
"source_file/1": {predicate.SourceFile, 1},
}

// RegistryNames is the list of the predicate names in the Registry.
Expand Down
2 changes: 1 addition & 1 deletion x/logic/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package keeper
import (
goctx "context"
"fmt"
"io/fs"

"github.com/cosmos/cosmos-sdk/codec"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
"github.com/okp4/okp4d/x/logic/fs"
"github.com/okp4/okp4d/x/logic/types"
"github.com/tendermint/tendermint/libs/log"
)
Expand Down
93 changes: 93 additions & 0 deletions x/logic/predicate/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package predicate

import (
"context"
"fmt"
"reflect"
"sort"

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

// SourceFile is a predicate that unify the given term with the currently loaded source file. The signature is as follows:
//
// source_file(?File).
//
// Where File represents a loaded source file.
//
// Example:
//
// # Query all the loaded source files, in alphanumeric order.
// - source_file(File).
//
// # Query the given source file is loaded.
// - source_file('foo.pl').
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)
}
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...)
}

func getLoadedSources(vm *engine.VM) map[string]struct{} {
loadedField := reflect.ValueOf(vm).Elem().FieldByName("loaded").MapKeys()
loaded := make(map[string]struct{}, len(loadedField))
for _, value := range loadedField {
loaded[value.String()] = struct{}{}
}

return loaded
}

func sortLoadedSources(sources map[string]struct{}) []string {
result := make([]string, 0, len(sources))
for filename := range sources {
result = append(result, filename)
}
sort.SliceStable(result, func(i, j int) bool {
return result[i] < result[j]
})

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
}
138 changes: 138 additions & 0 deletions x/logic/predicate/file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//nolint:gocognit
package predicate

import (
"fmt"
"net/url"
"testing"
"time"

"github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/golang/mock/gomock"
"github.com/ichiban/prolog/engine"
"github.com/okp4/okp4d/x/logic/fs"
"github.com/okp4/okp4d/x/logic/testutil"
"github.com/okp4/okp4d/x/logic/types"
. "github.com/smartystreets/goconvey/convey"
"github.com/tendermint/tendermint/libs/log"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
tmdb "github.com/tendermint/tm-db"
)

func TestSourceFile(t *testing.T) {
Convey("Given test cases", t, func() {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

cases := []struct {
query string
wantResult []types.TermResults
wantError error
wantSuccess bool
}{
{
query: "source_file(file).",
wantSuccess: false,
},
{
query: "consult(file1), consult(file2), source_file(file1).",
wantResult: []types.TermResults{{}},
wantSuccess: true,
},
{
query: "consult(file1), consult(file2), consult(file3), source_file(file2).",
wantResult: []types.TermResults{{}},
wantSuccess: true,
},
{
query: "consult(file1), consult(file2), source_file(file3).",
wantSuccess: false,
},
{
query: "source_file(X).",
wantSuccess: false,
},
{
query: "consult(file1), consult(file2), source_file(X).",
wantResult: []types.TermResults{{"X": "file1"}, {"X": "file2"}},
wantSuccess: true,
},
{
query: "consult(file2), consult(file3), consult(file1), source_file(X).",
wantResult: []types.TermResults{{"X": "file1"}, {"X": "file2"}, {"X": "file3"}},
wantSuccess: true,
},
{
query: "source_file(foo(bar)).",
wantResult: []types.TermResults{},
wantError: fmt.Errorf("source_file/1: cannot unify file with *engine.compound"),
},
}

for nc, tc := range cases {
Convey(fmt.Sprintf("Given the query #%d: %s", nc, tc.query), func() {
Convey("and a mocked file system", func() {
uri, _ := url.Parse("file://dump.pl")
mockedFS := testutil.NewMockFS(ctrl)
mockedFS.EXPECT().Open(gomock.Any()).AnyTimes().Return(fs.NewVirtualFile(
[]byte("dumb(dumber)."),
uri,
time.Now(),
), nil)

Convey("and a context", func() {
db := tmdb.NewMemDB()
stateStore := store.NewCommitMultiStore(db)
ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger())

Convey("and a vm", func() {
interpreter := testutil.NewInterpreterMust(ctx)
interpreter.FS = mockedFS
interpreter.Register1(engine.NewAtom("source_file"), SourceFile)

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

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

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

got = append(got, m)
}

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

if tc.wantSuccess {
So(len(got), ShouldBeGreaterThan, 0)
So(len(got), ShouldEqual, len(tc.wantResult))
for iGot, resultGot := range got {
for varGot, termGot := range resultGot {
So(testutil.ReindexUnknownVariables(termGot), ShouldEqual, tc.wantResult[iGot][varGot])
}
}
} else {
So(len(got), ShouldEqual, 0)
}
}
})
})
})
})
})
})
})
}
})
}
50 changes: 50 additions & 0 deletions x/logic/testutil/fs_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions x/logic/testutil/logic.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func NewInterpreterMust(ctx context.Context) (interpreter *prolog.Interpreter) {
interpreter.Register3(engine.NewAtom("op"), engine.Op)
interpreter.Register3(engine.NewAtom("compare"), engine.Compare)
interpreter.Register2(engine.NewAtom("="), engine.Unify)
interpreter.Register1(engine.NewAtom("consult"), engine.Consult)

err := interpreter.Compile(ctx, `
:-(op(1200, xfx, ':-')).
Expand Down

0 comments on commit e89fcf2

Please sign in to comment.