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.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +14 -13
- data/Makefile +10 -1
- data/README.md +6 -9
- data/Rakefile +0 -1
- data/bin/oso +7 -0
- data/ext/oso-oso/lib/libpolar.dylib +0 -0
- data/ext/oso-oso/lib/libpolar.so +0 -0
- data/ext/oso-oso/lib/polar.dll +0 -0
- data/lib/oso/http.rb +1 -1
- data/lib/oso/oso.rb +14 -33
- data/lib/oso/path_mapper.rb +1 -1
- data/lib/oso/polar.rb +1 -0
- data/lib/oso/polar/errors.rb +25 -9
- data/lib/oso/polar/ffi.rb +14 -5
- data/lib/oso/polar/ffi/error.rb +2 -2
- data/lib/oso/polar/ffi/message.rb +37 -0
- data/lib/oso/polar/ffi/polar.rb +28 -18
- data/lib/oso/polar/ffi/query.rb +25 -0
- data/lib/oso/polar/host.rb +248 -0
- data/lib/oso/polar/polar.rb +171 -305
- data/lib/oso/polar/query.rb +99 -39
- data/lib/oso/version.rb +1 -1
- data/oso-oso.gemspec +10 -7
- metadata +30 -11
- data/bin/console +0 -33
- data/bin/setup +0 -6
data/lib/oso/polar/ffi/query.rb
CHANGED
@@ -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
|
data/lib/oso/polar/polar.rb
CHANGED
@@ -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
|
-
@
|
12
|
-
@
|
13
|
-
@
|
14
|
-
@
|
15
|
-
|
16
|
-
|
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
|
-
#
|
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
|
26
|
-
|
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
|
-
|
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
|
-
|
100
|
+
ffi_polar.load_str(str, filename: filename)
|
42
101
|
loop do
|
43
|
-
next_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,
|
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
|
-
#
|
55
|
-
#
|
56
|
-
|
57
|
-
|
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
|
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
|
66
|
-
|
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)
|
107
|
-
|
108
|
-
|
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
|
-
|
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
|
-
#
|
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
|
281
|
-
# @
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
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 :
|
324
|
-
# @return [Hash<
|
325
|
-
attr_reader :
|
326
|
-
# @return [
|
327
|
-
attr_reader :
|
328
|
-
|
329
|
-
#
|
330
|
-
def
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
#
|
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
|
-
#
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
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
|
-
#
|
212
|
+
# Process a line of user input in the REPL.
|
358
213
|
#
|
359
|
-
# @param
|
360
|
-
#
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
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
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
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
|
-
|
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
|