Skip to content

Commit

Permalink
improve marshaling of slices and atoms
Browse files Browse the repository at this point in the history
  • Loading branch information
guregu committed Oct 13, 2022
1 parent 0f517f9 commit ec8a2f6
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 56 deletions.
66 changes: 64 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ It's pretty fast. Not as fast as native Trealla, but pretty dang fast (2-5x slow
go get github.com/trealla-prolog/go
```

**Note**: the module is under `github.com/trealla-prolog/go`, **not** `[...]/go/trealla`.
go.dev is confused about this and will pull a very old version if you try to `go get` the `trealla` package.

## Usage

This library uses WebAssembly to run Trealla, executing Prolog queries in an isolated environment.
Expand All @@ -41,7 +44,7 @@ func main() {
for query.Next(ctx) {
answer := query.Current()
x := answer.Solution["X"]
fmt.Println(x) // 1, trealla.Compound{Functor: "foo", Args: [trealla.Atom("bar")]}, "c"}
fmt.Println(x) // 1, trealla.Compound{Functor: "foo", Args: [trealla.Atom("bar")]}, "c"
}

// make sure to check the query for errors
Expand All @@ -51,7 +54,66 @@ func main() {
}
```

### Documentation
### Single query

Use `QueryOnce` when you only want a single answer.

```go
pl := trealla.New()
answer, err := pl.QueryOnce(ctx, "succ(41, N).")
if err != nil {
panic(err)
}

fmt.Println(answer.Stdout)
// Output: hello world
```

### Binding variables

You can bind variables in the query using the `WithBind` and `WithBinding` options.
This is a safe and convenient way to pass data into the query.
It is OK to pass these multiple times.

```go
pl := trealla.New()
answer, err := pl.QueryOnce(ctx, "write(X)", trealla.WithBind("X", trealla.Atom("hello world")))
if err != nil {
panic(err)
}

fmt.Println(answer.Stdout)
// Output: hello world
```

### Scanning solutions

You can scan an answer's substitutions directly into a struct or map, similar to ichiban/prolog.

Use the `prolog:"VariableName"` struct tag to manually specify a variable name.
Otherwise, the field's name is used.

```prolog
answer, err := pl.QueryOnce(ctx, `X = 123, Y = abc, Z = ["hello", "world"].`)
if err != nil {
panic(err)
}
var result struct {
X int
Y string
Hi []string `prolog:"Z"`
}
// make sure to pass a pointer to the struct!
if err := answer.Solution.Scan(&result); err != nil {
panic(err)
}
fmt.Printf("%+v", result)
// Output: {X:123 Y:abc Hi:[hello world]}
```

## Documentation

See **[package trealla's documentation](https://pkg.go.dev/github.com/trealla-prolog/go#section-directories)** for more details and examples.

Expand Down
15 changes: 3 additions & 12 deletions trealla/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type Query interface {
type query struct {
pl *prolog
goal string
bind []binding
bind bindings
subquery int32

queue []Answer
Expand Down Expand Up @@ -260,18 +260,9 @@ func (q *query) reify() error {
}

var sb strings.Builder
for _, bind := range q.bind {
sb.WriteString(bind.name)
sb.WriteString(" = ")
v, err := marshal(bind.value)
if err != nil {
return fmt.Errorf("trealla: failed to convert bound variable to Prolog (name: %s): %w", bind.name, err)
}
sb.WriteString(v)
sb.WriteString(", ")
}
sb.WriteString(q.bind.String())
sb.WriteString(", ")
sb.WriteString(q.goal)

q.goal = sb.String()
return nil
}
Expand Down
62 changes: 45 additions & 17 deletions trealla/substitution.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,18 @@ import (
"fmt"
"reflect"
"sort"
"strings"
)

// Substitution is a mapping of variable names to substitutions (terms).
// For query results, it's one answer to a query.
// Substitution can also be used to bind variables in queries via WithBinding.
type Substitution map[string]Term

type binding struct {
name string
value Term
}

func (sub Substitution) bindings() []binding {
bs := make([]binding, 0, len(sub))
for k, v := range sub {
bs = append(bs, binding{
name: k,
value: v,
})
}
sort.Slice(bs, func(i, j int) bool {
return bs[i].name < bs[j].name
})
return bs
// String returns a Prolog representation of this substitution in the same format
// as ISO variable_names/1 option for read_term/2.
func (sub Substitution) String() string {
return "[" + sub.bindings().String() + "]"
}

// UnmarshalJSON implements the encoding/json.Marshaler interface.
Expand All @@ -51,13 +39,53 @@ func (sol *Substitution) UnmarshalJSON(bs []byte) error {
return nil
}

type binding struct {
name string
value Term
}

// Scan sets any fields in obj that match variables in this substitution.
// obj must be a pointer to a struct or a map.
func (sub Substitution) Scan(obj any) error {
rv := reflect.ValueOf(obj)
return scan(sub, rv)
}

type bindings []binding

func (bs bindings) String() string {
var sb strings.Builder
for i, bind := range bs {
if i != 0 {
sb.WriteString(", ")
}
sb.WriteString(bind.name)
sb.WriteString(" = ")
v, err := marshal(bind.value)
if err != nil {
sb.WriteString(fmt.Sprintf("<error: %v>", err))
}
sb.WriteString(v)
}
return sb.String()
}

func (bs bindings) Less(i, j int) bool { return bs[i].name < bs[j].name }
func (bs bindings) Swap(i, j int) { bs[i], bs[j] = bs[j], bs[i] }
func (bs bindings) Len() int { return len(bs) }

func (sub Substitution) bindings() bindings {
bs := make(bindings, 0, len(sub))
for k, v := range sub {
bs = append(bs, binding{
name: k,
value: v,
})
}
sort.Sort(bs)
return bs
}

func scan(sub Substitution, rv reflect.Value) error {
if rv.Kind() == reflect.Map {
vtype := rv.Type().Elem()
Expand Down
2 changes: 1 addition & 1 deletion trealla/substitution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func ExampleSubstitution_Scan() {
panic(err)
}

answer, err := pl.QueryOnce(ctx, `X = 123, Y = abc, Z = ["hello","world"].`)
answer, err := pl.QueryOnce(ctx, `X = 123, Y = abc, Z = ["hello", "world"].`)
if err != nil {
panic(err)
}
Expand Down
71 changes: 47 additions & 24 deletions trealla/term.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ type Atom string

// String returns the Prolog text representation of this atom.
func (a Atom) String() string {
return a.escape()
if !a.needsEscape() {
return string(a)
}
return "'" + atomEscaper.Replace(string(a)) + "'"
}

// Indicator returns a predicate indicator for this atom ("foo/0").
func (a Atom) Indicator() string {
return fmt.Sprintf("%s/0", a.escape())
return fmt.Sprintf("%s/0", a.String())
}

// Of returns a Compound term with this atom as the principal functor.
Expand All @@ -43,13 +46,6 @@ func (a Atom) Of(args ...Term) Compound {
}
}

func (a Atom) escape() string {
if !a.needsEscape() {
return string(a)
}
return "'" + atomEscaper.Replace(string(a)) + "'"
}

func (a Atom) needsEscape() bool {
if len(a) == 0 {
return true
Expand All @@ -58,7 +54,7 @@ func (a Atom) needsEscape() bool {
if i == 0 && !unicode.IsLower(char) {
return true
}
if !unicode.IsLetter(char) {
if !(char == '_' || unicode.IsLetter(char) || unicode.IsDigit(char)) {
return true
}
}
Expand Down Expand Up @@ -230,6 +226,11 @@ func unmarshalTerm(bs []byte) (Term, error) {
return nil, fmt.Errorf("trealla: unhandled term json: %T %v", iface, iface)
}

// Marshal returns the Prolog text representation of term.
func Marshal(term Term) (string, error) {
return marshal(term)
}

func marshal(term Term) (string, error) {
switch x := term.(type) {
case string:
Expand All @@ -249,24 +250,46 @@ func marshal(term Term) (string, error) {
case Variable:
return x.String(), nil
case []Term:
var sb strings.Builder
sb.WriteRune('[')
for i, t := range x {
if i != 0 {
sb.WriteString(", ")
}
text, err := marshal(t)
if err != nil {
return "", err
}
sb.WriteString(text)
}
sb.WriteRune(']')
return sb.String(), nil
return marshalSlice(x)
case []any:
return marshalSlice(x)
case []string:
return marshalSlice(x)
case []int64:
return marshalSlice(x)
case []int:
return marshalSlice(x)
case []float64:
return marshalSlice(x)
case []*big.Int:
return marshalSlice(x)
case []Atom:
return marshalSlice(x)
case []Compound:
return marshalSlice(x)
case []Variable:
return marshalSlice(x)
}
return "", fmt.Errorf("trealla: can't marshal type %T, value: %v", term, term)
}

func marshalSlice[T any](slice []T) (string, error) {
var sb strings.Builder
sb.WriteRune('[')
for i, v := range slice {
if i != 0 {
sb.WriteString(", ")
}
text, err := marshal(v)
if err != nil {
return "", err
}
sb.WriteString(text)
}
sb.WriteRune(']')
return sb.String(), nil
}

func escapeString(str string) string {
return `"` + stringEscaper.Replace(str) + `"`
}
Expand Down
20 changes: 20 additions & 0 deletions trealla/term_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ func TestMarshal(t *testing.T) {
term: Atom("hello world"),
want: "'hello world'",
},
{
term: Atom("under_score"),
want: "under_score",
},
{
term: Atom("123"),
want: "'123'",
},
{
term: Atom("x1"),
want: "x1",
},
{
term: "string",
want: `"string"`,
Expand All @@ -57,6 +69,14 @@ func TestMarshal(t *testing.T) {
term: []Term{int64(1), int64(2)},
want: "[1, 2]",
},
{
term: []int64{int64(1), int64(2)},
want: "[1, 2]",
},
{
term: []any{int64(1), int64(2)},
want: "[1, 2]",
},
}

for _, tc := range cases {
Expand Down

0 comments on commit ec8a2f6

Please sign in to comment.