diff --git a/README.md b/README.md index eaaf0f4..385156c 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 @@ -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. diff --git a/trealla/query.go b/trealla/query.go index fd0ea9f..d7047a9 100644 --- a/trealla/query.go +++ b/trealla/query.go @@ -29,7 +29,7 @@ type Query interface { type query struct { pl *prolog goal string - bind []binding + bind bindings subquery int32 queue []Answer @@ -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 } diff --git a/trealla/substitution.go b/trealla/substitution.go index 029988a..20e18d8 100644 --- a/trealla/substitution.go +++ b/trealla/substitution.go @@ -6,6 +6,7 @@ import ( "fmt" "reflect" "sort" + "strings" ) // Substitution is a mapping of variable names to substitutions (terms). @@ -13,23 +14,10 @@ import ( // 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. @@ -51,6 +39,11 @@ 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 { @@ -58,6 +51,41 @@ func (sub Substitution) Scan(obj any) error { 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("", 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() diff --git a/trealla/substitution_test.go b/trealla/substitution_test.go index ac42cbe..716cbf8 100644 --- a/trealla/substitution_test.go +++ b/trealla/substitution_test.go @@ -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) } diff --git a/trealla/term.go b/trealla/term.go index c745884..e9deefc 100644 --- a/trealla/term.go +++ b/trealla/term.go @@ -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. @@ -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 @@ -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 } } @@ -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: @@ -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) + `"` } diff --git a/trealla/term_test.go b/trealla/term_test.go index c75a726..9ee074c 100644 --- a/trealla/term_test.go +++ b/trealla/term_test.go @@ -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"`, @@ -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 {