rjq 0.1.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 +7 -0
- data/ARCHITECTURE.md +38 -0
- data/CHANGELOG.md +7 -0
- data/COMPATIBILITY.md +48 -0
- data/CONTRIBUTING.md +30 -0
- data/LICENSE.txt +21 -0
- data/README.md +192 -0
- data/SECURITY.md +10 -0
- data/bin/rjq +9 -0
- data/lib/rjq/ast.rb +1475 -0
- data/lib/rjq/builtins/array.rb +3 -0
- data/lib/rjq/builtins/core.rb +3 -0
- data/lib/rjq/builtins/date.rb +3 -0
- data/lib/rjq/builtins/format.rb +3 -0
- data/lib/rjq/builtins/io.rb +3 -0
- data/lib/rjq/builtins/math.rb +3 -0
- data/lib/rjq/builtins/regex.rb +3 -0
- data/lib/rjq/builtins/sql.rb +3 -0
- data/lib/rjq/builtins/stream.rb +3 -0
- data/lib/rjq/builtins/string.rb +3 -0
- data/lib/rjq/builtins.rb +2103 -0
- data/lib/rjq/cli.rb +459 -0
- data/lib/rjq/color.rb +36 -0
- data/lib/rjq/compiler.rb +392 -0
- data/lib/rjq/errors.rb +76 -0
- data/lib/rjq/json/dumper.rb +191 -0
- data/lib/rjq/json/input_buffer.rb +99 -0
- data/lib/rjq/json/parser.rb +405 -0
- data/lib/rjq/json/stream_parser.rb +526 -0
- data/lib/rjq/json.rb +4 -0
- data/lib/rjq/lexer.rb +344 -0
- data/lib/rjq/math_functions.rb +168 -0
- data/lib/rjq/module_loader.rb +178 -0
- data/lib/rjq/modules.rb +88 -0
- data/lib/rjq/number.rb +189 -0
- data/lib/rjq/opcodes.rb +130 -0
- data/lib/rjq/parser.rb +755 -0
- data/lib/rjq/path.rb +250 -0
- data/lib/rjq/runtime.rb +377 -0
- data/lib/rjq/semantic_analyzer.rb +209 -0
- data/lib/rjq/value.rb +287 -0
- data/lib/rjq/version.rb +5 -0
- data/lib/rjq/vm.rb +1359 -0
- data/lib/rjq.rb +41 -0
- metadata +106 -0
data/lib/rjq/lexer.rb
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rjq
|
|
4
|
+
Token = Struct.new(:type, :value, :line, :column, :start_offset, :end_offset, :filename, keyword_init: true)
|
|
5
|
+
SourceFragment = Struct.new(:source, :filename, :line, :column, :start_offset, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
class Lexer
|
|
8
|
+
KEYWORDS = %w[
|
|
9
|
+
if then elif else end as def reduce foreach try catch label import include module
|
|
10
|
+
null true false and or not break
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
TWO_CHAR_OPERATORS = ['//', '==', '!=', '<=', '>=', '|=', '+=', '-=', '*=', '/=', '%=', '..'].freeze
|
|
14
|
+
THREE_CHAR_OPERATORS = ['//='].freeze
|
|
15
|
+
SINGLE_CHAR_TOKENS = {
|
|
16
|
+
'.' => :dot,
|
|
17
|
+
'|' => :pipe,
|
|
18
|
+
',' => :comma,
|
|
19
|
+
'(' => :lparen,
|
|
20
|
+
')' => :rparen,
|
|
21
|
+
'[' => :lbracket,
|
|
22
|
+
']' => :rbracket,
|
|
23
|
+
'{' => :lbrace,
|
|
24
|
+
'}' => :rbrace,
|
|
25
|
+
':' => :colon,
|
|
26
|
+
';' => :semicolon,
|
|
27
|
+
'?' => :question,
|
|
28
|
+
'+' => :operator,
|
|
29
|
+
'-' => :operator,
|
|
30
|
+
'*' => :operator,
|
|
31
|
+
'/' => :operator,
|
|
32
|
+
'%' => :operator,
|
|
33
|
+
'=' => :operator,
|
|
34
|
+
'<' => :operator,
|
|
35
|
+
'>' => :operator
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
attr_reader :tokens
|
|
39
|
+
|
|
40
|
+
def initialize(source, allow_comments: true, source_name: '<top-level>', initial_line: 1, initial_column: 1,
|
|
41
|
+
start_offset: 0)
|
|
42
|
+
@source = source.to_s
|
|
43
|
+
@allow_comments = allow_comments
|
|
44
|
+
@source_name = source_name
|
|
45
|
+
@offset_base = start_offset
|
|
46
|
+
@index = 0
|
|
47
|
+
@line = initial_line
|
|
48
|
+
@column = initial_column
|
|
49
|
+
@tokens = []
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def tokenize
|
|
53
|
+
until eof?
|
|
54
|
+
skip_space
|
|
55
|
+
break if eof?
|
|
56
|
+
|
|
57
|
+
start_line = @line
|
|
58
|
+
start_column = @column
|
|
59
|
+
start_offset = @index
|
|
60
|
+
char = current
|
|
61
|
+
token =
|
|
62
|
+
if char == '#'
|
|
63
|
+
unless @allow_comments
|
|
64
|
+
raise ParseError,
|
|
65
|
+
"comments are disabled at line #{start_line}, column #{start_column}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
skip_comment
|
|
69
|
+
next
|
|
70
|
+
elsif char == '"'
|
|
71
|
+
Token.new(type: :string, value: read_string, line: start_line, column: start_column)
|
|
72
|
+
elsif char == '$'
|
|
73
|
+
advance
|
|
74
|
+
Token.new(type: :variable, value: read_identifier, line: start_line, column: start_column)
|
|
75
|
+
elsif char == '@'
|
|
76
|
+
advance
|
|
77
|
+
Token.new(type: :format, value: read_identifier, line: start_line, column: start_column)
|
|
78
|
+
elsif number_start?(char) || (char == '.' && digit?(@source[@index + 1]))
|
|
79
|
+
Token.new(type: :number, value: read_number, line: start_line, column: start_column)
|
|
80
|
+
elsif identifier_start?(char)
|
|
81
|
+
identifier = read_identifier
|
|
82
|
+
type = KEYWORDS.include?(identifier) ? :keyword : :identifier
|
|
83
|
+
Token.new(type: type, value: identifier, line: start_line, column: start_column)
|
|
84
|
+
else
|
|
85
|
+
read_operator_or_punctuation(start_line, start_column)
|
|
86
|
+
end
|
|
87
|
+
if token
|
|
88
|
+
token.start_offset = @offset_base + start_offset
|
|
89
|
+
token.end_offset = @offset_base + @index
|
|
90
|
+
token.filename = @source_name
|
|
91
|
+
@tokens << token
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
offset = @offset_base + @index
|
|
95
|
+
@tokens << Token.new(type: :eof, value: nil, line: @line, column: @column, start_offset: offset,
|
|
96
|
+
end_offset: offset, filename: @source_name)
|
|
97
|
+
@tokens
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def read_operator_or_punctuation(line, column)
|
|
103
|
+
three = @source[@index, 3]
|
|
104
|
+
if THREE_CHAR_OPERATORS.include?(three)
|
|
105
|
+
3.times { advance }
|
|
106
|
+
return Token.new(type: :operator, value: three, line: line, column: column)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
two = @source[@index, 2]
|
|
110
|
+
if TWO_CHAR_OPERATORS.include?(two)
|
|
111
|
+
2.times { advance }
|
|
112
|
+
return Token.new(type: :operator, value: two, line: line, column: column)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
type = SINGLE_CHAR_TOKENS[current]
|
|
116
|
+
raise ParseError, "unexpected character #{current.inspect} at line #{line}, column #{column}" unless type
|
|
117
|
+
|
|
118
|
+
value = current
|
|
119
|
+
advance
|
|
120
|
+
Token.new(type: type, value: value, line: line, column: column)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def read_string
|
|
124
|
+
expect('"')
|
|
125
|
+
segments = []
|
|
126
|
+
buffer = +''
|
|
127
|
+
until eof?
|
|
128
|
+
char = advance
|
|
129
|
+
if char == '"'
|
|
130
|
+
return segments.empty? ? buffer : segments_with_buffer(segments, buffer)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
if char == '\\'
|
|
134
|
+
if current == '('
|
|
135
|
+
advance
|
|
136
|
+
segments << [:text, buffer] unless buffer.empty?
|
|
137
|
+
buffer = +''
|
|
138
|
+
fragment_line = @line
|
|
139
|
+
fragment_column = @column
|
|
140
|
+
fragment_offset = @offset_base + @index
|
|
141
|
+
fragment = SourceFragment.new(
|
|
142
|
+
source: read_interpolation,
|
|
143
|
+
filename: @source_name,
|
|
144
|
+
line: fragment_line,
|
|
145
|
+
column: fragment_column,
|
|
146
|
+
start_offset: fragment_offset
|
|
147
|
+
)
|
|
148
|
+
segments << [:expr, fragment]
|
|
149
|
+
else
|
|
150
|
+
buffer << read_escape
|
|
151
|
+
end
|
|
152
|
+
else
|
|
153
|
+
raise parse_error('unescaped control character in string') if char.ord < 0x20
|
|
154
|
+
|
|
155
|
+
buffer << char
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
raise parse_error('unterminated string')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def segments_with_buffer(segments, buffer)
|
|
162
|
+
segments << [:text, buffer] unless buffer.empty?
|
|
163
|
+
segments
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def read_interpolation
|
|
167
|
+
frames = [{ type: :expression, depth: 1 }]
|
|
168
|
+
start = @index
|
|
169
|
+
|
|
170
|
+
until eof?
|
|
171
|
+
char = advance
|
|
172
|
+
frame = frames.last
|
|
173
|
+
case frame.fetch(:type)
|
|
174
|
+
when :comment
|
|
175
|
+
frames.pop if char == "\n"
|
|
176
|
+
when :string
|
|
177
|
+
if char == '\\'
|
|
178
|
+
if current == '('
|
|
179
|
+
advance
|
|
180
|
+
frames << { type: :expression, depth: 1 }
|
|
181
|
+
else
|
|
182
|
+
advance unless eof?
|
|
183
|
+
end
|
|
184
|
+
elsif char == frame.fetch(:quote)
|
|
185
|
+
frames.pop
|
|
186
|
+
end
|
|
187
|
+
when :expression
|
|
188
|
+
case char
|
|
189
|
+
when '#'
|
|
190
|
+
frames << { type: :comment }
|
|
191
|
+
when '"', "'"
|
|
192
|
+
frames << { type: :string, quote: char }
|
|
193
|
+
when '('
|
|
194
|
+
frame[:depth] += 1
|
|
195
|
+
when ')'
|
|
196
|
+
frame[:depth] -= 1
|
|
197
|
+
if frame.fetch(:depth).zero?
|
|
198
|
+
frames.pop
|
|
199
|
+
return @source[start...(@index - 1)] if frames.empty?
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
raise parse_error('unterminated interpolation')
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def read_escape
|
|
209
|
+
raise parse_error('unterminated escape') if eof?
|
|
210
|
+
|
|
211
|
+
char = advance
|
|
212
|
+
case char
|
|
213
|
+
when '"', '\\', '/'
|
|
214
|
+
char
|
|
215
|
+
when 'b'
|
|
216
|
+
"\b"
|
|
217
|
+
when 'f'
|
|
218
|
+
"\f"
|
|
219
|
+
when 'n'
|
|
220
|
+
"\n"
|
|
221
|
+
when 'r'
|
|
222
|
+
"\r"
|
|
223
|
+
when 't'
|
|
224
|
+
"\t"
|
|
225
|
+
when 'u'
|
|
226
|
+
read_unicode_escape
|
|
227
|
+
else
|
|
228
|
+
raise parse_error("invalid escape \\#{char}")
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def read_unicode_escape
|
|
233
|
+
codepoint = read_hex4
|
|
234
|
+
if codepoint.between?(0xD800, 0xDBFF)
|
|
235
|
+
raise parse_error('missing low surrogate') unless @source[@index, 2] == '\\u'
|
|
236
|
+
|
|
237
|
+
2.times { advance }
|
|
238
|
+
low = read_hex4
|
|
239
|
+
raise parse_error('invalid low surrogate') unless low.between?(0xDC00, 0xDFFF)
|
|
240
|
+
|
|
241
|
+
codepoint = 0x10000 + ((codepoint - 0xD800) << 10) + (low - 0xDC00)
|
|
242
|
+
elsif codepoint.between?(0xDC00, 0xDFFF)
|
|
243
|
+
raise parse_error('unexpected low surrogate')
|
|
244
|
+
end
|
|
245
|
+
[codepoint].pack('U')
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def read_hex4
|
|
249
|
+
text = @source[@index, 4]
|
|
250
|
+
raise parse_error('invalid unicode escape') unless text&.match?(/\A[0-9a-fA-F]{4}\z/)
|
|
251
|
+
|
|
252
|
+
4.times { advance }
|
|
253
|
+
text.to_i(16)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def read_number
|
|
257
|
+
start = @index
|
|
258
|
+
leading_decimal = current == '.'
|
|
259
|
+
advance if leading_decimal
|
|
260
|
+
advance while digit?(current)
|
|
261
|
+
if !leading_decimal && current == '.'
|
|
262
|
+
advance
|
|
263
|
+
advance while digit?(current)
|
|
264
|
+
end
|
|
265
|
+
if %w[e E].include?(current)
|
|
266
|
+
advance
|
|
267
|
+
advance if ['+', '-'].include?(current)
|
|
268
|
+
advance while digit?(current)
|
|
269
|
+
end
|
|
270
|
+
text = @source[start...@index]
|
|
271
|
+
text = "0#{text}" if leading_decimal
|
|
272
|
+
Number.parse(text)
|
|
273
|
+
rescue ArgumentError
|
|
274
|
+
raise parse_error('invalid number')
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def read_identifier
|
|
278
|
+
start = @index
|
|
279
|
+
raise parse_error('expected identifier') unless identifier_start?(current)
|
|
280
|
+
|
|
281
|
+
advance while identifier_part?(current)
|
|
282
|
+
@source[start...@index]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def skip_space
|
|
286
|
+
loop do
|
|
287
|
+
advance while current&.match?(/[ \t\r\n]/)
|
|
288
|
+
break unless @allow_comments && current == '#'
|
|
289
|
+
|
|
290
|
+
skip_comment
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def skip_comment
|
|
295
|
+
advance until eof? || current == "\n"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def number_start?(char)
|
|
299
|
+
digit?(char)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def identifier_start?(char)
|
|
303
|
+
!char.nil? && char.match?(/[A-Za-z_]/)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def identifier_part?(char)
|
|
307
|
+
!char.nil? && char.match?(/[A-Za-z0-9_]/)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def digit?(char)
|
|
311
|
+
!char.nil? && char >= '0' && char <= '9'
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def expect(char)
|
|
315
|
+
raise parse_error("expected #{char}") unless current == char
|
|
316
|
+
|
|
317
|
+
advance
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def current
|
|
321
|
+
@source[@index]
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def eof?
|
|
325
|
+
@index >= @source.length
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def advance
|
|
329
|
+
char = @source[@index]
|
|
330
|
+
@index += char.length
|
|
331
|
+
if char == "\n"
|
|
332
|
+
@line += 1
|
|
333
|
+
@column = 1
|
|
334
|
+
else
|
|
335
|
+
@column += 1
|
|
336
|
+
end
|
|
337
|
+
char
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def parse_error(message)
|
|
341
|
+
ParseError.new("#{message} at #{@source_name}, line #{@line}, column #{@column}")
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fiddle/import'
|
|
4
|
+
|
|
5
|
+
module Rjq
|
|
6
|
+
module MathFunctions
|
|
7
|
+
CANDIDATE_LIBRARIES = [
|
|
8
|
+
'/usr/lib/libSystem.B.dylib',
|
|
9
|
+
'libm.so.6',
|
|
10
|
+
'libm.so',
|
|
11
|
+
'libm.dylib'
|
|
12
|
+
].freeze
|
|
13
|
+
NATIVE_SIGNATURES = {
|
|
14
|
+
fma: 'double fma(double, double, double)',
|
|
15
|
+
remainder: 'double remainder(double, double)',
|
|
16
|
+
scalb: 'double scalb(double, double)',
|
|
17
|
+
scalbln: 'double scalbln(double, long)'
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def bessel(name, *values)
|
|
23
|
+
if %w[jn yn].include?(name)
|
|
24
|
+
bessel_library.public_send(name, values.fetch(0).to_i, values.fetch(1).to_f)
|
|
25
|
+
else
|
|
26
|
+
bessel_library.public_send(name, values.fetch(0).to_f)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def fma(left, right, addend)
|
|
31
|
+
library = native_library(:fma)
|
|
32
|
+
return library.fma(left.to_f, right.to_f, addend.to_f) if library
|
|
33
|
+
|
|
34
|
+
raise Rjq::RuntimeError, 'native fused multiply-add is not available on this platform'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def remainder(left, right)
|
|
38
|
+
library = native_library(:remainder)
|
|
39
|
+
return library.remainder(left.to_f, right.to_f) if library
|
|
40
|
+
|
|
41
|
+
raise Rjq::RuntimeError, 'native IEEE remainder is not available on this platform'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def scalb(value, exponent)
|
|
45
|
+
library = native_library(:scalb)
|
|
46
|
+
if library
|
|
47
|
+
result = library.scalb(value.to_f, exponent.to_f)
|
|
48
|
+
return signed_max(value.to_f) if result.infinite?
|
|
49
|
+
|
|
50
|
+
return result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
exponent = exponent.to_f
|
|
54
|
+
return Float::NAN if exponent.nan?
|
|
55
|
+
return portable_scale(value.to_f, exponent.positive? ? 10_000 : -10_000) if exponent.infinite?
|
|
56
|
+
|
|
57
|
+
portable_scale(value.to_f, exponent.to_i)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def scalbln(value, exponent)
|
|
61
|
+
exponent = exponent.to_f
|
|
62
|
+
if RUBY_PLATFORM.include?('linux') && !exponent.finite?
|
|
63
|
+
# jq's Linux build passes non-finite values through a C long
|
|
64
|
+
# conversion, which yields zero on the supported libc targets.
|
|
65
|
+
return 0.0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
integral_exponent = c_long_exponent(exponent)
|
|
69
|
+
library = native_library(:scalbln)
|
|
70
|
+
if library
|
|
71
|
+
result = library.scalbln(value.to_f, integral_exponent)
|
|
72
|
+
return signed_max(value.to_f) if result.infinite?
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
portable_scale(value.to_f, integral_exponent)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def native_available?(name)
|
|
81
|
+
!native_library(name).nil?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def native_library(name)
|
|
85
|
+
@native_libraries ||= {}
|
|
86
|
+
return @native_libraries[name] if @native_libraries.key?(name)
|
|
87
|
+
|
|
88
|
+
signature = NATIVE_SIGNATURES.fetch(name)
|
|
89
|
+
@native_libraries[name] = CANDIDATE_LIBRARIES.lazy.filter_map do |library|
|
|
90
|
+
build_native_library(library, signature)
|
|
91
|
+
rescue Fiddle::DLError
|
|
92
|
+
nil
|
|
93
|
+
end.first
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def build_native_library(library, signature)
|
|
97
|
+
Module.new do
|
|
98
|
+
extend Fiddle::Importer
|
|
99
|
+
|
|
100
|
+
dlload library
|
|
101
|
+
extern signature
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def c_long_exponent(value)
|
|
106
|
+
bits = Fiddle::SIZEOF_LONG * 8
|
|
107
|
+
minimum = -(1 << (bits - 1))
|
|
108
|
+
maximum = (1 << (bits - 1)) - 1
|
|
109
|
+
return 0 if value.nan?
|
|
110
|
+
return value.positive? ? maximum : minimum if value.infinite?
|
|
111
|
+
|
|
112
|
+
[[value.to_i, minimum].max, maximum].min
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def portable_scale(value, exponent)
|
|
116
|
+
return value if value.zero?
|
|
117
|
+
return signed_max(value) if value.infinite?
|
|
118
|
+
|
|
119
|
+
_, value_exponent = Math.frexp(value.abs)
|
|
120
|
+
target_exponent = value_exponent + exponent
|
|
121
|
+
return signed_max(value) if target_exponent > 1024
|
|
122
|
+
return value.negative? ? -0.0 : 0.0 if target_exponent < -1074
|
|
123
|
+
|
|
124
|
+
result = Math.ldexp(value, exponent)
|
|
125
|
+
result.infinite? ? signed_max(value) : result
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def signed_max(value)
|
|
129
|
+
value.negative? ? -Float::MAX : Float::MAX
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def bessel_library
|
|
133
|
+
@bessel_library ||= load_bessel_library
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def bessel_available?
|
|
137
|
+
bessel_library
|
|
138
|
+
true
|
|
139
|
+
rescue Rjq::RuntimeError
|
|
140
|
+
false
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def load_bessel_library
|
|
144
|
+
errors = []
|
|
145
|
+
CANDIDATE_LIBRARIES.each do |library|
|
|
146
|
+
return build_bessel_library(library)
|
|
147
|
+
rescue Fiddle::DLError => e
|
|
148
|
+
errors << "#{library}: #{e.message}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
raise Rjq::RuntimeError, "C math library with Bessel functions is not available (#{errors.join('; ')})"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def build_bessel_library(library)
|
|
155
|
+
Module.new do
|
|
156
|
+
extend Fiddle::Importer
|
|
157
|
+
|
|
158
|
+
dlload library
|
|
159
|
+
extern 'double j0(double)'
|
|
160
|
+
extern 'double j1(double)'
|
|
161
|
+
extern 'double y0(double)'
|
|
162
|
+
extern 'double y1(double)'
|
|
163
|
+
extern 'double jn(int, double)'
|
|
164
|
+
extern 'double yn(int, double)'
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rjq
|
|
4
|
+
class ModuleLoader
|
|
5
|
+
Result = Struct.new(:program, :metadata, :variables, keyword_init: true)
|
|
6
|
+
MAX_DEPTH = 64
|
|
7
|
+
|
|
8
|
+
def initialize(resolver, allow_comments: true, max_filter_depth: Parser::DEFAULT_MAX_FILTER_DEPTH)
|
|
9
|
+
@resolver = resolver
|
|
10
|
+
@cache = {}
|
|
11
|
+
@allow_comments = allow_comments
|
|
12
|
+
@max_filter_depth = max_filter_depth
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def load(program, source_path: nil, stack: [])
|
|
16
|
+
raise CompileError, "module import depth exceeds #{MAX_DEPTH}" if stack.length >= MAX_DEPTH
|
|
17
|
+
program.directives.select { |directive| directive.type == :module }.each do |directive|
|
|
18
|
+
ConstantEvaluator.evaluate_object(directive.metadata)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
definitions = []
|
|
22
|
+
variables = {}
|
|
23
|
+
metadata = @resolver.initial_metadata.dup
|
|
24
|
+
program.directives.each do |directive|
|
|
25
|
+
next if directive.type == :module
|
|
26
|
+
|
|
27
|
+
options = directive.metadata ? ConstantEvaluator.evaluate_object(directive.metadata) : {}
|
|
28
|
+
data = directive.alias_name&.start_with?('$') || false
|
|
29
|
+
source = @resolver.resolve(directive.name, from: source_path, metadata: options, data: data)
|
|
30
|
+
raise CompileError, "circular module import: #{directive.name}" if stack.include?(source.path)
|
|
31
|
+
|
|
32
|
+
if data
|
|
33
|
+
value = JSON::Parser.parse_one(source.content)
|
|
34
|
+
name = directive.alias_name.delete_prefix('$')
|
|
35
|
+
variables[name] = value
|
|
36
|
+
definitions << AST::FunctionDefinition.new("#{name}::#{name}", [], AST::Literal.new(value))
|
|
37
|
+
metadata[directive.name] ||= data_metadata
|
|
38
|
+
next
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
loaded, parsed = @cache[source.path]
|
|
42
|
+
unless loaded
|
|
43
|
+
parsed = Parser.new(source.content, source_name: source.path, allow_comments: @allow_comments,
|
|
44
|
+
max_filter_depth: @max_filter_depth).parse
|
|
45
|
+
loaded = load(parsed, source_path: source.path, stack: stack + [source.path])
|
|
46
|
+
@cache[source.path] = [loaded, parsed]
|
|
47
|
+
end
|
|
48
|
+
metadata.merge!(loaded.metadata)
|
|
49
|
+
metadata[directive.name] = metadata_for(parsed)
|
|
50
|
+
variables.merge!(loaded.variables)
|
|
51
|
+
imported = loaded.program.definitions
|
|
52
|
+
imported = namespace_definitions(imported, directive.alias_name) if directive.type == :import
|
|
53
|
+
definitions.concat(imported)
|
|
54
|
+
end
|
|
55
|
+
definitions.concat(program.definitions)
|
|
56
|
+
Result.new(
|
|
57
|
+
program: AST::Program.new(program.body, definitions, []),
|
|
58
|
+
metadata: metadata,
|
|
59
|
+
variables: variables
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def metadata_for(program)
|
|
66
|
+
declaration = program.directives.find { |directive| directive.type == :module }
|
|
67
|
+
object = declaration ? ConstantEvaluator.evaluate_object(declaration.metadata) : { 'whatever' => nil }
|
|
68
|
+
object.merge(
|
|
69
|
+
'deps' => program.directives.filter_map { |directive| dependency_metadata(directive) },
|
|
70
|
+
'defs' => program.definitions.map { |definition| "#{definition.name}/#{definition.params.length}" }
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def dependency_metadata(directive)
|
|
75
|
+
return if directive.type == :module
|
|
76
|
+
|
|
77
|
+
data = directive.alias_name&.start_with?('$') || false
|
|
78
|
+
metadata = { 'is_data' => data, 'relpath' => directive.name }
|
|
79
|
+
metadata.merge!(ConstantEvaluator.evaluate_object(directive.metadata)) if directive.metadata
|
|
80
|
+
metadata['as'] = directive.alias_name.delete_prefix('$') if directive.alias_name
|
|
81
|
+
metadata['as'] ||= File.basename(directive.name) if directive.type == :include
|
|
82
|
+
metadata
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def data_metadata
|
|
86
|
+
{ 'whatever' => nil, 'deps' => [], 'defs' => [] }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def namespace_definitions(definitions, namespace)
|
|
90
|
+
cloned = Marshal.load(Marshal.dump(definitions))
|
|
91
|
+
signatures = cloned.to_h { |definition| [[definition.name, definition.params.length], true] }
|
|
92
|
+
cloned.each do |definition|
|
|
93
|
+
namespace_calls(definition.body, namespace, signatures)
|
|
94
|
+
definition.instance_variable_set(:@name, "#{namespace}::#{definition.name}")
|
|
95
|
+
end
|
|
96
|
+
cloned
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def namespace_calls(value, namespace, signatures)
|
|
100
|
+
if value.is_a?(AST::FunctionCall)
|
|
101
|
+
name = value.name
|
|
102
|
+
value.instance_variable_set(:@name, "#{namespace}::#{name}") if signatures[[name, value.args.length]]
|
|
103
|
+
end
|
|
104
|
+
nested_values(value).each { |child| namespace_calls(child, namespace, signatures) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def nested_values(value)
|
|
108
|
+
case value
|
|
109
|
+
when AST::Node
|
|
110
|
+
value.instance_variables.map { |name| value.instance_variable_get(name) }
|
|
111
|
+
when Array
|
|
112
|
+
value
|
|
113
|
+
when Hash
|
|
114
|
+
value.values
|
|
115
|
+
when Struct
|
|
116
|
+
value.to_a
|
|
117
|
+
else
|
|
118
|
+
[]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
class ConstantEvaluator
|
|
124
|
+
class << self
|
|
125
|
+
def evaluate_object(node)
|
|
126
|
+
value = evaluate(node)
|
|
127
|
+
raise CompileError, 'Module metadata must be an object' unless value.is_a?(Hash)
|
|
128
|
+
|
|
129
|
+
value
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def evaluate(node)
|
|
133
|
+
case node
|
|
134
|
+
when AST::Literal
|
|
135
|
+
Value.deep_copy(node.value)
|
|
136
|
+
when AST::StringLiteral
|
|
137
|
+
raise CompileError, 'Module metadata must be constant' unless node.value.is_a?(String)
|
|
138
|
+
|
|
139
|
+
node.value
|
|
140
|
+
when AST::ArrayLiteral
|
|
141
|
+
expression = node.instance_variable_get(:@expression)
|
|
142
|
+
expression ? evaluate_sequence(expression) : []
|
|
143
|
+
when AST::ObjectLiteral
|
|
144
|
+
evaluate_object_literal(node)
|
|
145
|
+
when AST::UnaryOp
|
|
146
|
+
evaluate_unary(node)
|
|
147
|
+
else
|
|
148
|
+
raise CompileError, 'Module metadata must be constant'
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def evaluate_sequence(node)
|
|
155
|
+
return evaluate_sequence(node.left) + evaluate_sequence(node.right) if node.is_a?(AST::Comma)
|
|
156
|
+
|
|
157
|
+
[evaluate(node)]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def evaluate_object_literal(node)
|
|
161
|
+
node.instance_variable_get(:@pairs).to_h do |pair|
|
|
162
|
+
key = pair.key
|
|
163
|
+
raise CompileError, 'Module metadata must be constant' unless key.is_a?(String)
|
|
164
|
+
|
|
165
|
+
[key, evaluate(pair.value)]
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def evaluate_unary(node)
|
|
170
|
+
op = node.instance_variable_get(:@op)
|
|
171
|
+
value = evaluate(node.instance_variable_get(:@expression))
|
|
172
|
+
raise CompileError, 'Module metadata must be constant' unless op == '-' && value.is_a?(Numeric)
|
|
173
|
+
|
|
174
|
+
-value
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|