oso-oso 0.2.2 → 0.4.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.
@@ -1,19 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'pp'
4
5
  require 'set'
6
+ require 'digest/md5'
7
+
8
+ # Missing Ruby type.
9
+ module PolarBoolean; end
10
+ # Monkey-patch Ruby true type.
11
+ class TrueClass; include PolarBoolean; end
12
+ # Monkey-patch Ruby false type.
13
+ class FalseClass; include PolarBoolean; end
5
14
 
6
15
  module Oso
7
16
  module Polar
8
17
  # Create and manage an instance of the Polar runtime.
9
18
  class Polar # rubocop:disable Metrics/ClassLength
19
+ # @return [Host]
20
+ attr_reader :host
21
+
10
22
  def initialize
11
- @ffi_instance = FFI::Polar.create
12
- @calls = {}
13
- @classes = {}
14
- @constructors = {}
15
- @instances = {}
16
- @load_queue = Set.new
23
+ @ffi_polar = FFI::Polar.create
24
+ @host = Host.new(ffi_polar)
25
+ @loaded_names = {}
26
+ @loaded_contents = {}
27
+
28
+ # Register built-in classes.
29
+ register_class PolarBoolean, name: 'Boolean'
30
+ register_class Integer
31
+ register_class Float
32
+ register_class Array, name: 'List'
33
+ register_class Hash, name: 'Dictionary'
34
+ register_class String
35
+ end
36
+
37
+ # Replace the current Polar instance but retain all registered classes and constructors.
38
+ def clear
39
+ loaded_names.clear
40
+ loaded_contents.clear
41
+ @ffi_polar = FFI::Polar.create
17
42
  end
18
43
 
19
44
  # Enqueue a Polar policy file for loading into the KB.
@@ -21,11 +46,25 @@ module Oso
21
46
  # @param name [String]
22
47
  # @raise [PolarFileExtensionError] if provided filename has invalid extension.
23
48
  # @raise [PolarFileNotFoundError] if provided filename does not exist.
24
- def load_file(name)
25
- raise PolarFileExtensionError unless ['.pol', '.polar'].include? File.extname(name)
26
- raise PolarFileNotFoundError, name unless File.file?(name)
49
+ def load_file(name) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
50
+ raise PolarFileExtensionError, name unless File.extname(name) == '.polar'
27
51
 
28
- load_queue << name
52
+ file_data = File.open(name, &:read)
53
+ hash = Digest::MD5.hexdigest(file_data)
54
+
55
+ if loaded_names.key?(name)
56
+ raise PolarFileAlreadyLoadedError, name if loaded_names[name] == hash
57
+
58
+ raise PolarFileContentsChangedError, name
59
+ elsif loaded_contents.key?(hash)
60
+ raise PolarFileNameChangedError, name, loaded_contents[hash]
61
+ else
62
+ load_str(file_data, filename: name)
63
+ loaded_names[name] = hash
64
+ loaded_contents[hash] = name
65
+ end
66
+ rescue Errno::ENOENT
67
+ raise PolarFileNotFoundError, name
29
68
  end
30
69
 
31
70
  # Load a Polar string into the KB.
@@ -38,344 +77,118 @@ module Oso
38
77
  def load_str(str, filename: nil) # rubocop:disable Metrics/MethodLength
39
78
  raise NullByteInPolarFileError if str.chomp("\0").include?("\0")
40
79
 
41
- ffi_instance.load_str(str, filename: filename)
80
+ ffi_polar.load_str(str, filename: filename)
42
81
  loop do
43
- next_query = ffi_instance.next_inline_query
82
+ next_query = ffi_polar.next_inline_query
44
83
  break if next_query.nil?
45
84
 
46
85
  begin
47
- Query.new(next_query, polar: self).results.next
86
+ Query.new(next_query, host: host).results.next
48
87
  rescue StopIteration
49
88
  raise InlineQueryFailedError
50
89
  end
51
90
  end
52
91
  end
53
92
 
54
- # Replace the current Polar instance but retain all registered classes and
55
- # constructors.
56
- def clear
57
- @ffi_instance = FFI::Polar.create
93
+ # Query for a predicate, parsing it if necessary.
94
+ #
95
+ # @overload query(query)
96
+ # @param query [String]
97
+ # @return [Enumerator] of resulting bindings
98
+ # @raise [Error] if the FFI call raises one.
99
+ # @overload query(query)
100
+ # @param query [Predicate]
101
+ # @return [Enumerator] of resulting bindings
102
+ # @raise [Error] if the FFI call raises one.
103
+ def query(query)
104
+ new_host = host.dup
105
+ case query
106
+ when String
107
+ ffi_query = ffi_polar.new_query_from_str(query)
108
+ when Predicate
109
+ ffi_query = ffi_polar.new_query_from_term(new_host.to_polar_term(query))
110
+ else
111
+ raise InvalidQueryTypeError
112
+ end
113
+ Query.new(ffi_query, host: new_host).results
58
114
  end
59
115
 
60
- # Query for a predicate.
116
+ # Query for a rule.
61
117
  #
62
118
  # @param name [String]
63
119
  # @param args [Array<Object>]
120
+ # @return [Enumerator] of resulting bindings
64
121
  # @raise [Error] if the FFI call raises one.
65
- def query_pred(name, args:)
66
- clear_query_state
67
- load_queued_files
68
- pred = Predicate.new(name, args: args)
69
- query_ffi_instance = ffi_instance.new_query_from_term(to_polar_term(pred))
70
- Query.new(query_ffi_instance, polar: self).results
122
+ def query_rule(name, *args)
123
+ query(Predicate.new(name, args: args))
71
124
  end
72
125
 
73
126
  # Start a REPL session.
74
127
  #
75
128
  # @raise [Error] if the FFI call raises one.
76
- def repl # rubocop:disable Metrics/MethodLength
77
- clear_query_state
78
- load_queued_files
79
- loop do
80
- query = Query.new(ffi_instance.new_query_from_repl, polar: self)
81
- results = query.results.to_a
129
+ def repl(load: false) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
130
+ ARGV.map { |f| load_file(f) } if load
131
+
132
+ loop do # rubocop:disable Metrics/BlockLength
133
+ print 'query> '
134
+ begin
135
+ query = $stdin.readline.chomp.chomp(';')
136
+ rescue EOFError
137
+ return
138
+ end
139
+
140
+ begin
141
+ ffi_query = ffi_polar.new_query_from_str(query)
142
+ rescue ParseError => e
143
+ puts "Parse error: #{e}"
144
+ next
145
+ end
146
+
147
+ begin
148
+ results = Query.new(ffi_query, host: host).results.to_a
149
+ rescue PolarRuntimeError => e
150
+ puts e
151
+ next
152
+ end
153
+
82
154
  if results.empty?
83
- puts 'False'
155
+ pp false
84
156
  else
85
157
  results.each do |result|
86
- puts result
158
+ if result.empty?
159
+ pp true
160
+ else
161
+ pp result
162
+ end
87
163
  end
88
164
  end
89
165
  end
90
166
  end
91
167
 
92
- # Get a unique ID from Polar.
93
- #
94
- # @return [Integer]
95
- # @raise [Error] if the FFI call raises one.
96
- def new_id
97
- ffi_instance.new_id
98
- end
99
-
100
168
  # Register a Ruby class with Polar.
101
169
  #
102
170
  # @param cls [Class]
103
171
  # @param name [String]
104
172
  # @param from_polar [Proc]
105
173
  # @raise [InvalidConstructorError] if provided an invalid 'from_polar' constructor.
106
- def register_class(cls, name: nil, from_polar: nil) # rubocop:disable Naming/MethodParameterName
107
- # TODO(gj): should this take 3 args: cls (Class), constructor_cls
108
- # (Option<Class>) that defaults to cls, and constructor_method
109
- # (Option<Symbol>) that defaults to :new?
110
- name = cls.name if name.nil?
111
- raise DuplicateClassAliasError, name: name, old: get_class(name), new: cls if classes.key? name
112
-
113
- classes[name] = cls
114
- if from_polar.nil?
115
- constructors[name] = :new
116
- elsif from_polar.respond_to? :call
117
- constructors[name] = from_polar
118
- else
119
- raise InvalidConstructorError
120
- end
121
-
174
+ def register_class(cls, name: nil, from_polar: nil)
175
+ from_polar = Proc.new if block_given?
176
+ name = host.cache_class(cls, name: name, constructor: from_polar)
122
177
  register_constant(name, value: cls)
123
178
  end
124
179
 
125
180
  def register_constant(name, value:)
126
- ffi_instance.register_constant(name, value: to_polar_term(value))
127
- end
128
-
129
- # Register a Ruby method call, wrapping the call result in a generator if
130
- # it isn't already one.
131
- #
132
- # @param method [#to_sym]
133
- # @param call_id [Integer]
134
- # @param instance [Hash]
135
- # @param args [Array<Hash>]
136
- # @raise [InvalidCallError] if the method doesn't exist on the instance or
137
- # the args passed to the method are invalid.
138
- def register_call(method, call_id:, instance:, args:)
139
- return if calls.key?(call_id)
140
-
141
- args = args.map { |a| to_ruby(a) }
142
- if instance["value"].has_key? "ExternalInstance"
143
- instance_id = instance["value"]["ExternalInstance"]["instance_id"]
144
- instance = get_instance(instance_id)
145
- else
146
- instance = to_ruby(instance)
147
- end
148
- result = instance.__send__(method, *args)
149
- result = [result].to_enum unless result.is_a? Enumerator # Call must be a generator.
150
- calls[call_id] = result.lazy
151
- rescue ArgumentError, NoMethodError
152
- raise InvalidCallError
153
- end
154
-
155
- # Retrieve the next result from a registered call and pass it to {#to_polar_term}.
156
- #
157
- # @param id [Integer]
158
- # @return [Hash]
159
- # @raise [StopIteration] if the call has been exhausted.
160
- def next_call_result(id)
161
- to_polar_term(calls[id].next)
162
- end
163
-
164
- # Construct and cache a Ruby instance.
165
- #
166
- # @param cls_name [String]
167
- # @param fields [Hash<String, Hash>]
168
- # @param id [Integer]
169
- # @raise [PolarRuntimeError] if instance construction fails.
170
- def make_instance(cls_name, fields:, id:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
171
- constructor = get_constructor(cls_name)
172
- fields = Hash[fields.map { |k, v| [k.to_sym, to_ruby(v)] }]
173
- instance = if constructor == :new
174
- if fields.empty?
175
- get_class(cls_name).__send__(:new)
176
- else
177
- get_class(cls_name).__send__(:new, **fields)
178
- end
179
- elsif fields.empty?
180
- constructor.call
181
- else
182
- constructor.call(**fields)
183
- end
184
- cache_instance(instance, id: id)
185
- rescue StandardError => e
186
- raise PolarRuntimeError, "Error constructing instance of #{cls_name}: #{e}"
187
- end
188
-
189
- # Check if an instance has been cached.
190
- #
191
- # @param id [Integer]
192
- # @return [Boolean]
193
- def instance?(id)
194
- instances.key? id
195
- end
196
-
197
- # Fetch a Ruby instance from the {#instances} cache.
198
- #
199
- # @param id [Integer]
200
- # @return [Object]
201
- # @raise [UnregisteredInstanceError] if the ID has not been registered.
202
- def get_instance(id)
203
- raise UnregisteredInstanceError, id unless instance? id
204
-
205
- instances[id]
206
- end
207
-
208
- # Check if the left class is more specific than the right class for the
209
- # given instance.
210
- #
211
- # @param instance_id [Integer]
212
- # @param left_tag [String]
213
- # @param right_tag [String]
214
- # @return [Boolean]
215
- def subspecializer?(instance_id, left_tag:, right_tag:)
216
- mro = get_instance(instance_id).class.ancestors
217
- mro.index(get_class(left_tag)) < mro.index(get_class(right_tag))
218
- rescue StandardError
219
- false
220
- end
221
-
222
- # Check if instance is an instance of class.
223
- #
224
- # @param instance_id [Integer]
225
- # @param class_tag [String]
226
- # @return [Boolean]
227
- def isa?(instance_id, class_tag:)
228
- instance = get_instance(instance_id)
229
- cls = get_class(class_tag)
230
- instance.is_a? cls
231
- rescue PolarRuntimeError
232
- false
233
- end
234
-
235
- # Check if two instances unify
236
- #
237
- # @param left_instance_id [Integer]
238
- # @param right_instance_id [Integer]
239
- # @return [Boolean]
240
- def unify?(left_instance_id, right_instance_id)
241
- left_instance = get_instance(left_instance_id)
242
- right_instance = get_instance(right_instance_id)
243
- left_instance == right_instance
244
- rescue PolarRuntimeError
245
- false
246
- end
247
-
248
- # Turn a Ruby value into a Polar term that's ready to be sent across the
249
- # FFI boundary.
250
- #
251
- # @param value [Object]
252
- # @return [Hash<String, Object>]
253
- def to_polar_term(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
254
- value = case true # rubocop:disable Lint/LiteralAsCondition
255
- when value.instance_of?(TrueClass) || value.instance_of?(FalseClass)
256
- { 'Boolean' => value }
257
- when value.instance_of?(Integer)
258
- { 'Number' => { 'Integer' => value } }
259
- when value.instance_of?(Float)
260
- { 'Number' => { 'Float' => value } }
261
- when value.instance_of?(String)
262
- { 'String' => value }
263
- when value.instance_of?(Array)
264
- { 'List' => value.map { |el| to_polar_term(el) } }
265
- when value.instance_of?(Hash)
266
- { 'Dictionary' => { 'fields' => value.transform_values { |v| to_polar_term(v) } } }
267
- when value.instance_of?(Predicate)
268
- { 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar_term(el) } } }
269
- when value.instance_of?(Variable)
270
- # This is supported so that we can query for unbound variables
271
- { 'Variable' => value }
272
- else
273
- { 'ExternalInstance' => { 'instance_id' => cache_instance(value) } }
274
- end
275
- { 'value' => value }
276
- end
277
-
278
- # Turn a Polar term passed across the FFI boundary into a Ruby value.
279
- #
280
- # @param data [Hash<String, Object>]
281
- # @option data [Integer] :id
282
- # @option data [Integer] :offset Character offset of the term in its source string.
283
- # @option data [Hash<String, Object>] :value
284
- # @return [Object]
285
- # @raise [UnexpectedPolarTypeError] if type cannot be converted to Ruby.
286
- def to_ruby(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
287
- tag, value = data['value'].first
288
- case tag
289
- when 'String', 'Boolean'
290
- value
291
- when 'Number'
292
- value.values.first
293
- when 'List'
294
- value.map { |el| to_ruby(el) }
295
- when 'Dictionary'
296
- value['fields'].transform_values { |v| to_ruby(v) }
297
- when 'ExternalInstance'
298
- get_instance(value['instance_id'])
299
- when 'Call'
300
- Predicate.new(value['name'], args: value['args'].map { |a| to_ruby(a) })
301
- else
302
- raise UnexpectedPolarTypeError, tag
303
- end
304
- end
305
-
306
- # Load all queued files, flushing the {#load_queue}.
307
- def load_queued_files
308
- load_queue.reject! do |filename|
309
- File.open(filename) { |file| load_str(file.read, filename: filename) }
310
- true
311
- end
181
+ ffi_polar.register_constant(name, value: host.to_polar_term(value))
312
182
  end
313
183
 
314
184
  private
315
185
 
316
- # @return [Hash<Integer, Enumerator>]
317
- attr_reader :calls
318
- # @return [Hash<String, Class>]
319
- attr_reader :classes
320
- # @return [Hash<String, Object>]
321
- attr_reader :constructors
322
186
  # @return [FFI::Polar]
323
- attr_reader :ffi_instance
324
- # @return [Hash<Integer, Object>]
325
- attr_reader :instances
326
- # @return [Array<String>]
327
- attr_reader :load_queue
328
-
329
- # Clear the instance and call caches.
330
- def clear_query_state
331
- calls.clear
332
- instances.clear
333
- end
334
-
335
- # Query for a Polar string.
336
- #
337
- # @param str [String]
338
- # @return [Enumerator]
339
- def query_str(str)
340
- clear_query_state
341
- load_queued_files
342
- query_ffi_instance = ffi_instance.new_query_from_str(str)
343
- Query.new(query_ffi_instance, polar: self).results
344
- end
345
-
346
- # Cache a Ruby instance, fetching a {#new_id} if one isn't provided.
347
- #
348
- # @param instance [Object]
349
- # @param id [Integer]
350
- # @return [Integer]
351
- def cache_instance(instance, id: nil)
352
- id = new_id if id.nil?
353
- instances[id] = instance
354
- id
355
- end
356
-
357
- # Fetch a Ruby class from the {#classes} cache.
358
- #
359
- # @param name [String]
360
- # @return [Class]
361
- # @raise [UnregisteredClassError] if the class has not been registered.
362
- def get_class(name)
363
- raise UnregisteredClassError, name unless classes.key? name
364
-
365
- classes[name]
366
- end
367
-
368
- # Fetch a constructor from the {#constructors} cache.
369
- #
370
- # @param name [String]
371
- # @return [Symbol] if constructor is the default of `:new`.
372
- # @return [Proc] if a custom constructor was registered.
373
- # @raise [UnregisteredConstructorError] if the constructor has not been registered.
374
- def get_constructor(name)
375
- raise MissingConstructorError, name unless constructors.key? name
376
-
377
- constructors[name]
378
- end
187
+ attr_reader :ffi_polar
188
+ # @return [Hash<String, String>]
189
+ attr_reader :loaded_names
190
+ # @return [Hash<String, String>]
191
+ attr_reader :loaded_contents
379
192
  end
380
193
  end
381
194
  end