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.
@@ -0,0 +1,392 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rjq
4
+ class CompiledProgram
5
+ attr_reader :ast, :program
6
+
7
+ def initialize(ast, program:)
8
+ @ast = ast
9
+ @program = program
10
+ freeze
11
+ end
12
+
13
+ def instructions
14
+ program.instructions
15
+ end
16
+
17
+ def run(input_value, opts = {})
18
+ VM.new(self, Runtime.normalize_options(opts)).run(input_value)
19
+ end
20
+
21
+ def run_with_instruction_budget(input_value, opts, budget)
22
+ normalized = Runtime.normalize_options(opts)
23
+ unless budget.is_a?(VM::InstructionBudget) && budget.maximum == normalized[:max_instructions]
24
+ raise ArgumentError, 'instruction budget must match max_instructions'
25
+ end
26
+
27
+ VM.new(self, normalized, instruction_budget: budget).run(input_value)
28
+ end
29
+
30
+ def disasm
31
+ program.disasm
32
+ end
33
+ end
34
+
35
+ class BytecodeCompiler
36
+ def initialize(allow_comments: true, max_filter_depth: Parser::DEFAULT_MAX_FILTER_DEPTH)
37
+ @constants = []
38
+ @constant_indices = {}
39
+ @current_nodes = []
40
+ @allow_comments = allow_comments
41
+ @max_filter_depth = max_filter_depth
42
+ end
43
+
44
+ def compile(ast, module_metadata: {}, module_variables: {})
45
+ instructions = compile_node(ast.body)
46
+ Rjq::Program.new(
47
+ instructions: instructions,
48
+ constants: @constants,
49
+ definitions: ast.definitions.map { |definition| compile_definition(definition) },
50
+ module_metadata: module_metadata,
51
+ module_variables: module_variables
52
+ )
53
+ end
54
+
55
+ private
56
+
57
+ def compile_node(node)
58
+ @current_nodes << node
59
+ case node
60
+ when AST::Identity
61
+ [instruction(:load_input)]
62
+ when AST::Literal
63
+ [instruction(:load_const, const(node.value))]
64
+ when AST::StringLiteral
65
+ compile_string(node)
66
+ when AST::Format
67
+ compile_format(node)
68
+ when AST::Variable
69
+ [instruction(:variable, node.name, nil, loc: node.source_span)]
70
+ when AST::Field
71
+ compile_node(node.base) + [instruction(:field, node.name)]
72
+ when AST::Index
73
+ compile_index(node)
74
+ when AST::Slice
75
+ compile_slice(node)
76
+ when AST::Iterate
77
+ compile_node(node.base) + [instruction(:each)]
78
+ when AST::Optional
79
+ [instruction(:optional, block_for(node.node))]
80
+ when AST::Pipe
81
+ compile_node(node.left) + [instruction(:pipe, block_for(node.right))]
82
+ when AST::Comma
83
+ compile_node(node.left) + [instruction(:append, block_for(node.right))]
84
+ when AST::Binding
85
+ [instruction(:binding, {
86
+ source: block_for(node.source),
87
+ pattern: node.pattern,
88
+ body: block_for(node.body)
89
+ })]
90
+ when AST::ArrayLiteral
91
+ expression = node.expression
92
+ [instruction(:array, expression ? block_for(expression) : nil)]
93
+ when AST::ObjectLiteral
94
+ [instruction(:object, compile_object_pairs(node.pairs))]
95
+ when AST::FunctionCall
96
+ compile_call(node)
97
+ when AST::BinaryOp
98
+ compile_binary(node)
99
+ when AST::UnaryOp
100
+ [instruction(:unary, node.op, block_for(node.expression))]
101
+ when AST::If
102
+ compile_if(node)
103
+ when AST::Try
104
+ [instruction(:try, { body: block_for(node.body), handler: optional_block(node.handler) })]
105
+ when AST::Reduce
106
+ [instruction(:reduce, {
107
+ generator: block_for(node.generator),
108
+ pattern: node.variable,
109
+ initial: block_for(node.initial),
110
+ update: block_for(node.update)
111
+ })]
112
+ when AST::Foreach
113
+ [instruction(:foreach, {
114
+ generator: block_for(node.generator),
115
+ pattern: node.variable,
116
+ initial: block_for(node.initial),
117
+ update: block_for(node.update),
118
+ extract: optional_block(node.extract)
119
+ })]
120
+ when AST::Label
121
+ [instruction(:label, node.label, block_for(node.body))]
122
+ when AST::Break
123
+ [instruction(:break, node.label)]
124
+ when AST::Assignment
125
+ [instruction(:assign,
126
+ { left: block_for(node.left), op: node.op, right: block_for(node.right) })]
127
+ when AST::ScopedDefinition
128
+ [instruction(:scoped_def, compile_definition(node.definition), block_for(node.body))]
129
+ when AST::Recurse
130
+ [instruction(:recurse)]
131
+ else
132
+ raise CompileError, "unsupported AST node #{node.class}"
133
+ end
134
+ ensure
135
+ @current_nodes.pop
136
+ end
137
+
138
+ def compile_string(node)
139
+ value = node.value
140
+ return [instruction(:load_const, const(value))] if value.is_a?(String)
141
+
142
+ [instruction(:string_interp, value.map { |kind, segment| compile_string_segment(kind, segment) })]
143
+ end
144
+
145
+ def compile_string_segment(kind, value)
146
+ return { kind: :text, value: value } if kind == :text
147
+
148
+ fragment = value.is_a?(SourceFragment) ? value : SourceFragment.new(
149
+ source: value, filename: '<top-level>', line: 1, column: 1, start_offset: 0
150
+ )
151
+ parsed = Parser.new(fragment.source, allow_comments: @allow_comments, source_name: fragment.filename,
152
+ initial_line: fragment.line, initial_column: fragment.column,
153
+ start_offset: fragment.start_offset,
154
+ max_filter_depth: @max_filter_depth).parse
155
+ { kind: :expr, block: block_for(parsed.body), definitions: parsed.definitions.map do |definition|
156
+ compile_definition(definition)
157
+ end }
158
+ end
159
+
160
+ def compile_format(node)
161
+ expression = node.expression
162
+ return [instruction(:format, node.name, nil)] unless expression
163
+
164
+ if expression.is_a?(AST::StringLiteral) && expression.value.is_a?(Array)
165
+ return [instruction(:format, node.name, { segments: expression.value.map do |kind, value|
166
+ compile_string_segment(kind, value)
167
+ end })]
168
+ end
169
+
170
+ [instruction(:format, node.name, { block: block_for(expression) })]
171
+ end
172
+
173
+ def compile_index(node)
174
+ base = compile_node(node.base)
175
+ index = node.index
176
+ return base + [instruction(:index_const, index.value)] if index.is_a?(AST::Literal)
177
+
178
+ base + [instruction(:index_filter, block_for(index))]
179
+ end
180
+
181
+ def compile_slice(node)
182
+ start_node = node.start_node
183
+ finish_node = node.finish_node
184
+ base = compile_node(node.base)
185
+ if literal_or_nil?(start_node) && literal_or_nil?(finish_node)
186
+ return base + [instruction(:slice_const, literal_value(start_node), literal_value(finish_node))]
187
+ end
188
+
189
+ base + [instruction(:slice_filter, { start: optional_block(start_node), finish: optional_block(finish_node) })]
190
+ end
191
+
192
+ def compile_call(node)
193
+ arg_blocks = node.args.map { |arg| block_for(arg) }
194
+ return [instruction(:path, arg_blocks.first)] if node.name == 'path' && arg_blocks.length == 1
195
+
196
+ [instruction(:call, node.name, arg_blocks)]
197
+ end
198
+
199
+ def compile_binary(node)
200
+ [instruction(:binary, node.op, [block_for(node.left), block_for(node.right)])]
201
+ end
202
+
203
+ def compile_if(node)
204
+ [instruction(
205
+ :branch,
206
+ block_for(node.condition),
207
+ [block_for(node.then_branch), block_for(node.else_branch)]
208
+ )]
209
+ end
210
+
211
+ def block_for(node)
212
+ BytecodeBlock.new(instructions: compile_node(node))
213
+ end
214
+
215
+ def optional_block(node)
216
+ node ? block_for(node) : nil
217
+ end
218
+
219
+ def compile_definition(definition)
220
+ compiled = BytecodeFunctionDefinition.new(
221
+ name: definition.name,
222
+ params: definition.params,
223
+ body: block_for(definition.body)
224
+ )
225
+ mark_tail_calls(compiled.body)
226
+ compiled
227
+ end
228
+
229
+ def mark_tail_calls(block)
230
+ instruction = block.instructions.last
231
+ return unless instruction
232
+
233
+ case instruction.op
234
+ when :call
235
+ instruction.op = :tail_call
236
+ when :pipe
237
+ mark_tail_calls(instruction.arg1)
238
+ when :branch
239
+ instruction.arg2.each { |branch| mark_tail_calls(branch) }
240
+ when :binding
241
+ mark_tail_calls(instruction.arg1.fetch(:body))
242
+ when :append
243
+ mark_tail_calls(instruction.arg1)
244
+ when :scoped_def
245
+ mark_tail_calls(instruction.arg2)
246
+ end
247
+ end
248
+
249
+ def compile_object_pairs(pairs)
250
+ pairs.map do |pair|
251
+ {
252
+ key: compile_object_key(pair.key),
253
+ value: block_for(pair.value)
254
+ }
255
+ end
256
+ end
257
+
258
+ def compile_object_key(key)
259
+ return { type: :filter, block: block_for(key) } if key.is_a?(AST::Node)
260
+ return { type: :filter, block: block_for(AST::StringLiteral.new(key)) } if key.is_a?(Array)
261
+
262
+ { type: :literal, value: key }
263
+ end
264
+
265
+ def literal_or_nil?(node)
266
+ node.nil? || node.is_a?(AST::Literal)
267
+ end
268
+
269
+ def literal_value(node)
270
+ node&.value
271
+ end
272
+
273
+ def const(value)
274
+ copy = Value.deep_copy(value)
275
+ key = JSON::Dumper.dump(copy, indent: nil, sort_keys: true)
276
+ return @constant_indices.fetch(key) if @constant_indices.key?(key)
277
+
278
+ @constants << copy
279
+ @constant_indices[key] = @constants.length - 1
280
+ end
281
+
282
+ def instruction(op, arg1 = nil, arg2 = nil, loc: nil)
283
+ inherited_location = @current_nodes.reverse_each.lazy.filter_map(&:source_span).first
284
+ location = loc || inherited_location || AST::SourceSpan.new(
285
+ filename: '<top-level>', line: 1, column: 1, start_offset: 0, end_offset: 0
286
+ )
287
+ Instruction.new(op: op, arg1: arg1, arg2: arg2, loc: location)
288
+ end
289
+ end
290
+
291
+ class Compiler
292
+ OPTION_KEYS = %i[allow_comments library_path max_filter_depth module_resolver source_path].freeze
293
+
294
+ class << self
295
+ def options_from(opts)
296
+ opts.slice(*OPTION_KEYS)
297
+ end
298
+
299
+ def validate_options!(opts)
300
+ raise ArgumentError, 'options must be a Hash' unless opts.is_a?(Hash)
301
+
302
+ unknown = opts.keys - OPTION_KEYS
303
+ raise ArgumentError, "unknown compiler option: #{unknown.first.inspect}" unless unknown.empty?
304
+
305
+ validate_boolean!(opts, :allow_comments)
306
+ validate_optional_string!(opts, :source_path)
307
+ validate_positive_integer!(opts, :max_filter_depth)
308
+ validate_library_path!(opts[:library_path]) if opts.key?(:library_path)
309
+ validate_module_resolver!(opts[:module_resolver]) if opts.key?(:module_resolver)
310
+ opts
311
+ end
312
+
313
+ def normalize_options(opts)
314
+ validate_options!(opts)
315
+ normalized = opts.dup
316
+ normalized[:library_path] = normalized[:library_path].dup.freeze if normalized[:library_path]
317
+ normalized.freeze
318
+ end
319
+
320
+ private
321
+
322
+ def validate_boolean!(opts, key)
323
+ return unless opts.key?(key)
324
+ return if opts[key] == true || opts[key] == false
325
+
326
+ raise ArgumentError, "#{key} must be true or false"
327
+ end
328
+
329
+ def validate_optional_string!(opts, key)
330
+ return unless opts.key?(key)
331
+ return if opts[key].nil? || opts[key].is_a?(String)
332
+
333
+ raise ArgumentError, "#{key} must be a String or nil"
334
+ end
335
+
336
+ def validate_positive_integer!(opts, key)
337
+ return unless opts.key?(key)
338
+ return if opts[key].is_a?(Integer) && opts[key].positive?
339
+
340
+ raise ArgumentError, "#{key} must be a positive Integer"
341
+ end
342
+
343
+ def validate_library_path!(paths)
344
+ return if paths.is_a?(Array) && paths.all? { |path| path.is_a?(String) }
345
+
346
+ raise ArgumentError, 'library_path must be an Array of Strings'
347
+ end
348
+
349
+ def validate_module_resolver!(resolver)
350
+ return if resolver.nil?
351
+ return if resolver.respond_to?(:resolve) && resolver.respond_to?(:initial_metadata)
352
+
353
+ raise ArgumentError, 'module_resolver must be nil or respond to resolve and initial_metadata'
354
+ end
355
+ end
356
+
357
+ def initialize(opts = {})
358
+ @opts = self.class.normalize_options(opts)
359
+ end
360
+
361
+ def compile(filter_string)
362
+ max_filter_depth = @opts.fetch(:max_filter_depth, Parser::DEFAULT_MAX_FILTER_DEPTH)
363
+ source_path = @opts[:source_path] || '<top-level>'
364
+ parsed = Parser.new(filter_string, allow_comments: @opts.fetch(:allow_comments, true),
365
+ source_name: source_path, max_filter_depth: max_filter_depth).parse
366
+ resolver = @opts[:module_resolver] || default_module_resolver
367
+ allow_comments = @opts.fetch(:allow_comments, true)
368
+ loaded = ModuleLoader.new(resolver, allow_comments: allow_comments,
369
+ max_filter_depth: max_filter_depth).load(parsed,
370
+ source_path: @opts[:source_path])
371
+ ast = loaded.program
372
+ program = BytecodeCompiler.new(allow_comments: allow_comments, max_filter_depth: max_filter_depth).compile(
373
+ ast,
374
+ module_metadata: loaded.metadata,
375
+ module_variables: loaded.variables
376
+ )
377
+ SemanticAnalyzer.new(program).validate!
378
+ program.finalize!
379
+ CompiledProgram.new(ast, program: program)
380
+ rescue SystemStackError
381
+ raise CompileError, "filter nesting exceeds safe parser/compiler depth in #{source_path}"
382
+ end
383
+
384
+ private
385
+
386
+ def default_module_resolver
387
+ paths = @opts.fetch(:library_path, [])
388
+ ModuleResolver.new(paths: paths, use_default_paths: paths.empty?)
389
+ end
390
+
391
+ end
392
+ end
data/lib/rjq/errors.rb ADDED
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rjq
4
+ class Error < StandardError; end
5
+
6
+ class ParseError < Error; end
7
+
8
+ class CompileError < Error; end
9
+
10
+ class RuntimeError < Error
11
+ attr_reader :outputs
12
+
13
+ def prepend_outputs(values)
14
+ @outputs = Array(values) + Array(@outputs)
15
+ self
16
+ end
17
+
18
+ def take_outputs
19
+ values = Array(@outputs)
20
+ @outputs = nil
21
+ values
22
+ end
23
+ end
24
+
25
+ # Execution budgets are process-safety boundaries and cannot be caught by
26
+ # jq filters such as `try` or `?`.
27
+ class ResourceLimitError < RuntimeError; end
28
+
29
+ class TypeError < RuntimeError; end
30
+
31
+ class InvalidPathError < TypeError
32
+ attr_reader :result
33
+
34
+ def initialize(message, result, outputs: nil)
35
+ @result = result
36
+ prepend_outputs(outputs)
37
+ super(message)
38
+ end
39
+ end
40
+
41
+ class ErrorValue < RuntimeError
42
+ attr_reader :value
43
+
44
+ def initialize(value, outputs: nil)
45
+ @value = value
46
+ super(value.to_s)
47
+ prepend_outputs(outputs)
48
+ end
49
+ end
50
+
51
+ # Halting terminates the whole jq program. It is deliberately not a
52
+ # RuntimeError: try/catch and the optional operator only catch ordinary
53
+ # filter failures.
54
+ class HaltError < Error
55
+ attr_reader :value, :status
56
+
57
+ def initialize(value = nil, status = 0)
58
+ @value = value
59
+ @status = status
60
+ super(value.nil? ? 'halt_error' : value.to_s)
61
+ end
62
+ end
63
+
64
+ class JSONParseError < Error; end
65
+
66
+ class BreakSignal < RuntimeError
67
+ attr_reader :label, :value, :outputs
68
+
69
+ def initialize(label, value = nil, outputs: nil)
70
+ @label = label
71
+ @value = value
72
+ @outputs = outputs
73
+ super("break #{label}")
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rjq
4
+ module JSON
5
+ module Dumper
6
+ module_function
7
+
8
+ def dump(value, indent: 2, sort_keys: false, ascii: false, tab: false, io: nil)
9
+ destination = io || +''
10
+ step = tab ? "\t" : (' ' * Integer(indent || 0))
11
+ write(value, destination, indent.nil? ? nil : step, sort_keys, ascii)
12
+ io || destination
13
+ end
14
+
15
+ def write(value, destination, indent, sort_keys, ascii)
16
+ active = {}
17
+ stack = [[:value, value, 0]]
18
+ until stack.empty?
19
+ type, item, depth = stack.pop
20
+ case type
21
+ when :text
22
+ destination << item
23
+ when :leave
24
+ active.delete(item)
25
+ when :array_items
26
+ write_array_item(item, depth, destination, stack, indent)
27
+ when :object_items
28
+ write_object_item(item, depth, destination, stack, indent, ascii)
29
+ when :value
30
+ write_value(item, depth, destination, stack, active, indent, sort_keys, ascii)
31
+ end
32
+ end
33
+ end
34
+ private_class_method :write
35
+
36
+ def write_value(value, depth, destination, stack, active, indent, sort_keys, ascii)
37
+ case value
38
+ when NilClass
39
+ destination << 'null'
40
+ when TrueClass
41
+ destination << 'true'
42
+ when FalseClass
43
+ destination << 'false'
44
+ when Number
45
+ destination << value.dump
46
+ when Integer
47
+ destination << value.to_s
48
+ when Float
49
+ destination << dump_float(value)
50
+ when String
51
+ destination << dump_string(value, ascii)
52
+ when Array
53
+ write_array(value, depth, destination, stack, active, indent)
54
+ when Hash
55
+ write_object(value, depth, destination, stack, active, indent, sort_keys, ascii)
56
+ else
57
+ raise TypeError, "unsupported value type: #{value.class}"
58
+ end
59
+ end
60
+ private_class_method :write_value
61
+
62
+ def write_array(value, depth, destination, stack, active, indent)
63
+ return destination << '[]' if value.empty?
64
+
65
+ enter_container(value, active)
66
+ destination << (indent ? "[\n" : '[')
67
+ stack << [:leave, value.object_id, nil]
68
+ stack << [:text, indent ? "\n#{indent * depth}]" : ']', nil]
69
+ stack << [:array_items, [value, 0], depth]
70
+ end
71
+ private_class_method :write_array
72
+
73
+ def write_array_item(payload, depth, destination, stack, indent)
74
+ value, index = payload
75
+ return if index >= value.length
76
+
77
+ destination << if index.zero?
78
+ indent ? indent * (depth + 1) : ''
79
+ else
80
+ indent ? ",\n#{indent * (depth + 1)}" : ','
81
+ end
82
+ stack << [:array_items, [value, index + 1], depth]
83
+ stack << [:value, value[index], depth + 1]
84
+ end
85
+ private_class_method :write_array_item
86
+
87
+ def write_object(value, depth, destination, stack, active, indent, sort_keys, ascii)
88
+ return destination << '{}' if value.empty?
89
+
90
+ validate_object_keys!(value)
91
+ enter_container(value, active)
92
+ destination << (indent ? "{\n" : '{')
93
+ keys = sort_keys ? value.keys.sort : value.keys
94
+ stack << [:leave, value.object_id, nil]
95
+ stack << [:text, indent ? "\n#{indent * depth}}" : '}', nil]
96
+ stack << [:object_items, [value, keys, 0], depth]
97
+ end
98
+ private_class_method :write_object
99
+
100
+ def write_object_item(payload, depth, destination, stack, indent, ascii)
101
+ value, keys, index = payload
102
+ return if index >= keys.length
103
+
104
+ destination << if index.zero?
105
+ indent ? indent * (depth + 1) : ''
106
+ else
107
+ indent ? ",\n#{indent * (depth + 1)}" : ','
108
+ end
109
+ key = keys[index]
110
+ destination << dump_string(key, ascii)
111
+ destination << (indent ? ': ' : ':')
112
+ stack << [:object_items, [value, keys, index + 1], depth]
113
+ stack << [:value, value.fetch(key), depth + 1]
114
+ end
115
+ private_class_method :write_object_item
116
+
117
+ def enter_container(value, active)
118
+ raise TypeError, 'cannot dump a cyclic JSON value' if active[value.object_id]
119
+
120
+ active[value.object_id] = true
121
+ end
122
+ private_class_method :enter_container
123
+
124
+ def validate_object_keys!(value)
125
+ invalid = value.keys.find { |key| !key.is_a?(String) }
126
+ raise TypeError, "object key must be a string, got #{invalid.class}" if invalid
127
+ end
128
+ private_class_method :validate_object_keys!
129
+
130
+ def dump_float(value)
131
+ return 'null' if value.nan?
132
+ return Float::MAX.to_s if value.infinite? == 1
133
+ return "-#{Float::MAX}" if value.infinite? == -1
134
+ return '-0' if value.zero? && (1.0 / value).infinite? == -1
135
+
136
+ text = value.to_s.sub(/\.0(?=e)/, '')
137
+ if value == value.to_i
138
+ integer_text = value.to_i.to_s
139
+ return integer_text if value.abs < 1e16 || integer_text.length <= text.length
140
+ end
141
+ text.include?('.') || text.match?(/[eE]/) ? text : "#{text}.0"
142
+ end
143
+ private_class_method :dump_float
144
+
145
+ def dump_string(value, ascii)
146
+ string = value.encode(Encoding::UTF_8)
147
+ raise TypeError, 'invalid UTF-8 string' unless string.valid_encoding?
148
+
149
+ escaped = string.each_codepoint.map { |codepoint| escape_codepoint(codepoint, ascii) }.join
150
+ "\"#{escaped}\""
151
+ rescue EncodingError
152
+ raise TypeError, 'invalid UTF-8 string'
153
+ end
154
+ private_class_method :dump_string
155
+
156
+ def escape_codepoint(codepoint, ascii)
157
+ case codepoint
158
+ when 0x22
159
+ '\"'
160
+ when 0x5C
161
+ '\\\\'
162
+ when 0x08
163
+ '\\b'
164
+ when 0x0C
165
+ '\\f'
166
+ when 0x0A
167
+ '\\n'
168
+ when 0x0D
169
+ '\\r'
170
+ when 0x09
171
+ '\\t'
172
+ when 0x00..0x1F
173
+ '\\u%04x' % codepoint
174
+ else
175
+ ascii && codepoint > 0x7F ? unicode_escape(codepoint) : [codepoint].pack('U')
176
+ end
177
+ end
178
+ private_class_method :escape_codepoint
179
+
180
+ def unicode_escape(codepoint)
181
+ return '\\u%04x' % codepoint if codepoint <= 0xFFFF
182
+
183
+ n = codepoint - 0x10000
184
+ high = 0xD800 + (n >> 10)
185
+ low = 0xDC00 + (n & 0x3FF)
186
+ format('\\u%04x\\u%04x', high, low)
187
+ end
188
+ private_class_method :unicode_escape
189
+ end
190
+ end
191
+ end