-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
term.go
149 lines (139 loc) · 3.6 KB
/
term.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package pengine
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/ichiban/prolog/engine"
)
// Solution is a mapping of variable names to values.
//
// answers, err := Ask[Solution](ctx, "between(1,6,X)")
// // ...
// for as.Next() {
// cur := as.Current()
// // grab variables by name
// x := cur["X"]
// }
type Solution map[string]Term
// Term represents a Prolog term.
// One of the fields should be "truthy".
// This can be handy for parsing query results in JSON format.
type Term struct {
Atom *string
Number *json.Number
Compound *Compound
Variable *string
Boolean *bool
List []Term
Dictionary map[string]Term
Null bool
}
// UnmarshalJSON implements json.Unmarshaler.
func (t *Term) UnmarshalJSON(b []byte) error {
var v any
dec := json.NewDecoder(bytes.NewReader(b))
dec.UseNumber()
if err := dec.Decode(&v); err != nil {
return err
}
switch x := v.(type) {
case json.Number:
t.Number = &x
case string:
// TODO: need to figure out how to disambiguate var(_) / atom('_')
// maybe you can't? or need to use prolog instead of json format?
if x == "_" {
variable := x
t.Variable = &variable
} else {
atom := x
t.Atom = &atom
}
case bool:
boolean := x
t.Boolean = &boolean
case []any:
return json.Unmarshal(b, &t.List)
case map[string]any:
if _, ok := x["functor"]; ok {
return json.Unmarshal(b, &t.Compound)
}
rawDict := make(map[string]json.RawMessage, len(x))
if err := json.Unmarshal(b, &rawDict); err != nil {
return err
}
t.Dictionary = make(map[string]Term, len(rawDict))
for k, raw := range rawDict {
var v Term
if err := json.Unmarshal(raw, &v); err != nil {
return err
}
t.Dictionary[k] = v
}
case nil:
t.Null = true
default:
panic(fmt.Errorf("pengine: can't parse term of type %T", x))
}
return nil
}
// Prolog converts this term to an ichiban/prolog term.
// Because pengine's JSON format is lossy in terms of Prolog types, this might not always be accurate.
// There is ambiguity between atoms, strings, and variables.
// If you are mainly dealing with Prolog terms, use AskProlog to use the Prolog format instead.
func (t Term) Prolog() engine.Term {
switch {
case t.Atom != nil:
return engine.Atom(*t.Atom)
case t.Number != nil:
// TODO(guregu): fix/document/make optional the Int/Float detection.
nstr := string(*t.Number)
if strings.ContainsRune(nstr, '.') {
f, err := strconv.ParseFloat(nstr, 64)
if err != nil {
panic(err)
}
return engine.Float(f)
}
n, err := strconv.ParseInt(nstr, 10, 64)
if err != nil {
panic(err)
}
return engine.Integer(n)
case t.Compound != nil:
args := make([]engine.Term, 0, len(t.Compound.Args))
for _, arg := range t.Compound.Args {
args = append(args, arg.Prolog())
}
return engine.Atom(t.Compound.Functor).Apply(args...)
case t.Variable != nil:
// TODO(guregu): what should this be? engine.NewVariable? Is this even useful?
return engine.Variable(*t.Variable)
case t.Boolean != nil:
// TODO(guregu): use `@(true)` instead?
if *t.Boolean {
return engine.Atom("true")
} else {
return engine.Atom("false")
}
case t.List != nil:
list := make([]engine.Term, 0, len(t.List))
for _, member := range t.List {
list = append(list, member.Prolog())
}
return engine.List(list...)
case t.Null:
return engine.Atom("null") // TODO(guregu): use `@(null)`?
}
return nil
}
// Compound is a Prolog compound: functor(args0, args1, ...).
type Compound struct {
Functor string `json:"functor"`
Args []Term `json:"args"`
}
func escapeAtom(atom string) string {
return stringify(engine.Atom(atom))
}