Skip to content

Commit

Permalink
Merge pull request #350 from okp4/feat/json_prolog
Browse files Browse the repository at this point in the history
🧠 Logic: 🧱 implement `json_prolog/2`
  • Loading branch information
bdeneux committed May 4, 2023
2 parents d3f22b1 + e41ea03 commit 1ab81d9
Show file tree
Hide file tree
Showing 6 changed files with 837 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ ADD https://github.com/CosmWasm/wasmvm/releases/download/v1.2.1/libwasmvm_muslc.

# hadolint ignore=DL4006
RUN set -eux \
&& apk add --no-cache ca-certificates=20220614-r0 build-base=0.5-r3 git=2.36.5-r0 linux-headers=5.16.7-r1 \
&& apk add --no-cache ca-certificates=20220614-r0 build-base=0.5-r3 git=2.36.6-r0 linux-headers=5.16.7-r1 \
&& sha256sum /lib/libwasmvm_muslc.aarch64.a | grep 86bc5fdc0f01201481c36e17cd3dfed6e9650d22e1c5c8983a5b78c231789ee0 \
&& sha256sum /lib/libwasmvm_muslc.x86_64.a | grep a00700aa19f5bfe0f46290ddf69bf51eb03a6dfcd88b905e1081af2e42dbbafc \
&& cp "/lib/libwasmvm_muslc.$(uname -m).a" /lib/libwasmvm_muslc.a
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 @@ -111,6 +111,7 @@ var registry = map[string]any{
"hex_bytes/2": predicate.HexBytes,
"bech32_address/2": predicate.Bech32Address,
"source_file/1": predicate.SourceFile,
"json_prolog/2": predicate.JSONProlog,
}

// RegistryNames is the list of the predicate names in the Registry.
Expand Down
182 changes: 182 additions & 0 deletions x/logic/predicate/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package predicate

import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ichiban/prolog/engine"
"github.com/okp4/okp4d/x/logic/util"
"github.com/samber/lo"
)

// AtomJSON is a term which represents a json as a compound term `json([Pair])`.
var AtomJSON = engine.NewAtom("json")

// JSONProlog is a predicate that will unify a JSON string into prolog terms and vice versa.
//
// json_prolog(?Json, ?Term) is det
//
// Where
// - `Json` is the string representation of the json
// - `Term` is an Atom that would be unified by the JSON representation as Prolog terms.
//
// In addition, when passing Json and Term, this predicate return true if both result match.
//
// Example:
//
// # JSON conversion to Prolog.
// - json_prolog('{"foo": "bar"}', json([foo-bar])).
func JSONProlog(vm *engine.VM, j, term engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return engine.Delay(func(ctx context.Context) *engine.Promise {
var result engine.Term

switch t1 := env.Resolve(j).(type) {
case engine.Variable:
case engine.Atom:
terms, err := jsonStringToTerms(t1.String())
if err != nil {
return engine.Error(fmt.Errorf("json_prolog/2: %w", err))
}
result = terms
default:
return engine.Error(fmt.Errorf("json_prolog/2: cannot unify json with %T", t1))
}

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.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))
}

b, err = sdk.SortJSON(b)
if err != nil {
return engine.Error(fmt.Errorf("json_prolog/2: %w", err))
}
return engine.Unify(vm, j, util.StringToTerm(string(b)), cont, env)
}
})
}

func jsonStringToTerms(j string) (engine.Term, error) {
var values any
decoder := json.NewDecoder(strings.NewReader(j))
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 jsonToTerms(values)
}

func termsToJSON(term engine.Term, env *engine.Env) ([]byte, error) {
switch t := term.(type) {
case engine.Atom:
return json.Marshal(t.String())
case engine.Integer:
return 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)
}

iter := engine.ListIterator{List: t, Env: env}

elements := make([]json.RawMessage, 0)
for iter.Next() {
element, err := termsToJSON(env.Resolve(iter.Current()), env)
if err != nil {
return nil, err
}
elements = append(elements, element)
}
return json.Marshal(elements)
case AtomJSON.String():
terms, err := ExtractJSONTerm(t, env)
if err != nil {
return nil, err
}

attributes := make(map[string]json.RawMessage, len(terms))
for key, term := range terms {
raw, err := termsToJSON(env.Resolve(term), env)
if err != nil {
return nil, err
}
attributes[key] = raw
}
return json.Marshal(attributes)
}

switch {
case AtomBool(true).Compare(t, env) == 0:
return json.Marshal(true)
case AtomBool(false).Compare(t, env) == 0:
return json.Marshal(false)
case AtomNull.Compare(t, env) == 0:
return json.Marshal(nil)
}

return nil, fmt.Errorf("invalid functor %s", t.Functor())
default:
return nil, fmt.Errorf("could not convert %s {%T} to json", t, t)
}
}

func jsonToTerms(value any) (engine.Term, error) {
switch v := value.(type) {
case string:
return util.StringToTerm(v), nil
case json.Number:
r, ok := math.NewIntFromString(string(v))
if !ok {
return nil, fmt.Errorf("could not convert number '%s' into integer term, decimal number is not handled yet", v)
}
if !r.IsInt64() {
return nil, fmt.Errorf("could not convert number '%s' into integer term, overflow", v)
}
return engine.Integer(r.Int64()), nil
case bool:
return AtomBool(v), nil
case nil:
return AtomNull, nil
case map[string]any:
keys := lo.Keys(v)
sort.Strings(keys)

attributes := make([]engine.Term, 0, len(v))
for _, key := range keys {
attributeValue, err := jsonToTerms(v[key])
if err != nil {
return nil, err
}
attributes = append(attributes, AtomPair.Apply(engine.NewAtom(key), attributeValue))
}
return AtomJSON.Apply(engine.List(attributes...)), nil
case []any:
elements := make([]engine.Term, 0, len(v))
for _, element := range v {
term, err := jsonToTerms(element)
if err != nil {
return nil, err
}
elements = append(elements, term)
}
return engine.List(elements...), nil
default:
return nil, fmt.Errorf("could not convert %s (%T) to a prolog term", v, v)
}
}
Loading

0 comments on commit 1ab81d9

Please sign in to comment.