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/modules.rb
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Rjq
|
|
6
|
+
class ModuleResolver
|
|
7
|
+
ResolvedModule = Struct.new(:name, :path, :content, :data, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
DEFAULT_MAX_BYTES = 1_048_576
|
|
10
|
+
|
|
11
|
+
def initialize(paths: [], use_default_paths: true, max_bytes: DEFAULT_MAX_BYTES)
|
|
12
|
+
configured = paths.map { |path| File.expand_path(path) }
|
|
13
|
+
if use_default_paths
|
|
14
|
+
configured.concat(ENV.fetch('JQ_LIBRARY_PATH', '').split(File::PATH_SEPARATOR).reject(&:empty?))
|
|
15
|
+
configured.concat([File.expand_path('~/.jq'), File.expand_path('~/.rjq')])
|
|
16
|
+
end
|
|
17
|
+
@roots = configured.uniq.freeze
|
|
18
|
+
@max_bytes = max_bytes
|
|
19
|
+
@cache = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def resolve(name, from: nil, metadata: {}, data: false)
|
|
23
|
+
validate_name!(name)
|
|
24
|
+
extension = data ? '.json' : '.jq'
|
|
25
|
+
candidates(name, extension, from, metadata).each do |candidate|
|
|
26
|
+
next unless File.file?(candidate)
|
|
27
|
+
|
|
28
|
+
path = File.realpath(candidate)
|
|
29
|
+
validate_root!(path)
|
|
30
|
+
return cached_source(name, path, data)
|
|
31
|
+
end
|
|
32
|
+
raise CompileError, "module #{name.inspect} not found"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initial_metadata
|
|
36
|
+
{}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def candidates(name, extension, from, metadata)
|
|
42
|
+
bases = search_bases(from, metadata)
|
|
43
|
+
filename = name.end_with?(extension) ? name : "#{name}#{extension}"
|
|
44
|
+
bases.flat_map do |base|
|
|
45
|
+
[File.expand_path(filename, base), File.expand_path(File.join(name, File.basename(filename)), base)]
|
|
46
|
+
end.uniq
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def search_bases(from, metadata)
|
|
50
|
+
search = metadata['search']
|
|
51
|
+
if search
|
|
52
|
+
raise CompileError, 'module search metadata must be a string' unless search.is_a?(String)
|
|
53
|
+
|
|
54
|
+
return [File.expand_path(search, from ? File.dirname(from) : Dir.pwd)]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
bases = []
|
|
58
|
+
bases << File.dirname(from) if from
|
|
59
|
+
bases.concat(@roots)
|
|
60
|
+
bases.uniq
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def validate_name!(name)
|
|
64
|
+
unless name.is_a?(String) && !name.empty? && !name.include?("\0") && !Pathname.new(name).absolute?
|
|
65
|
+
raise CompileError, 'invalid module path'
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate_root!(path)
|
|
70
|
+
roots = @roots.filter_map { |root| File.realpath(root) if File.directory?(root) }
|
|
71
|
+
return if roots.any? { |root| path == root || path.start_with?("#{root}#{File::SEPARATOR}") }
|
|
72
|
+
|
|
73
|
+
raise CompileError, "module path escapes configured library roots: #{path}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def cached_source(name, path, data)
|
|
77
|
+
key = [path, data]
|
|
78
|
+
@cache[key] ||= begin
|
|
79
|
+
size = File.size(path)
|
|
80
|
+
raise CompileError, "module exceeds #{@max_bytes} byte limit: #{path}" if size > @max_bytes
|
|
81
|
+
|
|
82
|
+
ResolvedModule.new(name: name, path: path, content: File.binread(path), data: data).freeze
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
Modules = ModuleResolver
|
|
88
|
+
end
|
data/lib/rjq/number.rb
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rjq
|
|
4
|
+
class Number < Numeric
|
|
5
|
+
include Comparable
|
|
6
|
+
|
|
7
|
+
NUMBER_PATTERN = /\A-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?\z/
|
|
8
|
+
|
|
9
|
+
attr_reader :literal
|
|
10
|
+
|
|
11
|
+
def self.parse(literal)
|
|
12
|
+
new(literal)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(literal)
|
|
16
|
+
@literal = literal.to_s.freeze
|
|
17
|
+
raise ArgumentError, 'invalid number literal' unless NUMBER_PATTERN.match?(@literal)
|
|
18
|
+
|
|
19
|
+
@value = Float(@literal)
|
|
20
|
+
parse_decimal_components
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def dump
|
|
25
|
+
return literal unless literal.match?(/[eE]/)
|
|
26
|
+
|
|
27
|
+
render_exponent_literal
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_f
|
|
31
|
+
@value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_i
|
|
35
|
+
@value.to_i
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_int
|
|
39
|
+
to_i
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_s
|
|
43
|
+
dump
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def inspect
|
|
47
|
+
"#<#{self.class} #{literal}>"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def <=>(other)
|
|
51
|
+
return decimal_compare(other) if other.is_a?(Number)
|
|
52
|
+
|
|
53
|
+
@value <=> numeric_value(other)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def ==(other)
|
|
57
|
+
return decimal_compare(other).zero? if other.is_a?(Number)
|
|
58
|
+
|
|
59
|
+
other.is_a?(Numeric) && @value == numeric_value(other)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def eql?(other)
|
|
63
|
+
other.is_a?(Numeric) && self == other
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def hash
|
|
67
|
+
@value.hash
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def coerce(other)
|
|
71
|
+
[numeric_value(other), @value]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def +(other) = @value + numeric_value(other)
|
|
75
|
+
def -(other) = @value - numeric_value(other)
|
|
76
|
+
def *(other) = @value * numeric_value(other)
|
|
77
|
+
def /(other) = @value / numeric_value(other)
|
|
78
|
+
def %(other) = @value % numeric_value(other)
|
|
79
|
+
def **(other) = @value**numeric_value(other)
|
|
80
|
+
def remainder(other) = @value.remainder(numeric_value(other))
|
|
81
|
+
def -@ = -@value
|
|
82
|
+
def +@ = @value
|
|
83
|
+
|
|
84
|
+
def abs = @value.abs
|
|
85
|
+
def ceil = @value.ceil
|
|
86
|
+
def floor = @value.floor
|
|
87
|
+
def round(...) = @value.round(...)
|
|
88
|
+
def truncate = @value.truncate
|
|
89
|
+
def finite? = @value.finite?
|
|
90
|
+
def infinite? = @value.infinite?
|
|
91
|
+
def nan? = @value.nan?
|
|
92
|
+
def negative? = @value.negative?
|
|
93
|
+
def positive? = @value.positive?
|
|
94
|
+
def zero? = @value.zero?
|
|
95
|
+
|
|
96
|
+
def decimal_compare(other)
|
|
97
|
+
sign_comparison = @decimal_sign <=> other.instance_variable_get(:@decimal_sign)
|
|
98
|
+
return sign_comparison unless sign_comparison.zero?
|
|
99
|
+
return 0 if @decimal_sign.zero?
|
|
100
|
+
|
|
101
|
+
other_digits = other.instance_variable_get(:@decimal_digits)
|
|
102
|
+
other_exponent = other.instance_variable_get(:@decimal_exponent)
|
|
103
|
+
magnitude_comparison = (@decimal_digits.length + @decimal_exponent) <=> (other_digits.length + other_exponent)
|
|
104
|
+
return magnitude_comparison * @decimal_sign unless magnitude_comparison.zero?
|
|
105
|
+
|
|
106
|
+
coefficient_comparison = compare_coefficients(@decimal_digits, other_digits)
|
|
107
|
+
coefficient_comparison * @decimal_sign
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def numeric_value(value)
|
|
113
|
+
return value.to_f if value.is_a?(Number)
|
|
114
|
+
return value if value.is_a?(Numeric)
|
|
115
|
+
|
|
116
|
+
raise TypeError, "#{value.class} can't be coerced into #{self.class}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def parse_decimal_components
|
|
120
|
+
sign, integer, fraction, exponent = literal.match(
|
|
121
|
+
/\A(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?\z/
|
|
122
|
+
).captures
|
|
123
|
+
fraction ||= ''
|
|
124
|
+
digits = (integer + fraction).sub(/\A0+/, '')
|
|
125
|
+
if digits.empty?
|
|
126
|
+
@decimal_sign = 0
|
|
127
|
+
@decimal_digits = '0'.freeze
|
|
128
|
+
@decimal_exponent = 0
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
trailing_zeros = digits[/0+\z/]&.length.to_i
|
|
133
|
+
@decimal_sign = sign == '-' ? -1 : 1
|
|
134
|
+
@decimal_digits = digits[0, digits.length - trailing_zeros].freeze
|
|
135
|
+
@decimal_exponent = (exponent ? Integer(exponent, 10) : 0) - fraction.length + trailing_zeros
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def compare_coefficients(left, right)
|
|
139
|
+
length = [left.length, right.length].max
|
|
140
|
+
length.times do |index|
|
|
141
|
+
comparison = (left.getbyte(index) || 48) <=> (right.getbyte(index) || 48)
|
|
142
|
+
return comparison unless comparison.zero?
|
|
143
|
+
end
|
|
144
|
+
0
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def render_exponent_literal
|
|
148
|
+
sign, integer, fraction, exponent = literal.match(
|
|
149
|
+
/\A(-?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)\z/
|
|
150
|
+
).captures
|
|
151
|
+
fraction ||= ''
|
|
152
|
+
exponent = Integer(exponent, 10)
|
|
153
|
+
digits = integer + fraction
|
|
154
|
+
first_significant = digits.index(/[1-9]/)
|
|
155
|
+
return render_zero(sign, fraction.length, exponent) unless first_significant
|
|
156
|
+
|
|
157
|
+
decimal_position = integer.length + exponent
|
|
158
|
+
scientific_exponent = decimal_position - first_significant - 1
|
|
159
|
+
significant = digits[first_significant..]
|
|
160
|
+
if scientific_exponent.positive? || scientific_exponent < -6
|
|
161
|
+
coefficient = significant.length == 1 ? significant : "#{significant[0]}.#{significant[1..]}"
|
|
162
|
+
return "#{sign}#{coefficient}E#{format_exponent(scientific_exponent)}"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
"#{sign}#{render_decimal(digits, decimal_position)}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def render_zero(sign, fraction_length, exponent)
|
|
169
|
+
scientific_exponent = exponent - fraction_length
|
|
170
|
+
return "#{sign}0E#{format_exponent(scientific_exponent)}" if scientific_exponent.positive? || scientific_exponent < -6
|
|
171
|
+
|
|
172
|
+
sign + render_decimal('0' * (fraction_length + 1), 1 + exponent)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def render_decimal(digits, decimal_position)
|
|
176
|
+
if decimal_position <= 0
|
|
177
|
+
"0.#{'0' * -decimal_position}#{digits}"
|
|
178
|
+
elsif decimal_position >= digits.length
|
|
179
|
+
digits + ('0' * (decimal_position - digits.length))
|
|
180
|
+
else
|
|
181
|
+
"#{digits[0...decimal_position]}.#{digits[decimal_position..]}"
|
|
182
|
+
end.sub(/\A0+(?=\d)/, '')
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def format_exponent(exponent)
|
|
186
|
+
exponent.positive? ? "+#{exponent}" : exponent.to_s
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
data/lib/rjq/opcodes.rb
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rjq
|
|
4
|
+
module Opcodes
|
|
5
|
+
ALL = %i[
|
|
6
|
+
load_input load_const string_interp format variable field index_const index_filter slice_const slice_filter
|
|
7
|
+
each path optional pipe append binding array object branch try reduce foreach label break unary binary assign
|
|
8
|
+
call tail_call recurse scoped_def
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
MNEMONICS = ALL.to_h { |opcode| [opcode, opcode.to_s] }.freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
Instruction = Struct.new(:op, :arg1, :arg2, :loc, keyword_init: true) do
|
|
15
|
+
def to_s
|
|
16
|
+
text = [Opcodes::MNEMONICS.fetch(op), format_arg(arg1), format_arg(arg2)].compact.join(' ')
|
|
17
|
+
loc ? "#{text} @ #{loc.filename}:#{loc.line}:#{loc.column}" : text
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def format_arg(value)
|
|
23
|
+
return nil if value.nil?
|
|
24
|
+
|
|
25
|
+
case value
|
|
26
|
+
when BytecodeBlock
|
|
27
|
+
"<block:#{value.instructions.length}>"
|
|
28
|
+
when BytecodeFunctionDefinition
|
|
29
|
+
"<def:#{value.name}/#{value.params.length}>"
|
|
30
|
+
when Array
|
|
31
|
+
"[#{value.map { |item| format_arg(item) }.join(',')}]"
|
|
32
|
+
when Hash
|
|
33
|
+
"{#{value.map { |key, item| "#{key}:#{format_arg(item)}" }.join(',')}}"
|
|
34
|
+
else
|
|
35
|
+
value.inspect
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
BytecodeBlock = Struct.new(:instructions, keyword_init: true)
|
|
41
|
+
|
|
42
|
+
TailCall = Struct.new(:input, :context, :definition, :arg_blocks, keyword_init: true)
|
|
43
|
+
|
|
44
|
+
BytecodeFunctionDefinition = Struct.new(:name, :params, :body, :closure, keyword_init: true) do
|
|
45
|
+
def with_closure(functions)
|
|
46
|
+
self.class.new(name: name, params: params, body: body, closure: functions)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class Program
|
|
51
|
+
attr_reader :instructions, :constants, :definitions, :module_metadata, :module_variables
|
|
52
|
+
|
|
53
|
+
def initialize(instructions:, constants: [], definitions: [], module_metadata: {}, module_variables: {})
|
|
54
|
+
@instructions = instructions
|
|
55
|
+
@constants = constants
|
|
56
|
+
@definitions = definitions
|
|
57
|
+
@module_metadata = module_metadata
|
|
58
|
+
@module_variables = module_variables
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def disasm
|
|
62
|
+
lines = []
|
|
63
|
+
append_disasm(lines, instructions, 'main', 0)
|
|
64
|
+
definitions.each do |definition|
|
|
65
|
+
append_disasm(lines, definition.body.instructions,
|
|
66
|
+
"definition:#{definition.name}/#{definition.params.length}", 0)
|
|
67
|
+
end
|
|
68
|
+
lines.join("\n")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def finalize!
|
|
72
|
+
freeze_value(instructions)
|
|
73
|
+
freeze_value(constants)
|
|
74
|
+
freeze_value(definitions)
|
|
75
|
+
freeze_value(module_metadata)
|
|
76
|
+
freeze_value(module_variables)
|
|
77
|
+
freeze
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def append_disasm(lines, block, name, indent)
|
|
83
|
+
pad = ' ' * indent
|
|
84
|
+
lines << "#{pad}== #{name} =="
|
|
85
|
+
block.each_with_index do |instruction, index|
|
|
86
|
+
lines << format("#{pad}%04d %s", index, instruction)
|
|
87
|
+
append_nested(lines, instruction, indent + 2)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def append_nested(lines, instruction, indent)
|
|
92
|
+
append_nested_value(lines, instruction.arg1, indent, 'arg1')
|
|
93
|
+
append_nested_value(lines, instruction.arg2, indent, 'arg2')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def append_nested_value(lines, value, indent, name)
|
|
97
|
+
case value
|
|
98
|
+
when BytecodeBlock
|
|
99
|
+
append_disasm(lines, value.instructions, name, indent)
|
|
100
|
+
when BytecodeFunctionDefinition
|
|
101
|
+
append_disasm(lines, value.body.instructions, "#{name}:#{value.name}/#{value.params.length}", indent)
|
|
102
|
+
when Array
|
|
103
|
+
value.each_with_index { |item, index| append_nested_value(lines, item, indent, "#{name}[#{index}]") }
|
|
104
|
+
when Hash
|
|
105
|
+
value.each { |key, item| append_nested_value(lines, item, indent, "#{name}.#{key}") }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def freeze_value(root)
|
|
110
|
+
stack = [root]
|
|
111
|
+
seen = {}
|
|
112
|
+
until stack.empty?
|
|
113
|
+
value = stack.pop
|
|
114
|
+
next if value.nil? || seen[value.object_id]
|
|
115
|
+
|
|
116
|
+
seen[value.object_id] = true
|
|
117
|
+
case value
|
|
118
|
+
when Array
|
|
119
|
+
stack.concat(value)
|
|
120
|
+
when Hash
|
|
121
|
+
stack.concat(value.keys)
|
|
122
|
+
stack.concat(value.values)
|
|
123
|
+
when Struct
|
|
124
|
+
stack.concat(value.to_a)
|
|
125
|
+
end
|
|
126
|
+
value.freeze
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|