oso-oso 0.2.5 → 0.5.0

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