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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.solargraph.yml +17 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +95 -0
- data/Makefile +16 -0
- data/README.md +30 -0
- data/Rakefile +9 -0
- data/bin/console +33 -0
- data/bin/setup +6 -0
- data/ext/oso-oso/lib/libpolar.dylib +0 -0
- data/ext/oso-oso/lib/libpolar.so +0 -0
- data/lib/oso.rb +14 -0
- data/lib/oso/http.rb +16 -0
- data/lib/oso/oso.rb +47 -0
- data/lib/oso/path_mapper.rb +29 -0
- data/lib/oso/polar.rb +18 -0
- data/lib/oso/polar/errors.rb +84 -0
- data/lib/oso/polar/ffi.rb +47 -0
- data/lib/oso/polar/ffi/error.rb +123 -0
- data/lib/oso/polar/ffi/polar.rb +97 -0
- data/lib/oso/polar/ffi/query.rb +55 -0
- data/lib/oso/polar/ffi/query_event.rb +23 -0
- data/lib/oso/polar/polar.rb +381 -0
- data/lib/oso/polar/predicate.rb +26 -0
- data/lib/oso/polar/query.rb +117 -0
- data/lib/oso/polar/query_event.rb +22 -0
- data/lib/oso/polar/variable.rb +20 -0
- data/lib/oso/version.rb +5 -0
- data/oso-oso.gemspec +35 -0
- metadata +157 -0
|
@@ -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
|