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 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