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,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
@@ -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