Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

🧠 Logic: 🗂️ source_file/1 predicate #311

Merged
merged 8 commits into from
Mar 17, 2023
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,
},
amimart marked this conversation as resolved.
Show resolved Hide resolved
{
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,
},
amimart marked this conversation as resolved.
Show resolved Hide resolved
{
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