oso-oso 0.2.2

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.
@@ -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