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,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rjq
4
+ class SemanticAnalyzer
5
+ PUSH_OPS = %i[
6
+ load_input load_const string_interp format variable path optional binding array object branch try reduce foreach
7
+ label unary binary assign call tail_call recurse scoped_def
8
+ ].freeze
9
+ TRANSFORM_OPS = %i[
10
+ field index_const index_filter slice_const slice_filter each pipe append
11
+ ].freeze
12
+
13
+ def initialize(program)
14
+ @program = program
15
+ end
16
+
17
+ def validate!
18
+ global_functions = @program.definitions.map { |definition| signature(definition) }.to_h { |item| [item, true] }
19
+ @program.definitions.each { |definition| validate_definition(definition, global_functions, {}) }
20
+ validate_instructions(@program.instructions, global_functions, {}, {})
21
+ @program
22
+ end
23
+
24
+ private
25
+
26
+ def validate_definition(definition, functions, labels)
27
+ local_functions = functions.merge(signature(definition) => true)
28
+ filter_parameters = definition.params.reject { |param| param.start_with?('$') }.to_h { |name| [name, true] }
29
+ validate_instructions(definition.body.instructions, local_functions, filter_parameters, labels)
30
+ end
31
+
32
+ def validate_instructions(instructions, functions, filter_parameters, labels)
33
+ validate_stack(instructions)
34
+ instructions.each do |instruction|
35
+ validate_constant_object_keys(instruction) if instruction.op == :object
36
+ if %i[call tail_call].include?(instruction.op)
37
+ validate_call(instruction, functions, filter_parameters)
38
+ elsif instruction.op == :scoped_def
39
+ validate_scoped_definition(instruction, functions, filter_parameters, labels)
40
+ next
41
+ elsif instruction.op == :label
42
+ validate_nested(instruction.arg2, functions, filter_parameters, labels.merge(instruction.arg1 => true))
43
+ next
44
+ elsif instruction.op == :break
45
+ raise CompileError, "label $#{instruction.arg1} is not defined" unless labels[instruction.arg1]
46
+
47
+ next
48
+ end
49
+ validate_nested(instruction.arg1, functions, filter_parameters, labels)
50
+ validate_nested(instruction.arg2, functions, filter_parameters, labels)
51
+ end
52
+ end
53
+
54
+ def validate_stack(instructions)
55
+ depth = 0
56
+ terminated = false
57
+ instructions.each do |instruction|
58
+ raise CompileError, "unknown opcode #{instruction.op}" unless Opcodes::ALL.include?(instruction.op)
59
+ raise CompileError, 'unreachable bytecode after break' if terminated
60
+
61
+ if PUSH_OPS.include?(instruction.op)
62
+ depth += 1
63
+ elsif TRANSFORM_OPS.include?(instruction.op)
64
+ raise CompileError, "bytecode stack underflow at #{instruction.op}" if depth.zero?
65
+ elsif instruction.op == :break
66
+ terminated = true
67
+ end
68
+ end
69
+ return if terminated
70
+ raise CompileError, "bytecode stack has #{depth} values, expected 1" unless depth == 1
71
+ end
72
+
73
+ def validate_call(instruction, functions, filter_parameters)
74
+ name = instruction.arg1
75
+ arity = instruction.arg2.length
76
+ return if arity.zero? && filter_parameters[name]
77
+ return if functions[[name, arity]]
78
+ return if Builtins.valid_arity?(name, arity)
79
+
80
+ raise CompileError, "#{name}/#{arity} is not defined"
81
+ end
82
+
83
+ def validate_scoped_definition(instruction, functions, filter_parameters, labels)
84
+ definition = instruction.arg1
85
+ local_functions = functions.merge(signature(definition) => true)
86
+ validate_definition(definition, local_functions, labels)
87
+ validate_nested(instruction.arg2, local_functions, filter_parameters, labels)
88
+ end
89
+
90
+ def validate_nested(value, functions, filter_parameters, labels)
91
+ case value
92
+ when BytecodeBlock
93
+ validate_instructions(value.instructions, functions, filter_parameters, labels)
94
+ when BytecodeFunctionDefinition
95
+ validate_definition(value, functions, labels)
96
+ when Array
97
+ value.each { |item| validate_nested(item, functions, filter_parameters, labels) }
98
+ when Hash
99
+ value.each_value { |item| validate_nested(item, functions, filter_parameters, labels) }
100
+ end
101
+ end
102
+
103
+ def signature(definition)
104
+ [definition.name, definition.params.length]
105
+ end
106
+
107
+ def validate_constant_object_keys(instruction)
108
+ instruction.arg1.each do |pair|
109
+ key = pair.fetch(:key)
110
+ next unless key.fetch(:type) == :filter
111
+
112
+ known, value = constant_filter_value(key.fetch(:block))
113
+ next unless known
114
+ next if value.is_a?(String)
115
+
116
+ dumped = JSON::Dumper.dump(value, indent: nil)
117
+ raise CompileError, "Cannot use #{Value.type_of(value)} (#{dumped}) as object key"
118
+ end
119
+ end
120
+
121
+ def constant_filter_value(block)
122
+ instructions = block.instructions
123
+ if instructions.length > 1 && instructions.last.op == :pipe
124
+ left = BytecodeBlock.new(instructions: instructions[0...-1])
125
+ right = instructions.last.arg1
126
+ left_known, left_value = constant_filter_value(left)
127
+ return [false, nil] unless left_known
128
+ return [true, left_value] if right.instructions.length == 1 && right.instructions.first.op == :load_input
129
+
130
+ return constant_filter_value(right)
131
+ end
132
+ return [false, nil] unless instructions.length == 1
133
+
134
+ instruction = instructions.first
135
+ case instruction.op
136
+ when :load_const
137
+ [true, @program.constants.fetch(instruction.arg1)]
138
+ when :array
139
+ return [true, []] unless instruction.arg1
140
+
141
+ constant_generator_block?(instruction.arg1) ? [true, []] : [false, nil]
142
+ when :object
143
+ constant_object_value(instruction.arg1)
144
+ when :binary
145
+ constant_binary_value(instruction)
146
+ else
147
+ [false, nil]
148
+ end
149
+ end
150
+
151
+ def constant_generator_block?(block)
152
+ instructions = block.instructions
153
+ return constant_filter_value(block).first if instructions.length == 1
154
+ return false unless instructions.last&.op == :append
155
+
156
+ left = BytecodeBlock.new(instructions: instructions[0...-1])
157
+ constant_generator_block?(left) && constant_generator_block?(instructions.last.arg1)
158
+ end
159
+
160
+ def constant_object_value(pairs)
161
+ object = {}
162
+ pairs.each do |pair|
163
+ key = pair.fetch(:key)
164
+ return [false, nil] unless key.fetch(:type) == :literal
165
+
166
+ known, value = constant_filter_value(pair.fetch(:value))
167
+ return [false, nil] unless known
168
+
169
+ object[key.fetch(:value)] = value
170
+ end
171
+ [true, object]
172
+ end
173
+
174
+ def constant_binary_value(instruction)
175
+ left_known, left = constant_filter_value(instruction.arg2[0])
176
+ right_known, right = constant_filter_value(instruction.arg2[1])
177
+ return [false, nil] unless left_known && right_known
178
+
179
+ case instruction.arg1
180
+ when '+'
181
+ return [true, right] if left.nil?
182
+ return [true, left] if right.nil?
183
+ return [true, ''] if left.is_a?(String) && right.is_a?(String)
184
+ return [true, []] if left.is_a?(Array) && right.is_a?(Array)
185
+ return [true, {}] if left.is_a?(Hash) && right.is_a?(Hash)
186
+ return [true, 0] if left.is_a?(Numeric) && right.is_a?(Numeric)
187
+ when '-'
188
+ return [true, []] if left.is_a?(Array) && right.is_a?(Array)
189
+ return [true, 0] if left.is_a?(Numeric) && right.is_a?(Numeric)
190
+ when '*'
191
+ return [true, ''] if (left.is_a?(String) && right.is_a?(Numeric)) ||
192
+ (left.is_a?(Numeric) && right.is_a?(String))
193
+ return [true, {}] if left.is_a?(Hash) && right.is_a?(Hash)
194
+ return [true, 0] if left.is_a?(Numeric) && right.is_a?(Numeric)
195
+ when '/'
196
+ return [true, []] if left.is_a?(String) && right.is_a?(String)
197
+ return [false, nil] if right.is_a?(Numeric) && right.zero?
198
+ return [true, 0] if left.is_a?(Numeric) && right.is_a?(Numeric)
199
+ when '%'
200
+ return [false, nil] if right.is_a?(Numeric) && right.zero?
201
+ return [true, 0] if left.is_a?(Numeric) && right.is_a?(Numeric)
202
+ when '==', '!=', '<', '<=', '>', '>='
203
+ return [true, true]
204
+ end
205
+
206
+ [false, nil]
207
+ end
208
+ end
209
+ end
data/lib/rjq/value.rb ADDED
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rjq
4
+ module Value
5
+ TYPE_ORDER = {
6
+ 'null' => 0,
7
+ 'boolean' => 1,
8
+ 'number' => 2,
9
+ 'string' => 3,
10
+ 'array' => 4,
11
+ 'object' => 5
12
+ }.freeze
13
+
14
+ module_function
15
+
16
+ def type_of(value)
17
+ case value
18
+ when NilClass
19
+ 'null'
20
+ when TrueClass, FalseClass
21
+ 'boolean'
22
+ when Numeric
23
+ 'number'
24
+ when String
25
+ 'string'
26
+ when Array
27
+ 'array'
28
+ when Hash
29
+ 'object'
30
+ else
31
+ raise TypeError, "unsupported value type: #{value.class}"
32
+ end
33
+ end
34
+
35
+ def truthy?(value)
36
+ !(value.nil? || value == false)
37
+ end
38
+
39
+ def validate!(value)
40
+ active = {}
41
+ stack = [[:value, value]]
42
+ until stack.empty?
43
+ type, item = stack.pop
44
+ if type == :leave
45
+ active.delete(item)
46
+ next
47
+ end
48
+
49
+ case item
50
+ when NilClass, TrueClass, FalseClass, Numeric
51
+ next
52
+ when String
53
+ raise TypeError, 'invalid UTF-8 string' unless item.encoding == Encoding::UTF_8 && item.valid_encoding?
54
+ when Array
55
+ enter_validation_container!(item, active)
56
+ stack << [:leave, item.object_id]
57
+ item.reverse_each { |child| stack << [:value, child] }
58
+ when Hash
59
+ invalid = item.keys.find { |key| !key.is_a?(String) }
60
+ raise TypeError, "object key must be a string, got #{invalid.class}" if invalid
61
+
62
+ enter_validation_container!(item, active)
63
+ stack << [:leave, item.object_id]
64
+ item.values.reverse_each { |child| stack << [:value, child] }
65
+ else
66
+ raise TypeError, "unsupported value type: #{item.class}"
67
+ end
68
+ end
69
+ value
70
+ end
71
+
72
+ def enter_validation_container!(value, active)
73
+ raise TypeError, 'cyclic JSON value' if active[value.object_id]
74
+
75
+ active[value.object_id] = true
76
+ end
77
+ private_class_method :enter_validation_container!
78
+
79
+ def equal?(left, right)
80
+ stack = [[left, right]]
81
+ until stack.empty?
82
+ left_value, right_value = stack.pop
83
+ left_type = type_of(left_value)
84
+ right_type = type_of(right_value)
85
+ if left_type == 'number' && right_type == 'number'
86
+ return false unless numeric_equal?(left_value, right_value)
87
+ next
88
+ end
89
+ return false unless left_type == right_type
90
+
91
+ case left_type
92
+ when 'array'
93
+ return false unless left_value.length == right_value.length
94
+
95
+ (left_value.length - 1).downto(0) { |index| stack << [left_value[index], right_value[index]] }
96
+ when 'object'
97
+ left_keys = left_value.keys.sort
98
+ return false unless left_keys == right_value.keys.sort
99
+
100
+ left_keys.reverse_each { |key| stack << [left_value.fetch(key), right_value.fetch(key)] }
101
+ else
102
+ return false unless left_value == right_value
103
+ end
104
+ end
105
+ true
106
+ end
107
+
108
+ def compare(left, right)
109
+ stack = [[:compare, left, right]]
110
+ until stack.empty?
111
+ action, left_value, right_value = stack.pop
112
+ if action == :result
113
+ return left_value unless left_value.zero?
114
+ next
115
+ end
116
+
117
+ left_type = type_of(left_value)
118
+ right_type = type_of(right_value)
119
+ type_cmp = TYPE_ORDER.fetch(left_type) <=> TYPE_ORDER.fetch(right_type)
120
+ return type_cmp unless type_cmp.zero?
121
+
122
+ cmp = case left_type
123
+ when 'null' then 0
124
+ when 'boolean' then bool_rank(left_value) <=> bool_rank(right_value)
125
+ when 'number' then numeric_compare(left_value, right_value)
126
+ when 'string' then left_value <=> right_value
127
+ when 'array'
128
+ push_array_comparison(stack, left_value, right_value)
129
+ 0
130
+ when 'object'
131
+ push_object_comparison(stack, left_value, right_value)
132
+ 0
133
+ end
134
+ return cmp unless cmp.zero?
135
+ end
136
+ 0
137
+ end
138
+
139
+ def merge_objects(left, right)
140
+ merged = left.merge(right)
141
+ stack = [[left, right, merged]]
142
+ until stack.empty?
143
+ left_object, right_object, output = stack.pop
144
+ right_object.each do |key, right_value|
145
+ left_value = left_object[key]
146
+ next unless left_value.is_a?(Hash) && right_value.is_a?(Hash)
147
+
148
+ child = left_value.merge(right_value)
149
+ output[key] = child
150
+ stack << [left_value, right_value, child]
151
+ end
152
+ end
153
+ merged
154
+ end
155
+
156
+ def deep_copy(value)
157
+ root = []
158
+ active = {}
159
+ stack = [[:copy, value, [:array, root, 0]]]
160
+ until stack.empty?
161
+ type, source, target = stack.pop
162
+ case type
163
+ when :leave
164
+ active.delete(source)
165
+ when :array_items
166
+ original, copy, index = source
167
+ next if index >= original.length
168
+
169
+ stack << [:array_items, [original, copy, index + 1], nil]
170
+ stack << [:copy, original[index], [:array, copy, index]]
171
+ when :object_items
172
+ original, copy, keys, index = source
173
+ next if index >= keys.length
174
+
175
+ key = keys[index]
176
+ stack << [:object_items, [original, copy, keys, index + 1], nil]
177
+ stack << [:copy, original.fetch(key), [:object, copy, key.dup]]
178
+ when :copy
179
+ copied = copy_scalar(source)
180
+ if source.is_a?(Array)
181
+ enter_copy_container!(source, active)
182
+ copied = Array.new(source.length)
183
+ stack << [:leave, source.object_id, nil]
184
+ stack << [:array_items, [source, copied, 0], nil]
185
+ elsif source.is_a?(Hash)
186
+ validate_copy_keys!(source)
187
+ enter_copy_container!(source, active)
188
+ copied = {}
189
+ stack << [:leave, source.object_id, nil]
190
+ stack << [:object_items, [source, copied, source.keys, 0], nil]
191
+ end
192
+ assign_copy(target, copied)
193
+ end
194
+ end
195
+ root.fetch(0)
196
+ end
197
+
198
+ def copy_scalar(value)
199
+ case value
200
+ when String then value.dup
201
+ when NilClass, TrueClass, FalseClass, Numeric, Array, Hash then value
202
+ else raise TypeError, "unsupported value type: #{value.class}"
203
+ end
204
+ end
205
+ private_class_method :copy_scalar
206
+
207
+ def enter_copy_container!(value, active)
208
+ raise TypeError, 'cannot copy a cyclic JSON value' if active[value.object_id]
209
+
210
+ active[value.object_id] = true
211
+ end
212
+ private_class_method :enter_copy_container!
213
+
214
+ def validate_copy_keys!(value)
215
+ invalid = value.keys.find { |key| !key.is_a?(String) }
216
+ raise TypeError, "object key must be a string, got #{invalid.class}" if invalid
217
+ end
218
+ private_class_method :validate_copy_keys!
219
+
220
+ def assign_copy(target, value)
221
+ _type, container, key = target
222
+ container[key] = value
223
+ end
224
+ private_class_method :assign_copy
225
+
226
+ def numeric_equal?(left, right)
227
+ return false if nan_number?(left) || nan_number?(right)
228
+
229
+ numeric_compare(left, right).zero?
230
+ end
231
+ private_class_method :numeric_equal?
232
+
233
+ def unsafe_integer?(value)
234
+ value.is_a?(Integer) && value.abs > (2**53)
235
+ end
236
+ private_class_method :unsafe_integer?
237
+
238
+ def bool_rank(value)
239
+ value ? 1 : 0
240
+ end
241
+ private_class_method :bool_rank
242
+
243
+ def numeric_compare(left, right)
244
+ left_nan = nan_number?(left)
245
+ right_nan = nan_number?(right)
246
+ return 0 if left_nan && right_nan
247
+ return -1 if left_nan
248
+ return 1 if right_nan
249
+
250
+ return left.decimal_compare(right) if left.is_a?(Number) && right.is_a?(Number)
251
+ return left.decimal_compare(Number.parse(right.to_s)) if left.is_a?(Number) && right.is_a?(Integer)
252
+ return -right.decimal_compare(Number.parse(left.to_s)) if left.is_a?(Integer) && right.is_a?(Number)
253
+
254
+ comparable_number(left) <=> comparable_number(right)
255
+ end
256
+ private_class_method :numeric_compare
257
+
258
+ def nan_number?(value)
259
+ value.respond_to?(:nan?) && value.nan?
260
+ end
261
+ private_class_method :nan_number?
262
+
263
+ def comparable_number(value)
264
+ return value.to_f if value.is_a?(Number) || unsafe_integer?(value)
265
+
266
+ value
267
+ end
268
+ private_class_method :comparable_number
269
+
270
+ def push_array_comparison(stack, left, right)
271
+ length = [left.length, right.length].min
272
+ stack << [:result, left.length <=> right.length, nil]
273
+ (length - 1).downto(0) { |index| stack << [:compare, left[index], right[index]] }
274
+ end
275
+ private_class_method :push_array_comparison
276
+
277
+ def push_object_comparison(stack, left, right)
278
+ left_keys = left.keys.sort
279
+ right_keys = right.keys.sort
280
+ if left_keys == right_keys
281
+ left_keys.reverse_each { |key| stack << [:compare, left.fetch(key), right.fetch(key)] }
282
+ end
283
+ push_array_comparison(stack, left_keys, right_keys)
284
+ end
285
+ private_class_method :push_object_comparison
286
+ end
287
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rjq
4
+ VERSION = '0.1.0'
5
+ end