oso-oso 0.2.5 → 0.5.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.
@@ -12,7 +12,9 @@ module Oso
12
12
  attach_function :debug_command, :polar_debug_command, [FFI::Query, :string], :int32
13
13
  attach_function :call_result, :polar_call_result, [FFI::Query, :uint64, :string], :int32
14
14
  attach_function :question_result, :polar_question_result, [FFI::Query, :uint64, :int32], :int32
15
+ attach_function :application_error, :polar_application_error, [FFI::Query, :string], :int32
15
16
  attach_function :next_event, :polar_next_query_event, [FFI::Query], FFI::QueryEvent
17
+ attach_function :next_message, :polar_next_query_message, [FFI::Query], FFI::Message
16
18
  attach_function :free, :query_free, [FFI::Query], :int32
17
19
  end
18
20
  private_constant :Rust
@@ -21,6 +23,7 @@ module Oso
21
23
  # @raise [FFI::Error] if the FFI call returns an error.
22
24
  def debug_command(cmd)
23
25
  res = Rust.debug_command(self, cmd)
26
+ process_messages
24
27
  raise FFI::Error.get if res.zero?
25
28
  end
26
29
 
@@ -41,14 +44,36 @@ module Oso
41
44
  raise FFI::Error.get if res.zero?
42
45
  end
43
46
 
47
+ # @param result [Boolean]
48
+ # @param call_id [Integer]
49
+ # @raise [FFI::Error] if the FFI call returns an error.
50
+ def application_error(message)
51
+ res = Rust.application_error(self, message)
52
+ raise FFI::Error.get if res.zero?
53
+ end
54
+
44
55
  # @return [::Oso::Polar::QueryEvent]
45
56
  # @raise [FFI::Error] if the FFI call returns an error.
46
57
  def next_event
47
58
  event = Rust.next_event(self)
59
+ process_messages
48
60
  raise FFI::Error.get if event.null?
49
61
 
50
62
  ::Oso::Polar::QueryEvent.new(JSON.parse(event.to_s))
51
63
  end
64
+
65
+ def next_message
66
+ Rust.next_message(self)
67
+ end
68
+
69
+ def process_messages
70
+ loop do
71
+ message = next_message
72
+ break if message.null?
73
+
74
+ message.process
75
+ end
76
+ end
52
77
  end
53
78
  end
54
79
  end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ # Translate between Polar and the host language (Ruby).
6
+ class Host # rubocop:disable Metrics/ClassLength
7
+ protected
8
+
9
+ # @return [FFI::Polar]
10
+ attr_reader :ffi_polar
11
+ # @return [Hash<String, Class>]
12
+ attr_reader :classes
13
+ # @return [Hash<String, Object>]
14
+ attr_reader :constructors
15
+ # @return [Hash<Integer, Object>]
16
+ attr_reader :instances
17
+
18
+ public
19
+
20
+ def initialize(ffi_polar)
21
+ @ffi_polar = ffi_polar
22
+ @classes = {}
23
+ @constructors = {}
24
+ @instances = {}
25
+ end
26
+
27
+ def initialize_copy(other)
28
+ @ffi_polar = other.ffi_polar
29
+ @classes = other.classes.dup
30
+ @constructors = other.constructors.dup
31
+ @instances = other.instances.dup
32
+ end
33
+
34
+ # Fetch a Ruby class from the {#classes} cache.
35
+ #
36
+ # @param name [String]
37
+ # @return [Class]
38
+ # @raise [UnregisteredClassError] if the class has not been registered.
39
+ def get_class(name)
40
+ raise UnregisteredClassError, name unless classes.key? name
41
+
42
+ classes[name]
43
+ end
44
+
45
+ # Store a Ruby class in the {#classes} cache.
46
+ #
47
+ # @param cls [Class] the class to cache
48
+ # @param name [String] the name to cache the class as. Defaults to the name of the class.
49
+ # @param constructor [Proc] optional custom constructor function. Defaults to the :new method.
50
+ # @return [String] the name the class is cached as.
51
+ # @raise [UnregisteredClassError] if the class has not been registered.
52
+ def cache_class(cls, name:, constructor:) # rubocop:disable Metrics/MethodLength
53
+ name = cls.name if name.nil?
54
+ raise DuplicateClassAliasError, name: name, old: get_class(name), new: cls if classes.key? name
55
+
56
+ classes[name] = cls
57
+ if constructor.nil?
58
+ constructors[name] = :new
59
+ elsif constructor.respond_to? :call
60
+ constructors[name] = constructor
61
+ else
62
+ raise InvalidConstructorError
63
+ end
64
+ name
65
+ end
66
+
67
+ # Fetch a constructor from the {#constructors} cache.
68
+ #
69
+ # @param name [String]
70
+ # @return [Symbol] if constructor is the default of `:new`.
71
+ # @return [Proc] if a custom constructor was registered.
72
+ # @raise [UnregisteredConstructorError] if the constructor has not been registered.
73
+ def get_constructor(name)
74
+ raise MissingConstructorError, name unless constructors.key? name
75
+
76
+ constructors[name]
77
+ end
78
+
79
+ # Check if an instance exists in the {#instances} cache.
80
+ #
81
+ # @param id [Integer]
82
+ # @return [Boolean]
83
+ def instance?(id)
84
+ case id
85
+ when Integer
86
+ instances.key? id
87
+ else
88
+ instances.value? id
89
+ end
90
+ end
91
+
92
+ # Fetch a Ruby instance from the {#instances} cache.
93
+ #
94
+ # @param id [Integer]
95
+ # @return [Object]
96
+ # @raise [UnregisteredInstanceError] if the ID has not been registered.
97
+ def get_instance(id)
98
+ raise UnregisteredInstanceError, id unless instance? id
99
+
100
+ instances[id]
101
+ end
102
+
103
+ # Cache a Ruby instance in the {#instances} cache, fetching a {#new_id}
104
+ # if one isn't provided.
105
+ #
106
+ # @param instance [Object]
107
+ # @param id [Integer]
108
+ # @return [Integer]
109
+ def cache_instance(instance, id: nil)
110
+ id = ffi_polar.new_id if id.nil?
111
+ instances[id] = instance
112
+ id
113
+ end
114
+
115
+ # Construct and cache a Ruby instance.
116
+ #
117
+ # @param cls_name [String]
118
+ # @param initargs [Hash<String, Hash>]
119
+ # @param id [Integer]
120
+ # @raise [PolarRuntimeError] if instance construction fails.
121
+ def make_instance(cls_name, initargs:, id:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
122
+ constructor = get_constructor(cls_name)
123
+ instance = if constructor == :new
124
+ if initargs.empty?
125
+ get_class(cls_name).__send__(:new)
126
+ elsif initargs.is_a? Array
127
+ get_class(cls_name).__send__(:new, *initargs)
128
+ elsif initargs.is_a? Hash
129
+ get_class(cls_name).__send__(:new, **initargs)
130
+ else
131
+ raise PolarRuntimeError, "Bad initargs: #{initargs}"
132
+ end
133
+ elsif initargs.empty?
134
+ constructor.call
135
+ elsif initargs.is_a? Array
136
+ constructor.call(*initargs)
137
+ elsif initargs.is_a? Hash
138
+ constructor.call(**initargs)
139
+ else
140
+ raise PolarRuntimeError, "Bad initargs: #{initargs}"
141
+ end
142
+ cache_instance(instance, id: id)
143
+ rescue StandardError => e
144
+ raise PolarRuntimeError, "Error constructing instance of #{cls_name}: #{e}"
145
+ end
146
+
147
+ # Check if the left class is more specific than the right class
148
+ # with respect to the given instance.
149
+ #
150
+ # @param instance_id [Integer]
151
+ # @param left_tag [String]
152
+ # @param right_tag [String]
153
+ # @return [Boolean]
154
+ def subspecializer?(instance_id, left_tag:, right_tag:)
155
+ mro = get_instance(instance_id).class.ancestors
156
+ mro.index(get_class(left_tag)) < mro.index(get_class(right_tag))
157
+ rescue StandardError
158
+ false
159
+ end
160
+
161
+ # Check if instance is an instance of class.
162
+ #
163
+ # @param instance [Hash<String, Object>]
164
+ # @param class_tag [String]
165
+ # @return [Boolean]
166
+ def isa?(instance, class_tag:)
167
+ instance = to_ruby(instance)
168
+ cls = get_class(class_tag)
169
+ instance.is_a? cls
170
+ rescue PolarRuntimeError
171
+ false
172
+ end
173
+
174
+ # Check if two instances unify
175
+ #
176
+ # @param left_instance_id [Integer]
177
+ # @param right_instance_id [Integer]
178
+ # @return [Boolean]
179
+ def unify?(left_instance_id, right_instance_id)
180
+ left_instance = get_instance(left_instance_id)
181
+ right_instance = get_instance(right_instance_id)
182
+ left_instance == right_instance
183
+ rescue PolarRuntimeError
184
+ false
185
+ end
186
+
187
+ # Turn a Ruby value into a Polar term that's ready to be sent across the
188
+ # FFI boundary.
189
+ #
190
+ # @param value [Object]
191
+ # @return [Hash<String, Object>]
192
+ def to_polar_term(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
193
+ value = case true # rubocop:disable Lint/LiteralAsCondition
194
+ when value.instance_of?(TrueClass) || value.instance_of?(FalseClass)
195
+ { 'Boolean' => value }
196
+ when value.instance_of?(Integer)
197
+ { 'Number' => { 'Integer' => value } }
198
+ when value.instance_of?(Float)
199
+ { 'Number' => { 'Float' => value } }
200
+ when value.instance_of?(String)
201
+ { 'String' => value }
202
+ when value.instance_of?(Array)
203
+ { 'List' => value.map { |el| to_polar_term(el) } }
204
+ when value.instance_of?(Hash)
205
+ { 'Dictionary' => { 'fields' => value.transform_values { |v| to_polar_term(v) } } }
206
+ when value.instance_of?(Predicate)
207
+ { 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar_term(el) } } }
208
+ when value.instance_of?(Variable)
209
+ # This is supported so that we can query for unbound variables
210
+ { 'Variable' => value }
211
+ else
212
+ { 'ExternalInstance' => { 'instance_id' => cache_instance(value), 'repr' => value.to_s } }
213
+ end
214
+ { 'value' => value }
215
+ end
216
+
217
+ # Turn a Polar term passed across the FFI boundary into a Ruby value.
218
+ #
219
+ # @param data [Hash<String, Object>]
220
+ # @option data [Integer] :id
221
+ # @option data [Integer] :offset Character offset of the term in its source string.
222
+ # @option data [Hash<String, Object>] :value
223
+ # @return [Object]
224
+ # @raise [UnexpectedPolarTypeError] if type cannot be converted to Ruby.
225
+ def to_ruby(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
226
+ tag, value = data['value'].first
227
+ case tag
228
+ when 'String', 'Boolean'
229
+ value
230
+ when 'Number'
231
+ value.values.first
232
+ when 'List'
233
+ value.map { |el| to_ruby(el) }
234
+ when 'Dictionary'
235
+ value['fields'].transform_values { |v| to_ruby(v) }
236
+ when 'ExternalInstance'
237
+ get_instance(value['instance_id'])
238
+ when 'Call'
239
+ Predicate.new(value['name'], args: value['args'].map { |a| to_ruby(a) })
240
+ when 'Variable'
241
+ Variable.new(value['name'])
242
+ else
243
+ raise UnexpectedPolarTypeError, tag
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -1,31 +1,90 @@
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
14
+
15
+ # https://github.com/ruby/ruby/blob/bb9ecd026a6cadd5d0f85ac061649216806ed935/lib/bundler/vendor/thor/lib/thor/shell/color.rb#L99-L105
16
+ def supports_color
17
+ $stdout.tty? && $stderr.tty? && ENV['NO_COLOR'].nil?
18
+ end
19
+
20
+ if supports_color
21
+ RESET = "\x1b[0m"
22
+ FG_BLUE = "\x1b[34m"
23
+ FG_RED = "\x1b[31m"
24
+ else
25
+ RESET = ''
26
+ FG_BLUE = ''
27
+ FG_RED = ''
28
+ end
29
+
30
+ def print_error(error)
31
+ warn FG_RED + error.class.name.split('::').last + RESET
32
+ warn error.message
33
+ end
5
34
 
6
35
  module Oso
7
36
  module Polar
8
37
  # Create and manage an instance of the Polar runtime.
9
38
  class Polar # rubocop:disable Metrics/ClassLength
39
+ # @return [Host]
40
+ attr_reader :host
41
+
10
42
  def initialize
11
- @ffi_instance = FFI::Polar.create
12
- @calls = {}
13
- @classes = {}
14
- @constructors = {}
15
- @instances = {}
16
- @load_queue = Set.new
43
+ @ffi_polar = FFI::Polar.create
44
+ @host = Host.new(ffi_polar)
45
+ @loaded_names = {}
46
+ @loaded_contents = {}
47
+
48
+ # Register built-in classes.
49
+ register_class PolarBoolean, name: 'Boolean'
50
+ register_class Integer
51
+ register_class Float
52
+ register_class Array, name: 'List'
53
+ register_class Hash, name: 'Dictionary'
54
+ register_class String
55
+ end
56
+
57
+ # Replace the current Polar instance but retain all registered classes and constructors.
58
+ def clear
59
+ loaded_names.clear
60
+ loaded_contents.clear
61
+ @ffi_polar = FFI::Polar.create
17
62
  end
18
63
 
19
- # Enqueue a Polar policy file for loading into the KB.
64
+ # Load a Polar policy file.
20
65
  #
21
66
  # @param name [String]
22
67
  # @raise [PolarFileExtensionError] if provided filename has invalid extension.
23
68
  # @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)
69
+ def load_file(name) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
70
+ raise PolarFileExtensionError, name unless File.extname(name) == '.polar'
71
+
72
+ file_data = File.open(name, &:read)
73
+ hash = Digest::MD5.hexdigest(file_data)
74
+
75
+ if loaded_names.key?(name)
76
+ raise PolarFileAlreadyLoadedError, name if loaded_names[name] == hash
27
77
 
28
- load_queue << name
78
+ raise PolarFileContentsChangedError, name
79
+ elsif loaded_contents.key?(hash)
80
+ raise PolarFileNameChangedError, name, loaded_contents[hash]
81
+ else
82
+ load_str(file_data, filename: name)
83
+ loaded_names[name] = hash
84
+ loaded_contents[hash] = name
85
+ end
86
+ rescue Errno::ENOENT
87
+ raise PolarFileNotFoundError, name
29
88
  end
30
89
 
31
90
  # Load a Polar string into the KB.
@@ -38,63 +97,50 @@ module Oso
38
97
  def load_str(str, filename: nil) # rubocop:disable Metrics/MethodLength
39
98
  raise NullByteInPolarFileError if str.chomp("\0").include?("\0")
40
99
 
41
- ffi_instance.load_str(str, filename: filename)
100
+ ffi_polar.load_str(str, filename: filename)
42
101
  loop do
43
- next_query = ffi_instance.next_inline_query
102
+ next_query = ffi_polar.next_inline_query
44
103
  break if next_query.nil?
45
104
 
46
105
  begin
47
- Query.new(next_query, polar: self).results.next
106
+ Query.new(next_query, host: host).results.next
48
107
  rescue StopIteration
49
108
  raise InlineQueryFailedError
50
109
  end
51
110
  end
52
111
  end
53
112
 
54
- # Replace the current Polar instance but retain all registered classes and
55
- # constructors.
56
- def clear
57
- @ffi_instance = FFI::Polar.create
113
+ # Query for a Polar predicate or string.
114
+ #
115
+ # @overload query(query)
116
+ # @param query [String]
117
+ # @return [Enumerator] of resulting bindings
118
+ # @raise [Error] if the FFI call raises one.
119
+ # @overload query(query)
120
+ # @param query [Predicate]
121
+ # @return [Enumerator] of resulting bindings
122
+ # @raise [Error] if the FFI call raises one.
123
+ def query(query)
124
+ new_host = host.dup
125
+ case query
126
+ when String
127
+ ffi_query = ffi_polar.new_query_from_str(query)
128
+ when Predicate
129
+ ffi_query = ffi_polar.new_query_from_term(new_host.to_polar_term(query))
130
+ else
131
+ raise InvalidQueryTypeError
132
+ end
133
+ Query.new(ffi_query, host: new_host).results
58
134
  end
59
135
 
60
- # Query for a predicate.
136
+ # Query for a rule.
61
137
  #
62
138
  # @param name [String]
63
139
  # @param args [Array<Object>]
140
+ # @return [Enumerator] of resulting bindings
64
141
  # @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
71
- end
72
-
73
- # Start a REPL session.
74
- #
75
- # @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
82
- if results.empty?
83
- puts 'False'
84
- else
85
- results.each do |result|
86
- puts result
87
- end
88
- end
89
- end
90
- end
91
-
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
142
+ def query_rule(name, *args)
143
+ query(Predicate.new(name, args: args))
98
144
  end
99
145
 
100
146
  # Register a Ruby class with Polar.
@@ -103,278 +149,98 @@ module Oso
103
149
  # @param name [String]
104
150
  # @param from_polar [Proc]
105
151
  # @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
-
152
+ def register_class(cls, name: nil, from_polar: nil)
153
+ from_polar = Proc.new if block_given?
154
+ name = host.cache_class(cls, name: name, constructor: from_polar)
122
155
  register_constant(name, value: cls)
123
156
  end
124
157
 
125
158
  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
159
+ ffi_polar.register_constant(name, value: host.to_polar_term(value))
233
160
  end
234
161
 
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.
162
+ # Start a REPL session.
279
163
  #
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
164
+ # @param files [Array<String>]
165
+ # @raise [Error] if the FFI call raises one.
166
+ def repl(files = [])
167
+ files.map { |f| load_file(f) }
168
+ prompt = "#{FG_BLUE}query>#{RESET} "
169
+ # Try loading the readline module from the Ruby stdlib. If we get a
170
+ # LoadError, fall back to the standard REPL with no readline support.
171
+ require 'readline'
172
+ repl_readline(prompt)
173
+ rescue LoadError
174
+ repl_standard(prompt)
312
175
  end
313
176
 
314
177
  private
315
178
 
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
179
  # @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
180
+ attr_reader :ffi_polar
181
+ # @return [Hash<String, String>]
182
+ attr_reader :loaded_names
183
+ # @return [Hash<String, String>]
184
+ attr_reader :loaded_contents
185
+
186
+ # The R and L in REPL for systems where readline is available.
187
+ def repl_readline(prompt)
188
+ while (buf = Readline.readline(prompt, true))
189
+ if /^\s*$/ =~ buf # Don't add empty entries to history.
190
+ Readline::HISTORY.pop
191
+ next
192
+ end
193
+ process_line(buf)
194
+ end
195
+ rescue Interrupt # rubocop:disable Lint/SuppressedException
344
196
  end
345
197
 
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
198
+ # The R and L in REPL for systems where readline is not available.
199
+ def repl_standard(prompt)
200
+ loop do
201
+ puts prompt
202
+ begin
203
+ buf = $stdin.readline
204
+ rescue EOFError
205
+ return
206
+ end
207
+ process_line(buf)
208
+ end
209
+ rescue Interrupt # rubocop:disable Lint/SuppressedException
355
210
  end
356
211
 
357
- # Fetch a Ruby class from the {#classes} cache.
212
+ # Process a line of user input in the REPL.
358
213
  #
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
214
+ # @param buf [String]
215
+ def process_line(buf) # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
216
+ query = buf.chomp.chomp(';')
217
+ begin
218
+ ffi_query = ffi_polar.new_query_from_str(query)
219
+ rescue ParseError => e
220
+ print_error(e)
221
+ return
222
+ end
367
223
 
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
224
+ begin
225
+ results = Query.new(ffi_query, host: host).results.to_a
226
+ rescue PolarRuntimeError => e
227
+ print_error(e)
228
+ return
229
+ end
376
230
 
377
- constructors[name]
231
+ if results.empty?
232
+ puts false
233
+ else
234
+ results.each do |result|
235
+ if result.empty?
236
+ puts true
237
+ else
238
+ result.each do |variable, value|
239
+ puts "#{variable} => #{value.inspect}"
240
+ end
241
+ end
242
+ end
243
+ end
378
244
  end
379
245
  end
380
246
  end