upl 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|