upl 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/History.txt +5 -0
- data/README.md +71 -21
- data/Rakefile +6 -0
- data/lib/upl/dict.rb +4 -5
- data/lib/upl/extern.rb +14 -0
- data/lib/upl/foreign.rb +42 -27
- data/lib/upl/inter.rb +4 -17
- data/lib/upl/runtime.rb +121 -55
- data/lib/upl/term.rb +10 -13
- data/lib/upl/tree.rb +2 -0
- data/lib/upl/variable.rb +16 -3
- data/lib/upl/variables.rb +2 -1
- data/lib/upl/version.rb +1 -1
- data/lib/upl.rb +22 -19
- data/scratch/register_predicate.rb +1 -25
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bcfc0c63c88755686e526182cb7ec46e5d6c7781adfce9671e8f24bb254d023e
|
4
|
+
data.tar.gz: a366bb2532d7f3734b2d0b47854bf585c513509acdb0cc07882b313194efc476
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ce81943acf58d09751d44c643d33e1efa3089e4921886ddfde30e5148e607b0cbde69bafc132c6d613b11b1066667535dcbc4c73842535674a2945dbc17963ae
|
7
|
+
data.tar.gz: 51a14597a059f3d24fd30a88eb7406386074768afbf67b0fc9f7533059477018181583a6b2d56c205acf3f82ef9e6b4963ba423d93bb7343f128982ff3daaf13
|
data/History.txt
CHANGED
data/README.md
CHANGED
@@ -1,29 +1,35 @@
|
|
1
1
|
# Upl
|
2
2
|
|
3
|
-
|
3
|
+
Use SWI-Prolog from ruby.
|
4
4
|
|
5
|
-
|
6
|
-
wrapper give you back your answers in terms of the variable names you specified.
|
7
|
-
Just like what happens in your common-or-garden prolog REPL. But with pry's
|
8
|
-
syntax colouring and pretty-printing :-)
|
5
|
+
prolog statements can be specified as strings or as a ruby DSL.
|
9
6
|
|
10
|
-
|
7
|
+
Define foreign predicates in ruby, so prolog can call back into ruby code.
|
8
|
+
|
9
|
+
Assert facts containing ruby objects, so prolog can query ruby data by calling ruby methods.
|
11
10
|
|
12
11
|
## Tutorial
|
13
12
|
|
13
|
+
The query api always returns an ```Enumerable``` of all values which satisfy the query.
|
14
|
+
|
14
15
|
### Queries
|
15
16
|
|
16
17
|
Query a built-in predicate, with a full expression:
|
18
|
+
|
17
19
|
``` ruby
|
18
|
-
[1] pry(main)> enum = Upl.query
|
20
|
+
[1] pry(main)> enum = Upl.query <<~prolog
|
21
|
+
member(K,[home,executable,shared_object_extension]),
|
22
|
+
current_prolog_flag(K,V)
|
23
|
+
prolog
|
19
24
|
=> #<Enumerator: ...>
|
20
25
|
[2] pry(main) enum.to_a
|
21
|
-
=> [{:K
|
22
|
-
|
23
|
-
|
26
|
+
=> [{:K=>:executable, :V=>:upl},
|
27
|
+
{:K=>:home, :V=>:"/usr/lib64/swipl"},
|
28
|
+
{:K=>:shared_object_extension, :V=>:so}]
|
24
29
|
```
|
25
30
|
|
26
31
|
To read rules from a prolog file:
|
32
|
+
|
27
33
|
``` ruby
|
28
34
|
[1] pry(main)> Upl.consult '/home/yours/funky_data.pl'
|
29
35
|
=> true
|
@@ -51,7 +57,7 @@ false.
|
|
51
57
|
And in Upl:
|
52
58
|
|
53
59
|
``` ruby
|
54
|
-
[1] pry(main)> fact = Upl::Term
|
60
|
+
[1] pry(main)> fact = Upl::Term :person, :john, :anderson
|
55
61
|
=> person/2(john,anderson)
|
56
62
|
[2] pry(main)> Upl.assertz fact
|
57
63
|
=> true
|
@@ -69,7 +75,7 @@ Also, with objects other than symbols. Obviously, this is a rabbit-hole of
|
|
69
75
|
Alician proportions. So, here we GOOOoooo...
|
70
76
|
|
71
77
|
``` ruby
|
72
|
-
[1] pry(main)> fact = Upl::Term
|
78
|
+
[1] pry(main)> fact = Upl::Term :person, :john, :anderson, (o = Object.new)
|
73
79
|
=> person/3(john,anderson,#<Object:0x0000563346a08e38 @_upl_atom=439429>)
|
74
80
|
[2] pry(main)> Upl.assertz fact
|
75
81
|
=> true
|
@@ -87,10 +93,10 @@ much wisdom. Hurhur. And at least one extra instance variable.
|
|
87
93
|
And now, the pièce de résistance - using an object as an input term:
|
88
94
|
|
89
95
|
``` ruby
|
90
|
-
fact = Upl::Term
|
96
|
+
fact = Upl::Term :person, :james, :madison, (o = Object.new)
|
91
97
|
Upl.assertz fact
|
92
98
|
|
93
|
-
fact2 = Upl::Term
|
99
|
+
fact2 = Upl::Term :person, :thomas, :paine, (thing2 = Object.new)
|
94
100
|
Upl.assertz fact2
|
95
101
|
|
96
102
|
# Note that both facts are in the result and the values for C are different
|
@@ -111,31 +117,75 @@ Array Upl::Runtime.term_vars_query query_term, query_vars
|
|
111
117
|
=> [{:A=>thomas, :B=>paine, :C=>#<Object:0x0000563f56d2b5b8 @_upl_atom=439813>}]
|
112
118
|
```
|
113
119
|
|
114
|
-
### Ruby
|
120
|
+
### Ruby Methods
|
121
|
+
You can call methods on ruby objects from prolog using the ```mcall(+Object, +Method, -Result)``` predicate:
|
115
122
|
|
116
|
-
You can define predicates in ruby.
|
117
123
|
``` ruby
|
124
|
+
def (obj = Object.new).voicemail
|
125
|
+
"Hey. For your logs."
|
126
|
+
end
|
127
|
+
|
128
|
+
query_term, query_vars = Upl::Runtime.term_vars "mcall(O,voicemail,St),string_codes(St,Co)"
|
129
|
+
query_vars.O = obj
|
130
|
+
Array Upl::Runtime.term_vars_query query_term, query_vars
|
131
|
+
=> [{:O=>#<Object:0x00005610453b0528 @_upl_atom=495109>,
|
132
|
+
:St=>"Hey. For your logs.",
|
133
|
+
:Co=>[72, 101, 121, 46, 32, 70, 111, 114, 32, 121, 111, 117, 114, 32, 108, 111, 103, 115, 46]}]
|
134
|
+
```
|
135
|
+
|
136
|
+
### Ruby Predicates
|
137
|
+
|
138
|
+
You can define predicates in ruby:
|
118
139
|
|
140
|
+
``` ruby
|
141
|
+
Upl::Foreign.register_semidet :special_concat do |arg0, arg1|
|
142
|
+
arg1 === "#{arg0}-special"
|
143
|
+
end
|
119
144
|
|
145
|
+
Array Upl.query 'special_concat(hello, A)'
|
146
|
+
=> [{:A=>"hello-special"}]
|
120
147
|
```
|
121
148
|
|
122
|
-
|
149
|
+
Some notes:
|
150
|
+
|
151
|
+
* ```===``` means unify
|
152
|
+
|
153
|
+
* return value from the register_semidet block will be treated as a ruby
|
154
|
+
truthy/falsy value and converted to a prolog true/false, that is as
|
155
|
+
success/failure. So you have to be careful here because, for example, returning nil
|
156
|
+
would be interpreted by prolog to mean failure, and you will get no results. Or conversely: returning true for a series of unifications where only the last succeeded would lead to incorrect results.
|
157
|
+
|
158
|
+
So you now you can define a query in prolog that searches a ruby object graph.
|
159
|
+
|
160
|
+
## Limitations
|
161
|
+
|
162
|
+
Although this is in-development code, I do use it for real work. For example, driving an address DCG on 50,000 addresses. Memory usage was stable.
|
123
163
|
|
124
|
-
|
164
|
+
It might be useful for you too.
|
125
165
|
|
126
|
-
|
166
|
+
### Specifically
|
167
|
+
|
168
|
+
You cannot talk to swipl from a Thread other than ```Thread::main```. See https://www.swi-prolog.org/pldoc/man?section=foreignthread
|
169
|
+
|
170
|
+
I've used it to drive an address DCG on 50,000 addresses. Memory usage was stable.
|
171
|
+
|
172
|
+
You cannot talk to swipl from a Thread other than ```Thread::main```
|
127
173
|
|
128
174
|
ruby has a GC. swipl has a GC. At some point they will disagree. I haven't reached that point yet.
|
129
175
|
|
176
|
+
UTF8-passthrough is not implemented, but there's a good chance you'll get what you want with the help of ```String#force_encoding('UTF-8')```.
|
177
|
+
|
178
|
+
There is not yet a way to register nondet predicates in ruby.
|
179
|
+
|
130
180
|
## Naming
|
131
181
|
|
132
182
|
```Upl```? Wat!? Why?
|
133
183
|
|
134
|
-
Well. ```swipl``` was taken. ```ripl``` was taken. So maybe in keeping with long tradition: ```rupl```.
|
184
|
+
Well. [```swipl```](https://github.com/meschbach/gem-swipl) was taken for the ```swipl``` gem which does some of what this does. ```ripl``` was taken. So maybe in keeping with long tradition: ```rupl```.
|
135
185
|
|
136
186
|
But that leads to ```pry -I. -rrupl``` which is Not Fun.
|
137
187
|
|
138
|
-
But ```upl``` gives you ```pry -I. -rupl``` So it's kinda like ```ubygems
|
188
|
+
But ```upl``` gives you ```pry -I. -rupl``` So it's kinda like ```ubygems``` used to be.
|
139
189
|
|
140
190
|
Also, ```Upl``` rhymes with tuple and/or supple. Depending on your pronunciation :-p
|
141
191
|
|
data/Rakefile
CHANGED
data/lib/upl/dict.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
module Upl
|
2
2
|
class Dict < Hash
|
3
|
-
def initialize( tag, default_value
|
3
|
+
def initialize( tag: nil, values: nil, default_value: nil, &default_blk )
|
4
4
|
@tag = tag
|
5
5
|
super default_value, &default_blk
|
6
|
+
replace values if values
|
6
7
|
end
|
7
8
|
|
8
9
|
# fetch the tag for the dict
|
@@ -27,9 +28,7 @@ module Upl
|
|
27
28
|
# copy dict_term_t into a ruby structure
|
28
29
|
def self.of_term( dict_term_t )
|
29
30
|
# Have to do a little hoop-jumping here. There are no c-level calls to
|
30
|
-
# access dicts, so we have to break them down with prolog predicates.
|
31
|
-
# we can't process queries that have dicts in their results, otherwise we
|
32
|
-
# have an endless recursion.
|
31
|
+
# access dicts, so we have to break them down with prolog predicates.
|
33
32
|
|
34
33
|
query_term, query_hash = Runtime.term_vars 'get_dict(K,Dict,V)'
|
35
34
|
|
@@ -42,7 +41,7 @@ module Upl
|
|
42
41
|
en = Upl::Runtime.term_vars_query query_term, query_hash
|
43
42
|
|
44
43
|
# map to a hash-y thing
|
45
|
-
en.each_with_object Dict.new(dict_tag
|
44
|
+
en.each_with_object Dict.new(tag: dict_tag(dict_term_t)) do |row,values|
|
46
45
|
values[row[:K]] = row[:V]
|
47
46
|
end
|
48
47
|
end
|
data/lib/upl/extern.rb
CHANGED
@@ -3,6 +3,7 @@ require 'fiddle/import'
|
|
3
3
|
require 'pathname'
|
4
4
|
|
5
5
|
module Upl
|
6
|
+
# ffi bindings for swipl.so
|
6
7
|
module Extern
|
7
8
|
extend Fiddle::Importer
|
8
9
|
|
@@ -72,6 +73,18 @@ module Upl
|
|
72
73
|
PL_Q_DETERMINISTIC = 0x0100 # call was deterministic
|
73
74
|
end
|
74
75
|
|
76
|
+
|
77
|
+
module ExtStatus
|
78
|
+
# PL_Q_EXT_STATUS return codes
|
79
|
+
# yes, there are some duplicates here from TRUE/FALSE
|
80
|
+
# but that's how it is in the header file
|
81
|
+
EXCEPTION = -1 # Query raised exception
|
82
|
+
FALSE = 0 # Query failed
|
83
|
+
TRUE = 1 # Query succeeded with choicepoint
|
84
|
+
LAST = 2 # Query succeeded without CP
|
85
|
+
end
|
86
|
+
|
87
|
+
|
75
88
|
extern 'fid_t PL_open_foreign_frame(void)'
|
76
89
|
extern 'void PL_rewind_foreign_frame(fid_t cid)'
|
77
90
|
extern 'void PL_close_foreign_frame(fid_t cid)'
|
@@ -221,6 +234,7 @@ module Upl
|
|
221
234
|
extern 'int PL_unify_arg(int index, term_t t, term_t a)' # set index-th arg of t to a
|
222
235
|
|
223
236
|
extern 'int PL_get_atom_chars(term_t t, char **a)'
|
237
|
+
# TODO deprecated, use something else
|
224
238
|
extern 'int PL_get_string(term_t t, char **s, size_t *len)'
|
225
239
|
extern 'int PL_get_integer(term_t t, int *i)'
|
226
240
|
extern 'int PL_get_int64(term_t t, int64_t *i)'
|
data/lib/upl/foreign.rb
CHANGED
@@ -1,50 +1,65 @@
|
|
1
1
|
module Upl
|
2
2
|
# Register a foreign predicate in prolog
|
3
|
-
# Upl::Extern.PL_register_foreign_in_module(char *mod, char *name, int arity, foreign_t (*f)(), int flags, ...)
|
4
|
-
|
5
|
-
# create the foreign predicate as a class in ruby
|
6
|
-
#
|
7
|
-
# closure = Class.new(Fiddle::Closure) {
|
8
|
-
# def call
|
9
|
-
# 10
|
10
|
-
# end
|
11
|
-
# }.new(Fiddle::TYPE_INT, [])
|
12
|
-
#
|
13
|
-
# func = Fiddle::Function.new(closure, [], Fiddle::TYPE_INT)
|
14
|
-
|
15
|
-
# create the foreign predicate as a block in ruby
|
16
|
-
#
|
17
|
-
# new(ctype, args, abi = Fiddle::Function::DEFAULT, &block)
|
18
|
-
# cb = Closure::BlockCaller.new(TYPE_INT, [TYPE_INT]) do |one|
|
19
|
-
# one
|
20
|
-
# end
|
21
3
|
#
|
22
|
-
#
|
23
|
-
|
4
|
+
# see https://www.swi-prolog.org/pldoc/man?section=modes
|
5
|
+
# for meanings of det, semidet, nondet
|
24
6
|
module Foreign
|
25
7
|
def self.predicates
|
26
8
|
@predicates ||= Hash.new
|
27
9
|
end
|
28
10
|
|
29
|
-
|
11
|
+
# name is the predicate name as called prolog
|
12
|
+
# mod is the prolog module name
|
13
|
+
# arity will be figured out from the blk unless you specify it
|
14
|
+
# blk will be called with Term and Tree instances, depending on
|
15
|
+
# ground-ness of the term on the prolog end of the call
|
16
|
+
def self.register_semidet name, arity = nil, mod: nil, &blk
|
30
17
|
arity ||= blk.arity
|
31
18
|
arg_types = arity.times.map{Fiddle::TYPE_VOIDP}
|
32
19
|
ruby_pred = Fiddle::Closure::BlockCaller.new Fiddle::TYPE_INT, arg_types do |*args|
|
33
|
-
|
20
|
+
# convert args to Upl::Tree or Upl::Variable instances
|
21
|
+
ruby_args = args.map do |term_t|
|
22
|
+
case term_t.term_type
|
23
|
+
when Extern::PL_VARIABLE
|
24
|
+
# TODO how to make sure this variable does not outlive the swipl frame?
|
25
|
+
Upl::Variable.new term_t
|
26
|
+
else
|
27
|
+
Upl::Tree.of_term term_t
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# now do the call and convert to swipl
|
32
|
+
case (rv = blk.call *ruby_args)
|
34
33
|
when true; Upl::Extern::TRUE
|
35
34
|
when false, NilClass; Upl::Extern::FALSE
|
35
|
+
|
36
|
+
# Upl::Extern::(TRUE, FALSE)
|
36
37
|
when 0, 1; rv
|
37
38
|
else Upl::Extern::TRUE
|
38
39
|
end
|
39
|
-
|
40
|
-
|
41
|
-
|
40
|
+
|
41
|
+
# yes, really catch all exceptions because this is the language boundary
|
42
|
+
rescue Exception => ex
|
43
|
+
# pass the actual exception object through prolog
|
44
|
+
# ultimately gets handled by Runtime.query and friends.
|
45
|
+
term = Upl::Term :ruby_error, ex
|
46
|
+
Extern::PL_raise_exception term.to_term_t
|
42
47
|
end
|
43
48
|
|
44
|
-
|
49
|
+
mod_ptr = Fiddle::Pointer[mod&.to_s || 0]
|
45
50
|
|
46
51
|
fn = Fiddle::Function.new ruby_pred, arg_types, Fiddle::TYPE_INT
|
47
|
-
|
52
|
+
|
53
|
+
# int PL_register_foreign_in_module(char *mod, char *name, int arity, foreign_t (*f)(), int flags, ...)
|
54
|
+
# flags == 0 for semidet, ie only one 'output' value and no need for retry
|
55
|
+
# https://www.swi-prolog.org/pldoc/doc_for?object=c(%27PL_register_foreign_in_module%27)
|
56
|
+
rv = Upl::Extern.PL_register_foreign_in_module \
|
57
|
+
mod_ptr,
|
58
|
+
name.to_s,
|
59
|
+
arity,
|
60
|
+
fn,
|
61
|
+
(flags=0)
|
62
|
+
|
48
63
|
rv == 1 or raise "can't register ruby predicate #{name}/#{arity}"
|
49
64
|
|
50
65
|
# NOTE you have to keep ruby_pred and fn around somewhere, otherwise they
|
data/lib/upl/inter.rb
CHANGED
@@ -2,6 +2,7 @@ require_relative 'extern'
|
|
2
2
|
require_relative 'foreign'
|
3
3
|
|
4
4
|
module Upl
|
5
|
+
# Inter operation
|
5
6
|
module Inter
|
6
7
|
# Try Term, then Fiddle::Pointer, then to_term_t.
|
7
8
|
# Return a term_t pointer
|
@@ -18,15 +19,12 @@ module Upl
|
|
18
19
|
|
19
20
|
# call any method on any object, from prolog
|
20
21
|
def self.register_mcall_predicate
|
21
|
-
Upl::Foreign.register_semidet :mcall do |
|
22
|
-
|
23
|
-
meth = Upl::Tree.of_term meth_term_t
|
24
|
-
v = obj.send meth
|
25
|
-
|
26
|
-
Upl::Extern::PL_unify v_term_t, v.to_term_t
|
22
|
+
Upl::Foreign.register_semidet :mcall do |obj,meth,val|
|
23
|
+
val === obj.send(meth)
|
27
24
|
end
|
28
25
|
end
|
29
26
|
|
27
|
+
# mcall(+Object, +Method, -Result)
|
30
28
|
register_mcall_predicate
|
31
29
|
|
32
30
|
# lst_term is a Term, or a Fiddle::Pointer to term_t
|
@@ -87,17 +85,6 @@ module Upl
|
|
87
85
|
# returns old fn ptr
|
88
86
|
Upl::Extern.PL_agc_hook @atom_hook_fn
|
89
87
|
end
|
90
|
-
|
91
|
-
def register_mcall_predicate
|
92
|
-
Upl::Foreign.register_semidet :mcall do |obj_term_t,meth_term_t,v_term_t|
|
93
|
-
obj = Upl::Tree.of_term obj_term_t
|
94
|
-
meth = Upl::Tree.of_term meth_term_t
|
95
|
-
v = obj.send meth
|
96
|
-
|
97
|
-
Upl::Extern::PL_unify v_term_t, v.to_term_t
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
88
|
end
|
102
89
|
end
|
103
90
|
|
data/lib/upl/runtime.rb
CHANGED
@@ -17,6 +17,21 @@ module Upl
|
|
17
17
|
module Runtime
|
18
18
|
Ptr = Fiddle::Pointer
|
19
19
|
|
20
|
+
class PrologException < RuntimeError
|
21
|
+
def initialize(term_tree)
|
22
|
+
@term_tree = term_tree
|
23
|
+
end
|
24
|
+
|
25
|
+
def message
|
26
|
+
@message ||= begin
|
27
|
+
# TODO need to use print_message_lines/3 to generate this string
|
28
|
+
pp = PP.new
|
29
|
+
@term_tree.args.each{|arg| arg.pretty_print pp}
|
30
|
+
"#{@term_tree.atom}: #{pp.output}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
20
35
|
def self.call st_or_term
|
21
36
|
term =
|
22
37
|
case st_or_term
|
@@ -62,20 +77,36 @@ module Upl
|
|
62
77
|
end
|
63
78
|
|
64
79
|
# once_only. Should probably be a singleton or something.
|
65
|
-
|
80
|
+
Thread::current[:upl] ||= init
|
66
81
|
|
67
82
|
def self.predicate name, arity, module_name = nil
|
68
83
|
Extern.PL_predicate Fiddle::Pointer[name.to_s], arity, NULL
|
69
84
|
end
|
70
85
|
|
71
|
-
|
72
|
-
|
73
|
-
|
86
|
+
def self.unify( term_a, term_b )
|
87
|
+
rv = Extern::PL_unify term_a.term_t, term_a.term_t
|
88
|
+
rv == 1 or raise "can't unify #{term_a} and #{term_b}"
|
89
|
+
end
|
90
|
+
|
91
|
+
# blk takes a fid_t
|
92
|
+
def self.with_frame &blk
|
93
|
+
fid_t = Extern.PL_open_foreign_frame
|
94
|
+
yield fid_t
|
95
|
+
ensure
|
96
|
+
fid_t and Extern.PL_close_foreign_frame fid_t
|
97
|
+
end
|
98
|
+
|
99
|
+
# Use prolog predicate to parse the string into a term (containing variables), along with its named
|
100
|
+
# variables as a hash of Name => _variable
|
101
|
+
#
|
102
|
+
# TODO need to use read_term_from_atom('pred(A,B,C)', Term, [variable_names(VarNames)]).
|
103
|
+
# remember Atom can also be a string for swipl
|
74
104
|
def self.term_vars st
|
75
105
|
rv = Extern::PL_call_predicate \
|
76
106
|
Extern::NULL, # module
|
77
107
|
0, # flags, see PL_open_query
|
78
108
|
(predicate 'atom_to_term', 3),
|
109
|
+
# 3 variables, first one determined
|
79
110
|
(args = TermVector[st.to_sym, nil, nil]).terms
|
80
111
|
|
81
112
|
vars = Inter.each_of_list(args[2]).each_with_object Variables.new do |term_t, vars|
|
@@ -84,51 +115,74 @@ module Upl
|
|
84
115
|
vars.store t.first.atom.to_sym, (Variable.new t.last.term_t, name: t.first.atom.to_sym)
|
85
116
|
end
|
86
117
|
|
118
|
+
# return term, {name => var...}
|
87
119
|
return args[1], vars
|
88
120
|
end
|
89
121
|
|
90
|
-
|
91
|
-
|
92
|
-
|
122
|
+
# just to make sure the query handle pointer is properly closed
|
123
|
+
# TODO should be private, because args are gnarly
|
124
|
+
def self.open_query qterm, args, mod: nil, flags: nil, &blk
|
125
|
+
# This will need a string for the module, eventually
|
126
|
+
# module is NULL, flags is 0
|
127
|
+
mod ||= Extern::NULL
|
128
|
+
flags ||= flags=Extern::Flags::PL_Q_EXT_STATUS | Extern::Flags::PL_Q_CATCH_EXCEPTION
|
129
|
+
|
130
|
+
query_id_p = Extern.PL_open_query mod, flags, qterm.to_predicate, args.terms
|
131
|
+
query_id_p != 0 or raise 'no space on environment stack, see SWI-Prolog docs for PL_open_query'
|
132
|
+
|
133
|
+
yield query_id_p
|
134
|
+
ensure
|
135
|
+
query_id_p&.to_i and Extern.PL_close_query query_id_p
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.raise_prolog_or_ruby query_id_p
|
139
|
+
tree = Tree.of_term Extern::PL_exception(query_id_p)
|
140
|
+
|
141
|
+
case tree.atom.to_ruby
|
142
|
+
# special case for errors that originated inside a predicate
|
143
|
+
# that was defined in ruby.
|
144
|
+
when :ruby_error
|
145
|
+
# re-raise the actual exception object from the predicate
|
146
|
+
raise tree.args.first
|
147
|
+
else
|
148
|
+
raise PrologException, tree
|
149
|
+
end
|
93
150
|
end
|
94
151
|
|
95
152
|
# do a query for the given term and vars, as parsed by term_vars
|
96
153
|
# qvars_hash is a hash of :VariableName => Term(PL_VARIABLE)
|
154
|
+
# and each variable is already bound in term.
|
155
|
+
# TODO much duplication between this and .query below
|
97
156
|
def self.term_vars_query qterm, qvars_hash
|
98
157
|
raise "not a term" unless Term === qterm
|
99
158
|
return enum_for __method__, qterm, qvars_hash unless block_given?
|
100
159
|
|
101
|
-
|
102
|
-
|
103
|
-
begin
|
160
|
+
with_frame do |fid_t|
|
104
161
|
# populate input values from qterm
|
105
162
|
args = TermVector.new qterm.arity do |idx| qterm[idx] end
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
163
|
+
open_query qterm, args do |query_id_p|
|
164
|
+
loop do
|
165
|
+
case Extern.PL_next_solution query_id_p
|
166
|
+
when Extern::ExtStatus::FALSE
|
167
|
+
break
|
168
|
+
|
169
|
+
when Extern::ExtStatus::EXCEPTION
|
170
|
+
raise_prolog_or_ruby query_id_p
|
171
|
+
|
172
|
+
# when Extern::ExtStatus::TRUE
|
173
|
+
# when Extern::ExtStatus::LAST
|
174
|
+
else
|
175
|
+
hash = qvars_hash.each_with_object Hash.new do |(name_sym,var),ha|
|
176
|
+
# var will be invalidated by the next call to PL_next_solution,
|
177
|
+
# so we need to construct a ruby tree copy of the value term.
|
178
|
+
ha[name_sym] = var.to_ruby
|
179
|
+
end
|
180
|
+
|
181
|
+
yield hash
|
182
|
+
end
|
120
183
|
end
|
121
|
-
|
122
|
-
yield hash
|
123
184
|
end
|
124
|
-
|
125
|
-
ensure
|
126
|
-
query_id_p&.to_i and Extern.PL_close_query query_id_p
|
127
185
|
end
|
128
|
-
|
129
|
-
ensure
|
130
|
-
# this also gets called after enum_for, so test for fid_t
|
131
|
-
fid_t and Extern.PL_close_foreign_frame fid_t
|
132
186
|
end
|
133
187
|
|
134
188
|
def self.predicate name, arity
|
@@ -137,42 +191,54 @@ module Upl
|
|
137
191
|
|
138
192
|
# Simple query with predicate / arity
|
139
193
|
# Returns an array of arrays.
|
194
|
+
# TODO remove, not really used
|
140
195
|
def self.squery predicate_str, arity
|
141
196
|
return enum_for :squery, predicate_str, arity unless block_given?
|
142
197
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
rv = Extern.PL_next_solution query_id_p
|
151
|
-
break if rv == 0
|
152
|
-
yield answer_lst.each_t.map{|term_t| Tree.of_term term_t}
|
198
|
+
# bit of a hack because open_query wants to call to_predicate
|
199
|
+
# and we have to construct that manually here because Upl::Term
|
200
|
+
# is slightly ill-suited.
|
201
|
+
qterm = Object.new
|
202
|
+
qterm.define_singleton_method :to_predicate do
|
203
|
+
p_functor = Extern::PL_new_functor predicate_str.to_sym.to_atom, arity
|
204
|
+
Extern::PL_pred p_functor, Extern::NULL
|
153
205
|
end
|
154
206
|
|
155
|
-
|
156
|
-
|
157
|
-
|
207
|
+
args = TermVector.new arity
|
208
|
+
open_query qterm, args do |query_id_p|
|
209
|
+
loop do
|
210
|
+
rv = Extern.PL_next_solution query_id_p
|
211
|
+
break if rv == 0
|
212
|
+
yield args.each_t.map{|term_t| Tree.of_term term_t}
|
213
|
+
end
|
214
|
+
end
|
158
215
|
end
|
159
216
|
|
217
|
+
# TODO much duplication between this and .term_vars_query
|
218
|
+
# Only used by the term branch of Upl.query
|
160
219
|
def self.query term
|
161
220
|
raise "not a Term" unless Term === term
|
162
221
|
return enum_for :query, term unless block_given?
|
163
222
|
|
164
223
|
answer_lst = TermVector.new term.arity do |idx| term[idx] end
|
165
|
-
query_id_p = Extern.PL_open_query Extern::NULL, 0, term.to_predicate, answer_lst.terms
|
166
224
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
225
|
+
open_query term, answer_lst do |query_id_p|
|
226
|
+
loop do
|
227
|
+
case Extern.PL_next_solution query_id_p
|
228
|
+
when Extern::ExtStatus::FALSE
|
229
|
+
break
|
172
230
|
|
173
|
-
|
174
|
-
|
175
|
-
|
231
|
+
when Extern::ExtStatus::EXCEPTION
|
232
|
+
raise_prolog_or_ruby query_id_p
|
233
|
+
|
234
|
+
# when Extern::ExtStatus::TRUE
|
235
|
+
# when Extern::ExtStatus::LAST
|
236
|
+
else
|
237
|
+
yield answer_lst.each_t.map{|term_t| Tree.of_term term_t}
|
238
|
+
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
176
242
|
end
|
177
243
|
end
|
178
244
|
end
|
data/lib/upl/term.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
module Upl
|
2
|
-
#
|
3
|
-
|
4
|
-
# OK, so I guess this thing's job is interacting with term_t, whereas Tree's
|
2
|
+
# This thing's job is interacting with term_t, whereas Tree's
|
5
3
|
# job is being a ruby copy of a term-tree.
|
6
4
|
class Term
|
7
5
|
def initialize term_or_string
|
@@ -30,18 +28,17 @@ module Upl
|
|
30
28
|
term_t
|
31
29
|
end
|
32
30
|
|
31
|
+
# returns a term
|
32
|
+
#
|
33
33
|
# args are things that can be converted to term_t pointers using to_term_t method
|
34
|
-
|
34
|
+
# TODO misnamed. functor means pred_name/n and this is actually
|
35
|
+
# :pred_name, arg1, arg2...
|
36
|
+
def self.predicate name, *args
|
35
37
|
# TODO maybe use a frame or something because this allocates quite a few sub-terms
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
Extern::PL_unify (arg_terms+idx), arg.to_term_t
|
41
|
-
end
|
42
|
-
|
43
|
-
term_t = Extern.PL_new_term_ref
|
44
|
-
rv = Extern.PL_cons_functor_v term_t, functor_t, arg_terms
|
38
|
+
rv = Extern.PL_cons_functor_v \
|
39
|
+
(term_t = Extern.PL_new_term_ref),
|
40
|
+
Extern.PL_new_functor(name.to_sym.to_atom, args.size),
|
41
|
+
TermVector[*args].terms
|
45
42
|
rv == 1 or raise "can't populate functor #{name}"
|
46
43
|
|
47
44
|
new term_t
|
data/lib/upl/tree.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
module Upl
|
2
|
+
# So Prolog terms are a rose tree. Who woulda thunkit?
|
3
|
+
#
|
2
4
|
# Convert a term into a tree of ruby objects. This is necessary because
|
3
5
|
# queries give back their results as terms which are invalidated as soon as
|
4
6
|
# the next set of results is calculated. So we need to turn those terms into a
|
data/lib/upl/variable.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Upl
|
2
|
-
#
|
2
|
+
# A variable, either from an existing term_t or a new one
|
3
3
|
class Variable
|
4
4
|
def initialize term_t = nil, name: nil
|
5
5
|
@term_t = term_t || self.class.to_term
|
@@ -21,6 +21,16 @@ module Upl
|
|
21
21
|
inst
|
22
22
|
end
|
23
23
|
|
24
|
+
def unify value
|
25
|
+
warn "don't pass a term_t in here" if Fiddle::Pointer === value
|
26
|
+
case Upl::Extern::PL_unify term_t, value.to_term_t
|
27
|
+
when Upl::Extern::TRUE; true
|
28
|
+
when Upl::Extern::FALSE; false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def === value; unify value end
|
33
|
+
|
24
34
|
# bit of a hack to create empty variables for a functor
|
25
35
|
def self.to_term
|
26
36
|
Extern.PL_new_term_ref
|
@@ -35,12 +45,15 @@ module Upl
|
|
35
45
|
|
36
46
|
def _string
|
37
47
|
@_string ||= begin
|
38
|
-
Extern::PL_get_chars \
|
48
|
+
rv = Extern::PL_get_chars \
|
39
49
|
term_t,
|
40
50
|
(str_ref = Runtime::Ptr[0].ref),
|
41
|
-
|
51
|
+
# need cvt_variable for normal variables, and cvt_write for clpfd variables
|
52
|
+
Extern::Convert::CVT_VARIABLE | Extern::Convert::CVT_WRITE | Extern::Convert::REP_UTF8 | Extern::Convert::BUF_MALLOC
|
53
|
+
# | Extern::CVT_ALL
|
42
54
|
|
43
55
|
str_ref.ptr.free = Runtime.swipl_free_fn
|
56
|
+
# TODO might need to force utf8 encoding here?
|
44
57
|
str_ref.ptr.to_s
|
45
58
|
end
|
46
59
|
end
|
data/lib/upl/variables.rb
CHANGED
data/lib/upl/version.rb
CHANGED
data/lib/upl.rb
CHANGED
@@ -14,12 +14,15 @@ require_relative 'upl/term_vector'
|
|
14
14
|
require_relative 'upl/foreign'
|
15
15
|
|
16
16
|
module Upl
|
17
|
+
# an enumerator yielding hashes keyed by the variables, mapping to the term
|
17
18
|
module_function def query string_or_term, vars = nil, &blk
|
18
19
|
if string_or_term.is_a?(Term) && vars
|
19
20
|
Runtime.term_vars_query string_or_term, vars
|
20
21
|
else
|
21
22
|
case string_or_term
|
22
23
|
when Term
|
24
|
+
# TODO this returns an array of values without variable names.
|
25
|
+
# So it doesn't really belong here.
|
23
26
|
Runtime.query string_or_term
|
24
27
|
when String
|
25
28
|
term, vars = Runtime.term_vars string_or_term
|
@@ -35,23 +38,6 @@ module Upl
|
|
35
38
|
Runtime::call %Q{["#{p.realpath.to_s}"]}
|
36
39
|
end
|
37
40
|
|
38
|
-
module_function def asserta term
|
39
|
-
Runtime.call Term.functor :asserta, term
|
40
|
-
end
|
41
|
-
|
42
|
-
module_function def assertz term
|
43
|
-
Runtime.call Term.functor :assertz, term
|
44
|
-
end
|
45
|
-
|
46
|
-
# behaves as if run under once, cos of the way call works
|
47
|
-
module_function def retract term
|
48
|
-
Runtime.call Term.functor :retract, term
|
49
|
-
end
|
50
|
-
|
51
|
-
def self.listing
|
52
|
-
(Upl.query 'with_output_to(string(Buffer),listing)').first[:Buffer]
|
53
|
-
end
|
54
|
-
|
55
41
|
# Nicer syntax for Term.functor. Construct a Term from a symbol and args that
|
56
42
|
# all respond to 'to_term_t'.
|
57
43
|
#
|
@@ -59,11 +45,28 @@ module Upl
|
|
59
45
|
#
|
60
46
|
# Upl.query 'current_prolog_flag(A,B)'
|
61
47
|
#
|
62
|
-
# is
|
48
|
+
# is similar to
|
63
49
|
#
|
64
50
|
# Upl.query Term :current_prolog_flag, Variable.new, Variable.new
|
65
51
|
#
|
66
52
|
module_function def Term name, *args
|
67
|
-
Term.
|
53
|
+
Term.predicate name, *args
|
54
|
+
end
|
55
|
+
|
56
|
+
module_function def asserta term
|
57
|
+
Runtime.call Term :asserta, term
|
58
|
+
end
|
59
|
+
|
60
|
+
module_function def assertz term
|
61
|
+
Runtime.call Term :assertz, term
|
62
|
+
end
|
63
|
+
|
64
|
+
# behaves as if run under once, cos of the way call works
|
65
|
+
module_function def retract term
|
66
|
+
Runtime.call Term :retract, term
|
67
|
+
end
|
68
|
+
|
69
|
+
module_function def listing
|
70
|
+
(Upl.query 'with_output_to(string(Buffer),listing)').first[:Buffer]
|
68
71
|
end
|
69
72
|
end
|
@@ -1,29 +1,5 @@
|
|
1
|
-
def register_test_predicate
|
2
|
-
Upl::Foreign.register_semidet :upl_block do |term_t0, term_t1|
|
3
|
-
fterm = (Upl::Tree.of_term term_t0)
|
4
|
-
p foreign: fterm
|
5
|
-
|
6
|
-
if Symbol === fterm then
|
7
|
-
Upl::Extern::PL_unify term_t1, :there.to_term_t
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
Array Upl.query "upl_block(hello,A)"
|
12
|
-
end
|
13
|
-
|
14
|
-
=begin
|
15
|
-
include UPL
|
16
|
-
vars = Variables.new :V
|
17
|
-
term = Term :mcall, (o = Object.new), :to_s, vars.V
|
18
|
-
def o.to_s; "This is from Ruby, with Love :-D"; end
|
19
|
-
Array Runtime.term_vars_query term, vars
|
20
|
-
=> [{:V=>"This is from Ruby, with Love :-D"}]
|
21
|
-
|
22
|
-
mcall(+Object, +Method, -Result)
|
23
|
-
=end
|
24
|
-
|
25
1
|
def doit
|
26
|
-
fact = Upl::Term
|
2
|
+
fact = Upl::Term :person, :john, :anderson, Object.new
|
27
3
|
Upl.assertz fact
|
28
4
|
vs = Array Upl.query 'person(A,B,C)'
|
29
5
|
p vs
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: upl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Anderson
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-02-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -133,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
133
133
|
- !ruby/object:Gem::Version
|
134
134
|
version: '0'
|
135
135
|
requirements: []
|
136
|
-
rubygems_version: 3.
|
136
|
+
rubygems_version: 3.1.2
|
137
137
|
signing_key:
|
138
138
|
specification_version: 4
|
139
139
|
summary: Access SWI-Prolog engine from ruby
|