oso-oso 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ module Oso
6
+ module Polar
7
+ module FFI
8
+ LIB = ::FFI::Platform::LIBPREFIX + 'polar.' + ::FFI::Platform::LIBSUFFIX
9
+ LIB_PATH = File.expand_path(File.join(__dir__, "../../../ext/oso-oso/lib/#{LIB}"))
10
+
11
+ # Wrapper classes defined upfront to fix Ruby loading issues. Actual
12
+ # implementations live in the sibling `ffi/` directory and are `require`d
13
+ # at the bottom of this file.
14
+
15
+ # Wrapper class for Polar FFI pointer + operations.
16
+ class Polar < ::FFI::AutoPointer
17
+ def self.release(ptr)
18
+ Rust.free(ptr) unless ptr.null?
19
+ end
20
+ end
21
+ # Wrapper class for Query FFI pointer + operations.
22
+ class Query < ::FFI::AutoPointer
23
+ def self.release(ptr)
24
+ Rust.free(ptr) unless ptr.null?
25
+ end
26
+ end
27
+ # Wrapper class for QueryEvent FFI pointer + operations.
28
+ class QueryEvent < ::FFI::AutoPointer
29
+ def self.release(ptr)
30
+ Rust.free(ptr) unless ptr.null?
31
+ end
32
+ end
33
+ # Wrapper class for Error FFI pointer + operations.
34
+ class Error < ::FFI::AutoPointer
35
+ def self.release(ptr)
36
+ Rust.free(ptr)
37
+ end
38
+ end
39
+ end
40
+ private_constant :FFI
41
+ end
42
+ end
43
+
44
+ require 'oso/polar/ffi/polar'
45
+ require 'oso/polar/ffi/query'
46
+ require 'oso/polar/ffi/query_event'
47
+ require 'oso/polar/ffi/error'
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ module FFI
6
+ # Wrapper class for Error FFI pointer + operations.
7
+ class Error < ::FFI::AutoPointer
8
+ def to_s
9
+ @to_s ||= read_string.force_encoding('UTF-8')
10
+ end
11
+
12
+ Rust = Module.new do
13
+ extend ::FFI::Library
14
+ ffi_lib FFI::LIB_PATH
15
+
16
+ attach_function :get, :polar_get_error, [], Error
17
+ attach_function :free, :string_free, [Error], :int32
18
+ end
19
+ private_constant :Rust
20
+
21
+ # Check for an FFI error and convert it into a Ruby exception.
22
+ #
23
+ # @return [::Oso::Polar::Error] if there's an FFI error.
24
+ # @return [::Oso::Polar::FFIErrorNotFound] if there isn't one.
25
+ def self.get # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
26
+ error = Rust.get
27
+ return ::Oso::Polar::FFIErrorNotFound if error.null?
28
+
29
+ error = JSON.parse(error.to_s)
30
+ msg = error['formatted']
31
+ kind, body = error['kind'].first
32
+ subkind, details = body.first
33
+ case kind
34
+ when 'Parse'
35
+ parse_error(subkind, msg: msg, details: details)
36
+ when 'Runtime'
37
+ runtime_error(subkind, msg: msg, details: details)
38
+ when 'Operational'
39
+ operational_error(subkind, msg: msg, details: details)
40
+ when 'Parameter'
41
+ api_error(subkind, msg: msg, details: details)
42
+ end
43
+ end
44
+
45
+ # Map FFI parse errors into Ruby exceptions.
46
+ #
47
+ # @param kind [String]
48
+ # @param msg [String]
49
+ # @param details [Hash<String, Object>]
50
+ # @return [::Oso::Polar::ParseError] the object converted into the expected format.
51
+ private_class_method def self.parse_error(kind, msg:, details:) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
52
+ case kind
53
+ when 'ExtraToken'
54
+ ::Oso::Polar::ParseError::ExtraToken.new(msg, details: details)
55
+ when 'IntegerOverflow'
56
+ ::Oso::Polar::ParseError::IntegerOverflow.new(msg, details: details)
57
+ when 'InvalidToken'
58
+ ::Oso::Polar::ParseError::InvalidToken.new(msg, details: details)
59
+ when 'InvalidTokenCharacter'
60
+ ::Oso::Polar::ParseError::InvalidTokenCharacter.new(msg, details: details)
61
+ when 'UnrecognizedEOF'
62
+ ::Oso::Polar::ParseError::UnrecognizedEOF.new(msg, details: details)
63
+ when 'UnrecognizedToken'
64
+ ::Oso::Polar::ParseError::UnrecognizedToken.new(msg, details: details)
65
+ else
66
+ ::Oso::Polar::ParseError.new(msg, details: details)
67
+ end
68
+ end
69
+
70
+ # Map FFI runtime errors into Ruby exceptions.
71
+ #
72
+ # @param kind [String]
73
+ # @param msg [String]
74
+ # @param details [Hash<String, Object>]
75
+ # @return [::Oso::Polar::PolarRuntimeError] the object converted into the expected format.
76
+ private_class_method def self.runtime_error(kind, msg:, details:) # rubocop:disable Metrics/MethodLength
77
+ case kind
78
+ when 'Serialization'
79
+ ::Oso::Polar::SerializationError.new(msg, details: details)
80
+ when 'Unsupported'
81
+ ::Oso::Polar::UnsupportedError.new(msg, details: details)
82
+ when 'TypeError'
83
+ ::Oso::Polar::PolarTypeError.new(msg, details: details)
84
+ when 'StackOverflow'
85
+ ::Oso::Polar::StackOverflowError.new(msg, details: details)
86
+ else
87
+ ::Oso::Polar::PolarRuntimeError.new(msg, details: details)
88
+ end
89
+ end
90
+
91
+ # Map FFI operational errors into Ruby exceptions.
92
+ #
93
+ # @param kind [String]
94
+ # @param msg [String]
95
+ # @param details [Hash<String, Object>]
96
+ # @return [::Oso::Polar::OperationalError] the object converted into the expected format.
97
+ private_class_method def self.operational_error(kind, msg:, details:)
98
+ case kind
99
+ when 'Unknown' # Rust panics.
100
+ ::Oso::Polar::UnknownError.new(msg, details: details)
101
+ else
102
+ ::Oso::Polar::OperationalError.new(msg, details: details)
103
+ end
104
+ end
105
+
106
+ # Map FFI API errors into Ruby exceptions.
107
+ #
108
+ # @param kind [String]
109
+ # @param msg [String]
110
+ # @param details [Hash<String, Object>]
111
+ # @return [::Oso::Polar::ApiError] the object converted into the expected format.
112
+ private_class_method def self.api_error(kind, msg:, details:)
113
+ case kind
114
+ when 'Parameter'
115
+ ::Oso::Polar::ParameterError.new(msg, details: details)
116
+ else
117
+ ::Oso::Polar::ApiError.new(msg, details: details)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ module FFI
6
+ # Wrapper class for Polar FFI pointer + operations.
7
+ class Polar < ::FFI::AutoPointer
8
+ Rust = Module.new do
9
+ extend ::FFI::Library
10
+ ffi_lib FFI::LIB_PATH
11
+
12
+ attach_function :new, :polar_new, [], FFI::Polar
13
+ attach_function :load_str, :polar_load, [FFI::Polar, :string, :string], :int32
14
+ attach_function :next_inline_query, :polar_next_inline_query, [FFI::Polar], FFI::Query
15
+ attach_function :new_id, :polar_get_external_id, [FFI::Polar], :uint64
16
+ attach_function :new_query_from_str, :polar_new_query, [FFI::Polar, :string], FFI::Query
17
+ attach_function :new_query_from_term, :polar_new_query_from_term, [FFI::Polar, :string], FFI::Query
18
+ attach_function :new_query_from_repl, :polar_query_from_repl, [FFI::Polar], FFI::Query
19
+ attach_function :register_constant, :polar_register_constant, [FFI::Polar, :string, :string], :int32
20
+ attach_function :free, :polar_free, [FFI::Polar], :int32
21
+ end
22
+ private_constant :Rust
23
+
24
+ # @return [FFI::Polar]
25
+ # @raise [FFI::Error] if the FFI call returns an error.
26
+ def self.create
27
+ polar = Rust.new
28
+ raise FFI::Error.get if polar.null?
29
+
30
+ polar
31
+ end
32
+
33
+ # @param src [String]
34
+ # @param filename [String]
35
+ # @raise [FFI::Error] if the FFI call returns an error.
36
+ def load_str(src, filename: nil)
37
+ raise FFI::Error.get if Rust.load_str(self, src, filename).zero?
38
+ end
39
+
40
+ # @return [FFI::Query] if there are remaining inline queries.
41
+ # @return [nil] if there are no remaining inline queries.
42
+ # @raise [FFI::Error] if the FFI call returns an error.
43
+ def next_inline_query
44
+ query = Rust.next_inline_query(self)
45
+ query.null? ? nil : query
46
+ end
47
+
48
+ # @return [Integer]
49
+ # @raise [FFI::Error] if the FFI call returns an error.
50
+ def new_id
51
+ id = Rust.new_id(self)
52
+ # TODO(gj): I don't think this error check is correct. If getting a new ID fails on the
53
+ # Rust side, it'll probably surface as a panic (e.g., the KB lock is poisoned).
54
+ raise FFI::Error.get if id.zero?
55
+
56
+ id
57
+ end
58
+
59
+ # @param str [String] Query string.
60
+ # @return [FFI::Query]
61
+ # @raise [FFI::Error] if the FFI call returns an error.
62
+ def new_query_from_str(str)
63
+ query = Rust.new_query_from_str(self, str)
64
+ raise FFI::Error.get if query.null?
65
+
66
+ query
67
+ end
68
+
69
+ # @param term [Hash<String, Object>]
70
+ # @return [FFI::Query]
71
+ # @raise [FFI::Error] if the FFI call returns an error.
72
+ def new_query_from_term(term)
73
+ query = Rust.new_query_from_term(self, JSON.dump(term))
74
+ raise FFI::Error.get if query.null?
75
+
76
+ query
77
+ end
78
+
79
+ # @return [FFI::Query]
80
+ # @raise [FFI::Error] if the FFI call returns an error.
81
+ def new_query_from_repl
82
+ query = Rust.new_query_from_repl(self)
83
+ raise FFI::Error.get if query.null?
84
+
85
+ query
86
+ end
87
+
88
+ # @param name [String]
89
+ # @param value [Hash<String, Object>]
90
+ # @raise [FFI::Error] if the FFI call returns an error.
91
+ def register_constant(name, value:)
92
+ raise FFI::Error.get if Rust.register_constant(self, name, JSON.dump(value)).zero?
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ module FFI
6
+ # Wrapper class for Query FFI pointer + operations.
7
+ class Query < ::FFI::AutoPointer
8
+ Rust = Module.new do
9
+ extend ::FFI::Library
10
+ ffi_lib FFI::LIB_PATH
11
+
12
+ attach_function :debug_command, :polar_debug_command, [FFI::Query, :string], :int32
13
+ attach_function :call_result, :polar_call_result, [FFI::Query, :uint64, :string], :int32
14
+ attach_function :question_result, :polar_question_result, [FFI::Query, :uint64, :int32], :int32
15
+ attach_function :next_event, :polar_next_query_event, [FFI::Query], FFI::QueryEvent
16
+ attach_function :free, :query_free, [FFI::Query], :int32
17
+ end
18
+ private_constant :Rust
19
+
20
+ # @param cmd [String]
21
+ # @raise [FFI::Error] if the FFI call returns an error.
22
+ def debug_command(cmd)
23
+ res = Rust.debug_command(self, cmd)
24
+ raise FFI::Error.get if res.zero?
25
+ end
26
+
27
+ # @param result [String]
28
+ # @param call_id [Integer]
29
+ # @raise [FFI::Error] if the FFI call returns an error.
30
+ def call_result(result, call_id:)
31
+ res = Rust.call_result(self, call_id, result)
32
+ raise FFI::Error.get if res.zero?
33
+ end
34
+
35
+ # @param result [Boolean]
36
+ # @param call_id [Integer]
37
+ # @raise [FFI::Error] if the FFI call returns an error.
38
+ def question_result(result, call_id:)
39
+ result = result ? 1 : 0
40
+ res = Rust.question_result(self, call_id, result)
41
+ raise FFI::Error.get if res.zero?
42
+ end
43
+
44
+ # @return [::Oso::Polar::QueryEvent]
45
+ # @raise [FFI::Error] if the FFI call returns an error.
46
+ def next_event
47
+ event = Rust.next_event(self)
48
+ raise FFI::Error.get if event.null?
49
+
50
+ ::Oso::Polar::QueryEvent.new(JSON.parse(event.to_s))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ module FFI
6
+ # Wrapper class for QueryEvent FFI pointer + operations.
7
+ class QueryEvent < ::FFI::AutoPointer
8
+ # @return [String]
9
+ def to_s
10
+ @to_s ||= read_string.force_encoding('UTF-8')
11
+ end
12
+
13
+ Rust = Module.new do
14
+ extend ::FFI::Library
15
+ ffi_lib FFI::LIB_PATH
16
+
17
+ attach_function :free, :string_free, [FFI::QueryEvent], :int32
18
+ end
19
+ private_constant :Rust
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,381 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'set'
5
+
6
+ module Oso
7
+ module Polar
8
+ # Create and manage an instance of the Polar runtime.
9
+ class Polar # rubocop:disable Metrics/ClassLength
10
+ def initialize
11
+ @ffi_instance = FFI::Polar.create
12
+ @calls = {}
13
+ @classes = {}
14
+ @constructors = {}
15
+ @instances = {}
16
+ @load_queue = Set.new
17
+ end
18
+
19
+ # Enqueue a Polar policy file for loading into the KB.
20
+ #
21
+ # @param name [String]
22
+ # @raise [PolarFileExtensionError] if provided filename has invalid extension.
23
+ # @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)
27
+
28
+ load_queue << name
29
+ end
30
+
31
+ # Load a Polar string into the KB.
32
+ #
33
+ # @param str [String] Polar string to load.
34
+ # @param filename [String] Name of Polar source file.
35
+ # @raise [NullByteInPolarFileError] if str includes a non-terminating null byte.
36
+ # @raise [InlineQueryFailedError] on the first failed inline query.
37
+ # @raise [Error] if any of the FFI calls raise one.
38
+ def load_str(str, filename: nil) # rubocop:disable Metrics/MethodLength
39
+ raise NullByteInPolarFileError if str.chomp("\0").include?("\0")
40
+
41
+ ffi_instance.load_str(str, filename: filename)
42
+ loop do
43
+ next_query = ffi_instance.next_inline_query
44
+ break if next_query.nil?
45
+
46
+ begin
47
+ Query.new(next_query, polar: self).results.next
48
+ rescue StopIteration
49
+ raise InlineQueryFailedError
50
+ end
51
+ end
52
+ end
53
+
54
+ # Replace the current Polar instance but retain all registered classes and
55
+ # constructors.
56
+ def clear
57
+ @ffi_instance = FFI::Polar.create
58
+ end
59
+
60
+ # Query for a predicate.
61
+ #
62
+ # @param name [String]
63
+ # @param args [Array<Object>]
64
+ # @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
98
+ end
99
+
100
+ # Register a Ruby class with Polar.
101
+ #
102
+ # @param cls [Class]
103
+ # @param name [String]
104
+ # @param from_polar [Proc]
105
+ # @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
+
122
+ register_constant(name, value: cls)
123
+ end
124
+
125
+ 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
312
+ end
313
+
314
+ private
315
+
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
+ # @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
379
+ end
380
+ end
381
+ end