├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── ATTRIBUTION ├── LICENSE ├── Makefile ├── README.md ├── doc.go ├── go.mod ├── go.sum └── trealla ├── answer.go ├── bench_test.go ├── decode.go ├── encode.go ├── error.go ├── example_test.go ├── interop.go ├── interop_test.go ├── library.go ├── libtpl.wasm ├── pool.go ├── pool_test.go ├── prolog.go ├── prolog_test.go ├── query.go ├── query_test.go ├── string.go ├── substitution.go ├── substitution_test.go ├── term.go ├── term_test.go ├── terms ├── example_test.go ├── term.go └── term_test.go ├── testdata ├── greeting.pl ├── subdirectory │ └── foo.txt └── tak.pl └── wasm.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | branches: 8 | - "*" 9 | pull_request: 10 | release: 11 | types: [created] 12 | 13 | jobs: 14 | test: 15 | name: Build & Test 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | os: 21 | - ubuntu-latest 22 | - macos-latest 23 | - windows-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Install Go 27 | uses: actions/setup-go@v3 28 | with: 29 | go-version: "1.23" 30 | - name: Deps 31 | run: go get ./trealla 32 | - name: Build 33 | run: go build ./trealla 34 | - name: Test 35 | run: go test -v ./trealla --short 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | trealla.test 2 | *.out 3 | *.bin 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/trealla"] 2 | path = src/trealla 3 | url = https://github.com/guregu/trealla 4 | -------------------------------------------------------------------------------- /ATTRIBUTION: -------------------------------------------------------------------------------- 1 | Trealla Prolog 2 | Copyright (c) 2020 Andrew George Davison 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a 5 | copy of this software and associated documentation files (the "Software"), 6 | to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 18 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 19 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 20 | OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | --------------------------------------------------------------------------- 23 | 24 | /* Portions of the Prolog library source code included here may be 25 | subject to copyright and are used with attribution. The rest may 26 | be as described or in the 'Prolog Commons'. 27 | */ 28 | 29 | /* 30 | /* Author: Mark Thom, Jan Wielemaker, and Richard O'Keefe 31 | Copyright (c) 2018-2021, Mark Thom 32 | Copyright (c) 2002-2020, University of Amsterdam 33 | VU University Amsterdam 34 | SWI-Prolog Solutions b.v. 35 | All rights reserved. 36 | Redistribution and use in source and binary forms, with or without 37 | modification, are permitted provided that the following conditions 38 | are met: 39 | 1. Redistributions of source code must retain the above copyright 40 | notice, this list of conditions and the following disclaimer. 41 | 2. Redistributions in binary form must reproduce the above copyright 42 | notice, this list of conditions and the following disclaimer in 43 | the documentation and/or other materials provided with the 44 | distribution. 45 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 46 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 47 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 48 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 49 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 50 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 51 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 52 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 53 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 54 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 55 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 56 | POSSIBILITY OF SUCH DAMAGE. 57 | */ 58 | 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Gregory Roseberry 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included 11 | in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 17 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean 2 | 3 | all: clean wasm 4 | 5 | clean: 6 | rm -f trealla/libtpl.wasm 7 | 8 | wasm: trealla/libtpl.wasm 9 | 10 | trealla/libtpl.wasm: 11 | cd src/trealla && $(MAKE) clean && $(MAKE) -j8 libtpl && \ 12 | cp libtpl.wasm ../../trealla/libtpl.wasm 13 | 14 | update: 15 | cd src/trealla && git fetch --all && git pull origin main 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # trealla-go [![GoDoc](https://godoc.org/github.com/trealla-prolog/go?status.svg)](https://godoc.org/github.com/trealla-prolog/go) 2 | `import "github.com/trealla-prolog/go/trealla"` 3 | 4 | Prolog interface for Go using [Trealla Prolog](https://github.com/trealla-prolog/trealla) and [wazero](https://wazero.io/). 5 | It's pretty fast. Not as fast as native Trealla, but pretty dang fast (about 2x slower than native). 6 | 7 | **Development Status**: inching closer to stability 8 | 9 | ### Caveats 10 | 11 | - Beta status, API will probably change 12 | - API is relatively stable now. 13 | 14 | ## Install 15 | 16 | ```bash 17 | go get github.com/trealla-prolog/go 18 | ``` 19 | 20 | **Note**: the module is under `github.com/trealla-prolog/go`, **not** `[...]/go/trealla`. 21 | go.dev is confused about this and will pull a very old version if you try to `go get` the `trealla` package. 22 | 23 | ## Usage 24 | 25 | This library uses WebAssembly to run Trealla, executing Prolog queries in an isolated environment. 26 | 27 | ```go 28 | import "github.com/trealla-prolog/go/trealla" 29 | 30 | func main() { 31 | // load the interpreter and (optionally) grant access to the current directory 32 | pl, err := trealla.New(trealla.WithPreopenDir(".")) 33 | if err != nil { 34 | panic(err) 35 | } 36 | // run a query; cancel context to abort it 37 | ctx := context.Background() 38 | query := pl.Query(ctx, "member(X, [1, foo(bar), c]).") 39 | 40 | // calling Close is not necessary if you iterate through the whole result set 41 | // but it doesn't hurt either 42 | defer query.Close() 43 | 44 | // iterate through answers 45 | for query.Next(ctx) { 46 | answer := query.Current() 47 | x := answer.Solution["X"] 48 | fmt.Println(x) // 1, trealla.Compound{Functor: "foo", Args: [trealla.Atom("bar")]}, "c" 49 | } 50 | 51 | // make sure to check the query for errors 52 | if err := query.Err(); err != nil { 53 | panic(err) 54 | } 55 | } 56 | ``` 57 | 58 | ### Single query 59 | 60 | Use `QueryOnce` when you only want a single answer. 61 | 62 | ```go 63 | pl := trealla.New() 64 | answer, err := pl.QueryOnce(ctx, "succ(41, N).") 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | fmt.Println(answer.Stdout) 70 | // Output: hello world 71 | ``` 72 | 73 | ### Binding variables 74 | 75 | You can bind variables in the query using the `WithBind` and `WithBinding` options. 76 | This is a safe and convenient way to pass data into the query. 77 | It is OK to pass these multiple times. 78 | 79 | ```go 80 | pl := trealla.New() 81 | answer, err := pl.QueryOnce(ctx, "write(X)", trealla.WithBind("X", trealla.Atom("hello world"))) 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | fmt.Println(answer.Stdout) 87 | // Output: hello world 88 | ``` 89 | 90 | ### Scanning solutions 91 | 92 | You can scan an answer's substitutions directly into a struct or map, similar to ichiban/prolog. 93 | 94 | Use the `prolog:"VariableName"` struct tag to manually specify a variable name. 95 | Otherwise, the field's name is used. 96 | 97 | ```go 98 | answer, err := pl.QueryOnce(ctx, `X = 123, Y = abc, Z = ["hello", "world"].`) 99 | if err != nil { 100 | panic(err) 101 | } 102 | 103 | var result struct { 104 | X int 105 | Y string 106 | Hi []string `prolog:"Z"` 107 | } 108 | // make sure to pass a pointer to the struct! 109 | if err := answer.Solution.Scan(&result); err != nil { 110 | panic(err) 111 | } 112 | 113 | fmt.Printf("%+v", result) 114 | // Output: {X:123 Y:abc Hi:[hello world]} 115 | ``` 116 | 117 | #### Struct compounds 118 | 119 | Prolog compounds can destructure into Go structs. A special field of type `trealla.Functor` will be set to the functor. 120 | The compound's arguments are matched with the exported struct fields in order. 121 | These structs can also be used to bind variables in queries. 122 | 123 | ```prolog 124 | ?- findall(kv(Flag, Value), current_prolog_flag(Flag, Value), Flags). 125 | Flags = [kv(double_quotes,chars),kv(char_conversion,off),kv(occurs_check,false),kv(character_escapes,true),...] 126 | ``` 127 | 128 | ```go 129 | // You can embed trealla.Functor to represent Prolog compounds using Go structs. 130 | 131 | // kv(Flag, Value) 132 | type pair struct { 133 | trealla.Functor `prolog:"-/2"` // tag is optional, but can be used to specify the functor/arity 134 | Flag trealla.Atom // 1st arg 135 | Value trealla.Term // 2nd arg 136 | } 137 | var result struct { 138 | Flags []pair // Flags variable 139 | } 140 | 141 | ctx := context.Background() 142 | pl, err := trealla.New() 143 | if err != nil { 144 | panic(err) 145 | } 146 | answer, err := pl.QueryOnce(ctx, ` 147 | findall(Flag-Value, (member(Flag, [double_quotes, encoding, max_arity]), current_prolog_flag(Flag, Value)), Flags). 148 | `) 149 | if err != nil { 150 | panic(err) 151 | } 152 | if err := answer.Solution.Scan(&result); err != nil { 153 | panic(err) 154 | } 155 | fmt.Printf("%v\n", result.Flags) 156 | // Output: [{- double_quotes chars} {- encoding 'UTF-8'} {- max_arity 255}] 157 | ``` 158 | 159 | ## Documentation 160 | 161 | See **[package trealla's documentation](https://pkg.go.dev/github.com/trealla-prolog/go#section-directories)** for more details and examples. 162 | 163 | ## Builtins 164 | 165 | These additional predicates are built in: 166 | 167 | - `crypto_data_hash/3` 168 | - `http_consult/1` 169 | - Argument can be URL string, or `my_module_name:"https://url.example"` 170 | 171 | ## WASM binary 172 | 173 | This library embeds the Trealla WebAssembly binary in itself, so you can use it without any external dependencies. 174 | The binaries are currently sourced from [guregu/trealla](https://github.com/guregu/trealla). 175 | 176 | ### Building the WASM binary 177 | 178 | If you'd like to build `libtpl.wasm` yourself: 179 | 180 | ```bash 181 | # The submodule in src/trealla points to the current version 182 | git submodule update --init --recursive 183 | # Use the Makefile in the root of this project 184 | make clean wasm 185 | # Run the tests and benchmark to make sure it works 186 | go test -v ./trealla -bench=. 187 | ``` 188 | 189 | ## Thanks 190 | 191 | - Andrew Davison ([@infradig](https://github.com/infradig)) and other contributors to [Trealla Prolog](https://github.com/trealla-prolog/trealla). 192 | - Nuno Cruces ([@ncruces](https://github.com/ncruces)) for the wazero port. 193 | - Jos De Roo ([@josd](https://github.com/josd)) for test cases and encouragement. 194 | - Aram Panasenco ([@panasenco](https://github.com/panasenco)) for his JSON library. 195 | 196 | ## License 197 | 198 | MIT. See ATTRIBUTION as well. 199 | 200 | ## See also 201 | 202 | - [trealla-js](https://github.com/guregu/trealla-js) is Trealla for Javascript. 203 | - [ichiban/prolog](https://github.com/ichiban/prolog) is a pure Go Prolog. 204 | - [guregu/pengine](https://github.com/guregu/pengine) is a Pengines (SWI-Prolog) library for Go. 205 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package treallago is the parent package for embedding Trealla Prolog. 2 | // Mostly this is an artifact of Go needing the repo root to have a go.mod. 3 | // See the trealla subpackage. 4 | package treallago 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/trealla-prolog/go 2 | 3 | go 1.23 4 | 5 | require github.com/tetratelabs/wazero v1.9.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= 2 | github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= 3 | -------------------------------------------------------------------------------- /trealla/answer.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // Answer is a query result. 10 | type Answer struct { 11 | // Query is the original query goal. 12 | Query string 13 | // Solution (substitutions) for a successful query. 14 | // Indexed by variable name. 15 | Solution Substitution `json:"answer"` 16 | // Stdout is captured standard output text from this query. 17 | Stdout string 18 | // Stderr is captured standard error text from this query. 19 | Stderr string 20 | } 21 | 22 | type response struct { 23 | Answer 24 | Status queryStatus 25 | Error json.RawMessage // ball 26 | } 27 | 28 | func (pl *prolog) parse(goal, answer, stdout, stderr string) (Answer, error) { 29 | // log.Println("parse:", goal, "stdout:", stdout, "stderr:", stderr) 30 | if len(strings.TrimSpace(answer)) == 0 { 31 | return Answer{}, fmt.Errorf("empty answer") 32 | } 33 | if pl.stdout != nil { 34 | pl.stdout.Println(stdout) 35 | } 36 | if pl.stderr != nil { 37 | pl.stderr.Println(stderr) 38 | } 39 | if pl.debug != nil { 40 | pl.debug.Println(string(answer)) 41 | } 42 | 43 | resp := response{ 44 | Answer: Answer{ 45 | Query: goal, 46 | Stdout: stdout, 47 | Stderr: stderr, 48 | }, 49 | } 50 | 51 | dec := json.NewDecoder(strings.NewReader(answer)) 52 | dec.UseNumber() 53 | if err := dec.Decode(&resp); err != nil { 54 | return resp.Answer, fmt.Errorf("trealla: decoding error: %w (resp = %s)", err, string(answer)) 55 | } 56 | 57 | // spew.Dump(resp) 58 | 59 | switch resp.Status { 60 | case statusSuccess: 61 | return resp.Answer, nil 62 | case statusFailure: 63 | return resp.Answer, ErrFailure{Query: goal, Stdout: stdout, Stderr: stderr} 64 | case statusError: 65 | ball, err := unmarshalTerm(resp.Error) 66 | if err != nil { 67 | return resp.Answer, err 68 | } 69 | return resp.Answer, ErrThrow{Query: goal, Ball: ball, Stdout: stdout, Stderr: stderr} 70 | default: 71 | return resp.Answer, fmt.Errorf("trealla: unexpected query status: %v", resp.Status) 72 | } 73 | } 74 | 75 | // queryStatus is the status of a query answer. 76 | type queryStatus string 77 | 78 | // Result values. 79 | const ( 80 | // statusSuccess is for queries that succeed. 81 | statusSuccess queryStatus = "success" 82 | // statusFailure is for queries that fail (find no answers). 83 | statusFailure queryStatus = "failure" 84 | // statusError is for queries that throw an error. 85 | statusError queryStatus = "error" 86 | ) 87 | -------------------------------------------------------------------------------- /trealla/bench_test.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkQuery(b *testing.B) { 10 | pl, err := New() 11 | if err != nil { 12 | b.Fatal(err) 13 | } 14 | ctx := context.Background() 15 | b.ResetTimer() 16 | for i := 0; i < b.N; i++ { 17 | q := pl.Query(ctx, "X=1, write(X)") 18 | if !q.Next(ctx) { 19 | b.Fatal("no answer", q.Err()) 20 | } 21 | if q.Err() != nil { 22 | b.Fatal(err) 23 | } 24 | q.Close() 25 | } 26 | } 27 | 28 | func BenchmarkNewProlog(b *testing.B) { 29 | for i := 0; i < b.N; i++ { 30 | pl, err := New() 31 | if err != nil { 32 | b.Fatal(err) 33 | } 34 | _ = pl 35 | pl.Close() 36 | } 37 | } 38 | 39 | func BenchmarkClone(b *testing.B) { 40 | pl, err := New() 41 | if err != nil { 42 | b.Fatal(err) 43 | } 44 | if err := pl.ConsultText(context.Background(), "user", strings.Repeat("hello(world). ", 1024)); err != nil { 45 | b.Fatal(err) 46 | } 47 | b.ResetTimer() 48 | for i := 0; i < b.N; i++ { 49 | clone, err := pl.Clone() 50 | if err != nil { 51 | b.Fatal(err) 52 | } 53 | _ = clone 54 | } 55 | } 56 | 57 | func BenchmarkRedo(b *testing.B) { 58 | pl, err := New() 59 | if err != nil { 60 | b.Fatal(err) 61 | } 62 | ctx := context.Background() 63 | q := pl.Query(ctx, "repeat.") 64 | b.ResetTimer() 65 | for i := 0; i < b.N; i++ { 66 | q.Next(ctx) 67 | if q.Err() != nil { 68 | b.Fatal(err) 69 | } 70 | } 71 | q.Close() 72 | } 73 | 74 | func BenchmarkTak(b *testing.B) { 75 | pl, err := New(WithPreopenDir(".")) 76 | if err != nil { 77 | b.Fatal(err) 78 | } 79 | ctx := context.Background() 80 | b.ResetTimer() 81 | for i := 0; i < b.N; i++ { 82 | _, err := pl.QueryOnce(ctx, "consult('testdata/tak'), run") 83 | if err != nil { 84 | b.Fatal(err) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /trealla/decode.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | compoundType = reflect.TypeFor[Compound]() 12 | functorType = reflect.TypeFor[Functor]() 13 | termType = reflect.TypeFor[Term]() 14 | atomType = reflect.TypeFor[Atom]() 15 | ) 16 | 17 | func scan(sub Substitution, rv reflect.Value) error { 18 | switch rv.Kind() { 19 | case reflect.Map: 20 | vtype := rv.Type().Elem() 21 | for k, v := range sub { 22 | vv := reflect.ValueOf(v) 23 | if !vv.CanConvert(vtype) { 24 | return fmt.Errorf("trealla: invalid element type for Scan: %v", vtype) 25 | } 26 | rv.SetMapIndex(reflect.ValueOf(k), vv.Convert(vtype)) 27 | } 28 | return nil 29 | case reflect.Pointer: 30 | rv = rv.Elem() 31 | // we can't set the inner elements of *map and *interface directly, 32 | // so they need to be swapped out with a new inner value 33 | switch rv.Kind() { 34 | case reflect.Map: 35 | ev := reflect.MakeMap(rv.Type()) 36 | if err := scan(sub, ev); err != nil { 37 | return err 38 | } 39 | rv.Set(ev) 40 | return nil 41 | case reflect.Interface: 42 | ev := reflect.New(rv.Elem().Type()) 43 | if err := scan(sub, ev); err != nil { 44 | return err 45 | } 46 | rv.Set(ev.Elem()) 47 | return nil 48 | case reflect.Struct: 49 | // happy path 50 | default: 51 | return fmt.Errorf("trealla: must pass pointer to struct or map for Scan. got: %v", rv.Type()) 52 | } 53 | 54 | rtype := rv.Type() 55 | fieldnum := rtype.NumField() 56 | fields := make(map[string]reflect.Value, fieldnum) 57 | info := make(map[string]reflect.StructField, fieldnum) 58 | for i := 0; i < fieldnum; i++ { 59 | f := rtype.Field(i) 60 | name := f.Name 61 | if tag := f.Tag.Get("prolog"); tag != "" { 62 | name = tag 63 | } 64 | fields[name] = rv.Field(i) 65 | info[name] = f 66 | } 67 | 68 | for k, v := range sub { 69 | fv, ok := fields[k] 70 | if !ok { 71 | continue 72 | } 73 | if err := convert(fv, reflect.ValueOf(v), info[k]); err != nil { 74 | return fmt.Errorf("trealla: error converting field %q in %v: %w", k, rv.Type(), err) 75 | } 76 | } 77 | return nil 78 | } 79 | 80 | return fmt.Errorf("trealla: can't scan into type: %v; must be pointer to struct or map", rv.Type()) 81 | } 82 | 83 | func convert(dstv, srcv reflect.Value, meta reflect.StructField) error { 84 | if !srcv.IsValid() || !srcv.CanInterface() { 85 | return fmt.Errorf("invalid src: %v", srcv) 86 | } 87 | 88 | ftype := dstv.Type() 89 | 90 | if ftype == termType { 91 | dstv.Set(srcv) 92 | return nil 93 | } 94 | 95 | if srcv.Kind() == reflect.Interface && !srcv.IsNil() { 96 | srcv = srcv.Elem() 97 | } 98 | 99 | if dstv.Kind() == reflect.Slice { 100 | length := srcv.Len() 101 | srctype := srcv.Type() 102 | etype := ftype.Elem() 103 | detype := dstv.Type().Elem() 104 | var preconvert bool 105 | switch { 106 | case srctype == atomType && srcv.Interface().(Atom) == "[]": 107 | // special case: empty list 108 | length = 0 109 | case srctype.Kind() == reflect.String: 110 | // special case: convert string → list 111 | runes := []rune(srcv.String()) 112 | srcv = reflect.ValueOf(runes) 113 | length = len(runes) 114 | // if []Atom or []Term 115 | if dstv.Type().Elem().ConvertibleTo(atomType) || termType.AssignableTo(detype) { 116 | preconvert = true 117 | etype = atomType 118 | } 119 | } 120 | slice := reflect.MakeSlice(ftype, length, length) 121 | for i := 0; i < length; i++ { 122 | x := srcv.Index(i) 123 | if preconvert { 124 | x = x.Convert(etype) 125 | } 126 | if err := convert(slice.Index(i), x, meta); err != nil { 127 | return fmt.Errorf("can't convert %v[%d]; error: %w", srctype, i, err) 128 | } 129 | } 130 | dstv.Set(slice) 131 | return nil 132 | } 133 | 134 | // handle the empty string (rendered as [], the empty list) 135 | if dstv.Kind() == reflect.String && srcv.Kind() == reflect.Slice && srcv.Len() == 0 { 136 | dstv.SetString("") 137 | return nil 138 | } 139 | 140 | // compound → struct 141 | if srcv.Type() == compoundType && dstv.Kind() == reflect.Struct { 142 | return decodeCompoundStruct(dstv, srcv.Interface().(Compound), meta) 143 | } 144 | 145 | if !srcv.CanConvert(ftype) { 146 | return fmt.Errorf("can't convert from type %v to type: %v", srcv.Type(), ftype) 147 | } 148 | dstv.Set(srcv.Convert(ftype)) 149 | return nil 150 | } 151 | 152 | // TODO: break out reflect stuff into something like this: 153 | // type structInfo struct { 154 | // fields []reflect.Value 155 | // meta []reflect.StructField 156 | // functor string 157 | // arity int 158 | // } 159 | 160 | func decodeCompoundStruct(dstv reflect.Value, src Compound, meta reflect.StructField) error { 161 | rtype := dstv.Type() 162 | fieldnum := rtype.NumField() 163 | fields := make([]reflect.Value, 0, fieldnum) 164 | fieldInfo := make([]reflect.StructField, 0, fieldnum) 165 | 166 | var functor reflect.Value 167 | 168 | var collect func(dstv reflect.Value) error 169 | collect = func(dstv reflect.Value) error { 170 | for i := 0; i < fieldnum; i++ { 171 | field := rtype.Field(i) 172 | fv := dstv.Field(i) 173 | tag := field.Tag.Get("prolog") 174 | if tag == "-" { 175 | continue 176 | } 177 | exported := field.IsExported() 178 | if field.Type == functorType && exported { 179 | functor = fv 180 | continue 181 | } 182 | if field.Anonymous && field.Type.Kind() == reflect.Struct { 183 | if err := collect(fv); err != nil { 184 | return err 185 | } 186 | continue 187 | } 188 | if !exported { 189 | continue 190 | } 191 | fields = append(fields, fv) 192 | fieldInfo = append(fieldInfo, field) 193 | } 194 | return nil 195 | } 196 | if err := collect(dstv); err != nil { 197 | return err 198 | } 199 | 200 | if functor.IsValid() && functor.CanSet() { 201 | // TODO: check tag? 202 | functor.Set(reflect.ValueOf(Functor(src.Functor))) 203 | } 204 | 205 | for i := 0; i < min(len(fields), len(src.Args)); i++ { 206 | info := fieldInfo[i] 207 | if err := convert(fields[i], reflect.ValueOf(src.Args[i]), info); err != nil { 208 | return fmt.Errorf("can't convert compound (%v) argument #%d (type %T, value: %v) into field %q: %w", 209 | src.pi().String(), i, src.Args[i], src.Args[i], info.Name, err) 210 | } 211 | } 212 | return nil 213 | } 214 | 215 | func encodeCompoundStruct(src any) (Compound, error) { 216 | marker, ok := src.(compoundStruct) 217 | if !ok { 218 | return Compound{}, fmt.Errorf("can't encode %T to compound; no Functor field found", src) 219 | } 220 | srcv := reflect.ValueOf(src) 221 | for srcv.Kind() == reflect.Pointer && !srcv.IsNil() { 222 | srcv = srcv.Elem() 223 | } 224 | if srcv.Kind() != reflect.Struct { 225 | return Compound{}, fmt.Errorf("not a struct: %T", src) 226 | } 227 | 228 | rtype := srcv.Type() 229 | fieldnum := rtype.NumField() 230 | fields := make([]reflect.Value, 0, fieldnum) 231 | fieldInfo := make([]reflect.StructField, 0, fieldnum) 232 | functor := marker.functor() 233 | 234 | // var functor reflect.Value 235 | // var expectFunctor Functor 236 | // expectArity := -1 237 | var expect string 238 | var arity int 239 | var collect func(dstv reflect.Value) error 240 | collect = func(dstv reflect.Value) error { 241 | for i := 0; i < fieldnum; i++ { 242 | field := rtype.Field(i) 243 | fv := dstv.Field(i) 244 | tag := field.Tag.Get("prolog") 245 | if tag == "-" { 246 | continue 247 | } 248 | exported := field.IsExported() 249 | if field.Type == functorType && exported { 250 | expect, arity = structTag(tag) 251 | continue 252 | } 253 | if field.Anonymous && field.Type.Kind() == reflect.Struct { 254 | if err := collect(fv); err != nil { 255 | return err 256 | } 257 | continue 258 | } 259 | if !exported { 260 | continue 261 | } 262 | fields = append(fields, fv) 263 | fieldInfo = append(fieldInfo, field) 264 | } 265 | return nil 266 | } 267 | if err := collect(srcv); err != nil { 268 | return Compound{}, err 269 | } 270 | 271 | c := Compound{Functor: Atom(functor), Args: make([]Term, 0, len(fields))} 272 | for i := 0; i < len(fields); i++ { 273 | // info := fieldInfo[i] 274 | iface := fields[i].Interface() 275 | // tt, err := marshal(iface.(Term)) 276 | // if err != nil { 277 | // return c, fmt.Errorf("can't encode compound (%v) argument #%d (type %T, value: %v) from field %q: %w", 278 | // functor, i, iface, iface, info.Name, err) 279 | // } 280 | c.Args = append(c.Args, iface) 281 | } 282 | if c.Functor == "" && expect != "" { 283 | c.Functor = Atom(expect) 284 | } 285 | if arity > 0 && len(c.Args) != arity { 286 | names := make([]string, len(fields)) 287 | for i := 0; i < len(fields); i++ { 288 | names[i] = fieldInfo[i].Name 289 | } 290 | return c, fmt.Errorf("# of fields in %T does not match arity of struct tag (%s/%d): have %d fields %v but expected %d", 291 | src, expect, arity, len(c.Args), names, arity) 292 | } 293 | 294 | return c, nil 295 | } 296 | 297 | func structTag(tag string) (name string, arity int) { 298 | if tag == "" { 299 | return 300 | } 301 | name, _, _ = strings.Cut(tag, ",") 302 | slash := strings.LastIndexByte(name, '/') 303 | if slash > 0 && slash < len(name)-1 { 304 | arity, _ = strconv.Atoi(name[slash+1:]) 305 | name = name[:slash] 306 | } 307 | return 308 | } 309 | -------------------------------------------------------------------------------- /trealla/encode.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Marshal returns the Prolog text representation of term. 12 | func Marshal(term Term) (string, error) { 13 | return marshal(term) 14 | } 15 | 16 | func marshal(term Term) (string, error) { 17 | switch x := term.(type) { 18 | case string: 19 | return escapeString(x), nil 20 | case int64: 21 | return strconv.FormatInt(x, 10), nil 22 | case int: 23 | return strconv.FormatInt(int64(x), 10), nil 24 | case uint64: 25 | return strconv.FormatUint(x, 10), nil 26 | case uint: 27 | return strconv.FormatUint(uint64(x), 10), nil 28 | case float64: 29 | return strconv.FormatFloat(x, 'f', -1, 64), nil 30 | case float32: 31 | return strconv.FormatFloat(float64(x), 'f', -1, 32), nil 32 | case *big.Int: 33 | return x.String(), nil 34 | case *big.Rat: 35 | n := x.Num().String() 36 | d := x.Denom().String() 37 | return n + " rdiv " + d, nil 38 | case Atom: 39 | return x.String(), nil 40 | case Compound: 41 | return x.String(), nil 42 | case Variable: 43 | return x.String(), nil 44 | case compoundStruct: 45 | c, err := encodeCompoundStruct(term) 46 | if err != nil { 47 | return "", fmt.Errorf("trealla: error marshaling term %#v: %w", term, err) 48 | } 49 | return c.String(), nil 50 | case []Term: 51 | return marshalSlice(x) 52 | case []any: 53 | return marshalSlice(x) 54 | case []string: 55 | return marshalSlice(x) 56 | case []int64: 57 | return marshalSlice(x) 58 | case []int: 59 | return marshalSlice(x) 60 | case []float64: 61 | return marshalSlice(x) 62 | case []*big.Int: 63 | return marshalSlice(x) 64 | case []Atom: 65 | return marshalSlice(x) 66 | case []Compound: 67 | return marshalSlice(x) 68 | case []Variable: 69 | return marshalSlice(x) 70 | default: 71 | rv := reflect.ValueOf(term) 72 | if !rv.IsValid() { 73 | break 74 | } 75 | for rv.Kind() == reflect.Pointer && !rv.IsNil() { 76 | rv = rv.Elem() 77 | } 78 | if !rv.CanInterface() { 79 | break 80 | } 81 | 82 | switch rv.Kind() { 83 | case reflect.Slice, reflect.Array: 84 | var sb strings.Builder 85 | sb.WriteByte('[') 86 | length := rv.Len() 87 | for i := 0; i < length; i++ { 88 | if i != 0 { 89 | sb.WriteByte(',') 90 | } 91 | elem, err := marshal(rv.Index(i).Interface()) 92 | if err != nil { 93 | return "", err 94 | } 95 | sb.WriteString(elem) 96 | } 97 | sb.WriteByte(']') 98 | return sb.String(), nil 99 | 100 | case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8: 101 | return strconv.FormatInt(rv.Int(), 10), nil 102 | case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8: 103 | return strconv.FormatUint(rv.Uint(), 10), nil 104 | case reflect.Float64: 105 | return strconv.FormatFloat(rv.Float(), 'f', -1, 64), nil 106 | case reflect.Float32: 107 | return strconv.FormatFloat(rv.Float(), 'f', -1, 32), nil 108 | case reflect.String: 109 | return escapeString(rv.String()), nil 110 | } 111 | } 112 | return "", fmt.Errorf("trealla: can't marshal type %T, value: %v", term, term) 113 | } 114 | 115 | func marshalSlice[T any](slice []T) (string, error) { 116 | var sb strings.Builder 117 | sb.WriteRune('[') 118 | for i, v := range slice { 119 | if i != 0 { 120 | sb.WriteString(", ") 121 | } 122 | text, err := marshal(v) 123 | if err != nil { 124 | return "", err 125 | } 126 | sb.WriteString(text) 127 | } 128 | sb.WriteRune(']') 129 | return sb.String(), nil 130 | } 131 | 132 | func escapeString(str string) string { 133 | return `"` + stringEscaper.Replace(str) + `"` 134 | } 135 | 136 | var stringEscaper = strings.NewReplacer( 137 | `\`, `\\`, 138 | `"`, `\"`, 139 | "\n", `\n`, 140 | "\t", `\t`, 141 | ) 142 | 143 | var atomEscaper = strings.NewReplacer( 144 | `\`, `\\`, 145 | `'`, `\'`, 146 | ) 147 | -------------------------------------------------------------------------------- /trealla/error.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | // ErrFailure is returned when a query fails (when it finds no solutions). 9 | type ErrFailure struct { 10 | // Query is the original query goal. 11 | Query string 12 | // Stdout output from the query. 13 | Stdout string 14 | // Stderr output from the query (useful for traces). 15 | Stderr string 16 | } 17 | 18 | // Error implements the error interface. 19 | func (err ErrFailure) Error() string { 20 | return "trealla: query failed: " + err.Query 21 | } 22 | 23 | // IsFailure returns true if the given error is a failed query error (ErrFailure). 24 | func IsFailure(err error) bool { 25 | return errors.As(err, &ErrFailure{}) 26 | } 27 | 28 | // ErrThrow is returned when an exception is thrown during a query. 29 | type ErrThrow struct { 30 | // Query is the original query goal. 31 | Query string 32 | // Ball is the term thrown by throw/1. 33 | Ball Term 34 | // Stdout output from the query. 35 | Stdout string 36 | // Stderr output from the query (useful for traces). 37 | Stderr string 38 | } 39 | 40 | // Error implements the error interface. 41 | func (err ErrThrow) Error() string { 42 | return fmt.Sprintf("trealla: exception thrown: %v", err.Ball) 43 | } 44 | 45 | func errUnexported(symbol string) error { 46 | return fmt.Errorf("trealla: failed to get wasm exported function: %q (symbol not found)", symbol) 47 | } 48 | 49 | var ( 50 | _ error = ErrFailure{} 51 | _ error = ErrThrow{} 52 | ) 53 | -------------------------------------------------------------------------------- /trealla/example_test.go: -------------------------------------------------------------------------------- 1 | package trealla_test 2 | 3 | import ( 4 | "context" 5 | "encoding/base32" 6 | "fmt" 7 | "iter" 8 | 9 | "github.com/trealla-prolog/go/trealla" 10 | ) 11 | 12 | func Example() { 13 | ctx := context.Background() 14 | 15 | // create a new Prolog interpreter 16 | pl, err := trealla.New() 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | // start a new query 22 | query := pl.Query(ctx, "use_module(library(lists)), member(X, [1, foo(bar), c]).") 23 | // calling Close is not necessary if you iterate through the whole query, but it doesn't hurt 24 | defer query.Close() 25 | 26 | // iterate through answers 27 | for query.Next(ctx) { 28 | answer := query.Current() 29 | x := answer.Solution["X"] 30 | fmt.Println(x) 31 | } 32 | 33 | // make sure to check the query for errors 34 | if err := query.Err(); err != nil { 35 | panic(err) 36 | } 37 | // Output: 1 38 | // foo(bar) 39 | // c 40 | } 41 | 42 | func ExampleWithBind() { 43 | ctx := context.Background() 44 | pl, err := trealla.New() 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | // bind the variable X to the atom 'hello world' through query options 50 | answer, err := pl.QueryOnce(ctx, "write(X).", trealla.WithBind("X", trealla.Atom("hello world"))) 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | fmt.Println(answer.Stdout) 56 | // Output: hello world 57 | } 58 | 59 | func ExampleFunctor() { 60 | // You can embed trealla.Functor to represent Prolog compounds using Go structs. 61 | 62 | // kv(Flag, Value) 63 | type pair struct { 64 | trealla.Functor `prolog:"-/2"` // tag is optional, but can be used to specify the functor/arity 65 | Flag trealla.Atom // 1st arg 66 | Value trealla.Term // 2nd arg 67 | } 68 | var result struct { 69 | Flags []pair // Flags variable 70 | } 71 | 72 | ctx := context.Background() 73 | pl, err := trealla.New() 74 | if err != nil { 75 | panic(err) 76 | } 77 | answer, err := pl.QueryOnce(ctx, ` 78 | findall(Flag-Value, (member(Flag, [double_quotes, encoding, max_arity]), current_prolog_flag(Flag, Value)), Flags). 79 | `) 80 | if err != nil { 81 | panic(err) 82 | } 83 | if err := answer.Solution.Scan(&result); err != nil { 84 | panic(err) 85 | } 86 | fmt.Printf("%v\n", result.Flags) 87 | // Output: [{- double_quotes chars} {- encoding 'UTF-8'} {- max_arity 255}] 88 | } 89 | 90 | func ExampleProlog_Register() { 91 | ctx := context.Background() 92 | pl, err := trealla.New() 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | // Let's add a base32 encoding predicate. 98 | // To keep it brief, this only handles one mode. 99 | // base32(+Input, -Output) is det. 100 | pl.Register(ctx, "base32", 2, func(_ trealla.Prolog, _ trealla.Subquery, goal0 trealla.Term) trealla.Term { 101 | // goal is the goal called by Prolog, such as: base32("hello", X). 102 | // Guaranteed to match up with the registered arity and name. 103 | goal := goal0.(trealla.Compound) 104 | 105 | // Check the Input argument's type, must be string. 106 | input, ok := goal.Args[0].(string) 107 | if !ok { 108 | // throw(error(type_error(list, X), base32/2)). 109 | return trealla.Atom("throw").Of(trealla.Atom("error").Of( 110 | trealla.Atom("type_error").Of("list", goal.Args[0]), 111 | trealla.Atom("/").Of(trealla.Atom("base32"), 2), 112 | )) 113 | } 114 | 115 | // Check Output type, must be string or var. 116 | switch goal.Args[1].(type) { 117 | case string: // ok 118 | case trealla.Variable: // ok 119 | default: 120 | // throw(error(type_error(list, X), base32/2)). 121 | // See: terms subpackage for convenience functions to create these errors. 122 | return trealla.Atom("throw").Of(trealla.Atom("error").Of( 123 | trealla.Atom("type_error").Of("chars", goal.Args[0]), 124 | trealla.Atom("/").Of(trealla.Atom("base32"), 2), 125 | )) 126 | } 127 | 128 | // Do the actual encoding work. 129 | output := base32.StdEncoding.EncodeToString([]byte(input)) 130 | 131 | // Return a goal that Trealla will unify with its input: 132 | // base32(Input, "output_goes_here"). 133 | return trealla.Atom("base32").Of(input, output) 134 | }) 135 | 136 | // Try it out. 137 | answer, err := pl.QueryOnce(ctx, `base32("hello", Encoded).`) 138 | if err != nil { 139 | panic(err) 140 | } 141 | fmt.Println(answer.Solution["Encoded"]) 142 | // Output: NBSWY3DP 143 | } 144 | 145 | func ExampleProlog_RegisterNondet() { 146 | ctx := context.Background() 147 | pl, err := trealla.New() 148 | if err != nil { 149 | panic(err) 150 | } 151 | 152 | // Let's add a native equivalent of between/3. 153 | // betwixt(+Min, +Max, ?N). 154 | pl.RegisterNondet(ctx, "betwixt", 3, func(_ trealla.Prolog, _ trealla.Subquery, goal0 trealla.Term) iter.Seq[trealla.Term] { 155 | pi := trealla.Atom("/").Of(trealla.Atom("betwixt"), 2) 156 | return func(yield func(trealla.Term) bool) { 157 | // goal is the goal called by Prolog, such as: base32("hello", X). 158 | // Guaranteed to match up with the registered arity and name. 159 | goal := goal0.(trealla.Compound) 160 | 161 | // Check Min and Max argument's type, must be integers (all integers are int64). 162 | min, ok := goal.Args[0].(int64) 163 | if !ok { 164 | // throw(error(type_error(integer, Min), betwixt/3)). 165 | yield(trealla.Atom("throw").Of(trealla.Atom("error").Of( 166 | trealla.Atom("type_error").Of("integer", goal.Args[0]), 167 | pi, 168 | ))) 169 | // See terms subpackage for an easier way: 170 | // yield(terms.Throw(terms.TypeError("integer", goal.Args[0], terms.PI(goal))) 171 | return 172 | } 173 | max, ok := goal.Args[1].(int64) 174 | if !ok { 175 | // throw(error(type_error(integer, Max), betwixt/3)). 176 | yield(trealla.Atom("throw").Of(trealla.Atom("error").Of( 177 | trealla.Atom("type_error").Of("integer", goal.Args[1]), 178 | pi, 179 | ))) 180 | return 181 | } 182 | 183 | if min > max { 184 | // Since we haven't yielded anything, this will fail. 185 | return 186 | } 187 | 188 | switch x := goal.Args[2].(type) { 189 | case int64: 190 | // If the 3rd argument is bound, we can do a simple check and stop iterating. 191 | if x >= min && x <= max { 192 | yield(goal) 193 | return 194 | } 195 | case trealla.Variable: 196 | // Create choice points unifying N from min to max 197 | for n := min; n <= max; n++ { 198 | goal.Args[2] = n 199 | if !yield(goal) { 200 | break 201 | } 202 | } 203 | default: 204 | yield(trealla.Atom("throw").Of(trealla.Atom("error").Of( 205 | trealla.Atom("type_error").Of("integer", goal.Args[2]), 206 | trealla.Atom("/").Of(trealla.Atom("base32"), 2), 207 | ))) 208 | } 209 | } 210 | }) 211 | 212 | // Try it out. 213 | answer, err := pl.QueryOnce(ctx, `findall(N, betwixt(1, 5, N), Ns), write(Ns).`) 214 | if err != nil { 215 | panic(err) 216 | } 217 | fmt.Println(answer.Stdout) 218 | // Output: [1,2,3,4,5] 219 | } 220 | -------------------------------------------------------------------------------- /trealla/interop.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "iter" 8 | ) 9 | 10 | // Predicate is a Prolog predicate implemented in Go. 11 | // subquery is an opaque number representing the current query. 12 | // goal is the goal called, which includes the arguments. 13 | // 14 | // Return value meaning: 15 | // - By default, the term returned will be unified with the goal. 16 | // - Return a throw/1 compound to throw instead. 17 | // - Return a call/1 compound to call a different goal instead. 18 | // - Return a 'fail' atom to fail instead. 19 | // - Return a 'true' atom to succeed without unifying anything. 20 | type Predicate func(pl Prolog, subquery Subquery, goal Term) Term 21 | 22 | // NondetPredicate works similarly to [Predicate], but can create multiple choice points. 23 | type NondetPredicate func(pl Prolog, subquery Subquery, goal Term) iter.Seq[Term] 24 | 25 | // Subquery is an opaque value representing an in-flight query. 26 | // It is unique as long as the query is alive, but may be re-used later on. 27 | type Subquery uint32 28 | 29 | type coroutine struct { 30 | next func() (Term, bool) 31 | stop func() 32 | } 33 | 34 | type coroer interface { 35 | CoroStart(subq Subquery, seq iter.Seq[Term]) int64 36 | CoroNext(subq Subquery, id int64) (Term, bool) 37 | CoroStop(subq Subquery, id int64) 38 | } 39 | 40 | func (pl *prolog) Register(ctx context.Context, name string, arity int, proc Predicate) error { 41 | pl.mu.Lock() 42 | defer pl.mu.Unlock() 43 | if pl.instance == nil { 44 | return io.EOF 45 | } 46 | return pl.register(ctx, name, arity, proc) 47 | } 48 | 49 | func (pl *prolog) register(ctx context.Context, name string, arity int, proc Predicate) error { 50 | functor := Atom(name) 51 | pi := piTerm(functor, arity) 52 | pl.procs[pi.String()] = proc 53 | vars := numbervars(arity) 54 | head := functor.Of(vars...) 55 | body := Atom(":").Of(Atom("wasm_generic"), Atom("host_rpc").Of(head)) 56 | clause := fmt.Sprintf(`%s :- %s.`, head.String(), body.String()) 57 | return pl.consultText(ctx, "user", clause) 58 | } 59 | 60 | func (pl *prolog) RegisterNondet(ctx context.Context, name string, arity int, proc NondetPredicate) error { 61 | pl.mu.Lock() 62 | defer pl.mu.Unlock() 63 | if pl.instance == nil { 64 | return io.EOF 65 | } 66 | return pl.registerNondet(ctx, name, arity, proc) 67 | } 68 | 69 | func (pl *prolog) registerNondet(ctx context.Context, name string, arity int, proc NondetPredicate) error { 70 | shim := func(pl2 Prolog, subquery Subquery, goal Term) Term { 71 | plc := pl2.(coroer) 72 | seq := proc(pl2, subquery, goal) 73 | id := plc.CoroStart(subquery, seq) 74 | // call: call_cleanup('$coro_next'(ID), '$coro_stop'(ID)) 75 | return Atom("call").Of( 76 | Atom("call_cleanup").Of( 77 | Atom("$coro_next").Of(id, goal), 78 | Atom("$coro_stop").Of(id), 79 | ), 80 | ) 81 | } 82 | return pl.register(ctx, name, arity, shim) 83 | } 84 | 85 | // '$coro_next'(+ID, ?Goal) 86 | func sys_coro_next_2(pl Prolog, subquery Subquery, goal Term) Term { 87 | plc := pl.(coroer) 88 | g := goal.(Compound) 89 | id, ok := g.Args[0].(int64) 90 | if !ok { 91 | return throwTerm(domainError("integer", g.Args[0], g.pi())) 92 | } 93 | result, ok := plc.CoroNext(subquery, id) 94 | if !ok || result == nil { 95 | return Atom("fail") 96 | } 97 | // call(( wasm_generic:host_rpc_eval(Goal, Result, [], []) ; '$coro_next'(ID, Goal) )) 98 | return Atom("call").Of( 99 | Atom(";").Of( 100 | Atom(":").Of(Atom("wasm_generic"), Atom("host_rpc_eval").Of(result, g.Args[1], Atom("[]"), Atom("[]"))), 101 | Atom("$coro_next").Of(id, g.Args[1]), 102 | ), 103 | ) 104 | } 105 | 106 | // '$coro_stop'(+ID) 107 | func sys_coro_stop_1(pl Prolog, subquery Subquery, goal Term) Term { 108 | plc := pl.(coroer) 109 | g := goal.(Compound) 110 | id, ok := g.Args[0].(int64) 111 | if !ok { 112 | return throwTerm(domainError("integer", g.Args[0], g.pi())) 113 | } 114 | plc.CoroStop(subquery, id) 115 | return goal 116 | } 117 | 118 | func (pl *prolog) CoroStart(subq Subquery, seq iter.Seq[Term]) int64 { 119 | pl.coron++ 120 | id := pl.coron 121 | next, stop := iter.Pull(seq) 122 | pl.coros[id] = coroutine{ 123 | next: next, 124 | stop: stop, 125 | } 126 | if query := pl.subquery(uint32(subq)); query != nil { 127 | if query.coros == nil { 128 | query.coros = make(map[int64]struct{}) 129 | } 130 | query.coros[id] = struct{}{} 131 | } 132 | return id 133 | } 134 | 135 | func (pl *prolog) CoroNext(subq Subquery, id int64) (Term, bool) { 136 | coro, ok := pl.coros[id] 137 | if !ok { 138 | return Atom("false"), false 139 | } 140 | next, ok := coro.next() 141 | if !ok { 142 | delete(pl.coros, id) 143 | if query := pl.subquery(uint32(subq)); query != nil { 144 | delete(query.coros, id) 145 | } 146 | } 147 | return next, ok 148 | } 149 | 150 | func (pl *prolog) CoroStop(subq Subquery, id int64) { 151 | if query := pl.subquery(uint32(subq)); query != nil { 152 | delete(query.coros, id) 153 | } 154 | coro, ok := pl.coros[id] 155 | if !ok { 156 | return 157 | } 158 | coro.stop() 159 | delete(pl.coros, id) 160 | } 161 | 162 | func (pl *lockedProlog) CoroStart(subq Subquery, seq iter.Seq[Term]) int64 { 163 | return pl.prolog.CoroStart(subq, seq) 164 | } 165 | 166 | func (pl *lockedProlog) CoroNext(subq Subquery, id int64) (Term, bool) { 167 | return pl.prolog.CoroNext(subq, id) 168 | } 169 | 170 | func (pl *lockedProlog) CoroStop(subq Subquery, id int64) { 171 | pl.prolog.CoroStop(subq, id) 172 | } 173 | 174 | func hostCall(ctx context.Context, subquery, msgptr, msgsize, reply_pp, replysize_p uint32) uint32 { 175 | // extern int32_t host_call(int32_t subquery, const char *msg, size_t msg_size, char **reply, size_t *reply_size); 176 | subq := ctx.Value(queryContext{}).(*query) 177 | pl := subq.pl 178 | 179 | msgraw, err := pl.gets(msgptr, msgsize) 180 | if err != nil { 181 | panic(err) 182 | } 183 | 184 | msg, err := unmarshalTerm([]byte(msgraw)) 185 | if err != nil { 186 | err = fmt.Errorf("%w (raw msg: %s)", err, msgraw) 187 | panic(err) 188 | } 189 | 190 | reply := func(str string) error { 191 | msg, err := newCString(pl, str) 192 | if err != nil { 193 | return err 194 | } 195 | pl.memory.WriteUint32Le(reply_pp, msg.ptr) 196 | pl.memory.WriteUint32Le(replysize_p, uint32(msg.size-1)) 197 | return nil 198 | } 199 | 200 | goal, ok := msg.(atomicTerm) 201 | if !ok { 202 | expr := typeError("atomic", msg, piTerm("$host_call", 2)) 203 | if err := reply(expr.String()); err != nil { 204 | panic(err) 205 | } 206 | return wasmTrue 207 | } 208 | 209 | proc, ok := pl.procs[goal.Indicator()] 210 | if !ok { 211 | expr := Atom("throw").Of( 212 | Atom("error").Of( 213 | Atom("existence_error").Of(Atom("procedure"), goal.pi()), 214 | piTerm("$host_call", 2), 215 | )) 216 | if err := reply(expr.String()); err != nil { 217 | panic(err) 218 | } 219 | return wasmTrue 220 | } 221 | 222 | if err := subq.readOutput(); err != nil { 223 | panic(err) 224 | } 225 | // log.Println("SAVING", subq.stderr.String()) 226 | 227 | locked := &lockedProlog{prolog: pl} 228 | continuation := catch(proc, locked, Subquery(subquery), goal) 229 | locked.kill() 230 | expr, err := marshal(continuation) 231 | if err != nil { 232 | panic(err) 233 | } 234 | if err := reply(expr); err != nil { 235 | panic(err) 236 | } 237 | 238 | if err := subq.readOutput(); err != nil { 239 | panic(err) 240 | } 241 | return wasmTrue 242 | } 243 | 244 | func hostPushAnswer(ctx context.Context, subquery, msgptr, msgsize uint32) { 245 | // extern void host_push_answer(int32_t subquery, const char *msg, size_t msg_size); 246 | // pl := ctx.Value(prologKey{}).(*prolog) 247 | subq := ctx.Value(queryContext{}).(*query) 248 | pl := subq.pl 249 | if subq == nil { 250 | panic(fmt.Sprintf("could not find subquery: %d", subquery)) 251 | } 252 | 253 | msg, err := pl.gets(msgptr, msgsize) 254 | if err != nil { 255 | subq.setError(err) 256 | return 257 | } 258 | 259 | if err := subq.readOutput(); err != nil { 260 | subq.setError(err) 261 | return 262 | } 263 | stdout := subq.stdout.String() 264 | stderr := subq.stderr.String() 265 | subq.resetOutput() 266 | 267 | ans, err := pl.parse(subq.goal, msg, stdout, stderr) 268 | if err != nil { 269 | subq.setError(err) 270 | return 271 | } 272 | subq.push(ans) 273 | } 274 | 275 | func catch(pred Predicate, pl Prolog, subq Subquery, goal Term) (result Term) { 276 | defer func() { 277 | if threw := recover(); threw != nil { 278 | switch ball := threw.(type) { 279 | case Atom: 280 | result = throwTerm(ball) 281 | case Compound: 282 | if ball.Functor == "throw" && len(ball.Args) == 1 { 283 | result = ball 284 | } else { 285 | result = throwTerm(ball) 286 | } 287 | default: 288 | result = throwTerm( 289 | Atom("system_error").Of( 290 | Atom("panic").Of(fmt.Sprint(threw)), 291 | goal.(atomicTerm).pi(), 292 | ), 293 | ) 294 | } 295 | } 296 | }() 297 | result = pred(pl, subq, goal) 298 | return 299 | } 300 | 301 | func hostResume(_, _, _ uint32) uint32 { 302 | // extern int32_t host_resume(int32_t subquery, char **reply, size_t *reply_size); 303 | return wasmFalse 304 | } 305 | 306 | var ( 307 | _ coroer = (*prolog)(nil) 308 | _ coroer = (*lockedProlog)(nil) 309 | ) 310 | -------------------------------------------------------------------------------- /trealla/interop_test.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "iter" 7 | "log" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestInterop(t *testing.T) { 13 | pl, err := New(WithDebugLog(log.Default())) 14 | 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | ctx := context.Background() 20 | pl.Register(ctx, "interop_test", 1, func(pl Prolog, _ Subquery, goal Term) Term { 21 | want := Atom("interop_test").Of(Variable{Name: "A"}) 22 | if !reflect.DeepEqual(want, goal) { 23 | t.Error("bad goal. want:", want, "got:", goal) 24 | } 25 | 26 | // clone will have its own stack, making reentrancy less scary 27 | clone, err := pl.Clone() 28 | if err != nil { 29 | t.Error(err) 30 | return throwTerm(systemError(err.Error())) 31 | } 32 | 33 | ans1, err := clone.QueryOnce(ctx, "X is 1 + 1.") 34 | if err != nil { 35 | t.Error(err) 36 | return throwTerm(systemError(err.Error())) 37 | } 38 | 39 | ans2, err := clone.QueryOnce(ctx, "Y is X + 1.", WithBind("X", ans1.Solution["X"])) 40 | if err != nil { 41 | t.Error(err) 42 | return throwTerm(systemError(err.Error())) 43 | } 44 | 45 | return Atom("interop_test").Of(ans2.Solution["Y"]) 46 | }) 47 | 48 | tests := []struct { 49 | name string 50 | want []Answer 51 | err error 52 | }{ 53 | { 54 | name: "crypto_data_hash/3", 55 | want: []Answer{ 56 | { 57 | Query: `crypto_data_hash("foo", X, [algorithm(A)]).`, 58 | Solution: Substitution{"A": Atom("sha256"), "X": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}, 59 | }, 60 | }, 61 | }, 62 | { 63 | name: "http_consult/1", 64 | want: []Answer{ 65 | { 66 | Query: `http_consult(fizzbuzz:"https://raw.githubusercontent.com/guregu/worker-prolog/978c956801ffff83f190450e5c0325a9d34b064a/src/views/examples/fizzbuzz.pl"), fizzbuzz:fizzbuzz(1, 21), !`, 67 | Solution: Substitution{}, 68 | Stdout: "1\n2\nfizz\n4\nbuzz\nfizz\n7\n8\nfizz\nbuzz\n11\nfizz\n13\n14\nfizzbuzz\n16\n17\nfizz\n19\nbuzz\nfizz\n", 69 | }, 70 | }, 71 | }, 72 | { 73 | name: "custom function", 74 | want: []Answer{ 75 | { 76 | Query: `interop_test(X).`, 77 | Solution: Substitution{"X": int64(3)}, 78 | }, 79 | }, 80 | }, 81 | // { 82 | // name: "http_fetch/3", 83 | // want: []Answer{ 84 | // { 85 | // Query: `http_fetch("https://jsonplaceholder.typicode.com/todos/1", Result, [as(json)]).`, 86 | // Solution: Substitution{"Result": Compound{Functor: "{}", Args: []Term{Compound{Functor: ",", Args: []Term{Compound{Functor: ":", Args: []Term{"userId", int64(1)}}, Compound{Functor: ",", Args: []Term{Compound{Functor: ":", Args: []Term{"id", int64(1)}}, Compound{Functor: ",", Args: []Term{Compound{Functor: ":", Args: []Term{"title", "delectus aut autem"}}, Compound{Functor: ":", Args: []Term{"completed", "false"}}}}}}}}}}}, 87 | // }, 88 | // }, 89 | // }, 90 | } 91 | 92 | for _, tc := range tests { 93 | // TODO: these (used to be) flakey on Linux 94 | // seems to be concurrency causing too much wasm stack to be used 95 | // cloning the pl instance "fixes" it 96 | t.Run(tc.name, func(t *testing.T) { 97 | ctx := context.Background() 98 | q := pl.Query(ctx, tc.want[0].Query) 99 | var ans []Answer 100 | for q.Next(ctx) { 101 | ans = append(ans, q.Current()) 102 | } 103 | q.Close() 104 | err := q.Err() 105 | if tc.err == nil && err != nil { 106 | t.Fatal(err) 107 | } else if tc.err != nil && !errors.Is(err, tc.err) { 108 | t.Errorf("unexpected error: %#v (%v) ", err, err) 109 | } 110 | if tc.err == nil && !reflect.DeepEqual(ans, tc.want) { 111 | t.Errorf("bad answer. \nwant: %#v\ngot: %#v\n", tc.want, ans) 112 | } 113 | }) 114 | } 115 | } 116 | 117 | func TestInteropNondet(t *testing.T) { 118 | pred := func(pl Prolog, subquery Subquery, goal Term) iter.Seq[Term] { 119 | return func(yield func(Term) bool) { 120 | g := goal.(Compound) 121 | n, ok := goal.(Compound).Args[0].(int64) 122 | if !ok { 123 | yield(throwTerm(typeError("integer", goal.(Compound).Args[0], goal.(atomicTerm).pi()))) 124 | return 125 | } 126 | for i := int64(0); i < n; i++ { 127 | g.Args[1] = i 128 | if !yield(g) { 129 | break 130 | } 131 | } 132 | } 133 | } 134 | ctx := context.Background() 135 | pl, err := New(WithDebugLog(log.Default())) 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | pl.RegisterNondet(ctx, "countdown", 2, pred) 140 | 141 | t.Run("success", func(t *testing.T) { 142 | q := pl.Query(ctx, "countdown(10, X)") 143 | var n int64 144 | for answer := range q.All(ctx) { 145 | t.Logf("got: %v", answer) 146 | if answer.Solution["X"] != n { 147 | t.Error("unexpected solution:", answer) 148 | } 149 | n++ 150 | } 151 | if n != 10 { 152 | t.Error("not enough iterations") 153 | } 154 | if q.Err() != nil { 155 | t.Error(q.Err()) 156 | } 157 | }) 158 | 159 | t.Run("bad arg", func(t *testing.T) { 160 | q := pl.Query(ctx, "countdown(foobar, X)") 161 | for q.Next(ctx) { 162 | } 163 | if q.Err() == nil { 164 | t.Error("expected error") 165 | } 166 | }) 167 | 168 | t.Run("abandoned", func(t *testing.T) { 169 | q := pl.Query(ctx, "countdown(10, X)") 170 | if !q.Next(ctx) { 171 | t.Fatal("expected success") 172 | } 173 | if err := q.Close(); err != nil { 174 | t.Fatal(err) 175 | } 176 | if q.Err() != nil { 177 | t.Error(err) 178 | } 179 | }) 180 | 181 | if leftovers := len(pl.(*prolog).coros); leftovers > 0 { 182 | t.Error("coroutines weren't cleaned up:", leftovers) 183 | } 184 | } 185 | 186 | func BenchmarkInteropNondet(b *testing.B) { 187 | pred := func(pl Prolog, subquery Subquery, goal Term) iter.Seq[Term] { 188 | return func(yield func(Term) bool) { 189 | for i := 0; ; i++ { 190 | goal.(Compound).Args[0] = i 191 | if !yield(goal) { 192 | break 193 | } 194 | } 195 | } 196 | } 197 | ctx := context.Background() 198 | pl, err := New() 199 | if err != nil { 200 | b.Fatal(err) 201 | } 202 | pl.RegisterNondet(ctx, "churn", 1, pred) 203 | query := pl.Query(ctx, "churn(X).") 204 | b.ResetTimer() 205 | for i := 0; i < b.N; i++ { 206 | if !query.Next(ctx) { 207 | b.Fatal("query failed", query.Err()) 208 | } 209 | } 210 | if err := query.Close(); err != nil { 211 | b.Error("close error:", err) 212 | } 213 | if leftovers := len(pl.(*prolog).coros); leftovers > 0 { 214 | b.Error("coroutines weren't cleaned up:", leftovers) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /trealla/library.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/sha1" 7 | "crypto/sha256" 8 | "crypto/sha512" 9 | "encoding/hex" 10 | "fmt" 11 | "io" 12 | "net/http" 13 | "net/url" 14 | "strings" 15 | ) 16 | 17 | var builtins = []struct { 18 | name string 19 | arity int 20 | proc Predicate 21 | }{ 22 | {"$coro_next", 2, sys_coro_next_2}, 23 | {"$coro_stop", 1, sys_coro_stop_1}, 24 | {"crypto_data_hash", 3, crypto_data_hash_3}, 25 | {"http_consult", 1, http_consult_1}, 26 | {"http_fetch", 3, http_fetch_3}, 27 | } 28 | 29 | func (pl *prolog) loadBuiltins() error { 30 | ctx := context.Background() 31 | for _, predicate := range builtins { 32 | if err := pl.register(ctx, predicate.name, predicate.arity, predicate.proc); err != nil { 33 | return err 34 | } 35 | } 36 | return nil 37 | } 38 | 39 | // TODO: needs to support forms, headers, etc. 40 | func http_fetch_3(_ Prolog, _ Subquery, goal Term) Term { 41 | cmp, _ := goal.(Compound) 42 | result := cmp.Args[1] 43 | opts := cmp.Args[2] 44 | 45 | str, ok := cmp.Args[0].(string) 46 | if !ok { 47 | return typeError("chars", cmp.Args[0], piTerm("http_fetch", 3)) 48 | } 49 | href, err := url.Parse(str) 50 | if err != nil { 51 | return domainError("url", cmp.Args[0], piTerm("http_fetch", 3)) 52 | } 53 | 54 | method := findOption[Atom](opts, "method", "get") 55 | as := findOption[Atom](opts, "as", "string") 56 | bodystr := findOption(opts, "body", "") 57 | var body io.Reader 58 | if bodystr != "" { 59 | body = strings.NewReader(bodystr) 60 | } 61 | 62 | req, err := http.NewRequest(strings.ToUpper(string(method)), href.String(), body) 63 | if err != nil { 64 | return domainError("url", cmp.Args[0], err.Error()) 65 | } 66 | // req.Header.Add("Accept", "application/x-prolog") 67 | req.Header.Set("User-Agent", "trealla-prolog/go") 68 | 69 | resp, err := http.DefaultClient.Do(req) 70 | if err != nil { 71 | return systemError(err.Error()) 72 | } 73 | defer resp.Body.Close() 74 | 75 | switch resp.StatusCode { 76 | case http.StatusOK: // ok 77 | case http.StatusNoContent: 78 | return goal 79 | case http.StatusNotFound, http.StatusGone: 80 | return existenceError("source_sink", str, piTerm("http_fetch", 3)) 81 | case http.StatusForbidden, http.StatusUnauthorized: 82 | return permissionError("open,source_sink", str, piTerm("http_fetch", 3)) 83 | default: 84 | return systemError(fmt.Errorf("http_consult/1: unexpected status code: %d", resp.StatusCode)) 85 | } 86 | 87 | var buf bytes.Buffer 88 | if _, err := io.Copy(&buf, resp.Body); err != nil { 89 | return resourceError(Atom(err.Error()), piTerm("http_fetch", 3)) 90 | } 91 | 92 | switch as { 93 | case "json": 94 | js := Variable{Name: "_JS"} 95 | return Atom("call").Of(Atom(",").Of(Atom("=").Of(result, js), Atom("json_chars").Of(js, buf.String()))) 96 | } 97 | 98 | return Atom(cmp.Functor).Of(str, buf.String(), Variable{Name: "_"}) 99 | } 100 | 101 | func http_consult_1(_ Prolog, _ Subquery, goal Term) Term { 102 | cmp, ok := goal.(Compound) 103 | if !ok { 104 | return typeError("compound", goal, piTerm("http_consult", 1)) 105 | } 106 | if len(cmp.Args) != 1 { 107 | return systemError(piTerm("http_consult", 1)) 108 | } 109 | module := Atom("user") 110 | var addr string 111 | switch x := cmp.Args[0].(type) { 112 | case string: 113 | addr = x 114 | case Compound: 115 | // http_consult(module_name:"http://...") 116 | if x.Functor != ":" || len(x.Args) != 2 { 117 | return typeError("chars", cmp.Args[0], piTerm("http_consult", 1)) 118 | } 119 | var ok bool 120 | module, ok = x.Args[0].(Atom) 121 | if !ok { 122 | return typeError("atom", x.Args[0], piTerm("http_consult", 1)) 123 | } 124 | addr, ok = x.Args[1].(string) 125 | if !ok { 126 | return typeError("chars", x.Args[1], piTerm("http_consult", 1)) 127 | } 128 | } 129 | href, err := url.Parse(addr) 130 | if err != nil { 131 | return domainError("url", cmp.Args[0], piTerm("http_consult", 1)) 132 | } 133 | 134 | // TODO: grab context somehow 135 | req, err := http.NewRequest(http.MethodGet, href.String(), nil) 136 | if err != nil { 137 | return domainError("url", cmp.Args[0], err.Error()) 138 | } 139 | req.Header.Add("Accept", "application/x-prolog") 140 | req.Header.Set("User-Agent", "trealla-prolog/go") 141 | 142 | resp, err := http.DefaultClient.Do(req) 143 | if err != nil { 144 | return systemError(err.Error()) 145 | } 146 | defer resp.Body.Close() 147 | 148 | switch resp.StatusCode { 149 | case http.StatusOK: // ok 150 | case http.StatusNoContent: 151 | return goal 152 | case http.StatusNotFound, http.StatusGone: 153 | return existenceError("source_sink", addr, piTerm("http_consult", 1)) 154 | case http.StatusForbidden, http.StatusUnauthorized: 155 | return permissionError("open,source_sink", addr, piTerm("http_consult", 1)) 156 | default: 157 | return systemError(fmt.Errorf("http_consult/1: unexpected status code: %d", resp.StatusCode)) 158 | } 159 | 160 | var buf bytes.Buffer 161 | if _, err := io.Copy(&buf, resp.Body); err != nil { 162 | return resourceError(Atom(err.Error()), piTerm("http_consult", 1)) 163 | } 164 | 165 | // call(load_text(Text, module(URL))). 166 | return Atom("call").Of(Atom("load_text").Of(buf.String(), []Term{Atom("module").Of(module)})) 167 | } 168 | 169 | func crypto_data_hash_3(pl Prolog, _ Subquery, goal Term) Term { 170 | cmp, ok := goal.(Compound) 171 | if !ok { 172 | return typeError("compound", goal, piTerm("crypto_data_hash", 3)) 173 | } 174 | if len(cmp.Args) != 3 { 175 | return systemError(piTerm("crypto_data_hash", 3)) 176 | } 177 | data := cmp.Args[0] 178 | hash := cmp.Args[1] 179 | opts := cmp.Args[2] 180 | str, ok := data.(string) 181 | if !ok { 182 | return typeError("chars", data, piTerm("crypto_data_hash", 3)) 183 | } 184 | switch hash.(type) { 185 | case Variable, string: // ok 186 | default: 187 | return typeError("chars", hash, piTerm("crypto_data_hash", 3)) 188 | } 189 | if !isList(opts) { 190 | return typeError("list", opts, piTerm("crypto_data_hash", 3)) 191 | } 192 | algo := findOption[Atom](opts, "algorithm", "sha256") 193 | var digest []byte 194 | switch algo { 195 | case Atom("sha256"): 196 | sum := sha256.Sum256([]byte(str)) 197 | digest = sum[:] 198 | case Atom("sha512"): 199 | sum := sha512.Sum512([]byte(str)) 200 | digest = sum[:] 201 | case Atom("sha1"): 202 | sum := sha1.Sum([]byte(str)) 203 | digest = sum[:] 204 | default: 205 | return domainError("algorithm", algo, piTerm("crypto_data_hash", 3)) 206 | } 207 | return Atom("crypto_data_hash").Of(data, hex.EncodeToString(digest), opts) 208 | } 209 | 210 | func typeError(want Atom, got Term, ctx Term) Compound { 211 | return throwTerm(Atom("error").Of(Atom("type_error").Of(want, got), ctx)) 212 | } 213 | 214 | func domainError(domain Atom, got Term, ctx Term) Compound { 215 | return throwTerm(Atom("error").Of(Atom("domain_error").Of(domain, got), ctx)) 216 | } 217 | 218 | func existenceError(what Atom, got Term, ctx Term) Compound { 219 | return throwTerm(Atom("error").Of(Atom("existence_error").Of(what, got), ctx)) 220 | } 221 | 222 | func permissionError(what Atom, got Term, ctx Term) Compound { 223 | return throwTerm(Atom("error").Of(Atom("permission_error").Of(what, got), ctx)) 224 | } 225 | 226 | func resourceError(what Atom, ctx Term) Compound { 227 | return throwTerm(Atom("error").Of(Atom("resource_error").Of(what), ctx)) 228 | } 229 | 230 | func systemError(ctx Term) Compound { 231 | return throwTerm(Atom("error").Of(Atom("system_error"), ctx)) 232 | } 233 | 234 | func throwTerm(ball Term) Compound { 235 | return Compound{Functor: "throw", Args: []Term{ball}} 236 | } 237 | 238 | func findOption[T Term](opts Term, functor Atom, fallback T) T { 239 | if empty, ok := opts.(Atom); ok && empty == "[]" { 240 | return fallback 241 | } 242 | list, ok := opts.([]Term) 243 | if !ok { 244 | var empty T 245 | return empty 246 | } 247 | for i, x := range list { 248 | switch x := x.(type) { 249 | case Compound: 250 | if x.Functor != functor || len(x.Args) != 1 { 251 | continue 252 | } 253 | switch arg := x.Args[0].(type) { 254 | case T: 255 | return arg 256 | case Variable: 257 | list[i] = functor.Of(fallback) 258 | return fallback 259 | } 260 | } 261 | } 262 | return fallback 263 | } 264 | 265 | func isList(x Term) bool { 266 | switch x := x.(type) { 267 | case []Term: 268 | return true 269 | case Atom: 270 | return x == "[]" 271 | } 272 | return false 273 | } 274 | -------------------------------------------------------------------------------- /trealla/libtpl.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trealla-prolog/go/dfb4a7de74c464093828412d617bb2155142128a/trealla/libtpl.wasm -------------------------------------------------------------------------------- /trealla/pool.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "sync" 7 | ) 8 | 9 | // Pool is a pool of Prolog interpreters that distributes read requests to replicas. 10 | type Pool struct { 11 | canon *prolog 12 | children []*prolog 13 | idle chan *prolog 14 | mu *sync.RWMutex 15 | 16 | // options 17 | size int 18 | cfg []Option 19 | } 20 | 21 | // NewPool creates a new pool with the given options. 22 | // By default, the pool size will match the number of available CPUs. 23 | func NewPool(options ...PoolOption) (*Pool, error) { 24 | pool := &Pool{ 25 | size: runtime.NumCPU(), 26 | mu: new(sync.RWMutex), 27 | } 28 | for _, opt := range options { 29 | if err := opt(pool); err != nil { 30 | return nil, err 31 | } 32 | } 33 | pl, err := New(pool.cfg...) 34 | if err != nil { 35 | return nil, err 36 | } 37 | pool.canon = pl.(*prolog) 38 | pool.children = make([]*prolog, pool.size) 39 | pool.idle = make(chan *prolog, pool.size) 40 | for i := range pool.children { 41 | var err error 42 | pool.children[i], err = pool.spawn() 43 | if err != nil { 44 | return nil, err 45 | } 46 | pool.idle <- pool.children[i] 47 | } 48 | return pool, nil 49 | } 50 | 51 | // WriteTx executes a write transaction against this Pool. 52 | // Use this when modifying the knowledgebase (assert/retract, consulting files, loading modules, and so on). 53 | func (pool *Pool) WriteTx(tx func(Prolog) error) error { 54 | pool.mu.Lock() 55 | defer pool.mu.Unlock() 56 | pl := &lockedProlog{prolog: pool.canon} 57 | defer pl.kill() 58 | err := tx(pl) 59 | 60 | // Eagerly update the replicas. 61 | // This seems to be faster than lazily updating them. 62 | if err == nil { 63 | for _, child := range pool.children { 64 | if err := child.become(pool.canon); err != nil { 65 | return err 66 | } 67 | } 68 | } 69 | 70 | return err 71 | } 72 | 73 | // ReadTx executes a read transaction against this Pool. 74 | // Queries in a read transaction must not modify the knowledgebase. 75 | func (pool *Pool) ReadTx(tx func(Prolog) error) error { 76 | pool.mu.RLock() 77 | defer pool.mu.RUnlock() 78 | child := pool.child() 79 | defer pool.done(child) 80 | child.mu.Lock() 81 | defer child.mu.Unlock() 82 | pl := &lockedProlog{prolog: child} 83 | defer pl.kill() 84 | err := tx(pl) 85 | return err 86 | } 87 | 88 | func (pool *Pool) Stats() Stats { 89 | pool.mu.RLock() 90 | defer pool.mu.RUnlock() 91 | child := pool.child() 92 | defer pool.done(child) 93 | return child.Stats() 94 | } 95 | 96 | func (pool *Pool) spawn() (*prolog, error) { 97 | return pool.canon.clone() 98 | } 99 | 100 | func (pool *Pool) child() *prolog { 101 | return <-pool.idle 102 | } 103 | 104 | func (pool *Pool) done(child *prolog) { 105 | pool.idle <- child 106 | } 107 | 108 | // PoolOption is an option for configuring a Pool. 109 | type PoolOption func(*Pool) error 110 | 111 | // WithPoolSize configures the size (number of replicas) of the Pool. 112 | func WithPoolSize(replicas int) PoolOption { 113 | return func(pool *Pool) error { 114 | if replicas < 1 { 115 | return fmt.Errorf("trealla: pool size too low: %d", replicas) 116 | } 117 | pool.size = replicas 118 | return nil 119 | } 120 | } 121 | 122 | // WithPoolPrologOption configures interpreter options for the instances of a Pool. 123 | func WithPoolPrologOption(options ...Option) PoolOption { 124 | return func(pool *Pool) error { 125 | pool.cfg = append(pool.cfg, options...) 126 | return nil 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /trealla/pool_test.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "context" 5 | "runtime" 6 | "sync" 7 | "testing" 8 | ) 9 | 10 | const concurrency = 100 11 | 12 | func TestPool(t *testing.T) { 13 | pool, err := NewPool() 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | err = pool.WriteTx(func(pl Prolog) error { 19 | return pl.ConsultText(context.Background(), "user", "test(123).") 20 | }) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | var wg sync.WaitGroup 26 | for i := 0; i < 5000; i++ { 27 | wg.Add(1) 28 | go func() { 29 | defer wg.Done() 30 | pool.ReadTx(func(p Prolog) error { 31 | _, err := p.QueryOnce(context.Background(), "test(X).") 32 | if err != nil { 33 | return err 34 | } 35 | // t.Log(i, ans) 36 | return nil 37 | }) 38 | }() 39 | } 40 | wg.Wait() 41 | } 42 | 43 | func BenchmarkPool4(b *testing.B) { 44 | benchmarkPool(b, 4) 45 | } 46 | 47 | func BenchmarkPool16(b *testing.B) { 48 | benchmarkPool(b, 16) 49 | } 50 | 51 | func BenchmarkPool256(b *testing.B) { 52 | benchmarkPool(b, 256) 53 | } 54 | 55 | func BenchmarkPoolCPUs(b *testing.B) { 56 | benchmarkPool(b, runtime.NumCPU()) 57 | } 58 | 59 | func benchmarkPool(b *testing.B, size int) { 60 | b.Helper() 61 | 62 | db, err := NewPool(WithPoolSize(size)) 63 | if err != nil { 64 | b.Fatal(err) 65 | } 66 | 67 | err = db.WriteTx(func(p Prolog) error { 68 | return p.ConsultText(context.Background(), "user", "test(123).") 69 | }) 70 | if err != nil { 71 | b.Fatal(err) 72 | } 73 | b.ResetTimer() 74 | 75 | for n := 0; n < b.N; n++ { 76 | var wg sync.WaitGroup 77 | for i := 0; i < concurrency; i++ { 78 | wg.Add(1) 79 | go func() { 80 | defer wg.Done() 81 | db.ReadTx(func(p Prolog) error { 82 | _, err := p.QueryOnce(context.Background(), "test(X).") 83 | if err != nil { 84 | return err 85 | } 86 | return nil 87 | }) 88 | }() 89 | } 90 | wg.Wait() 91 | } 92 | } 93 | 94 | func BenchmarkContendedMutex(b *testing.B) { 95 | pl, _ := New() 96 | pl.ConsultText(context.Background(), "user", "test(123).") 97 | b.ResetTimer() 98 | 99 | for n := 0; n < b.N; n++ { 100 | var wg sync.WaitGroup 101 | for i := 0; i < concurrency; i++ { 102 | wg.Add(1) 103 | go func() { 104 | defer wg.Done() 105 | _, err := pl.QueryOnce(context.Background(), "test(X).") 106 | if err != nil { 107 | panic(err) 108 | } 109 | }() 110 | } 111 | wg.Wait() 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /trealla/prolog.go: -------------------------------------------------------------------------------- 1 | // Package trealla provides a Prolog interpreter. 2 | // Powered by Trealla Prolog running under WebAssembly. 3 | package trealla 4 | 5 | import ( 6 | "context" 7 | "crypto/rand" 8 | "fmt" 9 | "io" 10 | "io/fs" 11 | "log" 12 | "maps" 13 | "runtime" 14 | "sync" 15 | 16 | "github.com/tetratelabs/wazero" 17 | "github.com/tetratelabs/wazero/api" 18 | ) 19 | 20 | const defaultConcurrency = 256 21 | 22 | // Prolog is a Prolog interpreter. 23 | type Prolog interface { 24 | // Query executes a query. 25 | Query(ctx context.Context, query string, options ...QueryOption) Query 26 | // QueryOnce executes a query, retrieving a single answer and ignoring others. 27 | QueryOnce(ctx context.Context, query string, options ...QueryOption) (Answer, error) 28 | // Consult loads a Prolog file with the given path. 29 | Consult(ctx context.Context, filename string) error 30 | // ConsultText loads Prolog text into module. Use "user" for the global module. 31 | ConsultText(ctx context.Context, module string, text string) error 32 | // Register a native Go predicate. 33 | // NOTE: this is *experimental* and its API will likely change. 34 | Register(ctx context.Context, name string, arity int, predicate Predicate) error 35 | // Register a native Go nondeterminate predicate. 36 | // By returning a sequence of terms, a [NondetPredicate] can create multiple choice points. 37 | RegisterNondet(ctx context.Context, name string, arity int, predicate NondetPredicate) error 38 | // Clone creates a new clone of this interpreter. 39 | Clone() (Prolog, error) 40 | // Close destroys the Prolog instance. 41 | // If this isn't called and the Prolog variable goes out of scope, runtime finalizers will try to free the memory. 42 | Close() 43 | // Stats returns diagnostic information. 44 | Stats() Stats 45 | } 46 | 47 | type prolog struct { 48 | ctx context.Context 49 | instance api.Module 50 | memory api.Memory 51 | closing bool 52 | running map[uint32]*query 53 | spawning map[uint32]*query 54 | limiter chan struct{} 55 | 56 | ptr uint32 57 | // from stdlib 58 | realloc wasmFunc 59 | free wasmFunc 60 | // from trealla.h 61 | pl_consult wasmFunc 62 | pl_capture wasmFunc 63 | pl_capture_read wasmFunc 64 | pl_capture_reset wasmFunc 65 | pl_capture_free wasmFunc 66 | pl_query wasmFunc 67 | pl_redo wasmFunc 68 | pl_done wasmFunc 69 | 70 | procs map[string]Predicate 71 | coros map[int64]coroutine 72 | coron int64 73 | 74 | dirs map[string]string 75 | fs map[string]fs.FS 76 | library string 77 | trace bool 78 | quiet bool 79 | max int 80 | 81 | stdout *log.Logger 82 | stderr *log.Logger 83 | debug *log.Logger 84 | 85 | mu *sync.Mutex 86 | } 87 | 88 | type prologKey struct{} 89 | 90 | // New creates a new Prolog interpreter. 91 | func New(opts ...Option) (Prolog, error) { 92 | pl := &prolog{ 93 | running: make(map[uint32]*query), 94 | spawning: make(map[uint32]*query), 95 | procs: make(map[string]Predicate), 96 | coros: make(map[int64]coroutine), 97 | mu: new(sync.Mutex), 98 | max: defaultConcurrency, 99 | } 100 | for _, opt := range opts { 101 | opt(pl) 102 | } 103 | if pl.max > 0 { 104 | pl.limiter = make(chan struct{}, pl.max) 105 | } 106 | return pl, pl.init(nil) 107 | } 108 | 109 | func (pl *prolog) argv() []string { 110 | args := []string{"tpl", "--ns"} 111 | if pl.library != "" { 112 | args = append(args, "--library", pl.library) 113 | } 114 | if pl.trace { 115 | args = append(args, "-t") 116 | } 117 | if pl.quiet { 118 | args = append(args, "-q") 119 | } 120 | return args 121 | } 122 | 123 | func (pl *prolog) init(parent *prolog) error { 124 | argv := pl.argv() 125 | fs := wazero.NewFSConfig() 126 | for alias, dir := range pl.dirs { 127 | fs = fs.WithDirMount(dir, alias) 128 | } 129 | for alias, fsys := range pl.fs { 130 | fs = fs.WithFSMount(fsys, alias) 131 | } 132 | 133 | cfg := wazero.NewModuleConfig().WithName("").WithArgs(argv...).WithFSConfig(fs). 134 | WithSysWalltime().WithSysNanotime().WithSysNanosleep(). 135 | WithOsyield(runtime.Gosched). 136 | // WithStdout(os.Stdout).WithStderr(os.Stderr). // for debugging output capture 137 | WithRandSource(rand.Reader) 138 | 139 | // run once to initialize global interpreter 140 | if parent != nil { 141 | cfg = cfg.WithStartFunctions() 142 | } 143 | 144 | pl.ctx = context.WithValue(context.Background(), prologKey{}, pl) 145 | instance, err := wasmEngine.InstantiateModule(pl.ctx, wasmModule, cfg) 146 | if err != nil { 147 | return err 148 | } 149 | pl.instance = instance 150 | 151 | mem := instance.Memory() 152 | if mem == nil { 153 | return fmt.Errorf("trealla: failed to get memory") 154 | } 155 | pl.memory = mem 156 | 157 | pl.realloc, err = pl.function("canonical_abi_realloc") 158 | if err != nil { 159 | return err 160 | } 161 | 162 | pl.free, err = pl.function("canonical_abi_free") 163 | if err != nil { 164 | return err 165 | } 166 | 167 | pl.pl_capture, err = pl.function("pl_capture") 168 | if err != nil { 169 | return err 170 | } 171 | 172 | pl.pl_capture_read, err = pl.function("pl_capture_read") 173 | if err != nil { 174 | return err 175 | } 176 | 177 | pl.pl_capture_reset, err = pl.function("pl_capture_reset") 178 | if err != nil { 179 | return err 180 | } 181 | 182 | pl.pl_capture_free, err = pl.function("pl_capture_free") 183 | if err != nil { 184 | return err 185 | } 186 | 187 | pl.pl_query, err = pl.function("pl_query") 188 | if err != nil { 189 | return err 190 | } 191 | 192 | pl.pl_redo, err = pl.function("pl_redo") 193 | if err != nil { 194 | return err 195 | } 196 | 197 | pl.pl_done, err = pl.function("pl_done") 198 | if err != nil { 199 | return err 200 | } 201 | 202 | // pl.get_error, err = pl.function("get_error") 203 | // if err != nil { 204 | // return err 205 | // } 206 | 207 | pl.pl_consult, err = pl.function("pl_consult") 208 | if err != nil { 209 | return err 210 | } 211 | 212 | if parent != nil { 213 | if pl.ptr == 0 { 214 | runtime.SetFinalizer(pl, (*prolog).Close) 215 | } 216 | pl.ptr = parent.ptr 217 | pl.mu = new(sync.Mutex) 218 | pl.running = make(map[uint32]*query) 219 | pl.spawning = make(map[uint32]*query) 220 | 221 | pl.procs = maps.Clone(parent.procs) 222 | pl.coros = make(map[int64]coroutine) // TODO: copy over? probably not 223 | 224 | pl.dirs = parent.dirs 225 | pl.fs = parent.fs 226 | pl.library = parent.library 227 | pl.quiet = parent.quiet 228 | pl.trace = parent.trace 229 | pl.debug = parent.debug 230 | if parent.max > 0 { 231 | pl.max = parent.max 232 | pl.limiter = make(chan struct{}, pl.max) 233 | } 234 | 235 | if err := pl.become(parent); err != nil { 236 | return err 237 | } 238 | 239 | // if any queries are running while we clone, they get copied over as zombies 240 | // free them 241 | for pp := range parent.spawning { 242 | if ptr := pl.indirect(pp); ptr != 0 { 243 | if _, err := pl.pl_done.Call(pl.ctx, uint64(ptr)); err != nil { 244 | return err 245 | } 246 | } 247 | } 248 | for ptr := range parent.running { 249 | if _, err := pl.pl_done.Call(pl.ctx, uint64(ptr)); err != nil { 250 | return err 251 | } 252 | } 253 | 254 | return nil 255 | } 256 | 257 | runtime.SetFinalizer(pl, (*prolog).Close) 258 | 259 | pl_global, err := pl.function("pl_global") 260 | if err != nil { 261 | return err 262 | } 263 | ptr, err := pl_global.Call(pl.ctx) 264 | if err != nil { 265 | return fmt.Errorf("trealla: failed to get interpreter: %w", err) 266 | } 267 | pl.ptr = uint32(ptr[0]) 268 | 269 | _, err = pl.pl_capture.Call(pl.ctx, uint64(pl.ptr)) 270 | if err != nil { 271 | return err 272 | } 273 | 274 | if err := pl.loadBuiltins(); err != nil { 275 | return fmt.Errorf("trealla: failed to load builtins: %w", err) 276 | } 277 | 278 | return nil 279 | } 280 | 281 | func (pl *prolog) Clone() (Prolog, error) { 282 | pl.mu.Lock() 283 | defer pl.mu.Unlock() 284 | return pl.clone() 285 | } 286 | 287 | func (pl *prolog) clone() (*prolog, error) { 288 | clone := new(prolog) 289 | err := clone.init(pl) 290 | return clone, err 291 | } 292 | 293 | func (pl *prolog) become(parent *prolog) error { 294 | mySize, _ := pl.memory.Grow(0) 295 | parentSize, _ := parent.memory.Grow(0) 296 | if parentSize > mySize { 297 | if _, ok := pl.memory.Grow(parentSize - mySize); !ok { 298 | panic("trealla: failed to become") 299 | } 300 | } 301 | myBuffer, _ := pl.memory.Read(0, pl.memory.Size()) 302 | parentBuffer, _ := parent.memory.Read(0, parent.memory.Size()) 303 | copy(myBuffer, parentBuffer) 304 | return nil 305 | } 306 | 307 | func (pl *prolog) function(symbol string) (wasmFunc, error) { 308 | export := pl.instance.ExportedFunction(symbol) 309 | if export == nil { 310 | return nil, errUnexported(symbol) 311 | } 312 | return export, nil 313 | } 314 | 315 | func (pl *prolog) alloc(size uint32) (uint32, error) { 316 | ptrv, err := pl.realloc.Call(pl.ctx, 0, 0, align, uint64(size)) 317 | if err != nil { 318 | return 0, err 319 | } 320 | ptr := uint32(ptrv[0]) 321 | if ptr == 0 { 322 | return 0, fmt.Errorf("trealla: failed to allocate wasm memory (out of memory?)") 323 | } 324 | return ptr, nil 325 | } 326 | 327 | func (pl *prolog) subquery(addr uint32) *query { 328 | if addr == 0 { 329 | return nil 330 | } 331 | if q, ok := pl.running[addr]; ok { 332 | return q 333 | } 334 | for spawn, q := range pl.spawning { 335 | if ptr := pl.indirect(spawn); ptr != 0 { 336 | if ptr == addr { 337 | // if q.pl.debug != nil { 338 | // pl.debug.Println("indirecting", spawn, ptr) 339 | // } 340 | // pl.running[ptr] = q 341 | // q.subquery = ptr 342 | return q 343 | } 344 | } 345 | } 346 | return nil 347 | } 348 | 349 | func (pl *prolog) Close() { 350 | pl.mu.Lock() 351 | defer pl.mu.Unlock() 352 | if pl.instance != nil { 353 | pl.instance.Close(context.Background()) 354 | } 355 | pl.instance = nil 356 | pl.memory = nil 357 | } 358 | 359 | func (pl *prolog) ConsultText(ctx context.Context, module, text string) error { 360 | pl.mu.Lock() 361 | defer pl.mu.Unlock() 362 | if pl.instance == nil { 363 | return io.EOF 364 | } 365 | return pl.consultText(ctx, module, text) 366 | } 367 | 368 | func (pl *prolog) consultText(ctx context.Context, module, text string) error { 369 | // load_text(Text, [module(Module)]). 370 | goal := Atom("load_text").Of(text, []Term{Atom("module").Of(Atom(module))}) 371 | _, err := pl.queryOnce(ctx, goal.String()) 372 | if err != nil { 373 | err = fmt.Errorf("trealla: consult text failed: %w", err) 374 | } 375 | return err 376 | } 377 | 378 | func (pl *prolog) Consult(_ context.Context, filename string) error { 379 | pl.mu.Lock() 380 | defer pl.mu.Unlock() 381 | if pl.instance == nil { 382 | return io.EOF 383 | } 384 | return pl.consult(filename) 385 | } 386 | 387 | func (pl *prolog) consult(filename string) error { 388 | fstr, err := newCString(pl, filename) 389 | if err != nil { 390 | return err 391 | } 392 | defer fstr.free(pl) 393 | 394 | ret, err := pl.pl_consult.Call(pl.ctx, uint64(pl.ptr), uint64(fstr.ptr)) 395 | if err != nil { 396 | return err 397 | } 398 | if uint32(ret[0]) == 0 { 399 | return fmt.Errorf("trealla: failed to consult file: %s", filename) 400 | } 401 | return nil 402 | } 403 | 404 | func (pl *prolog) indirect(ptr uint32) uint32 { 405 | if ptr == 0 { 406 | return 0 407 | } 408 | 409 | v, _ := pl.memory.ReadUint32Le(ptr) 410 | return v 411 | } 412 | 413 | type Stats struct { 414 | MemorySize int 415 | } 416 | 417 | func (pl *prolog) Stats() Stats { 418 | pl.mu.Lock() 419 | defer pl.mu.Unlock() 420 | return pl.stats() 421 | } 422 | 423 | func (pl *prolog) stats() Stats { 424 | if pl.memory == nil { 425 | return Stats{} 426 | } 427 | size, _ := pl.memory.Grow(0) 428 | return Stats{ 429 | MemorySize: int(size) * pageSize, 430 | } 431 | } 432 | 433 | // func (pl *prolog) DumpMemory(filename string) { 434 | // pages, _ := pl.memory.Grow(0) 435 | // buf, _ := pl.memory.Read(0, pages*pageSize) 436 | // if err := os.WriteFile(filename, buf, 0600); err != nil { 437 | // panic(err) 438 | // } 439 | // } 440 | 441 | // lockedProlog skips the locking the normal *prolog does. 442 | // It's only valid during a single RPC call. 443 | type lockedProlog struct { 444 | prolog *prolog 445 | dead bool 446 | } 447 | 448 | func (pl *lockedProlog) kill() { 449 | pl.dead = true 450 | pl.prolog = nil 451 | } 452 | func (pl *lockedProlog) DumpMemory(string) { 453 | 454 | } 455 | 456 | func (pl *lockedProlog) ensure() error { 457 | if pl.dead { 458 | return fmt.Errorf("trealla: using invalid reference to interpreter") 459 | } 460 | return nil 461 | } 462 | 463 | func (pl *lockedProlog) Clone() (Prolog, error) { 464 | if err := pl.ensure(); err != nil { 465 | return nil, err 466 | } 467 | return pl.prolog.clone() 468 | } 469 | 470 | func (pl *lockedProlog) Query(ctx context.Context, ask string, options ...QueryOption) Query { 471 | if err := pl.ensure(); err != nil { 472 | return &query{err: err} 473 | } 474 | return pl.prolog.Query(ctx, ask, append(options, withoutLock)...) 475 | } 476 | 477 | func (pl *lockedProlog) QueryOnce(ctx context.Context, query string, options ...QueryOption) (Answer, error) { 478 | if err := pl.ensure(); err != nil { 479 | return Answer{}, err 480 | } 481 | return pl.prolog.queryOnce(ctx, query, options...) 482 | } 483 | 484 | func (pl *lockedProlog) ConsultText(ctx context.Context, module, text string) error { 485 | if err := pl.ensure(); err != nil { 486 | return err 487 | } 488 | return pl.prolog.consultText(ctx, module, text) 489 | } 490 | 491 | func (pl *lockedProlog) Consult(_ context.Context, filename string) error { 492 | if err := pl.ensure(); err != nil { 493 | return err 494 | } 495 | return pl.prolog.consult(filename) 496 | } 497 | 498 | func (pl *lockedProlog) Register(ctx context.Context, name string, arity int, proc Predicate) error { 499 | if err := pl.ensure(); err != nil { 500 | return err 501 | } 502 | return pl.prolog.register(ctx, name, arity, proc) 503 | } 504 | 505 | func (pl *lockedProlog) RegisterNondet(ctx context.Context, name string, arity int, proc NondetPredicate) error { 506 | if err := pl.ensure(); err != nil { 507 | return err 508 | } 509 | return pl.prolog.registerNondet(ctx, name, arity, proc) 510 | } 511 | 512 | func (pl *lockedProlog) Close() { 513 | if err := pl.ensure(); err != nil { 514 | return 515 | } 516 | pl.prolog.closing = true 517 | } 518 | 519 | func (pl *lockedProlog) Stats() Stats { 520 | if err := pl.ensure(); err != nil { 521 | return Stats{} 522 | } 523 | return pl.prolog.stats() 524 | } 525 | 526 | // Option is an optional parameter for New. 527 | type Option func(*prolog) 528 | 529 | // WithPreopenDir sets the root directory (also called the preopen directory) to dir, granting access to it. Calling this again will overwrite it. 530 | // Equivalent to `WithMapDir("/", dir)`. 531 | func WithPreopenDir(dir string) Option { 532 | return WithMapDir("/", dir) 533 | } 534 | 535 | // WithMapDir sets alias to point to directory dir, granting access to it. 536 | // This can be called multiple times with different aliases. 537 | func WithMapDir(alias, dir string) Option { 538 | return func(pl *prolog) { 539 | if pl.dirs == nil { 540 | pl.dirs = make(map[string]string) 541 | } 542 | pl.dirs[alias] = dir 543 | } 544 | } 545 | 546 | // WithMapFS sets alias to point to [fs.FS] dir, granting access to it. 547 | // This can be called multiple times with different aliases. 548 | func WithMapFS(alias string, fsys fs.FS) Option { 549 | return func(pl *prolog) { 550 | if pl.fs == nil { 551 | pl.fs = make(map[string]fs.FS) 552 | } 553 | pl.fs[alias] = fsys 554 | } 555 | } 556 | 557 | // WithLibraryPath sets the global library path for the interpreter. 558 | // `use_module(library(foo))` will point to here. 559 | // Equivalent to Trealla's `--library` flag. 560 | func WithLibraryPath(path string) Option { 561 | return func(pl *prolog) { 562 | pl.library = path 563 | } 564 | } 565 | 566 | // WithTrace enables tracing for all queries. Traces write to to the query's standard error text stream. 567 | // You can also use the `trace/0` predicate to enable tracing for specific queries. 568 | // Use together with WithStderrLog for automatic tracing. 569 | func WithTrace() Option { 570 | return func(pl *prolog) { 571 | pl.trace = true 572 | } 573 | } 574 | 575 | // WithQuiet enables the quiet option. This disables some warning messages. 576 | func WithQuiet() Option { 577 | return func(pl *prolog) { 578 | pl.quiet = true 579 | } 580 | } 581 | 582 | // WithStdoutLog sets the standard output logger, writing all stdout input from queries. 583 | func WithStdoutLog(logger *log.Logger) Option { 584 | return func(pl *prolog) { 585 | pl.stdout = logger 586 | } 587 | } 588 | 589 | // WithStderrLog sets the standard error logger, writing all stderr input from queries. 590 | // Note that traces are written to stderr. 591 | func WithStderrLog(logger *log.Logger) Option { 592 | return func(pl *prolog) { 593 | pl.stderr = logger 594 | } 595 | } 596 | 597 | // WithDebugLog writes debug messages to the given logger. 598 | func WithDebugLog(logger *log.Logger) Option { 599 | return func(pl *prolog) { 600 | pl.debug = logger 601 | } 602 | } 603 | 604 | // WithMaxConcurrency sets the maximum number of simultaneously running queries. 605 | // This is useful for limiting the amount of memory an interpreter will use. 606 | // Set to 0 to disable concurrency limits. Default is 256. 607 | // Note that interpreters are single-threaded, so only one query is truly executing 608 | // at once, but pending queries can still consume memory (which is currently limited to 4GB). 609 | // This knob will limit the number of queries that can actively consume the interpreter's memory. 610 | func WithMaxConcurrency(queries int) Option { 611 | return func(pl *prolog) { 612 | pl.max = queries 613 | } 614 | } 615 | 616 | var ( 617 | _ Prolog = (*prolog)(nil) 618 | _ Prolog = (*lockedProlog)(nil) 619 | ) 620 | -------------------------------------------------------------------------------- /trealla/prolog_test.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestClose(t *testing.T) { 11 | pl, err := New() 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | pl.Close() 16 | _, err = pl.QueryOnce(context.Background(), "true") 17 | if err != io.EOF { 18 | t.Error("unexpected error", err) 19 | } 20 | } 21 | 22 | func TestClone(t *testing.T) { 23 | pl, err := New() 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | if err := pl.ConsultText(context.Background(), "user", `abc("xyz").`); err != nil { 28 | t.Fatal(err) 29 | } 30 | clone, err := pl.Clone() 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | ans, err := clone.QueryOnce(context.Background(), "abc(X).") 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | want := Term("xyz") 39 | got := ans.Solution["X"] 40 | if want != got { 41 | t.Error("want:", want, "got:", got) 42 | } 43 | t.Log(ans) 44 | 45 | if err := pl.ConsultText(context.Background(), "user", `foo(bar).`); err != nil { 46 | t.Error(err) 47 | } 48 | 49 | _, err = clone.QueryOnce(context.Background(), "foo(X).") 50 | if err == nil { 51 | t.Error("expected error, got:", err) 52 | } 53 | if _, ok := err.(ErrThrow); !ok { 54 | t.Error("expected throw, got:", err) 55 | } 56 | } 57 | 58 | func TestLeakCheck(t *testing.T) { 59 | check := func(goal string, limit int) func(t *testing.T) { 60 | return func(t *testing.T) { 61 | t.Parallel() 62 | ctx := context.Background() 63 | 64 | pl, err := New() 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | defer pl.Close() 69 | 70 | pl.Register(ctx, "interop_simple", 1, func(pl Prolog, subquery Subquery, goal Term) Term { 71 | return Atom("interop_simple").Of(int64(42)) 72 | }) 73 | pl.Register(ctx, "interop_test", 1, func(pl Prolog, _ Subquery, goal Term) Term { 74 | want := Atom("interop_test").Of(Variable{Name: "A"}) 75 | if !reflect.DeepEqual(want, goal) { 76 | t.Error("bad goal. want:", want, "got:", goal) 77 | } 78 | 79 | ans1, err := pl.QueryOnce(ctx, "X is 1 + 1.") 80 | if err != nil { 81 | t.Error(err) 82 | } 83 | 84 | ans2, err := pl.QueryOnce(ctx, "Y is X + 1.", WithBind("X", ans1.Solution["X"])) 85 | if err != nil { 86 | t.Error(err) 87 | } 88 | 89 | return Atom("interop_test").Of(ans2.Solution["Y"]) 90 | }) 91 | size := 0 92 | for i := 0; i < 2048; i++ { 93 | q := pl.Query(ctx, goal) 94 | n := 0 95 | for q.Next(ctx) { 96 | n++ 97 | if limit > 0 && n >= limit { 98 | break 99 | } 100 | } 101 | if err := q.Err(); err != nil && !IsFailure(err) { 102 | t.Fatal(err, "iter=", i) 103 | } 104 | q.Close() 105 | 106 | current := pl.Stats().MemorySize 107 | if size == 0 { 108 | size = current 109 | } 110 | if current > size { 111 | t.Fatal(goal, "possible leak: memory grew to:", current, "initial:", size) 112 | } 113 | } 114 | t.Logf("goal: %s size: %d", goal, size) 115 | } 116 | } 117 | t.Run("true", check("true.", 0)) 118 | t.Run("between(1,3,X)", check("between(1,3,X).", 0)) 119 | t.Run("between(1,3,X) limit 1", check("between(1,3,X).", 1)) 120 | 121 | // BUG(guregu): queries ending in a ; fail branch leak for some reason 122 | // but it's not enough to trigger the leak check (~20B/query) 123 | t.Run("failing branch", check("true ; true ; true ; foo=bar.", 0)) 124 | 125 | t.Run("fail", check("fail ; fail", 0)) 126 | 127 | t.Run("simple interop", check("interop_simple(X)", 0)) 128 | // t.Run("complex interop", check("interop_test(X)")) 129 | } 130 | -------------------------------------------------------------------------------- /trealla/query.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "iter" 9 | "runtime" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | const stx = '\x02' // START OF TEXT 15 | const etx = '\x03' // END OF TEXT 16 | 17 | type queryContext struct{} 18 | 19 | // Query is a Prolog query iterator. 20 | type Query interface { 21 | // Next computes the next solution. Returns true if it found one and false if there are no more results. 22 | // Be sure to check for errors by calling Err afterwards. 23 | Next(context.Context) bool 24 | // All returns an iterator over query results. 25 | // Be sure to check for errors by calling Err afterwards. 26 | // This is a single-use iterator. 27 | All(context.Context) iter.Seq[Answer] 28 | // Current returns the current solution prepared by Next. 29 | Current() Answer 30 | // Close destroys this query. It is not necessary to call this if you exhaust results via Next. 31 | Close() error 32 | // Err returns this query's error. Always check this after iterating. 33 | // Query failures are represented as [ErrFailure] and queries that throw an exception as [ErrThrow]. 34 | Err() error 35 | } 36 | 37 | type query struct { 38 | pl *prolog 39 | goal string 40 | bind bindings 41 | subquery uint32 // pl_sub_query* 42 | 43 | // in-flight coroutines 44 | coros map[int64]struct{} 45 | 46 | cur Answer 47 | answers []Answer 48 | err error 49 | done bool 50 | dead bool 51 | iter int 52 | 53 | // output capture pointers 54 | stdoutptr uint32 // char** 55 | stdoutlen uint32 // size_t* 56 | stderrptr uint32 // char** 57 | stderrlen uint32 // size_t* 58 | 59 | stdout *bytes.Buffer 60 | stderr *bytes.Buffer 61 | 62 | lock bool 63 | mu *sync.Mutex 64 | } 65 | 66 | // Query executes a query, returning an iterator for results. 67 | func (pl *prolog) Query(ctx context.Context, goal string, options ...QueryOption) Query { 68 | q := pl.start(ctx, goal, options...) 69 | runtime.SetFinalizer(q, (*query).Close) 70 | return q 71 | } 72 | 73 | func (pl *prolog) QueryOnce(ctx context.Context, goal string, options ...QueryOption) (Answer, error) { 74 | pl.mu.Lock() 75 | defer pl.mu.Unlock() 76 | return pl.queryOnce(ctx, goal, options...) 77 | } 78 | 79 | func (pl *prolog) queryOnce(ctx context.Context, goal string, options ...QueryOption) (Answer, error) { 80 | options = append(options, withoutLock) 81 | q := pl.start(ctx, goal, options...) 82 | var ans Answer 83 | if q.Next(ctx) { 84 | ans = q.Current() 85 | } 86 | q.Close() 87 | return ans, q.Err() 88 | } 89 | 90 | func (q *query) allocCapture() error { 91 | pl := q.pl 92 | var err error 93 | if q.stdoutptr == 0 { 94 | q.stdoutptr, err = pl.alloc(ptrSize) 95 | if err != nil { 96 | return err 97 | } 98 | } 99 | if q.stdoutlen == 0 { 100 | q.stdoutlen, err = pl.alloc(ptrSize) 101 | if err != nil { 102 | return err 103 | } 104 | } 105 | if q.stderrptr == 0 { 106 | q.stderrptr, err = pl.alloc(ptrSize) 107 | if err != nil { 108 | return err 109 | } 110 | } 111 | if q.stderrlen == 0 { 112 | q.stderrlen, err = pl.alloc(ptrSize) 113 | if err != nil { 114 | return err 115 | } 116 | } 117 | return nil 118 | } 119 | 120 | func (q *query) readOutput() error { 121 | pl := q.pl 122 | var err error 123 | 124 | if err := q.allocCapture(); err != nil { 125 | return err 126 | } 127 | 128 | _, err = pl.pl_capture_read.Call(pl.ctx, uint64(pl.ptr), 129 | uint64(q.stdoutptr), uint64(q.stdoutlen), 130 | uint64(q.stderrptr), uint64(q.stderrlen)) 131 | if err != nil { 132 | return err 133 | } 134 | defer pl.pl_capture_reset.Call(pl.ctx, uint64(pl.ptr)) 135 | 136 | stdoutlen := pl.indirect(q.stdoutlen) 137 | stdoutptr := pl.indirect(q.stdoutptr) 138 | stderrlen := pl.indirect(q.stderrlen) 139 | stderrptr := pl.indirect(q.stderrptr) 140 | 141 | stdout, err := pl.gets(stdoutptr, stdoutlen) 142 | if err != nil { 143 | return err 144 | } 145 | q.stdout.WriteString(stdout) 146 | 147 | stderr, err := pl.gets(stderrptr, stderrlen) 148 | if err != nil { 149 | return err 150 | } 151 | q.stderr.WriteString(stderr) 152 | 153 | return nil 154 | } 155 | 156 | func (q *query) resetOutput() { 157 | q.stdout.Reset() 158 | q.stderr.Reset() 159 | } 160 | 161 | func (pl *prolog) start(ctx context.Context, goal string, options ...QueryOption) *query { 162 | q := &query{ 163 | pl: pl, 164 | goal: goal, 165 | lock: true, 166 | stdout: new(bytes.Buffer), 167 | stderr: new(bytes.Buffer), 168 | mu: new(sync.Mutex), 169 | } 170 | for _, opt := range options { 171 | opt(q) 172 | } 173 | 174 | if pl.limiter != nil { 175 | pl.limiter <- struct{}{} 176 | } 177 | 178 | if q.lock { 179 | pl.mu.Lock() 180 | defer pl.mu.Unlock() 181 | } 182 | if q.pl.instance == nil || pl.closing { 183 | q.setError(io.EOF) 184 | return q 185 | } 186 | 187 | ctx = context.WithValue(ctx, queryContext{}, q) 188 | 189 | if err := q.reify(); err != nil { 190 | q.setError(err) 191 | return q 192 | } 193 | goalstr, err := newCString(pl, escapeQuery(q.goal)) 194 | if err != nil { 195 | q.setError(err) 196 | return q 197 | } 198 | 199 | if pl.debug != nil { 200 | pl.debug.Println("query:", q.goal) 201 | } 202 | 203 | subqptr, err := pl.alloc(ptrSize) 204 | if err != nil { 205 | q.setError(fmt.Errorf("trealla: failed to allocate subquery pointer")) 206 | return q 207 | } 208 | pl.spawning[subqptr] = q 209 | defer func(ptr uint32) { 210 | delete(pl.spawning, subqptr) 211 | }(subqptr) 212 | defer pl.free.Call(pl.ctx, uint64(subqptr), 4, 1) 213 | 214 | if err := q.allocCapture(); err != nil { 215 | q.setError(err) 216 | return q 217 | } 218 | 219 | // ch := make(chan error, 2) 220 | var ret uint32 221 | v, err := pl.pl_query.Call(ctx, uint64(pl.ptr), uint64(goalstr.ptr), uint64(subqptr), 0) 222 | if err == nil { 223 | ret = uint32(v[0]) 224 | } 225 | goalstr.free(pl) 226 | // go func() { 227 | // defer func() { 228 | // if ex := recover(); ex != nil { 229 | // ch <- fmt.Errorf("trealla: panic: %v", ex) 230 | // } 231 | // }() 232 | 233 | // v, err := pl.pl_query.Call(pl.ctx, uint64(pl.ptr), uint64(goalstr.ptr), uint64(subqptr), 0) 234 | // if err == nil { 235 | // ret = uint32(v[0]) 236 | // } 237 | // goalstr.free(pl) 238 | // ch <- err 239 | // }() 240 | 241 | // select { 242 | // case <-ctx.Done(): 243 | // q.setError(fmt.Errorf("trealla: canceled: %w", ctx.Err())) 244 | // return q 245 | 246 | // case err := <-ch: 247 | q.done = ret == 0 248 | 249 | if err != nil { 250 | q.setError(fmt.Errorf("trealla: query error: %w", err)) 251 | return q 252 | } 253 | 254 | // grab subquery pointer 255 | if !q.done { 256 | var err error 257 | q.subquery = pl.indirect(subqptr) 258 | if q.subquery == 0 { 259 | q.setError(fmt.Errorf("trealla: couldn't read subquery pointer: %w", err)) 260 | return q 261 | } 262 | q.pl.running[q.subquery] = q 263 | } 264 | 265 | if pl.closing { 266 | pl.Close() 267 | } 268 | 269 | return q 270 | // } 271 | } 272 | 273 | func (q *query) redo(ctx context.Context) bool { 274 | if q.lock { 275 | q.pl.mu.Lock() 276 | defer q.pl.mu.Unlock() 277 | } 278 | if q.pl.instance == nil { 279 | q.setError(io.EOF) 280 | return false 281 | } 282 | 283 | if q.pl.debug != nil { 284 | q.pl.debug.Println("redo:", q.subquery, q.goal) 285 | } 286 | 287 | pl := q.pl 288 | ctx = context.WithValue(ctx, queryContext{}, q) 289 | 290 | // ch := make(chan error, 2) 291 | var ret uint32 292 | // go func() { 293 | // defer func() { 294 | // if ex := recover(); ex != nil { 295 | // ch <- fmt.Errorf("trealla: panic: %v", ex) 296 | // } 297 | // }() 298 | 299 | // v, err := pl.pl_redo.Call(pl.ctx, uint64(q.subquery)) 300 | // if err == nil { 301 | // ret = uint32(v[0]) 302 | // } 303 | // ch <- err 304 | // }() 305 | 306 | v, err := pl.pl_redo.Call(ctx, uint64(q.subquery)) 307 | q.iter++ 308 | if err == nil { 309 | ret = uint32(v[0]) 310 | } 311 | 312 | // select { 313 | // case <-ctx.Done(): 314 | // q.setError(fmt.Errorf("trealla: canceled: %w", ctx.Err())) 315 | // q.Close() 316 | // return false 317 | 318 | // case err := <-ch: 319 | q.done = ret == 0 320 | if err != nil { 321 | q.setError(fmt.Errorf("trealla: query error: %w", err)) 322 | q.close() 323 | return false 324 | } 325 | 326 | // var erroring bool 327 | // var errcode uint64 328 | // { 329 | // retv, err2 := pl.get_error.Call(ctx, uint64(pl.ptr)) 330 | // if err2 != nil { 331 | // q.setError(fmt.Errorf("trealla: get_error internal error: %w", err)) 332 | // return false 333 | // } 334 | // errcode = retv[0] 335 | // erroring = errcode != 0 336 | // } 337 | 338 | if q.done { 339 | delete(pl.running, q.subquery) 340 | defer q.close() 341 | } 342 | 343 | if pl.closing { 344 | pl.Close() 345 | } 346 | 347 | if q.err != nil { 348 | return false 349 | } 350 | return true 351 | } 352 | 353 | func (q *query) Next(ctx context.Context) bool { 354 | q.mu.Lock() 355 | defer q.mu.Unlock() 356 | 357 | if q.err != nil { 358 | return false 359 | } 360 | 361 | if q.pop() { 362 | return true 363 | } 364 | 365 | if q.done { 366 | return false 367 | } 368 | 369 | if q.redo(ctx) { 370 | got := q.pop() 371 | return got 372 | } 373 | 374 | return false 375 | } 376 | 377 | func (q *query) push(a Answer) { 378 | q.answers = append(q.answers, a) 379 | } 380 | 381 | func (q *query) pop() bool { 382 | if len(q.answers) == 0 { 383 | return false 384 | } 385 | a := q.answers[0] 386 | q.answers = q.answers[1:] 387 | q.cur = a 388 | return true 389 | } 390 | 391 | // All returns an iterator over query results. 392 | // Be sure to check for errors by calling Err afterwards. 393 | // This is a single-use iterator. 394 | func (q *query) All(ctx context.Context) iter.Seq[Answer] { 395 | return func(yield func(Answer) bool) { 396 | for q.Next(ctx) { 397 | if !yield(q.Current()) { 398 | break 399 | } 400 | } 401 | if err := q.Close(); err != nil { 402 | q.setError(err) 403 | } 404 | } 405 | } 406 | 407 | func (q *query) Current() Answer { 408 | q.mu.Lock() 409 | defer q.mu.Unlock() 410 | return q.cur 411 | } 412 | 413 | func (q *query) Close() error { 414 | q.mu.Lock() 415 | defer q.mu.Unlock() 416 | 417 | if q.lock { 418 | q.pl.mu.Lock() 419 | defer q.pl.mu.Unlock() 420 | } 421 | 422 | return q.close() 423 | } 424 | 425 | func (q *query) close() error { 426 | if !q.dead { 427 | q.dead = true 428 | if q.pl.limiter != nil { 429 | defer func() { 430 | <-q.pl.limiter 431 | }() 432 | } 433 | for coro := range q.coros { 434 | if q.pl.debug != nil { 435 | q.pl.debug.Println("killing coroutine:", coro, "subquery:", q.subquery, "(query closed)") 436 | } 437 | q.pl.CoroStop(Subquery(q.subquery), coro) 438 | } 439 | } 440 | 441 | if q.subquery != 0 { 442 | delete(q.pl.running, q.subquery) 443 | } 444 | 445 | if !q.done && q.subquery != 0 { 446 | q.pl.pl_done.Call(q.pl.ctx, uint64(q.subquery)) 447 | q.done = true 448 | q.subquery = 0 449 | q.pl.pl_capture_free.Call(q.pl.ctx, uint64(q.pl.ptr)) 450 | } 451 | 452 | if q.stdoutptr != 0 { 453 | q.pl.free.Call(q.pl.ctx, uint64(q.stdoutptr), ptrSize, align) 454 | q.stdoutptr = 0 455 | } 456 | if q.stdoutlen != 0 { 457 | q.pl.free.Call(q.pl.ctx, uint64(q.stdoutlen), ptrSize, align) 458 | q.stdoutlen = 0 459 | } 460 | if q.stderrptr != 0 { 461 | q.pl.free.Call(q.pl.ctx, uint64(q.stderrptr), ptrSize, align) 462 | q.stderrptr = 0 463 | } 464 | if q.stderrlen != 0 { 465 | q.pl.free.Call(q.pl.ctx, uint64(q.stderrlen), ptrSize, align) 466 | q.stderrlen = 0 467 | } 468 | 469 | // q.pl = nil 470 | 471 | return nil 472 | } 473 | 474 | func (q *query) bindVar(name string, value Term) { 475 | for i, bind := range q.bind { 476 | if bind.name == name { 477 | bind.value = value 478 | q.bind[i] = bind 479 | return 480 | } 481 | } 482 | q.bind = append(q.bind, binding{ 483 | name: name, 484 | value: value, 485 | }) 486 | } 487 | 488 | func (q *query) reify() error { 489 | if len(q.bind) == 0 { 490 | return nil 491 | } 492 | 493 | var sb strings.Builder 494 | sb.WriteString(q.bind.String()) 495 | sb.WriteString(", ") 496 | sb.WriteString(q.goal) 497 | q.goal = sb.String() 498 | return nil 499 | } 500 | 501 | func (q *query) setError(err error) { 502 | if err != nil && q.err == nil { 503 | q.err = err 504 | } 505 | } 506 | 507 | func (q *query) Err() error { 508 | q.mu.Lock() 509 | defer q.mu.Unlock() 510 | return q.err 511 | } 512 | 513 | func escapeQuery(query string) string { 514 | query = queryEscaper.Replace(query) 515 | return fmt.Sprintf(`'$json_ask'(%s).`, escapeString(query)) 516 | } 517 | 518 | // QueryOption is an optional parameter for queries. 519 | type QueryOption func(*query) 520 | 521 | // WithBind binds the given variable to the given term. 522 | // This can be handy for passing data into queries. 523 | // `WithBind("X", "foo")` is equivalent to prepending `X = "foo",` to the query. 524 | func WithBind(variable string, value Term) QueryOption { 525 | return func(q *query) { 526 | q.bindVar(variable, value) 527 | } 528 | } 529 | 530 | // WithBinding binds a map of variables to terms. 531 | // This can be handy for passing data into queries. 532 | func WithBinding(subs Substitution) QueryOption { 533 | return func(q *query) { 534 | for _, bind := range subs.bindings() { 535 | q.bindVar(bind.name, bind.value) 536 | } 537 | } 538 | } 539 | 540 | func withoutLock(q *query) { 541 | q.lock = false 542 | } 543 | 544 | var queryEscaper = strings.NewReplacer("\t", " ", "\n", " ", "\r", "") 545 | 546 | var _ Query = (*query)(nil) 547 | -------------------------------------------------------------------------------- /trealla/query_test.go: -------------------------------------------------------------------------------- 1 | package trealla_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "math/big" 9 | "os" 10 | "reflect" 11 | "runtime" 12 | "sort" 13 | "sync" 14 | "testing" 15 | "testing/fstest" 16 | 17 | "github.com/trealla-prolog/go/trealla" 18 | ) 19 | 20 | var testfs = fstest.MapFS{ 21 | "fs.pl": &fstest.MapFile{ 22 | Data: []byte(`go_fs(works).`), 23 | Mode: 0600, 24 | }, 25 | } 26 | 27 | func TestQuery(t *testing.T) { 28 | testdata := "./testdata" 29 | if _, err := os.Stat(testdata); os.IsNotExist(err) { 30 | testdata = "./trealla/testdata" 31 | } 32 | 33 | pl, err := trealla.New( 34 | trealla.WithPreopenDir("."), 35 | trealla.WithMapFS("/custom_fs", testfs), 36 | trealla.WithLibraryPath("testdata"), 37 | trealla.WithDebugLog(log.Default())) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | t.Run("files", func(t *testing.T) { 43 | if runtime.GOOS == "windows" { 44 | t.Skip("wonky on windows") 45 | } 46 | 47 | ctx := context.Background() 48 | q, err := pl.QueryOnce(ctx, `directory_files("/", X)`) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | t.Logf("%+v", q.Solution) 53 | // spew.Dump(q) 54 | }) 55 | 56 | t.Run("consult", func(t *testing.T) { 57 | if err := pl.Consult(context.Background(), "/testdata/greeting.pl"); err != nil { 58 | t.Error(err) 59 | } 60 | }) 61 | 62 | tests := []struct { 63 | name string 64 | want []trealla.Answer 65 | err error 66 | }{ 67 | { 68 | name: "true/0", 69 | want: []trealla.Answer{ 70 | { 71 | Query: `true.`, 72 | Solution: trealla.Substitution{}, 73 | }, 74 | }, 75 | }, 76 | { 77 | name: "false/0", 78 | want: []trealla.Answer{ 79 | { 80 | Query: `false.`, 81 | }, 82 | }, 83 | err: trealla.ErrFailure{Query: "false."}, 84 | }, 85 | { 86 | name: "failure with output", 87 | want: []trealla.Answer{ 88 | { 89 | Query: `write(foo), write(user_error, bar), fail.`, 90 | }, 91 | }, 92 | err: trealla.ErrFailure{ 93 | Query: "write(foo), write(user_error, bar), fail.", 94 | Stdout: "foo", 95 | Stderr: "bar", 96 | }, 97 | }, 98 | { 99 | name: "write to stdout", 100 | want: []trealla.Answer{ 101 | { 102 | Query: `write(hello), nl.`, 103 | Solution: trealla.Substitution{}, 104 | Stdout: "hello\n", 105 | }, 106 | }, 107 | }, 108 | { 109 | name: "write to stderr", 110 | want: []trealla.Answer{ 111 | { 112 | Query: `write(user_error, hello).`, 113 | Solution: trealla.Substitution{}, 114 | Stderr: "hello", 115 | }, 116 | }, 117 | }, 118 | { 119 | name: "consulted", 120 | want: []trealla.Answer{ 121 | { 122 | Query: `hello(X).`, 123 | Solution: trealla.Substitution{ 124 | "X": trealla.Atom("world"), 125 | }, 126 | }, 127 | { 128 | Query: `hello(X).`, 129 | Solution: trealla.Substitution{ 130 | "X": trealla.Atom("Welt"), 131 | }, 132 | }, 133 | { 134 | Query: `hello(X).`, 135 | Solution: trealla.Substitution{ 136 | "X": trealla.Atom("世界"), 137 | }, 138 | }, 139 | }, 140 | }, 141 | { 142 | name: "assertz/1", 143 | want: []trealla.Answer{ 144 | { 145 | Query: `assertz(こんにちは(世界)).`, 146 | Solution: trealla.Substitution{}, 147 | }, 148 | }, 149 | }, 150 | { 151 | name: "assertz/1 (did it persist?)", 152 | want: []trealla.Answer{ 153 | { 154 | Query: `こんにちは(X).`, 155 | Solution: trealla.Substitution{"X": trealla.Atom("世界")}, 156 | }, 157 | }, 158 | }, 159 | { 160 | name: "member/2", 161 | want: []trealla.Answer{ 162 | { 163 | Query: `member(X, [1,foo(bar),4.2,"baz",'boop', [q, '"x'], '\\', '\n']).`, 164 | Solution: trealla.Substitution{"X": int64(1)}, 165 | }, 166 | { 167 | Query: `member(X, [1,foo(bar),4.2,"baz",'boop', [q, '"x'], '\\', '\n']).`, 168 | Solution: trealla.Substitution{"X": trealla.Compound{Functor: "foo", Args: []trealla.Term{trealla.Atom("bar")}}}}, 169 | { 170 | Query: `member(X, [1,foo(bar),4.2,"baz",'boop', [q, '"x'], '\\', '\n']).`, 171 | Solution: trealla.Substitution{"X": 4.2}}, 172 | { 173 | Query: `member(X, [1,foo(bar),4.2,"baz",'boop', [q, '"x'], '\\', '\n']).`, 174 | Solution: trealla.Substitution{"X": "baz"}}, 175 | { 176 | Query: `member(X, [1,foo(bar),4.2,"baz",'boop', [q, '"x'], '\\', '\n']).`, 177 | Solution: trealla.Substitution{"X": trealla.Atom("boop")}}, 178 | { 179 | Query: `member(X, [1,foo(bar),4.2,"baz",'boop', [q, '"x'], '\\', '\n']).`, 180 | Solution: trealla.Substitution{"X": []trealla.Term{trealla.Atom("q"), trealla.Atom(`"x`)}}}, 181 | { 182 | Query: `member(X, [1,foo(bar),4.2,"baz",'boop', [q, '"x'], '\\', '\n']).`, 183 | Solution: trealla.Substitution{"X": trealla.Atom(`\`)}}, 184 | { 185 | Query: `member(X, [1,foo(bar),4.2,"baz",'boop', [q, '"x'], '\\', '\n']).`, 186 | Solution: trealla.Substitution{"X": trealla.Atom("\n")}}, 187 | }, 188 | }, 189 | { 190 | name: "tak & WithLibraryPath", 191 | want: []trealla.Answer{ 192 | { 193 | // TODO: flake? need to retry once for 'run' to be found 194 | Query: "use_module(library(tak)), run.", 195 | Solution: trealla.Substitution{}, 196 | Stdout: "''([34,13,8],13).\n", 197 | }, 198 | }, 199 | }, 200 | { 201 | name: "bigint", 202 | want: []trealla.Answer{ 203 | { 204 | Query: "X=9999999999999999, Y = -9999999999999999, Z = 123.", 205 | Solution: trealla.Substitution{"X": big.NewInt(9999999999999999), "Y": big.NewInt(-9999999999999999), "Z": int64(123)}, 206 | }, 207 | }, 208 | }, 209 | { 210 | name: "empty list", 211 | want: []trealla.Answer{ 212 | { 213 | Query: "X = [].", 214 | Solution: trealla.Substitution{"X": []trealla.Term{}}, 215 | }, 216 | }, 217 | }, 218 | { 219 | name: "empty atom", 220 | want: []trealla.Answer{ 221 | { 222 | Query: "X = foo(bar, '').", 223 | Solution: trealla.Substitution{"X": trealla.Compound{Functor: "foo", Args: []trealla.Term{trealla.Atom("bar"), trealla.Atom("")}}}, 224 | }, 225 | }, 226 | }, 227 | { 228 | name: "changing user_output", 229 | want: []trealla.Answer{ 230 | { 231 | Query: `tell('/testdata/test.txt'), write(hello), flush_output, X = 1, read_file_to_string("/testdata/test.txt", Content, []), delete_file("/testdata/test.txt")`, 232 | Solution: trealla.Substitution{"X": int64(1), "Content": "hello"}, 233 | }, 234 | }, 235 | }, 236 | { 237 | name: "fs.FS support", 238 | want: []trealla.Answer{ 239 | { 240 | Query: `consult('/custom_fs/fs.pl'), go_fs(X), directory_files("/custom_fs", Files).`, 241 | Solution: trealla.Substitution{"X": trealla.Atom("works"), "Files": []trealla.Term{".", "..", "fs.pl"}}, 242 | }, 243 | }, 244 | }, 245 | // TODO: this is flaking atm, reporting `dif(X, _)` instead of `dif(X, Y)` 246 | // need to investigate 247 | // { 248 | // name: "residual goals", 249 | // want: []trealla.Answer{ 250 | // { 251 | // Query: "dif(X, Y).", 252 | // Solution: trealla.Substitution{ 253 | // "X": trealla.Variable{Name: "X", Attr: []trealla.Term{trealla.Compound{Functor: ":", Args: []trealla.Term{trealla.Atom("dif"), trealla.Compound{Functor: "dif", Args: []trealla.Term{trealla.Variable{Name: "X"}, trealla.Variable{Name: "Y"}}}}}}}, 254 | // "Y": trealla.Variable{Name: "Y", Attr: nil}, 255 | // }, 256 | // }, 257 | // }, 258 | // }, 259 | } 260 | for _, tc := range tests { 261 | t.Run(tc.name, func(t *testing.T) { 262 | ctx := context.Background() 263 | q := pl.Query(ctx, tc.want[0].Query) 264 | var ans []trealla.Answer 265 | for q.Next(ctx) { 266 | ans = append(ans, q.Current()) 267 | } 268 | err := q.Err() 269 | if tc.err == nil && err != nil { 270 | if trealla.IsFailure(err) { 271 | if stderr := err.(trealla.ErrFailure).Stderr; stderr != "" { 272 | fmt.Println(stderr) 273 | } 274 | } 275 | t.Fatal(err) 276 | } else if tc.err != nil && !errors.Is(err, tc.err) { 277 | t.Errorf("unexpected error: %#v (%v) ", err, err) 278 | } 279 | if tc.err == nil && !reflect.DeepEqual(ans, tc.want) { 280 | t.Errorf("bad answer. \nwant: %#v\n got: %#v\n", tc.want, ans) 281 | } 282 | q.Close() 283 | }) 284 | } 285 | 286 | } 287 | 288 | func TestThrow(t *testing.T) { 289 | pl, err := trealla.New(trealla.WithPreopenDir("testdata")) 290 | if err != nil { 291 | t.Fatal(err) 292 | } 293 | 294 | ctx := context.Background() 295 | q := pl.Query(ctx, `write(hello), throw(ball).`) 296 | if q.Next(ctx) { 297 | t.Error("unexpected result", q.Current()) 298 | } 299 | err = q.Err() 300 | 301 | var ex trealla.ErrThrow 302 | if !errors.As(err, &ex) { 303 | t.Fatal("unexpected error:", err, "want ErrThrow") 304 | } 305 | 306 | if ex.Ball != trealla.Atom("ball") { 307 | t.Error(`unexpected error value. want: "ball" got:`, ex.Ball) 308 | } 309 | if ex.Stdout != "hello" { 310 | t.Error("unexpected stdout:", ex.Stdout) 311 | } 312 | } 313 | 314 | // func TestInterpError(t *testing.T) { 315 | // pl, err := trealla.New() 316 | // if err != nil { 317 | // t.Fatal(err) 318 | // } 319 | 320 | // ctx := context.Background() 321 | // q := pl.Query(ctx, `abort.`) 322 | // if q.Next(ctx) { 323 | // t.Error("unexpected result", q.Current()) 324 | // } 325 | // err = q.Err() 326 | // t.Fatal(err) 327 | // if err == nil { 328 | // t.Fatal("expected error") 329 | // } 330 | // } 331 | 332 | func TestPreopen(t *testing.T) { 333 | if runtime.GOOS == "windows" { 334 | t.Skip("skipping unixy test") 335 | } 336 | 337 | pl, err := trealla.New(trealla.WithPreopenDir("testdata"), trealla.WithMapDir("/foo", "testdata/subdirectory")) 338 | if err != nil { 339 | t.Fatal(err) 340 | } 341 | ctx := context.Background() 342 | 343 | t.Run("WithPreopenDir", func(t *testing.T) { 344 | q, err := pl.QueryOnce(ctx, `directory_files("/", X)`) 345 | if err != nil { 346 | t.Fatal(err) 347 | } 348 | want := []trealla.Term{".", "..", "subdirectory", "greeting.pl", "tak.pl"} 349 | got := q.Solution["X"].([]trealla.Term) 350 | sort.Slice(want, func(i, j int) bool { 351 | return want[i].(string) < want[j].(string) 352 | }) 353 | sort.Slice(got, func(i, j int) bool { 354 | return got[i].(string) < got[j].(string) 355 | }) 356 | if !reflect.DeepEqual(want, got) { 357 | t.Error("bad preopen. want:", want, "got:", got) 358 | } 359 | }) 360 | 361 | t.Run("WithMapDir", func(t *testing.T) { 362 | q, err := pl.QueryOnce(ctx, `directory_files("/foo", X)`) 363 | if err != nil { 364 | t.Fatal(err) 365 | } 366 | want := []trealla.Term{".", "..", "foo.txt"} 367 | got := q.Solution["X"] 368 | if !reflect.DeepEqual(want, got) { 369 | t.Error("bad preopen. want:", want, "got:", got) 370 | } 371 | }) 372 | } 373 | 374 | func TestSyntaxError(t *testing.T) { 375 | t.Parallel() 376 | 377 | pl, err := trealla.New(trealla.WithPreopenDir("testdata")) 378 | if err != nil { 379 | t.Fatal(err) 380 | } 381 | 382 | ctx := context.Background() 383 | q := pl.Query(ctx, `hello(`) 384 | if q.Next(ctx) { 385 | t.Error("unexpected result", q.Current()) 386 | } 387 | err = q.Err() 388 | 389 | var ex trealla.ErrThrow 390 | if !errors.As(err, &ex) { 391 | t.Fatal("unexpected error:", err, "want ErrThrow") 392 | } 393 | want := trealla.Compound{Functor: "error", Args: []trealla.Term{ 394 | trealla.Compound{ 395 | Functor: "syntax_error", 396 | Args: []trealla.Term{trealla.Atom("mismatched_parens_or_brackets_or_braces")}, 397 | }, 398 | trealla.Compound{ 399 | Functor: "/", 400 | Args: []trealla.Term{trealla.Atom("read_term_from_chars"), int64(3)}, 401 | }, 402 | }} 403 | 404 | if !reflect.DeepEqual(ex.Ball, want) { 405 | t.Error(`unexpected error value. want:`, want, `got:`, ex.Ball) 406 | } 407 | } 408 | 409 | func TestBind(t *testing.T) { 410 | t.Parallel() 411 | 412 | ctx := context.Background() 413 | pl, err := trealla.New() 414 | if err != nil { 415 | t.Fatal(err) 416 | } 417 | 418 | want := int64(123) 419 | atom := trealla.Atom("abc") 420 | validate := func(t *testing.T, ans trealla.Answer) { 421 | t.Helper() 422 | if x := ans.Solution["X"]; x != want { 423 | t.Error("unexpected value. want:", want, "got:", x) 424 | } 425 | if y := ans.Solution["Y"]; y != want { 426 | t.Error("unexpected value. want:", want, "got:", y) 427 | } 428 | if z := ans.Solution["Z"]; z != atom { 429 | t.Error("unexpected value. want:", atom, "got:", z) 430 | } 431 | } 432 | 433 | t.Run("WithBind", func(t *testing.T) { 434 | ans, err := pl.QueryOnce(ctx, "Y = X.", trealla.WithBind("X", 123), trealla.WithBind("Z", trealla.Atom("abc"))) 435 | if err != nil { 436 | t.Fatal(err) 437 | } 438 | validate(t, ans) 439 | }) 440 | 441 | t.Run("WithBinding", func(t *testing.T) { 442 | ans, err := pl.QueryOnce(ctx, "Y = X.", trealla.WithBinding(trealla.Substitution{"X": want, "Z": atom})) 443 | if err != nil { 444 | t.Fatal(err) 445 | } 446 | validate(t, ans) 447 | }) 448 | 449 | t.Run("overwriting", func(t *testing.T) { 450 | ans, err := pl.QueryOnce(ctx, "Y = X.", trealla.WithBinding(trealla.Substitution{"X": -1, "Z": atom}), trealla.WithBind("X", want)) 451 | if err != nil { 452 | t.Fatal(err) 453 | } 454 | validate(t, ans) 455 | }) 456 | 457 | t.Run("lists", func(t *testing.T) { 458 | ans, err := pl.QueryOnce(ctx, "Y = X.", trealla.WithBind("X", []trealla.Term{int64(555)})) 459 | if err != nil { 460 | t.Fatal(err) 461 | } 462 | want := []trealla.Term{int64(555)} 463 | if x := ans.Solution["X"]; !reflect.DeepEqual(x, want) { 464 | t.Error("unexpected value. want:", want, "got:", x) 465 | } 466 | }) 467 | 468 | t.Run("tricky json atoms", func(t *testing.T) { 469 | ans, err := pl.QueryOnce(ctx, "X=true(true, aaaa, '', false(a), null, ''(q), _).") 470 | if err != nil { 471 | t.Fatal(err) 472 | } 473 | want := trealla.Compound{Functor: "true", Args: []trealla.Term{ 474 | trealla.Atom("true"), 475 | trealla.Atom("aaaa"), 476 | trealla.Atom(""), 477 | trealla.Compound{Functor: "false", Args: []trealla.Term{trealla.Atom("a")}}, 478 | trealla.Atom("null"), 479 | trealla.Compound{Functor: "", Args: []trealla.Term{trealla.Atom("q")}}, 480 | trealla.Variable{Name: "_"}, 481 | }} 482 | if x := ans.Solution["X"]; !reflect.DeepEqual(x, want) { 483 | t.Error("unexpected value. want:", want, "got:", x) 484 | } 485 | }) 486 | 487 | t.Run("appended string", func(t *testing.T) { 488 | ans, err := pl.QueryOnce(ctx, `Y = "ar", append("foo", [b|Y], X).`) 489 | if err != nil { 490 | t.Fatal(err) 491 | } 492 | want := "foobar" 493 | if x := ans.Solution["X"]; !reflect.DeepEqual(x, want) { 494 | t.Error("unexpected value. want:", want, "got:", x) 495 | } 496 | }) 497 | 498 | t.Run("rationals", func(t *testing.T) { 499 | ans, err := pl.QueryOnce(ctx, `A is 1 rdiv 3, B is 9999999999999999 rdiv 2, C is 1 rdiv 9999999999999999.`) 500 | if err != nil { 501 | t.Fatal(err) 502 | } 503 | want := trealla.Substitution{"A": big.NewRat(1, 3), "B": big.NewRat(9999999999999999, 2), "C": big.NewRat(1, 9999999999999999)} 504 | for k, v := range ans.Solution { 505 | if v.(*big.Rat).Cmp(want[k].(*big.Rat)) != 0 { 506 | t.Error("bad", k, "want:", want[k], "got:", v) 507 | } 508 | } 509 | }) 510 | } 511 | 512 | func TestConcurrencySemidet(t *testing.T) { 513 | t.Run("10", testConcurrencySemidet(10)) 514 | // t.Run("100", testConcurrencySemidet(100)) 515 | // t.Run("1k", testConcurrencySemidet(1000)) 516 | // t.Run("10k", testConcurrencySemidet(10000)) 517 | } 518 | 519 | // testConcurrencySemidet returns a test case that runs N semidet queries 520 | // against the same interpreter and waits for them to finish. 521 | func testConcurrencySemidet(count int) func(*testing.T) { 522 | return func(t *testing.T) { 523 | pl, err := trealla.New() 524 | if err != nil { 525 | t.Fatal(err) 526 | } 527 | 528 | var wg sync.WaitGroup 529 | wg.Add(count) 530 | for i := 0; i < count; i++ { 531 | go func() { 532 | defer wg.Done() 533 | ctx := context.Background() 534 | q := pl.Query(ctx, "between(1,10,X)") 535 | for i := 0; i < 3; i++ { 536 | if !q.Next(ctx) { 537 | t.Fatal("next failed at", i) 538 | } 539 | } 540 | if err := q.Err(); err != nil { 541 | panic(fmt.Sprintf("error: %v, %+v", err, pl.Stats())) 542 | } 543 | got := q.Current().Solution["X"] 544 | want := int64(3) 545 | if want != got { 546 | t.Error("bad answer. want:", want, "got:", got, pl.Stats()) 547 | } 548 | q.Close() 549 | }() 550 | } 551 | wg.Wait() 552 | } 553 | } 554 | 555 | func TestConcurrencyDet10K(t *testing.T) { 556 | pl, _ := trealla.New() 557 | 558 | pl.ConsultText(context.Background(), "user", "test(123).") 559 | 560 | var wg sync.WaitGroup 561 | for i := 0; i < 10_000; i++ { 562 | wg.Add(1) 563 | go func() { 564 | defer wg.Done() 565 | pl.QueryOnce(context.Background(), "test(X).") 566 | }() 567 | } 568 | wg.Wait() 569 | } 570 | 571 | func TestConcurrencyDet100K(t *testing.T) { 572 | if !testing.Short() { 573 | t.Skip("skipping slow tests") 574 | } 575 | 576 | pl, _ := trealla.New() 577 | 578 | pl.ConsultText(context.Background(), "user", "test(123).") 579 | 580 | var wg sync.WaitGroup 581 | for i := 0; i < 100_000; i++ { 582 | wg.Add(1) 583 | go func() { 584 | defer wg.Done() 585 | pl.QueryOnce(context.Background(), "test(X).") 586 | }() 587 | } 588 | wg.Wait() 589 | } 590 | -------------------------------------------------------------------------------- /trealla/string.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type cstring struct { 8 | ptr uint32 9 | size int 10 | } 11 | 12 | func newCString(pl *prolog, str string) (*cstring, error) { 13 | cstr := &cstring{ 14 | size: len(str) + 1, 15 | } 16 | 17 | ptrv, err := pl.realloc.Call(pl.ctx, 0, 0, align, uint64(cstr.size)) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | cstr.ptr = uint32(ptrv[0]) 23 | if cstr.ptr == 0 { 24 | return nil, fmt.Errorf("trealla: failed to allocate string: %s", str) 25 | } 26 | 27 | data, _ := pl.memory.Read(cstr.ptr, uint32(len(str)+1)) 28 | data[len(str)] = 0 29 | copy(data, []byte(str)) 30 | return cstr, nil 31 | } 32 | 33 | func (str *cstring) free(pl *prolog) error { 34 | if str.ptr == 0 { 35 | return nil 36 | } 37 | 38 | _, err := pl.free.Call(pl.ctx, uint64(str.ptr), uint64(str.size), 1) 39 | str.ptr = 0 40 | str.size = 0 41 | return err 42 | } 43 | 44 | func (pl *prolog) gets(addr, size uint32) (string, error) { 45 | if addr == 0 || size == 0 { 46 | return "", nil 47 | } 48 | data, ok := pl.memory.Read(addr, size) 49 | if !ok { 50 | return "", fmt.Errorf("invalid string of %d length at: %d", size, addr) 51 | } 52 | // fmt.Println("gets", addr, size, string(data[ptr:end])) 53 | return string(data), nil 54 | } 55 | -------------------------------------------------------------------------------- /trealla/substitution.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | // Substitution is a mapping of variable names to substitutions (terms). 13 | // For query results, it's one answer to a query. 14 | // Substitution can also be used to bind variables in queries via WithBinding. 15 | type Substitution map[string]Term 16 | 17 | // String returns a Prolog representation of this substitution in the same format 18 | // as ISO variable_names/1 option for read_term/2. 19 | func (sub Substitution) String() string { 20 | return "[" + sub.bindings().String() + "]" 21 | } 22 | 23 | // UnmarshalJSON implements the encoding/json.Marshaler interface. 24 | func (sub *Substitution) UnmarshalJSON(bs []byte) error { 25 | var raws map[string]json.RawMessage 26 | dec := json.NewDecoder(bytes.NewReader(bs)) 27 | dec.UseNumber() 28 | if err := dec.Decode(&raws); err != nil { 29 | return err 30 | } 31 | *sub = make(Substitution, len(raws)) 32 | for k, raw := range raws { 33 | term, err := unmarshalTerm(raw) 34 | if err != nil { 35 | return err 36 | } 37 | (*sub)[k] = term 38 | } 39 | return nil 40 | } 41 | 42 | type binding struct { 43 | name string 44 | value Term 45 | } 46 | 47 | // Scan sets any fields in obj that match variables in this substitution. 48 | // obj must be a pointer to a struct or a map. 49 | func (sub Substitution) Scan(obj any) error { 50 | rv := reflect.ValueOf(obj) 51 | return scan(sub, rv) 52 | } 53 | 54 | type bindings []binding 55 | 56 | func (bs bindings) String() string { 57 | var sb strings.Builder 58 | for i, bind := range bs { 59 | if i != 0 { 60 | sb.WriteString(", ") 61 | } 62 | sb.WriteString(bind.name) 63 | sb.WriteString(" = ") 64 | v, err := marshal(bind.value) 65 | if err != nil { 66 | sb.WriteString(fmt.Sprintf("", err)) 67 | } 68 | sb.WriteString(v) 69 | } 70 | return sb.String() 71 | } 72 | 73 | func (bs bindings) Less(i, j int) bool { return bs[i].name < bs[j].name } 74 | func (bs bindings) Swap(i, j int) { bs[i], bs[j] = bs[j], bs[i] } 75 | func (bs bindings) Len() int { return len(bs) } 76 | 77 | func (sub Substitution) bindings() bindings { 78 | bs := make(bindings, 0, len(sub)) 79 | for k, v := range sub { 80 | bs = append(bs, binding{ 81 | name: k, 82 | value: v, 83 | }) 84 | } 85 | sort.Sort(bs) 86 | return bs 87 | } 88 | -------------------------------------------------------------------------------- /trealla/substitution_test.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestScan(t *testing.T) { 11 | type pair struct { 12 | Functor `prolog:"-/2"` 13 | Key Term 14 | Value Term 15 | } 16 | type fieldDef struct { 17 | Functor `prolog:"field/5"` 18 | Path Atom 19 | Type Term 20 | Options []Term 21 | Rules []pair 22 | Cols []Atom 23 | } 24 | type complexResult struct { 25 | Fields []fieldDef 26 | } 27 | 28 | cases := []struct { 29 | sub Substitution 30 | want any 31 | }{ 32 | { 33 | sub: Substitution{"X": Atom("foo")}, 34 | want: struct{ X string }{X: "foo"}, 35 | }, 36 | { 37 | sub: Substitution{"X": int64(123)}, 38 | want: struct{ X int }{X: 123}, 39 | }, 40 | { 41 | sub: Substitution{"X": []Term{Atom("foo"), Atom("bar")}}, 42 | want: struct{ X []string }{X: []string{"foo", "bar"}}, 43 | }, 44 | { 45 | sub: Substitution{"X": "abc"}, 46 | want: struct { 47 | ABC string `prolog:"X"` 48 | }{ 49 | ABC: "abc", 50 | }, 51 | }, 52 | { 53 | sub: Substitution{"X": []Term{}}, 54 | want: struct{ X string }{X: ""}, 55 | }, 56 | { 57 | sub: Substitution{"X": []Term{}}, 58 | want: struct{ X Atom }{X: ""}, 59 | }, 60 | { 61 | sub: Substitution{"X": []Term{}}, 62 | want: struct{ X, Y []Term }{X: []Term{}}, 63 | }, 64 | { 65 | sub: Substitution{"X": "x", "Y": "y"}, 66 | want: map[string]any{"X": "x", "Y": "y"}, 67 | }, 68 | // strings → slices 69 | { 70 | sub: Substitution{"X": "xyz"}, 71 | want: struct{ X []Atom }{X: []Atom{"x", "y", "z"}}, 72 | }, 73 | { 74 | sub: Substitution{"X": "xyzあ"}, 75 | want: struct{ X []Term }{X: []Term{Atom("x"), Atom("y"), Atom("z"), Atom("あ")}}, 76 | }, 77 | // compound → struct 78 | { 79 | sub: Substitution{"Test": Compound{Functor: "test", Args: []Term{int64(123), Atom("abc"), "hi", "ho"}}}, 80 | want: struct { 81 | Test struct { 82 | Functor 83 | N int64 84 | A Atom 85 | L []Atom 86 | Z []Term 87 | } 88 | }{ 89 | Test: struct { 90 | Functor 91 | N int64 92 | A Atom 93 | L []Atom 94 | Z []Term 95 | }{"test", int64(123), "abc", []Atom{"h", "i"}, []Term{Atom("h"), Atom("o")}}}, 96 | }, 97 | { 98 | sub: Substitution{"Fields": []Term{ 99 | Compound{Functor: "field", Args: []Term{Atom("hello"), Atom("list").Of(Atom("string")), Atom("[]"), []Term{Atom("-").Of(Atom("foo"), Atom("bar"))}, "abc"}}, 100 | }}, 101 | want: complexResult{Fields: []fieldDef{ 102 | {Functor: "field", Path: Atom("hello"), Type: Atom("list").Of(Atom("string")), Options: []Term{}, Rules: []pair{{"-", Atom("foo"), Atom("bar")}}, Cols: []Atom{Atom("a"), Atom("b"), Atom("c")}}, 103 | }}, 104 | }, 105 | } 106 | 107 | for _, tc := range cases { 108 | t.Run(fmt.Sprintf("%v", tc.sub), func(t *testing.T) { 109 | got := reflect.New(reflect.TypeOf(tc.want)).Elem().Interface() 110 | if err := tc.sub.Scan(&got); err != nil { 111 | t.Fatal(err) 112 | } 113 | if !reflect.DeepEqual(tc.want, got) { 114 | t.Errorf("bad scan result.\nwant: %#v\n got: %#v", tc.want, got) 115 | } 116 | }) 117 | } 118 | } 119 | 120 | func ExampleSubstitution_Scan() { 121 | ctx := context.Background() 122 | pl, err := New() 123 | if err != nil { 124 | panic(err) 125 | } 126 | 127 | answer, err := pl.QueryOnce(ctx, `X = 123, Y = abc, Z = ["hello", "world"].`) 128 | if err != nil { 129 | panic(err) 130 | } 131 | var result struct { 132 | X int 133 | Y string 134 | Hi []string `prolog:"Z"` 135 | } 136 | if err := answer.Solution.Scan(&result); err != nil { 137 | panic(err) 138 | } 139 | 140 | fmt.Printf("%+v", result) 141 | // Output: {X:123 Y:abc Hi:[hello world]} 142 | } 143 | -------------------------------------------------------------------------------- /trealla/term.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "math/big" 9 | "strconv" 10 | "strings" 11 | "unicode" 12 | ) 13 | 14 | // Term is a Prolog term. 15 | // 16 | // One of the following types: 17 | // - string 18 | // - int64 19 | // - float64 20 | // - *big.Int 21 | // - *big.Rat 22 | // - Atom 23 | // - Compound 24 | // - Variable 25 | // - Slices of any supported type 26 | type Term any 27 | 28 | // Atom is a Prolog atom. 29 | type Atom string 30 | 31 | // String returns the Prolog text representation of this atom. 32 | func (a Atom) String() string { 33 | if !a.needsEscape() { 34 | return string(a) 35 | } 36 | return "'" + atomEscaper.Replace(string(a)) + "'" 37 | } 38 | 39 | // Indicator returns a predicate indicator for this atom ("foo/0"). 40 | func (a Atom) Indicator() string { 41 | return a.pi().String() 42 | } 43 | 44 | func (a Atom) pi() Compound { 45 | return Atom("/").Of(a, int64(0)) 46 | } 47 | 48 | // Of returns a Compound term with this atom as the principal functor. 49 | func (a Atom) Of(args ...Term) Compound { 50 | return Compound{ 51 | Functor: a, 52 | Args: args, 53 | } 54 | } 55 | 56 | func (a *Atom) UnmarshalJSON(text []byte) error { 57 | if string(text) == "[]" { 58 | *a = "" 59 | return nil 60 | } 61 | var s string 62 | if err := json.Unmarshal(text, &s); err != nil { 63 | return err 64 | } 65 | *a = Atom(s) 66 | return nil 67 | } 68 | 69 | func (a Atom) needsEscape() bool { 70 | if len(a) == 0 { 71 | return true 72 | } 73 | for i, char := range a { 74 | if i == 0 && !unicode.IsLower(char) { 75 | return true 76 | } 77 | if !(char == '_' || unicode.IsLetter(char) || unicode.IsDigit(char)) { 78 | return true 79 | } 80 | } 81 | return false 82 | } 83 | 84 | // Compound is a Prolog compound type. 85 | type Compound struct { 86 | // Functor is the principal functor of the compound. 87 | // Example: the Functor of foo(bar) is "foo". 88 | Functor Atom 89 | // Args are the arguments of the compound. 90 | Args []Term 91 | } 92 | 93 | // Indicator returns the procedure indicator of this compound in Functor/Arity format. 94 | func (c Compound) Indicator() string { 95 | return c.pi().String() 96 | } 97 | 98 | func (c Compound) pi() Compound { 99 | return piTerm(c.Functor, len(c.Args)) 100 | } 101 | 102 | // String returns a Prolog representation of this Compound. 103 | func (c Compound) String() string { 104 | if len(c.Args) == 0 { 105 | return c.Functor.String() 106 | } 107 | 108 | var buf strings.Builder 109 | 110 | // special case these two operators for now? 111 | if len(c.Args) == 2 { 112 | switch c.Functor { 113 | case "/", ":": 114 | left, err := marshal(c.Args[0]) 115 | if err != nil { 116 | buf.WriteString(fmt.Sprintf("", err)) 117 | } 118 | buf.WriteString(left) 119 | buf.WriteString(string(c.Functor)) 120 | right, err := marshal(c.Args[1]) 121 | if err != nil { 122 | buf.WriteString(fmt.Sprintf("", err)) 123 | } 124 | buf.WriteString(right) 125 | return buf.String() 126 | } 127 | } 128 | 129 | buf.WriteString(c.Functor.String()) 130 | buf.WriteRune('(') 131 | for i, arg := range c.Args { 132 | if i > 0 { 133 | buf.WriteString(", ") 134 | } 135 | text, err := marshal(arg) 136 | if err != nil { 137 | buf.WriteString(fmt.Sprintf("", err)) 138 | continue 139 | } 140 | buf.WriteString(text) 141 | } 142 | buf.WriteRune(')') 143 | return buf.String() 144 | } 145 | 146 | func piTerm(functor Atom, arity int) Compound { 147 | return Compound{Functor: "/", Args: []Term{functor, int64(arity)}} 148 | } 149 | 150 | type atomicTerm interface { 151 | Term 152 | Indicator() string 153 | pi() Compound 154 | } 155 | 156 | type Rational[T int64 | *big.Int] struct { 157 | Numerator T 158 | Denominator T 159 | } 160 | 161 | // Variable is an unbound Prolog variable. 162 | type Variable struct { 163 | Name string 164 | Attr []Term 165 | } 166 | 167 | // Functor is a special type that represents the functor of a compound struct. 168 | // For example, hello/1 as in `hello(world)` could be represented as: 169 | // 170 | // type Hello struct { 171 | // trealla.Functor `prolog:"hello/1"` 172 | // Planet trealla.Atom 173 | // } 174 | type Functor Atom 175 | 176 | func (f Functor) functor() Functor { return f } 177 | 178 | type compoundStruct interface { 179 | functor() Functor 180 | } 181 | 182 | // String returns the Prolog text representation of this variable. 183 | func (v Variable) String() string { 184 | if len(v.Attr) == 0 { 185 | return v.Name 186 | } 187 | var sb strings.Builder 188 | for i, attr := range v.Attr { 189 | if i != 0 { 190 | sb.WriteString(", ") 191 | } 192 | text, err := marshal(attr) 193 | if err != nil { 194 | return fmt.Sprintf("", err) 195 | } 196 | sb.WriteString(text) 197 | } 198 | return sb.String() 199 | } 200 | 201 | func numbervars(n int) []Term { 202 | vars := make([]Term, n) 203 | for i := 0; i < n; i++ { 204 | if i < 26 { 205 | vars[i] = Variable{Name: string(rune('A' + i))} 206 | } else { 207 | vars[i] = Variable{Name: "_" + strconv.Itoa(i)} 208 | } 209 | } 210 | return vars 211 | } 212 | 213 | func unmarshalTerm(bs []byte) (Term, error) { 214 | var iface any 215 | dec := json.NewDecoder(bytes.NewReader(bs)) 216 | dec.UseNumber() 217 | if err := dec.Decode(&iface); err != nil { 218 | return nil, err 219 | } 220 | 221 | switch x := iface.(type) { 222 | case string: 223 | return x, nil 224 | case json.Number: 225 | str := string(x) 226 | if strings.ContainsRune(str, '.') { 227 | return strconv.ParseFloat(str, 64) 228 | } 229 | return strconv.ParseInt(str, 10, 64) 230 | case []any: 231 | var raws []json.RawMessage 232 | dec := json.NewDecoder(bytes.NewReader(bs)) 233 | dec.UseNumber() 234 | if err := dec.Decode(&raws); err != nil { 235 | return nil, err 236 | } 237 | list := make([]Term, 0, len(raws)) 238 | for _, raw := range raws { 239 | term, err := unmarshalTerm(raw) 240 | if err != nil { 241 | return nil, err 242 | } 243 | list = append(list, term) 244 | } 245 | return list, nil 246 | case map[string]any: 247 | var raws map[string]json.RawMessage 248 | dec := json.NewDecoder(bytes.NewReader(bs)) 249 | dec.UseNumber() 250 | if err := dec.Decode(&raws); err != nil { 251 | return nil, err 252 | } 253 | 254 | type internalTerm struct { 255 | Functor Atom 256 | Args []json.RawMessage 257 | Var string 258 | Attr []json.RawMessage 259 | Number string // deprecated 260 | Int json.RawMessage 261 | Numerator struct{ Int json.RawMessage } 262 | Denominator struct{ Int json.RawMessage } 263 | } 264 | var term internalTerm 265 | dec = json.NewDecoder(bytes.NewReader(bs)) 266 | dec.UseNumber() 267 | if err := dec.Decode(&term); err != nil { 268 | return nil, err 269 | } 270 | 271 | if len(term.Int) > 0 { 272 | if term.Int[0] == '"' { 273 | n := new(big.Int) 274 | unquoted := string(term.Int[1 : len(term.Int)-1]) 275 | if _, ok := n.SetString(unquoted, 10); !ok { 276 | return nil, fmt.Errorf("trealla: failed to decode number: %s", unquoted) 277 | } 278 | return n, nil 279 | } 280 | return strconv.ParseInt(string(term.Int), 10, 64) 281 | } 282 | 283 | if term.Number != "" { 284 | n := new(big.Int) 285 | if _, ok := n.SetString(term.Number, 10); !ok { 286 | return nil, fmt.Errorf("trealla: failed to decode number: %s", term.Number) 287 | } 288 | return n, nil 289 | } 290 | 291 | switch { 292 | case len(term.Numerator.Int) == 0 && len(term.Denominator.Int) == 0: 293 | case len(term.Numerator.Int) == 0 && len(term.Denominator.Int) > 0: 294 | return nil, fmt.Errorf("trealla: failed to decode rational, missing numerator: %s", string(bs)) 295 | case len(term.Numerator.Int) > 0 && len(term.Denominator.Int) == 0: 296 | return nil, fmt.Errorf("trealla: failed to decode rational, missing denominator: %s", string(bs)) 297 | case len(term.Numerator.Int) > 0 && len(term.Denominator.Int) > 0: 298 | bigN := term.Numerator.Int[0] == '"' 299 | bigD := term.Denominator.Int[0] == '"' 300 | if !bigN && !bigD { 301 | n, err1 := strconv.ParseInt(string(term.Numerator.Int), 10, 64) 302 | d, err2 := strconv.ParseInt(string(term.Denominator.Int), 10, 64) 303 | return big.NewRat(n, d), errors.Join(err1, err2) 304 | } 305 | 306 | var str []byte 307 | if bigN { 308 | str = term.Numerator.Int[1 : len(term.Numerator.Int)-1] 309 | } else { 310 | str = term.Numerator.Int 311 | } 312 | str = append(str, '/') 313 | if bigD { 314 | str = append(str, term.Denominator.Int[1:len(term.Denominator.Int)-1]...) 315 | } else { 316 | str = append(str, term.Denominator.Int...) 317 | } 318 | 319 | rat, ok := new(big.Rat).SetString(string(str)) 320 | if !ok { 321 | return nil, fmt.Errorf("trealla: failed to create rational for %s", string(str)) 322 | } 323 | return rat, nil 324 | } 325 | 326 | if term.Var != "" { 327 | attr := make([]Term, 0, len(term.Attr)) 328 | for _, raw := range term.Attr { 329 | at, err := unmarshalTerm(raw) 330 | if err != nil { 331 | return nil, err 332 | } 333 | attr = append(attr, at) 334 | } 335 | if len(attr) == 0 { 336 | attr = nil 337 | } 338 | return Variable{Name: term.Var, Attr: attr}, nil 339 | } 340 | 341 | if len(term.Args) == 0 { 342 | return Atom(term.Functor), nil 343 | } 344 | 345 | args := make([]Term, 0, len(term.Args)) 346 | for _, raw := range term.Args { 347 | arg, err := unmarshalTerm(raw) 348 | if err != nil { 349 | return nil, err 350 | } 351 | args = append(args, arg) 352 | } 353 | return Compound{ 354 | Functor: term.Functor, 355 | Args: args, 356 | }, nil 357 | case bool: 358 | return x, nil 359 | case nil: 360 | return nil, nil 361 | } 362 | 363 | return nil, fmt.Errorf("trealla: unhandled term json: %T %v", iface, iface) 364 | } 365 | -------------------------------------------------------------------------------- /trealla/term_test.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "math/big" 5 | "testing" 6 | ) 7 | 8 | func TestCompound(t *testing.T) { 9 | c0 := Compound{ 10 | Functor: "foo", 11 | Args: []Term{Atom("bar"), 4.2}, 12 | } 13 | want := "foo(bar, 4.2)" 14 | if c0.String() != want { 15 | t.Errorf("bad string. want: %v got: %v", want, c0.String()) 16 | } 17 | pi := "foo/2" 18 | if c0.Indicator() != pi { 19 | t.Errorf("bad indicator. want: %v got: %v", pi, c0.Indicator()) 20 | } 21 | } 22 | 23 | func TestMarshal(t *testing.T) { 24 | cases := []struct { 25 | term Term 26 | want string 27 | }{ 28 | { 29 | term: Atom("foo"), 30 | want: "foo", 31 | }, 32 | { 33 | term: Atom("Bar"), 34 | want: "'Bar'", 35 | }, 36 | { 37 | term: Atom("hello world"), 38 | want: "'hello world'", 39 | }, 40 | { 41 | term: Atom("under_score"), 42 | want: "under_score", 43 | }, 44 | { 45 | term: Atom("123"), 46 | want: "'123'", 47 | }, 48 | { 49 | term: Atom("x1"), 50 | want: "x1", 51 | }, 52 | { 53 | term: "string", 54 | want: `"string"`, 55 | }, 56 | { 57 | term: `foo\bar`, 58 | want: `"foo\\bar"`, 59 | }, 60 | { 61 | term: big.NewInt(9999999999999999), 62 | want: "9999999999999999", 63 | }, 64 | { 65 | term: big.NewRat(1, 3), 66 | want: "1 rdiv 3", 67 | }, 68 | { 69 | term: Variable{Name: "X", Attr: []Term{Compound{Functor: ":", Args: []Term{Atom("dif"), Compound{Functor: "dif", Args: []Term{Variable{Name: "X"}, Variable{Name: "Y"}}}}}}}, 70 | want: "dif:dif(X, Y)", 71 | }, 72 | { 73 | term: []Term{int64(1), int64(2)}, 74 | want: "[1, 2]", 75 | }, 76 | { 77 | term: []int64{int64(1), int64(2)}, 78 | want: "[1, 2]", 79 | }, 80 | { 81 | term: []any{int64(1), int64(2)}, 82 | want: "[1, 2]", 83 | }, 84 | { 85 | term: Atom("/").Of(Atom("foo"), 1), 86 | want: "foo/1", 87 | }, 88 | { 89 | term: coordinate{Functor: "/", X: 6, Y: 9}, 90 | want: "6/9", 91 | }, 92 | } 93 | 94 | for _, tc := range cases { 95 | t.Run(tc.want, func(t *testing.T) { 96 | text, err := marshal(tc.term) 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | if text != tc.want { 101 | t.Error("bad result. want:", tc.want, "got:", text) 102 | } 103 | }) 104 | } 105 | } 106 | 107 | // compound of X/Y 108 | type coordinate struct { 109 | Functor `prolog:"//2"` 110 | X, Y int 111 | } 112 | -------------------------------------------------------------------------------- /trealla/terms/example_test.go: -------------------------------------------------------------------------------- 1 | package terms_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "iter" 7 | 8 | "github.com/trealla-prolog/go/trealla" 9 | "github.com/trealla-prolog/go/trealla/terms" 10 | ) 11 | 12 | func Example_nondet_predicate() { 13 | ctx := context.Background() 14 | pl, err := trealla.New() 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | // Let's add a native equivalent of between/3. 20 | // betwixt(+Min, +Max, ?N). 21 | pl.RegisterNondet(ctx, "betwixt", 3, func(_ trealla.Prolog, _ trealla.Subquery, goal0 trealla.Term) iter.Seq[trealla.Term] { 22 | return func(yield func(trealla.Term) bool) { 23 | // goal is the goal called by Prolog, such as: betwixt(1, 10, X). 24 | // Guaranteed to match up with the registered arity and name. 25 | goal := goal0.(trealla.Compound) 26 | 27 | // Check Min and Max argument's type, must be integers (all integers are int64). 28 | // TODO: should throw instantiation_error instead of type_error if Min or Max are variables. 29 | min, ok := goal.Args[0].(int64) 30 | if !ok { 31 | // throw(error(type_error(integer, Min), betwixt/3)). 32 | yield(terms.Throw(terms.TypeError("integer", goal.Args[0], terms.PI(goal)))) 33 | return 34 | } 35 | max, ok := goal.Args[1].(int64) 36 | if !ok { 37 | // throw(error(type_error(integer, Max), betwixt/3)). 38 | yield(terms.Throw(terms.TypeError("integer", goal.Args[1], terms.PI(goal)))) 39 | return 40 | } 41 | 42 | if min > max { 43 | // Since we haven't yielded anything, this will fail. 44 | return 45 | } 46 | 47 | switch x := goal.Args[2].(type) { 48 | case int64: 49 | // If the 3rd argument is bound, we can do a simple check and stop iterating. 50 | if x >= min && x <= max { 51 | yield(goal) 52 | return 53 | } 54 | case trealla.Variable: 55 | // Create choice points unifying N from min to max 56 | for n := min; n <= max; n++ { 57 | goal.Args[2] = n 58 | if !yield(goal) { 59 | break 60 | } 61 | } 62 | default: 63 | yield(terms.Throw(terms.TypeError("integer", goal.Args[2], terms.PI(goal)))) 64 | } 65 | } 66 | }) 67 | 68 | // Try it out. 69 | answer, err := pl.QueryOnce(ctx, `findall(N, betwixt(1, 5, N), Ns), write(Ns).`) 70 | if err != nil { 71 | panic(err) 72 | } 73 | fmt.Println(answer.Stdout) 74 | // Output: [1,2,3,4,5] 75 | } 76 | -------------------------------------------------------------------------------- /trealla/terms/term.go: -------------------------------------------------------------------------------- 1 | // Package terms contains utilities for manipulating Prolog terms. 2 | // It should be helpful for writing Prolog predicates in Go. 3 | package terms 4 | 5 | import ( 6 | "math/big" 7 | "slices" 8 | 9 | "github.com/trealla-prolog/go/trealla" 10 | ) 11 | 12 | // TypeError returns a term in the form of error(type_error(Want, Got), Ctx). 13 | func TypeError(want trealla.Atom, got trealla.Term, ctx trealla.Term) trealla.Compound { 14 | return trealla.Atom("error").Of(trealla.Atom("type_error").Of(want, got), ctx) 15 | } 16 | 17 | // DomainError returns a term in the form of error(domain_error(Domain, Got), Ctx). 18 | func DomainError(domain trealla.Atom, got trealla.Term, ctx trealla.Term) trealla.Compound { 19 | return trealla.Atom("error").Of(trealla.Atom("domain_error").Of(domain, got), ctx) 20 | } 21 | 22 | // ExistenceError returns a term in the form of error(existence_error(What, Got), Ctx). 23 | func ExistenceError(what trealla.Atom, got trealla.Term, ctx trealla.Term) trealla.Compound { 24 | return trealla.Atom("error").Of(trealla.Atom("existence_error").Of(what, got), ctx) 25 | } 26 | 27 | // PermissionError returns a term in the form of error(permission_error(What, Got), Ctx). 28 | func PermissionError(what trealla.Atom, got trealla.Term, ctx trealla.Term) trealla.Compound { 29 | return trealla.Atom("error").Of(trealla.Atom("permission_error").Of(what, got), ctx) 30 | } 31 | 32 | // ResourceError returns a term in the form of error(resource_error(What), Ctx). 33 | func ResourceError(what trealla.Atom, ctx trealla.Term) trealla.Compound { 34 | return trealla.Atom("error").Of(trealla.Atom("resource_error").Of(what), ctx) 35 | } 36 | 37 | // SystemError returns a term in the form of error(system_error(What), Ctx). 38 | func SystemError(what, ctx trealla.Term) trealla.Compound { 39 | return trealla.Atom("error").Of(trealla.Atom("system_error").Of(what), ctx) 40 | } 41 | 42 | // Throw returns a term in the form of throw(Ball). 43 | func Throw(ball trealla.Term) trealla.Compound { 44 | return trealla.Compound{Functor: "throw", Args: []trealla.Term{ball}} 45 | } 46 | 47 | // PI returns the predicate indicator for the given term as a compound of //2, such as some_atom/0. 48 | // Returns nil for incompatible terms. 49 | func PI(atomic trealla.Term) trealla.Term { 50 | switch x := atomic.(type) { 51 | case trealla.Atom: 52 | return trealla.Compound{Functor: "/", Args: []trealla.Term{x, int64(0)}} 53 | case trealla.Compound: 54 | return trealla.Compound{Functor: "/", Args: []trealla.Term{x.Functor, int64(len(x.Args))}} 55 | case string, []trealla.Term, []any, []string, []int64, []int, []float64, []*big.Int, []trealla.Atom, []trealla.Compound, []trealla.Variable: 56 | return trealla.Compound{Functor: "/", Args: []trealla.Term{trealla.Atom("."), int64(2)}} 57 | } 58 | return nil 59 | } 60 | 61 | // ResolveOption searches through "options lists" in the form of `[foo(V1), bar(V2), ...]` 62 | // as seen in open/4. It returns the argument of the compound matching functor, 63 | // or if not found returns fallback. 64 | // If the argument is a variable, it is replaced with fallback. 65 | func ResolveOption[T trealla.Term](opts trealla.Term, functor trealla.Atom, fallback T) T { 66 | if empty, ok := opts.(trealla.Atom); ok && empty == "[]" { 67 | return fallback 68 | } 69 | list, ok := opts.([]trealla.Term) 70 | if !ok { 71 | var empty T 72 | return empty 73 | } 74 | for i, x := range list { 75 | switch x := x.(type) { 76 | case trealla.Compound: 77 | if x.Functor != functor || len(x.Args) != 1 { 78 | continue 79 | } 80 | switch arg := x.Args[0].(type) { 81 | case T: 82 | return arg 83 | case trealla.Variable: 84 | list[i] = functor.Of(fallback) 85 | return fallback 86 | } 87 | } 88 | } 89 | return fallback 90 | } 91 | 92 | func IsList(x trealla.Term) bool { 93 | switch x := x.(type) { 94 | case string, []trealla.Term, []any, []string, []int64, []int, []float64, []*big.Int, []trealla.Atom, []trealla.Compound, []trealla.Variable: 95 | return true 96 | case trealla.Atom: 97 | return x == "[]" 98 | } 99 | return false 100 | } 101 | 102 | // Substitute returns a copy of the term x with its arguments replaced by args. 103 | // A nil argument will be kept as-is. 104 | // If len(args) > 0, x must be [trealla.Compound]. 105 | func Substitute(x trealla.Term, args ...trealla.Term) trealla.Term { 106 | if len(args) == 0 { 107 | return x 108 | } 109 | cmp, ok := x.(trealla.Compound) 110 | if !ok { 111 | pi := PI(x) 112 | if pi == nil { 113 | pi = trealla.Atom("/").Of(trealla.Atom("go$terms.Substitute"), int64(len(args))) 114 | } 115 | return Throw(TypeError("compound", x, pi)) 116 | } 117 | goal := trealla.Compound{Functor: cmp.Functor, Args: slices.Clone(cmp.Args)} 118 | for i, arg := range args { 119 | if arg != nil { 120 | goal.Args[i] = arg 121 | } 122 | } 123 | return goal 124 | } 125 | -------------------------------------------------------------------------------- /trealla/terms/term_test.go: -------------------------------------------------------------------------------- 1 | package terms_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/trealla-prolog/go/trealla" 8 | "github.com/trealla-prolog/go/trealla/terms" 9 | ) 10 | 11 | func TestPI(t *testing.T) { 12 | table := []struct { 13 | name string 14 | in trealla.Term 15 | out trealla.Term 16 | }{ 17 | { 18 | name: "atom", 19 | in: trealla.Atom("hello"), 20 | out: trealla.Atom("/").Of(trealla.Atom("hello"), int64(0)), 21 | }, 22 | { 23 | name: "compound", 24 | in: trealla.Atom("hello").Of("world"), 25 | out: trealla.Atom("/").Of(trealla.Atom("hello"), int64(1)), 26 | }, 27 | { 28 | name: "string", 29 | in: "foo", 30 | out: trealla.Atom("/").Of(trealla.Atom("."), int64(2)), 31 | }, 32 | { 33 | name: "list", 34 | in: []trealla.Term{trealla.Atom("hello"), trealla.Atom("world")}, 35 | out: trealla.Atom("/").Of(trealla.Atom("."), int64(2)), 36 | }, 37 | } 38 | 39 | for _, tc := range table { 40 | want := tc.out 41 | got := terms.PI(tc.in) 42 | if !reflect.DeepEqual(want, got) { 43 | t.Error(tc.name, "bad pi. want:", want, "got:", got) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /trealla/testdata/greeting.pl: -------------------------------------------------------------------------------- 1 | hello(world). 2 | hello('Welt'). 3 | hello(世界). 4 | -------------------------------------------------------------------------------- /trealla/testdata/subdirectory/foo.txt: -------------------------------------------------------------------------------- 1 | hello -------------------------------------------------------------------------------- /trealla/testdata/tak.pl: -------------------------------------------------------------------------------- 1 | /* 2 | From: https://github.com/josd/eyeglass/tree/master/tak 3 | 4 | MIT License 5 | 6 | Copyright 2021-2022 Jos De Roo 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | ''([X,Y,Z],Z) :- 28 | X =< Y, 29 | !. 30 | ''([X,Y,Z],A) :- 31 | X1 is X-1, 32 | ''([X1,Y,Z],A1), 33 | Y1 is Y-1, 34 | ''([Y1,Z,X],A2), 35 | Z1 is Z-1, 36 | ''([Z1,X,Y],A3), 37 | ''([A1,A2,A3],A). 38 | 39 | % query 40 | query(''([34,13,8],_ANSWER)). 41 | 42 | run :- 43 | query(Q), 44 | Q, 45 | writeq(Q), 46 | write('.\n'). 47 | -------------------------------------------------------------------------------- /trealla/wasm.go: -------------------------------------------------------------------------------- 1 | package trealla 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | 7 | "github.com/tetratelabs/wazero" 8 | "github.com/tetratelabs/wazero/api" 9 | "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" 10 | ) 11 | 12 | //go:embed libtpl.wasm 13 | var tplWASM []byte 14 | 15 | type wasmFunc = api.Function 16 | 17 | var wasmEngine wazero.Runtime 18 | var wasmModule wazero.CompiledModule 19 | 20 | func init() { 21 | ctx := context.Background() 22 | wasmEngine = wazero.NewRuntime(ctx) 23 | wasi_snapshot_preview1.MustInstantiate(ctx, wasmEngine) 24 | 25 | _, err := wasmEngine.NewHostModuleBuilder("trealla"). 26 | NewFunctionBuilder().WithFunc(hostCall).Export("host-call"). 27 | NewFunctionBuilder().WithFunc(hostResume).Export("host-resume"). 28 | NewFunctionBuilder().WithFunc(hostPushAnswer).Export("host-push-answer"). 29 | Instantiate(ctx) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | wasmModule, err = wasmEngine.CompileModule(ctx, tplWASM) 35 | if err != nil { 36 | panic(err) 37 | } 38 | } 39 | 40 | var ( 41 | wasmFalse uint32 = 0 42 | wasmTrue uint32 = 1 43 | ) 44 | 45 | const ( 46 | ptrSize = 4 47 | align = 1 48 | pageSize = 64 * 1024 49 | ) 50 | --------------------------------------------------------------------------------