Skip to content

Commit

Permalink
Merge pull request #14 from trealla-prolog/coro
Browse files Browse the repository at this point in the history
Coroutine-based native predicates & Go 1.23 iterators
  • Loading branch information
guregu authored Aug 18, 2024
2 parents 4c08bdd + 8effaba commit f85c9c9
Show file tree
Hide file tree
Showing 10 changed files with 558 additions and 72 deletions.
66 changes: 33 additions & 33 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
name: Test
name: Test

on:
push:
tags:
- "v*"
branches:
- "*"
pull_request:
release:
types: [created]
on:
push:
tags:
- "v*"
branches:
- "*"
pull_request:
release:
types: [created]

jobs:
test:
name: Build & Test
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
steps:
- uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: "1.21"
- name: Deps
run: go get ./trealla
- name: Build
run: go build ./trealla
- name: Test
run: go test -v ./trealla --short
jobs:
test:
name: Build & Test
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
steps:
- uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: "1.23"
- name: Deps
run: go get ./trealla
- name: Build
run: go build ./trealla
- name: Test
run: go test -v ./trealla --short
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module github.com/trealla-prolog/go

go 1.19
go 1.23

require github.com/tetratelabs/wazero v1.7.3
79 changes: 78 additions & 1 deletion trealla/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/base32"
"fmt"
"iter"

"github.com/trealla-prolog/go/trealla"
)
Expand Down Expand Up @@ -88,7 +89,7 @@ func Example_register() {
// throw(error(type_error(list, X), base32/2)).
// See: terms subpackage for convenience functions to create these errors.
return trealla.Atom("throw").Of(trealla.Atom("error").Of(
trealla.Atom("type_error").Of("list", goal.Args[0]),
trealla.Atom("type_error").Of("chars", goal.Args[0]),
trealla.Atom("/").Of(trealla.Atom("base32"), 2),
))
}
Expand All @@ -109,3 +110,79 @@ func Example_register() {
fmt.Println(answer.Solution["Encoded"])
// Output: NBSWY3DP
}

func Example_register_nondet() {
ctx := context.Background()
pl, err := trealla.New()
if err != nil {
panic(err)
}

// Let's add a native equivalent of between/3.
// betwixt(+Min, +Max, ?N).
pl.RegisterNondet(ctx, "betwixt", 3, func(_ trealla.Prolog, _ trealla.Subquery, goal0 trealla.Term) iter.Seq[trealla.Term] {
pi := trealla.Atom("/").Of(trealla.Atom("betwixt"), 2)
return func(yield func(trealla.Term) bool) {
// goal is the goal called by Prolog, such as: base32("hello", X).
// Guaranteed to match up with the registered arity and name.
goal := goal0.(trealla.Compound)

// Check Min and Max argument's type, must be integers (all integers are int64).
min, ok := goal.Args[0].(int64)
if !ok {
// throw(error(type_error(integer, Min), betwixt/3)).
yield(trealla.Atom("throw").Of(trealla.Atom("error").Of(
trealla.Atom("type_error").Of("integer", goal.Args[0]),
pi,
)))
// See terms subpackage for an easier way:
// yield(terms.Throw(terms.TypeError("integer", goal.Args[0], terms.PI(goal)))
return
}
max, ok := goal.Args[1].(int64)
if !ok {
// throw(error(type_error(integer, Max), betwixt/3)).
yield(trealla.Atom("throw").Of(trealla.Atom("error").Of(
trealla.Atom("type_error").Of("integer", goal.Args[1]),
pi,
)))
return
}

if min > max {
// Since we haven't yielded anything, this will fail.
return
}

switch x := goal.Args[2].(type) {
case int64:
// If the 3rd argument is bound, we can do a simple check and stop iterating.
if x >= min && x <= max {
yield(goal)
return
}
case trealla.Variable:
// Create choice points unifying N from min to max
for n := min; n <= max; n++ {
goal.Args[2] = n
if !yield(goal) {
break
}
}
default:
yield(trealla.Atom("throw").Of(trealla.Atom("error").Of(
trealla.Atom("type_error").Of("integer", goal.Args[2]),
trealla.Atom("/").Of(trealla.Atom("base32"), 2),
)))
}
}
})

// Try it out.
answer, err := pl.QueryOnce(ctx, `findall(N, betwixt(1, 5, N), Ns), write(Ns).`)
if err != nil {
panic(err)
}
fmt.Println(answer.Stdout)
// Output: [1,2,3,4,5]
}
190 changes: 182 additions & 8 deletions trealla/interop.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package trealla
import (
"context"
"fmt"
"io"
"iter"
)

// Predicate is a Prolog predicate implemented in Go.
Expand All @@ -17,10 +19,158 @@ import (
// - Return a 'true' atom to succeed without unifying anything.
type Predicate func(pl Prolog, subquery Subquery, goal Term) Term

// NondetPredicate works similarly to [Predicate], but can create multiple choice points.
type NondetPredicate func(pl Prolog, subquery Subquery, goal Term) iter.Seq[Term]

// Subquery is an opaque value representing an in-flight query.
// It is unique as long as the query is alive, but may be re-used later on.
type Subquery uint32

type coroutine struct {
next func() (Term, bool)
stop func()
}

type coroer interface {
CoroStart(subq Subquery, seq iter.Seq[Term]) int64
CoroNext(subq Subquery, id int64) (Term, bool)
CoroStop(subq Subquery, id int64)
}

func (pl *prolog) Register(ctx context.Context, name string, arity int, proc Predicate) error {
pl.mu.Lock()
defer pl.mu.Unlock()
if pl.instance == nil {
return io.EOF
}
return pl.register(ctx, name, arity, proc)
}

func (pl *prolog) register(ctx context.Context, name string, arity int, proc Predicate) error {
functor := Atom(name)
pi := piTerm(functor, arity)
pl.procs[pi.String()] = proc
vars := numbervars(arity)
head := functor.Of(vars...)
body := Atom("host_rpc").Of(head)
clause := fmt.Sprintf(`%s :- %s.`, head.String(), body.String())
return pl.consultText(ctx, "user", clause)
}

func (pl *prolog) RegisterNondet(ctx context.Context, name string, arity int, proc NondetPredicate) error {
pl.mu.Lock()
defer pl.mu.Unlock()
if pl.instance == nil {
return io.EOF
}
return pl.registerNondet(ctx, name, arity, proc)
}

func (pl *prolog) registerNondet(ctx context.Context, name string, arity int, proc NondetPredicate) error {
shim := func(pl2 Prolog, subquery Subquery, goal Term) Term {
plc := pl2.(coroer)
seq := proc(pl2, subquery, goal)
id := plc.CoroStart(subquery, seq)
// call: call_cleanup('$coro_next'(ID), '$coro_stop'(ID))
return Atom("call").Of(
Atom("call_cleanup").Of(
Atom("$coro_next").Of(id, goal),
Atom("$coro_stop").Of(id),
),
)
}
return pl.register(ctx, name, arity, shim)
}

// '$coro_next'(+ID, ?Goal)
func sys_coro_next_2(pl Prolog, subquery Subquery, goal Term) Term {
plc := pl.(coroer)
g := goal.(Compound)
id, ok := g.Args[0].(int64)
if !ok {
return throwTerm(domainError("integer", g.Args[0], g.pi()))
}
result, ok := plc.CoroNext(subquery, id)
if !ok || result == nil {
return Atom("fail")
}
// call(( wasm_generic:host_rpc_eval(Goal, Result, [], []) ; '$coro_next'(ID, Goal) ))
return Atom("call").Of(
Atom(";").Of(
Atom(":").Of(Atom("wasm_generic"), Atom("host_rpc_eval").Of(result, g.Args[1], Atom("[]"), Atom("[]"))),
Atom("$coro_next").Of(id, g.Args[1]),
),
)
}

// '$coro_stop'(+ID)
func sys_coro_stop_1(pl Prolog, subquery Subquery, goal Term) Term {
plc := pl.(coroer)
g := goal.(Compound)
id, ok := g.Args[0].(int64)
if !ok {
return throwTerm(domainError("integer", g.Args[0], g.pi()))
}
plc.CoroStop(subquery, id)
return goal
}

func (pl *prolog) CoroStart(subq Subquery, seq iter.Seq[Term]) int64 {
pl.coron++
id := pl.coron
next, stop := iter.Pull(seq)
pl.coros[id] = coroutine{
next: next,
stop: stop,
}
if query := pl.subquery(uint32(subq)); query != nil {
if query.coros == nil {
query.coros = make(map[int64]struct{})
}
query.coros[id] = struct{}{}
}
return id
}

func (pl *prolog) CoroNext(subq Subquery, id int64) (Term, bool) {
coro, ok := pl.coros[id]
if !ok {
return Atom("false"), false
}
next, ok := coro.next()
if !ok {
delete(pl.coros, id)
if query := pl.subquery(uint32(subq)); query != nil {
delete(query.coros, id)
}
}
return next, ok
}

func (pl *prolog) CoroStop(subq Subquery, id int64) {
if query := pl.subquery(uint32(subq)); query != nil {
delete(query.coros, id)
}
coro, ok := pl.coros[id]
if !ok {
return
}
coro.stop()
delete(pl.coros, id)
}

func (pl *lockedProlog) CoroStart(subq Subquery, seq iter.Seq[Term]) int64 {
return pl.prolog.CoroStart(subq, seq)
}

func (pl *lockedProlog) CoroNext(subq Subquery, id int64) (Term, bool) {
return pl.prolog.CoroNext(subq, id)
}

func (pl *lockedProlog) CoroStop(subq Subquery, id int64) {
pl.prolog.CoroStop(subq, id)
}

func hostCall(ctx context.Context, subquery, msgptr, msgsize, reply_pp, replysize_p uint32) uint32 {
// extern int32_t host_call(int32_t subquery, const char *msg, size_t msg_size, char **reply, size_t *reply_size);
pl := ctx.Value(prologKey{}).(*prolog)
Expand Down Expand Up @@ -79,7 +229,7 @@ func hostCall(ctx context.Context, subquery, msgptr, msgsize, reply_pp, replysiz
// log.Println("SAVING", subq.stderr.String())

locked := &lockedProlog{prolog: pl}
continuation := proc(locked, Subquery(subquery), goal)
continuation := catch(proc, locked, Subquery(subquery), goal)
locked.kill()
expr, err := marshal(continuation)
if err != nil {
Expand All @@ -92,17 +242,41 @@ func hostCall(ctx context.Context, subquery, msgptr, msgsize, reply_pp, replysiz
if err := subq.readOutput(); err != nil {
panic(err)
}
// if _, err := pl.pl_capture.Call(pl.store, pl.ptr); err != nil {
// return 0, wasmtime.NewTrap(err.Error())
// }
// if _, err := pl.pl_capture.Call(pl.ctx, uint64(pl.ptr)); err != nil {
// panic(err)
// }

return wasmTrue
}

func catch(pred Predicate, pl Prolog, subq Subquery, goal Term) (result Term) {
defer func() {
if threw := recover(); threw != nil {
switch ball := threw.(type) {
case Atom:
result = throwTerm(ball)
case Compound:
if ball.Functor == "throw" && len(ball.Args) == 1 {
result = ball
} else {
result = throwTerm(ball)
}
default:
result = throwTerm(
Atom("system_error").Of(
Atom("panic").Of(fmt.Sprint(threw)),
goal.(atomicTerm).pi(),
),
)
}
}
}()
result = pred(pl, subq, goal)
return
}

func hostResume(_, _, _ uint32) uint32 {
// extern int32_t host_resume(int32_t subquery, char **reply, size_t *reply_size);
return wasmFalse
}

var (
_ coroer = (*prolog)(nil)
_ coroer = (*lockedProlog)(nil)
)
Loading

0 comments on commit f85c9c9

Please sign in to comment.