electra 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/CHANGELOG.md +5 -0
- data/LICENSE-SPIRV-Headers.txt +26 -0
- data/LICENSE.txt +21 -0
- data/README.md +141 -0
- data/examples/example_shaders.rb +55 -0
- data/lib/electra/binary.rb +475 -0
- data/lib/electra/data_compat.rb +25 -0
- data/lib/electra/function.rb +380 -0
- data/lib/electra/grammar.rb +968 -0
- data/lib/electra/module.rb +299 -0
- data/lib/electra/type.rb +11 -0
- data/lib/electra/value.rb +39 -0
- data/lib/electra/version.rb +6 -0
- data/lib/electra.rb +32 -0
- data/sig/electra.rbs +105 -0
- metadata +58 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "grammar"
|
|
4
|
+
|
|
5
|
+
module Electra
|
|
6
|
+
# Grammar-driven instruction encoding and strict binary framing. Semantic
|
|
7
|
+
# validation is deliberately the job of spirv-val, not a second compiler.
|
|
8
|
+
module Binary
|
|
9
|
+
# SPIR-V binary header magic.
|
|
10
|
+
MAGIC = 0x07230203
|
|
11
|
+
# Universal maximum ID bound from the SPIR-V specification.
|
|
12
|
+
MAX_BOUND = 0x3fffff
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def word(value)
|
|
16
|
+
raise Error, "expected an unsigned 32-bit word: #{value.inspect}" unless value.is_a?(Integer) && value.between?(0, 0xffffffff)
|
|
17
|
+
value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def string_words(value)
|
|
21
|
+
raise Error, "SPIR-V strings must be UTF-8 without NUL" unless value.is_a?(String) && value.dup.force_encoding(Encoding::UTF_8).valid_encoding? && !value.include?("\0")
|
|
22
|
+
bytes = value.b + "\0"
|
|
23
|
+
(bytes + "\0" * ((-bytes.bytesize) % 4)).unpack("V*")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Encode the SPIR-V textual string escaping rules (not JSON escaping).
|
|
27
|
+
def quote(value)
|
|
28
|
+
'"' + value.gsub(/["\\]/) { |character| "\\#{character}" } + '"'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def unquote(token)
|
|
32
|
+
raise Error, "unterminated string" unless token.end_with?('"') && token.length >= 2
|
|
33
|
+
token[1...-1].gsub(/\\([\s\S])/) { Regexp.last_match(1) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def enum(kind, value)
|
|
37
|
+
category, entries = OPERAND_KINDS.fetch(kind)
|
|
38
|
+
return [word(value), []] if value.is_a?(Integer) && !entries.values.any? { |entry| entry[0] == value }
|
|
39
|
+
if value.is_a?(Integer)
|
|
40
|
+
names = if category == "BitEnum" && value != 0
|
|
41
|
+
entries.select { |_, (number, _)| number != 0 && (value & number) == number }.keys
|
|
42
|
+
else
|
|
43
|
+
[entries.key(entries.values.find { |entry| entry[0] == value })]
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
names = value.to_s.split("|")
|
|
47
|
+
end
|
|
48
|
+
raise Error, "invalid #{kind}: #{value.inspect}" if names.empty? || (category != "BitEnum" && names.size != 1)
|
|
49
|
+
number = 0
|
|
50
|
+
parameters = []
|
|
51
|
+
names.each do |name|
|
|
52
|
+
entry = entries[name] or raise Error, "unknown #{kind} #{name}"
|
|
53
|
+
number |= entry[0]
|
|
54
|
+
parameters.concat(entry[1])
|
|
55
|
+
end
|
|
56
|
+
[number, parameters]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def encode(name, values)
|
|
60
|
+
opcode, schema = INSTRUCTIONS.fetch(name.to_s) { raise Error, "unknown instruction #{name}" }
|
|
61
|
+
words = encode_operands(name, schema, values)
|
|
62
|
+
raise Error, "instruction too long" if words.size >= 0xffff
|
|
63
|
+
[(words.size + 1) << 16 | opcode, *words]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def encode_operands(name, schema, values)
|
|
67
|
+
words = []
|
|
68
|
+
index = 0
|
|
69
|
+
walk = lambda do |operands|
|
|
70
|
+
operands.each do |kind, quantifier|
|
|
71
|
+
count = quantifier == "*" ? values.size - index : 1
|
|
72
|
+
count = 0 if quantifier == "?" && index == values.size
|
|
73
|
+
count.times do
|
|
74
|
+
raise Error, "missing #{kind} in #{name}" if index >= values.size
|
|
75
|
+
category, = OPERAND_KINDS.fetch(kind)
|
|
76
|
+
if category == "Composite"
|
|
77
|
+
walk.call(OPERAND_KINDS.fetch(kind)[2].map { |base| [base, ""] })
|
|
78
|
+
break if quantifier == "*" && index == values.size
|
|
79
|
+
next
|
|
80
|
+
end
|
|
81
|
+
value = values[index]
|
|
82
|
+
index += 1
|
|
83
|
+
case category
|
|
84
|
+
when "Id"
|
|
85
|
+
value = value.to_i if value.respond_to?(:to_i) && !value.is_a?(String)
|
|
86
|
+
raise Error, "invalid id #{value.inspect}" unless value.is_a?(Integer) && value.between?(1, MAX_BOUND - 1)
|
|
87
|
+
words << value
|
|
88
|
+
when "ValueEnum", "BitEnum"
|
|
89
|
+
number, parameters = enum(kind, value)
|
|
90
|
+
words << number
|
|
91
|
+
walk.call(parameters)
|
|
92
|
+
else
|
|
93
|
+
if kind == "LiteralString"
|
|
94
|
+
words.concat(string_words(value))
|
|
95
|
+
elsif kind == "LiteralSpecConstantOpInteger"
|
|
96
|
+
number = value.is_a?(Integer) ? value : INSTRUCTIONS.fetch("Op#{value.to_s.delete_prefix('Op')}") { raise Error, "unknown specialization opcode" }[0]
|
|
97
|
+
words << word(number)
|
|
98
|
+
definition = fetch_spec_opcode(number)
|
|
99
|
+
walk.call(definition[1].reject { |operand, _| ["IdResultType", "IdResult"].include?(operand) })
|
|
100
|
+
elsif kind == "LiteralFloat" && !value.is_a?(Integer)
|
|
101
|
+
words << float_bits(Float(value))
|
|
102
|
+
elsif kind == "LiteralContextDependentNumber" && value.is_a?(Array)
|
|
103
|
+
words.concat(value.map { |item| word(item) })
|
|
104
|
+
else
|
|
105
|
+
words << word(value)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
walk.call(schema)
|
|
112
|
+
raise Error, "extra operands in #{name}" unless index == values.size
|
|
113
|
+
words
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def read(binary)
|
|
117
|
+
raise Error, "SPIR-V must be a word-aligned binary String" unless binary.is_a?(String) && binary.bytesize >= 20 && binary.bytesize % 4 == 0
|
|
118
|
+
little = binary.unpack1("V") == MAGIC
|
|
119
|
+
words = binary.unpack(little ? "V*" : "N*")
|
|
120
|
+
raise Error, "invalid SPIR-V magic" unless words[0] == MAGIC
|
|
121
|
+
version = words[1]
|
|
122
|
+
raise Error, "unsupported SPIR-V version" unless (version & 0xff0000ff).zero? && (version >> 16) == 1 && ((version >> 8) & 255) <= 6
|
|
123
|
+
raise Error, "invalid id bound" unless words[3].between?(1, MAX_BOUND)
|
|
124
|
+
raise Error, "reserved schema must be zero" unless words[4].zero?
|
|
125
|
+
|
|
126
|
+
[words.first(5), read_instructions(words), little]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def read_instructions(words)
|
|
130
|
+
instructions = []
|
|
131
|
+
offset = 5
|
|
132
|
+
while offset < words.size
|
|
133
|
+
count, opcode = words[offset] >> 16, words[offset] & 0xffff
|
|
134
|
+
raise Error, "invalid instruction length at word #{offset}" if count.zero? || count > words.size - offset
|
|
135
|
+
raise Error, "unknown opcode #{opcode}" unless OPCODES.key?(opcode)
|
|
136
|
+
instructions << [opcode, words.slice(offset + 1, count - 1)]
|
|
137
|
+
offset += count
|
|
138
|
+
end
|
|
139
|
+
instructions
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def decode(opcode, words, bound:, types: {})
|
|
143
|
+
name, schema = OPCODES.fetch(opcode)
|
|
144
|
+
tokens, result = decode_operands(name, schema, words, bound:, types:)
|
|
145
|
+
record_type_declaration(types, name, words)
|
|
146
|
+
types[[:import, words[0]]] = unquote(tokens[0]) if name == "OpExtInstImport"
|
|
147
|
+
[name, tokens, result]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def decode_operands(name, schema, words, bound:, types:)
|
|
151
|
+
tokens = []
|
|
152
|
+
index = 0
|
|
153
|
+
result = nil
|
|
154
|
+
result_type = nil
|
|
155
|
+
take = lambda do
|
|
156
|
+
raise Error, "truncated #{name}" if index >= words.size
|
|
157
|
+
value = words[index]
|
|
158
|
+
index += 1
|
|
159
|
+
value
|
|
160
|
+
end
|
|
161
|
+
walk = lambda do |operands|
|
|
162
|
+
operands.each do |kind, quantifier|
|
|
163
|
+
loop do
|
|
164
|
+
break if ["?", "*"].include?(quantifier) && index == words.size
|
|
165
|
+
category, entries, bases = OPERAND_KINDS.fetch(kind)
|
|
166
|
+
if category == "Composite"
|
|
167
|
+
walk.call(bases.map { |base| [base, ""] })
|
|
168
|
+
elsif kind == "LiteralString"
|
|
169
|
+
tokens << decode_string(take)
|
|
170
|
+
elsif kind == "LiteralContextDependentNumber"
|
|
171
|
+
tokens << decode_number(take, types[result_type])
|
|
172
|
+
else
|
|
173
|
+
number = take.call
|
|
174
|
+
if category == "Id"
|
|
175
|
+
raise Error, "id #{number} outside bound #{bound}" unless number.between?(1, bound - 1)
|
|
176
|
+
result_type = number if kind == "IdResultType"
|
|
177
|
+
if kind == "IdResult"
|
|
178
|
+
result = "%#{number}"
|
|
179
|
+
else
|
|
180
|
+
tokens << "%#{number}"
|
|
181
|
+
end
|
|
182
|
+
elsif ["ValueEnum", "BitEnum"].include?(category)
|
|
183
|
+
selected = if category == "BitEnum" && number != 0
|
|
184
|
+
entries.select { |_, (value, _)| value != 0 && (number & value) == value }
|
|
185
|
+
else
|
|
186
|
+
entries.select { |_, (value, _)| value == number }.first(1).to_h
|
|
187
|
+
end
|
|
188
|
+
matched = selected.values.reduce(0) { |mask, (value, _)| mask | value }
|
|
189
|
+
if selected.empty? || matched != number
|
|
190
|
+
tokens << number.to_s
|
|
191
|
+
else
|
|
192
|
+
tokens << selected.keys.join("|")
|
|
193
|
+
selected.each_value { |_, parameters| walk.call(parameters) }
|
|
194
|
+
end
|
|
195
|
+
else
|
|
196
|
+
if kind == "LiteralSpecConstantOpInteger"
|
|
197
|
+
definition = fetch_spec_opcode(number)
|
|
198
|
+
tokens << definition[0].delete_prefix("Op")
|
|
199
|
+
walk.call(definition[1].reject { |operand, _| ["IdResultType", "IdResult"].include?(operand) })
|
|
200
|
+
elsif kind == "LiteralExtInstInteger" && types[[:import, words[2]]] == "GLSL.std.450"
|
|
201
|
+
tokens << (GLSL_INSTRUCTIONS.key(number) || number.to_s)
|
|
202
|
+
elsif kind == "LiteralFloat"
|
|
203
|
+
tokens << float_text(number, 32)
|
|
204
|
+
else
|
|
205
|
+
tokens << number.to_s
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
break unless quantifier == "*"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
walk.call(schema)
|
|
214
|
+
raise Error, "extra words in #{name}" unless index == words.size
|
|
215
|
+
[tokens, result]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def decode_string(take)
|
|
219
|
+
bytes = +"".b
|
|
220
|
+
loop do
|
|
221
|
+
chunk = [take.call].pack("V")
|
|
222
|
+
nul = chunk.index("\0")
|
|
223
|
+
if nul
|
|
224
|
+
raise Error, "nonzero string padding" unless chunk.bytes.drop(nul).all?(&:zero?)
|
|
225
|
+
bytes << chunk.byteslice(0, nul)
|
|
226
|
+
break
|
|
227
|
+
end
|
|
228
|
+
bytes << chunk
|
|
229
|
+
end
|
|
230
|
+
bytes.force_encoding(Encoding::UTF_8)
|
|
231
|
+
raise Error, "invalid UTF-8 string" unless bytes.valid_encoding?
|
|
232
|
+
quote(bytes)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def decode_number(take, type)
|
|
236
|
+
width = type ? type[1] : 32
|
|
237
|
+
raise Error, "unsupported scalar width #{width}" unless [16, 32, 64].include?(width)
|
|
238
|
+
bits = take.call
|
|
239
|
+
bits |= take.call << 32 if width == 64
|
|
240
|
+
return float_text(bits, width) if type && type[0] == :float
|
|
241
|
+
bits -= 1 << width if type && type[2] == 1 && bits[width - 1] == 1
|
|
242
|
+
bits.to_s
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def fetch_spec_opcode(number)
|
|
246
|
+
definition = OPCODES[number] or raise Error, "unknown specialization opcode"
|
|
247
|
+
raise Error, "recursive specialization instruction" if definition[1].any? { |operand, _| operand == "LiteralSpecConstantOpInteger" }
|
|
248
|
+
definition
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# SPIR-V's hexadecimal float syntax represents infinity/NaN with exponent
|
|
252
|
+
# one above the finite range, retaining payload bits for exact round trips.
|
|
253
|
+
def float_text(bits, width)
|
|
254
|
+
exponent_bits, fraction_bits = {16 => [5, 10], 32 => [8, 23], 64 => [11, 52]}.fetch(width)
|
|
255
|
+
sign = bits[width - 1] == 1 ? "-" : ""
|
|
256
|
+
exponent = (bits >> fraction_bits) & ((1 << exponent_bits) - 1)
|
|
257
|
+
fraction = bits & ((1 << fraction_bits) - 1)
|
|
258
|
+
if exponent == (1 << exponent_bits) - 1
|
|
259
|
+
digits = (fraction_bits + 3) / 4
|
|
260
|
+
mantissa = (fraction << (digits * 4 - fraction_bits)).to_s(16).rjust(digits, "0")
|
|
261
|
+
"#{sign}0x1.#{mantissa}p+#{1 << (exponent_bits - 1)}"
|
|
262
|
+
elsif width == 16
|
|
263
|
+
value = (exponent.zero? ? fraction : fraction + (1 << fraction_bits)) * 2.0**((exponent.zero? ? 1 : exponent) - 15 - fraction_bits)
|
|
264
|
+
format("%a", sign.empty? ? value : -value)
|
|
265
|
+
else
|
|
266
|
+
value = [bits].pack(width == 32 ? "V" : "Q<").unpack1(width == 32 ? "e" : "E")
|
|
267
|
+
format("%a", value)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Round-to-nearest, ties-to-even from Ruby's Float64. String#pack('e') on
|
|
272
|
+
# some Ruby versions overflows just-above-FLT_MAX values prematurely,
|
|
273
|
+
# including the nine-digit decimal spelling emitted by spirv-dis.
|
|
274
|
+
def float_bits(value, width = 32)
|
|
275
|
+
return [value.to_f].pack("E").unpack1("Q<") if width == 64
|
|
276
|
+
exponent_bits, fraction_bits = {16 => [5, 10], 32 => [8, 23]}.fetch(width)
|
|
277
|
+
source = [value.to_f].pack("E").unpack1("Q<")
|
|
278
|
+
sign = (source >> 63) << (width - 1)
|
|
279
|
+
exponent = source >> 52 & 0x7ff
|
|
280
|
+
fraction = source & ((1 << 52) - 1)
|
|
281
|
+
exponent_max = (1 << exponent_bits) - 1
|
|
282
|
+
if exponent == 0x7ff
|
|
283
|
+
payload = fraction >> (52 - fraction_bits)
|
|
284
|
+
payload = 1 if fraction != 0 && payload.zero?
|
|
285
|
+
return sign | (exponent_max << fraction_bits) | payload
|
|
286
|
+
end
|
|
287
|
+
return sign if exponent.zero? && fraction.zero?
|
|
288
|
+
target_exponent = (exponent.zero? ? 1 : exponent) - 1023 + (1 << (exponent_bits - 1)) - 1
|
|
289
|
+
mantissa = exponent.zero? ? fraction : fraction | (1 << 52)
|
|
290
|
+
shift = 52 - fraction_bits + [1 - target_exponent, 0].max
|
|
291
|
+
rounded = mantissa >> shift
|
|
292
|
+
remainder = mantissa & ((1 << shift) - 1)
|
|
293
|
+
halfway = 1 << (shift - 1)
|
|
294
|
+
rounded += 1 if remainder > halfway || (remainder == halfway && rounded.odd?)
|
|
295
|
+
if target_exponent <= 0
|
|
296
|
+
return sign | rounded
|
|
297
|
+
end
|
|
298
|
+
if rounded >= 1 << (fraction_bits + 1)
|
|
299
|
+
rounded >>= 1
|
|
300
|
+
target_exponent += 1
|
|
301
|
+
end
|
|
302
|
+
return sign | (exponent_max << fraction_bits) if target_exponent >= exponent_max
|
|
303
|
+
sign | (target_exponent << fraction_bits) | (rounded & ((1 << fraction_bits) - 1))
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Internal implementation of Electra.disassemble.
|
|
307
|
+
def disassemble(binary)
|
|
308
|
+
header, instructions, little = read(binary)
|
|
309
|
+
types = {}
|
|
310
|
+
lines = ["; SPIR-V", "; Version: #{header[1] >> 16}.#{header[1] >> 8 & 255}", "; Generator: #{header[2]}", "; Bound: #{header[3]}", "; Schema: #{header[4]}", "; Endian: #{little ? 'little' : 'big'}"]
|
|
311
|
+
instructions.each do |opcode, words|
|
|
312
|
+
name, tokens, result = decode(opcode, words, bound: header[3], types: types)
|
|
313
|
+
lines << [result && "#{result} =", name, *tokens].compact.join(" ")
|
|
314
|
+
end
|
|
315
|
+
lines.join("\n") + "\n"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def assemble(source)
|
|
319
|
+
raise Error, "assembly must be a String" unless source.is_a?(String)
|
|
320
|
+
parsed_statements = statements(source)
|
|
321
|
+
version, generator, bound, schema, little = parse_header(parsed_statements)
|
|
322
|
+
words, max_id = encode_statements(parsed_statements)
|
|
323
|
+
header = [MAGIC, version, generator, bound || max_id + 1, schema].map { |value| word(value) }
|
|
324
|
+
binary = [*header, *words].pack(little ? "V*" : "N*")
|
|
325
|
+
disassemble(binary) # Validate framing, operands and bounds before returning.
|
|
326
|
+
binary
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def parse_header(statements)
|
|
330
|
+
version, generator, bound, schema, little = 0x10000, 0, nil, 0, true
|
|
331
|
+
statements.each do |_, tokens|
|
|
332
|
+
case tokens.first
|
|
333
|
+
when /\A; Version: (\d+)\.(\d+)/ then version = ($1.to_i << 16) | ($2.to_i << 8)
|
|
334
|
+
when /\A; Generator: (\d+)\s*$/ then generator = $1.to_i
|
|
335
|
+
when /\A; Bound: (\d+)/ then bound = $1.to_i
|
|
336
|
+
when /\A; Schema: (\d+)/ then schema = $1.to_i
|
|
337
|
+
when /\A; Endian: big/ then little = false
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
[version, generator, bound, schema, little]
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def encode_statements(statements)
|
|
344
|
+
types = {}
|
|
345
|
+
max_id = 0
|
|
346
|
+
words = statements.each_with_object([]) do |(line_number, tokens), result|
|
|
347
|
+
next if tokens.first.start_with?(";")
|
|
348
|
+
instruction, instruction_max_id = encode_statement(tokens, line_number, types)
|
|
349
|
+
result.concat(instruction)
|
|
350
|
+
max_id = [max_id, instruction_max_id].max
|
|
351
|
+
rescue ArgumentError, TypeError => error
|
|
352
|
+
raise Error, "line #{line_number}: #{error.message}"
|
|
353
|
+
end
|
|
354
|
+
[words, max_id]
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def encode_statement(tokens, line_number, types)
|
|
358
|
+
name, tokens = parse_instruction(tokens, line_number)
|
|
359
|
+
values, max_id = parse_operands(tokens)
|
|
360
|
+
record_type_declaration(types, name, values)
|
|
361
|
+
types[[:import, values[0]]] = values[1] if name == "OpExtInstImport"
|
|
362
|
+
resolve_context_dependent_operands(name, tokens, values, types)
|
|
363
|
+
[encode(name, values), max_id]
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def parse_instruction(tokens, line_number)
|
|
367
|
+
result, name, operands = tokens[1] == "=" ? [tokens[0], tokens[2], tokens.drop(3)] : [nil, tokens[0], tokens.drop(1)]
|
|
368
|
+
definition = INSTRUCTIONS[name] or raise Error, "unknown instruction #{name} at line #{line_number}"
|
|
369
|
+
result_index = definition[1].index { |kind, _| kind == "IdResult" }
|
|
370
|
+
if result_index
|
|
371
|
+
raise Error, "missing result id at line #{line_number}" unless result
|
|
372
|
+
operands.insert(result_index, result)
|
|
373
|
+
elsif result
|
|
374
|
+
raise Error, "unexpected result id at line #{line_number}"
|
|
375
|
+
end
|
|
376
|
+
[name, operands]
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def parse_operands(tokens)
|
|
380
|
+
max_id = 0
|
|
381
|
+
values = tokens.map do |token|
|
|
382
|
+
value = parse_operand(token)
|
|
383
|
+
max_id = [max_id, value].max if token.start_with?("%")
|
|
384
|
+
value
|
|
385
|
+
end
|
|
386
|
+
[values, max_id]
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def record_type_declaration(types, name, values)
|
|
390
|
+
types[values[0]] = [:float, values[1]] if name == "OpTypeFloat"
|
|
391
|
+
types[values[0]] = [:int, values[1], values[2]] if name == "OpTypeInt"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def resolve_context_dependent_operands(name, tokens, values, types)
|
|
395
|
+
if name == "OpExtInst" && types[[:import, values[2]]] == "GLSL.std.450" && values[3].is_a?(String)
|
|
396
|
+
values[3] = GLSL_INSTRUCTIONS.fetch(values[3]) { raise Error, "unknown GLSL instruction #{values[3]}" }
|
|
397
|
+
end
|
|
398
|
+
if ["OpConstant", "OpSpecConstant"].include?(name)
|
|
399
|
+
type = types[values[0]] or raise Error, "constant type must be declared first"
|
|
400
|
+
values[2] = number_words(tokens[2], type)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def parse_operand(token)
|
|
405
|
+
if token.start_with?("%")
|
|
406
|
+
raise Error, "only numeric ids are supported" unless token.match?(/\A%[1-9]\d*\z/)
|
|
407
|
+
token.delete_prefix("%").to_i
|
|
408
|
+
elsif token.start_with?('"')
|
|
409
|
+
unquote(token)
|
|
410
|
+
elsif token.match?(/\A-?(?:0x[\da-f]+|\d+)\z/i)
|
|
411
|
+
Integer(token, 0) rescue Integer(token, 10)
|
|
412
|
+
else
|
|
413
|
+
token
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Tokenize statements, preserving multiline strings and ignoring comments.
|
|
418
|
+
def statements(source)
|
|
419
|
+
result = []
|
|
420
|
+
tokens = []
|
|
421
|
+
line_number = 1
|
|
422
|
+
first_line = 1
|
|
423
|
+
source.scan(/"(?:\\[\s\S]|[^"\\])*"|;[^\r\n]*|\r?\n|[^\s;]+/).each do |token|
|
|
424
|
+
if token.start_with?(";")
|
|
425
|
+
result << [line_number, [token]] if tokens.empty?
|
|
426
|
+
elsif token == "\n" || token == "\r\n"
|
|
427
|
+
result << [first_line, tokens] unless tokens.empty?
|
|
428
|
+
tokens = []
|
|
429
|
+
line_number += 1
|
|
430
|
+
first_line = line_number
|
|
431
|
+
else
|
|
432
|
+
tokens << token
|
|
433
|
+
line_number += token.count("\n")
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
result << [first_line, tokens] unless tokens.empty?
|
|
437
|
+
result
|
|
438
|
+
rescue ArgumentError => error
|
|
439
|
+
raise Error, "invalid assembly encoding: #{error.message}"
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Encode context-dependent integer/float literal words for a scalar type.
|
|
443
|
+
def number_words(token, type)
|
|
444
|
+
kind, width, = type
|
|
445
|
+
bits = kind == :float ? float_literal_bits(token, width) : integer_literal_bits(token, width)
|
|
446
|
+
width <= 32 ? [bits] : [bits & 0xffffffff, bits >> 32]
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def float_literal_bits(token, width)
|
|
450
|
+
raise Error, "assembly supports 16/32/64-bit float constants" unless [16, 32, 64].include?(width)
|
|
451
|
+
exponent_bits, fraction_bits = {16 => [5, 10], 32 => [8, 23], 64 => [11, 52]}.fetch(width)
|
|
452
|
+
max_exponent = 1 << (exponent_bits - 1)
|
|
453
|
+
match = token.match(/\A(-?)0x1(?:\.([\da-f]+))?p\+#{max_exponent}\z/i)
|
|
454
|
+
return float_bits(Float(token), width) unless match
|
|
455
|
+
|
|
456
|
+
digits = match[2] || "0"
|
|
457
|
+
fraction = Integer(digits, 16) << fraction_bits
|
|
458
|
+
fraction >>= digits.size * 4
|
|
459
|
+
bits = (((1 << exponent_bits) - 1) << fraction_bits) | fraction
|
|
460
|
+
match[1] == "-" ? bits | (1 << (width - 1)) : bits
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def integer_literal_bits(token, width)
|
|
464
|
+
value = Integer(token, 0) rescue Integer(token, 10)
|
|
465
|
+
raise Error, "integer constant outside #{width}-bit range" unless value.between?(-(1 << (width - 1)), (1 << width) - 1)
|
|
466
|
+
value & ((1 << width) - 1)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
private_class_method :encode_operands, :read_instructions, :decode_operands, :decode_string,
|
|
470
|
+
:decode_number, :fetch_spec_opcode, :parse_header,
|
|
471
|
+
:encode_statements, :encode_statement, :parse_instruction, :parse_operands,
|
|
472
|
+
:record_type_declaration, :resolve_context_dependent_operands, :parse_operand,
|
|
473
|
+
:float_literal_bits, :integer_literal_bits
|
|
474
|
+
end
|
|
475
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ruby 3.2 added Data; Struct provides the same record operations used here.
|
|
4
|
+
unless defined?(Data)
|
|
5
|
+
Data = Struct
|
|
6
|
+
def Data.define(*members, &block)
|
|
7
|
+
Struct.new(*members) do
|
|
8
|
+
members.each { |member| undef_method :"#{member}=" }
|
|
9
|
+
define_method(:initialize) do |*values, **keywords|
|
|
10
|
+
if keywords.any?
|
|
11
|
+
raise ArgumentError, "expected either positional or keyword members" unless values.empty? && keywords.keys.sort == members.sort
|
|
12
|
+
values = members.map { |member| keywords.fetch(member) }
|
|
13
|
+
end
|
|
14
|
+
raise ArgumentError, "wrong number of members" unless values.length == members.length
|
|
15
|
+
super(*values)
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
define_method(:with) do |**changes|
|
|
19
|
+
changes.empty? ? self : self.class.new(**to_h.merge(changes))
|
|
20
|
+
end
|
|
21
|
+
class_eval(&block) if block
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|