oso-oso 0.2.2 → 0.4.0

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