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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74895ae80e02af92445e7d4fb47413dbb67b0996ced75014b250c18bba5bb444
4
- data.tar.gz: 7288b2b9b91bb4a7127ff05341ccb053d5a159e7dcdd70537d4d6a21cfc186bb
3
+ metadata.gz: bcfc0c63c88755686e526182cb7ec46e5d6c7781adfce9671e8f24bb254d023e
4
+ data.tar.gz: a366bb2532d7f3734b2d0b47854bf585c513509acdb0cc07882b313194efc476
5
5
  SHA512:
6
- metadata.gz: 516ebd68a80b61d75d12d1ca91833409cce8bc0d84d3d44346e07fc7b444c70f3d9d11eaca42cdbe39ff78f28e06fcc15af53c0659fa29ece17e316364e23706
7
- data.tar.gz: ded90962d4080696c29e87cf83ec3a429e8feca7b693b01ae2b2b2596fa7980a6ea0d44af876c014fff8b8f371bf27b3810dd8f2515bf5f157ccc9efb4e7cf5a
6
+ metadata.gz: ce81943acf58d09751d44c643d33e1efa3089e4921886ddfde30e5148e607b0cbde69bafc132c6d613b11b1066667535dcbc4c73842535674a2945dbc17963ae
7
+ data.tar.gz: 51a14597a059f3d24fd30a88eb7406386074768afbf67b0fc9f7533059477018181583a6b2d56c205acf3f82ef9e6b4963ba423d93bb7343f128982ff3daaf13
data/History.txt CHANGED
@@ -1,3 +1,8 @@
1
+ == 0.1.0
2
+ * register ruby predicates with exception handling
3
+ * Some DSL improvements
4
+ * tightening api
5
+
1
6
  == 0.0.3
2
7
  * rectify README errors
3
8
  * specs to handle variable unification from variables from top-level queries.
data/README.md CHANGED
@@ -1,29 +1,35 @@
1
1
  # Upl
2
2
 
3
- A ruby wrapper for SWI-Prolog that goes both ways.
3
+ Use SWI-Prolog from ruby.
4
4
 
5
- The main idea being that you want to just put in a chunk of prolog, and have the
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
- Also, do prolog-style queries on objects :-DD
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 'current_prolog_flag(K,V), member(K,[home,executable,shared_object_extension])'
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=>home, :V=>/usr/lib64/swipl-7.7.18},
22
- {:K=>executable, :V=>/usr/local/rvm/rubies/ruby-2.6.0-preview2/bin/ruby},
23
- {:K=>shared_object_extension, :V=>so}]
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.functor :person, :john, :anderson
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.functor :person, :john, :anderson, (o = Object.new)
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.functor :person, :james, :madison, (o = Object.new)
96
+ fact = Upl::Term :person, :james, :madison, (o = Object.new)
91
97
  Upl.assertz fact
92
98
 
93
- fact2 = Upl::Term.functor :person, :thomas, :paine, (thing2 = Object.new)
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 Predicates
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
- So you can (theoretically) define a query in prolog that searches a ruby object graph.
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
- ## Disclaimer
164
+ It might be useful for you too.
125
165
 
126
- This is in-development code. I use it for some things other than just playing with. It might be useful for you too.
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
@@ -4,3 +4,9 @@ require "rspec/core/rake_task"
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
6
  task :default => :spec
7
+
8
+ task :pry do
9
+ require 'pry'
10
+ require 'upl'
11
+ Pry.start
12
+ end
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 = nil, &default_blk )
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. But
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 dict_term_t) do |row,values|
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
- # func = Function.new(cb, [TYPE_INT], TYPE_INT)
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
- def self.register_semidet name, arity = nil, module: nil, &blk
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
- case (rv = blk.call *args)
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
- rescue
40
- # TODO raise an exception here, otherwise errors get lost in an empty result set.
41
- Upl::Extern::FALSE
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
- module_name = Fiddle::Pointer[module_name&.to_s || 0]
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
- rv = Upl::Extern.PL_register_foreign_in_module module_name, name.to_s, arity, fn, 0
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 |obj_term_t,meth_term_t,v_term_t|
22
- obj = Upl::Tree.of_term obj_term_t
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
- @inited ||= init
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
- # Use prolog predicate to parse the string into a term, with its named variables as a hash of Name => _variable
72
- # TODO maybe use read_term_from_chars, or at least don't force the term to be an atom
73
- # TODO need to use read_term_from_atom('retry(A,B,C)', Term, [variable_names(VarNames)]).
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
- def self.unify( term_a, term_b )
91
- rv = Extern::PL_unify term_a.term_t, term_a.term_t
92
- rv == 1 or raise "can't unify #{term_a} and #{term_b}"
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
- fid_t = Extern.PL_open_foreign_frame
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
- # module is NULL, flags is 0
108
- query_id_p = Extern.PL_open_query Extern::NULL, 0, qterm.to_predicate, args.terms
109
- query_id_p != 0 or raise 'no space on environment stack, see SWI-Prolog docs for PL_open_query'
110
-
111
- loop do
112
- # TODO handle PL_Q_EXT_STATUS
113
- res = Extern.PL_next_solution query_id_p
114
- break if res == 0
115
-
116
- hash = qvars_hash.each_with_object Hash.new do |(name_sym,var),ha|
117
- # var will be invalidated by the next call to PL_next_solution,
118
- # so we need to construct a ruby tree copy of the value term.
119
- ha[name_sym] = var.to_ruby
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
- p_functor = Extern::PL_new_functor predicate_str.to_sym.to_atom, arity
144
- p_predicate = Extern::PL_pred p_functor, Extern::NULL
145
-
146
- answer_lst = TermVector.new arity
147
- query_id_p = Extern.PL_open_query Extern::NULL, 0, p_predicate, answer_lst.terms
148
-
149
- loop do
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
- ensure
156
- # NOTE this also gets called after enum_for
157
- query_id_p&.to_i and Extern.PL_close_query query_id_p
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
- loop do
168
- rv = Extern.PL_next_solution query_id_p
169
- break if rv == 0
170
- yield answer_lst.each_t.map{|term_t| Tree.of_term term_t}
171
- end
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
- ensure
174
- # NOTE this also gets called after enum_for
175
- query_id_p&.to_i and Extern.PL_close_query query_id_p
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
- # So Prolog terms are a rose tree. Who woulda thunkit?
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
- def self.functor name, *args
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
- functor_t = Extern.PL_new_functor name.to_sym.to_atom, args.size
37
-
38
- arg_terms = Extern.PL_new_term_refs args.size
39
- args.each_with_index do |arg,idx|
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
- # Really this is just an empty term.
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
- Extern::Convert::CVT_VARIABLE | Extern::Convert::REP_UTF8 | Extern::Convert::BUF_MALLOC # | Extern::CVT_ALL
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
@@ -15,7 +15,8 @@ module Upl
15
15
  end
16
16
 
17
17
  def method_missing meth, *args
18
- name = meth.to_s
18
+ # unfreeze
19
+ name = meth.to_s.dup
19
20
 
20
21
  the_method =
21
22
  if name.chomp! '='
data/lib/upl/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Upl
2
- VERSION = '0.0.3'
2
+ VERSION = '0.1.0'
3
3
  end
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 moreorless the same as
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.functor name, *args
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.functor :person, :john, :anderson, Object.new
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.3
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: 2019-11-15 00:00:00.000000000 Z
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.0.6
136
+ rubygems_version: 3.1.2
137
137
  signing_key:
138
138
  specification_version: 4
139
139
  summary: Access SWI-Prolog engine from ruby