relaton-un 2.1.0 → 2.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d4039da98331ed950c1b39945ac3ae22fcca20279b6fe151d91c1e7e65c0811
4
- data.tar.gz: 10ed51aa8d99ad31c732fc280dc120f482663e44bfc8c25191408835ad17eb6a
3
+ metadata.gz: bd62341915793af74a836d522b901c108bda7fbc57825c0f663203d2f048777a
4
+ data.tar.gz: 26133140a50fb40d46db8eb3959beb9fe4bbbcf51d0716d7f1a5bcbbff782664
5
5
  SHA512:
6
- metadata.gz: 0504dc57daa40acdbd2255ce64c8244794183c0c9e24e1f14c82e723f3655be36ba628af91568cb950442fcfd6aad9a8e57b8bb5beb8451cd39f2a02125580ff
7
- data.tar.gz: 1929cc7e26616e2f2245d562b0c20927b4396a5363a23faee610f6723ddff02ad93223473516897c5455f25f9a983a8ca4de4011f5f561d563cfcb2d8b244f37
6
+ metadata.gz: 13fa9b4d6f59851cbff14ade0621bee34ab7a2d152300a4feebab83673daa4923b46789ec21b417b86969e3c5935095f68950c511ebc72afaf70a443c5da63e9
7
+ data.tar.gz: 8fed8aefd9a06f23f27dbc2d9c5dc6f7dc4793fd884a734f56fedf69facd70d31dbbb46fd9e81d1ea4146f4dd7df81afe6f92dcf3122072834311e9c7195ae65
@@ -4,7 +4,7 @@ name: rake
4
4
 
5
5
  on:
6
6
  push:
7
- branches: [ master, main ]
7
+ branches: [ master, main, lutaml-integration ]
8
8
  tags: [ v* ]
9
9
  pull_request:
10
10
  workflow_dispatch:
data/Gemfile CHANGED
@@ -10,4 +10,5 @@ gem "rspec", "~> 3.0"
10
10
  gem "ruby-jing"
11
11
  gem "simplecov"
12
12
  gem "vcr"
13
+ gem "wasmtime", "~> 41.0", group: :development # kept for golden-token regeneration only
13
14
  gem "webmock"
@@ -1,16 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- begin
4
- require "wasmtime"
5
- WASMTIME_LOADED = true
6
- rescue LoadError
7
- WASMTIME_LOADED = false
8
- end
3
+ require_relative "wasm"
9
4
 
10
5
  module Relaton
11
6
  module Un
12
7
  module TokenGenerator
13
- WASMTIME_AVAILABLE = WASMTIME_LOADED
14
8
  WASM_PATH = File.join(__dir__, "wasm_v_bg.wasm")
15
9
  DOMAIN = "documents.un.org"
16
10
 
@@ -18,11 +12,6 @@ module Relaton
18
12
  # Tokens are cached per-minute since the WASM output changes each minute.
19
13
  # @return [String] decimal string representation of the i64 token
20
14
  def self.generate
21
- unless WASMTIME_AVAILABLE
22
- warn "[relaton-un] wasmtime gem is not available on this platform. Token generation is disabled."
23
- return nil
24
- end
25
-
26
15
  now = Time.now.utc
27
16
  key = [now.year, now.month, now.day, now.hour, now.min]
28
17
  return @cached_token if @cached_key == key && @cached_token
@@ -31,92 +20,49 @@ module Relaton
31
20
  @cached_token = call_wasm(*key)
32
21
  end
33
22
 
34
- def self.call_wasm(year, month, day, hour, minute) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
35
- engine = Wasmtime::Engine.new
36
- mod = Wasmtime::Module.new(engine, File.binread(WASM_PATH))
37
- store = Wasmtime::Store.new(engine)
38
- linker = Wasmtime::Linker.new(engine)
39
-
23
+ def self.call_wasm(year, month, day, hour, minute)
24
+ mod = (@module ||= Wasm::Module.parse(File.binread(WASM_PATH)))
40
25
  heap = Heap.new
41
26
  fake_window = Object.new
42
27
  fake_location = Object.new
43
-
44
- # Pre-allocate well-known objects in the heap
45
28
  heap.alloc(fake_window) # index 4
46
29
  heap.alloc(fake_location) # index 5
47
30
 
48
- define_imports(linker, heap, fake_window, fake_location)
49
-
50
- instance = linker.instantiate(store, mod)
51
- result = instance.invoke("check", year, month, day, hour, minute)
52
- result.to_s
31
+ imports = { "wbg" => build_imports(heap, fake_window, fake_location) }
32
+ instance = Wasm::Instance.new(mod, imports)
33
+ instance.invoke("check", year, month, day, hour, minute).to_s
53
34
  end
54
35
 
55
- def self.define_imports(linker, heap, fake_window, fake_location) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
56
- linker.func_new("wbg", "__wbindgen_object_drop_ref", [:i32], []) do |_caller, idx|
57
- heap.drop(idx)
58
- end
59
-
60
- linker.func_new("wbg", "__wbg_instanceof_Window_acc97ff9f5d2c7b4", [:i32], [:i32]) do |_caller, _idx|
61
- 1
62
- end
63
-
64
- linker.func_new("wbg", "__wbg_location_8cc8ccf27e342c0a", [:i32], [:i32]) do |_caller, _idx|
65
- heap.alloc(fake_location)
66
- end
67
-
68
- define_host_import(linker, heap)
69
-
70
- linker.func_new("wbg", "__wbg_newnoargs_b5b063fc6c2f0376", [:i32, :i32], [:i32]) do |_caller, _ptr, _len|
71
- heap.alloc(Object.new)
72
- end
73
-
74
- linker.func_new("wbg", "__wbg_call_97ae9d8645dc388b", [:i32, :i32], [:i32]) do |_caller, _func_idx, _this_idx|
75
- heap.alloc(fake_window)
76
- end
77
-
78
- linker.func_new("wbg", "__wbindgen_object_clone_ref", [:i32], [:i32]) do |_caller, idx|
79
- heap.alloc(heap.get(idx))
80
- end
81
-
36
+ def self.build_imports(heap, fake_window, fake_location) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
37
+ w = {}
38
+ w["__wbindgen_object_drop_ref"] = ->(_c, idx) { heap.drop(idx); nil }
39
+ w["__wbg_instanceof_Window_acc97ff9f5d2c7b4"] = ->(_c, _idx) { 1 }
40
+ w["__wbg_location_8cc8ccf27e342c0a"] = ->(_c, _idx) { heap.alloc(fake_location) }
41
+ w["__wbg_newnoargs_b5b063fc6c2f0376"] = ->(_c, _ptr, _len) { heap.alloc(Object.new) }
42
+ w["__wbg_call_97ae9d8645dc388b"] = ->(_c, _f, _t) { heap.alloc(fake_window) }
43
+ w["__wbindgen_object_clone_ref"] = ->(_c, idx) { heap.alloc(heap.get(idx)) }
82
44
  %w[
83
45
  __wbg_self_6d479506f72c6a71
84
46
  __wbg_window_f2557cc78490aceb
85
47
  __wbg_globalThis_7f206bda628d5286
86
48
  __wbg_global_ba75c50d1cf384f4
87
- ].each do |name|
88
- linker.func_new("wbg", name, [], [:i32]) do |_caller|
89
- heap.alloc(fake_window)
90
- end
91
- end
92
-
93
- linker.func_new("wbg", "__wbindgen_is_undefined", [:i32], [:i32]) do |_caller, idx|
94
- heap.get(idx) == :undefined ? 1 : 0
95
- end
96
-
97
- linker.func_new("wbg", "__wbindgen_debug_string", [:i32, :i32], []) do |_caller, _ret_ptr, _idx|
98
- # no-op
99
- end
100
-
101
- linker.func_new("wbg", "__wbindgen_throw", [:i32, :i32], []) do |caller, ptr, len|
49
+ ].each { |name| w[name] = ->(_c) { heap.alloc(fake_window) } }
50
+ w["__wbindgen_is_undefined"] = ->(_c, idx) { heap.get(idx) == :undefined ? 1 : 0 }
51
+ w["__wbindgen_debug_string"] = ->(_c, _ret_ptr, _idx) { nil }
52
+ w["__wbindgen_throw"] = lambda do |caller, ptr, len|
102
53
  mem = caller.export("memory").to_memory
103
- msg = mem.read(ptr, len)
104
- raise msg
54
+ raise mem.read(ptr, len)
105
55
  end
106
- end
107
-
108
- # The host import writes the domain string into WASM memory.
109
- # It allocates space via __wbindgen_malloc, copies the UTF-8 bytes,
110
- # and writes (ptr, len) at the return pointer address.
111
- def self.define_host_import(linker, _heap)
112
- linker.func_new("wbg", "__wbg_host_f82dbbd8bb5ef24a", [:i32, :i32], []) do |caller, ret_ptr, _loc_idx|
56
+ w["__wbg_host_f82dbbd8bb5ef24a"] = lambda do |caller, ret_ptr, _loc_idx|
113
57
  mem = caller.export("memory").to_memory
114
- malloc_fn = caller.export("__wbindgen_malloc").to_func
58
+ malloc = caller.export("__wbindgen_malloc").to_func
115
59
  host_bytes = DOMAIN.encode("utf-8")
116
- ptr = malloc_fn.call(host_bytes.bytesize)
60
+ ptr = malloc.call(host_bytes.bytesize)
117
61
  mem.write(ptr, host_bytes)
118
62
  mem.write(ret_ptr, [ptr, host_bytes.bytesize].pack("V2"))
63
+ nil
119
64
  end
65
+ w
120
66
  end
121
67
 
122
68
  # Manages a slab-allocated heap of Ruby objects indexed by i32,
@@ -153,7 +99,7 @@ module Relaton
153
99
  end
154
100
  end
155
101
 
156
- private_class_method :call_wasm, :define_imports, :define_host_import
102
+ private_class_method :call_wasm, :build_imports
157
103
  end
158
104
  end
159
105
  end
@@ -1,5 +1,5 @@
1
1
  module Relaton
2
2
  module Un
3
- VERSION = "2.1.0".freeze
3
+ VERSION = "2.1.1".freeze
4
4
  end
5
5
  end
@@ -0,0 +1,434 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Un
5
+ module Wasm
6
+ # Binary WebAssembly decoder. Handles the subset of WASM MVP plus the
7
+ # sign-extension proposal that the shipped wasm_v_bg.wasm uses. No
8
+ # floats, no SIMD, no bulk-memory, no reference types beyond funcref.
9
+ class Decoder
10
+ MAGIC = "\x00asm".b
11
+ VERSION = "\x01\x00\x00\x00".b
12
+
13
+ VALTYPE_I32 = 0x7F
14
+ VALTYPE_I64 = 0x7E
15
+ VALTYPE_F32 = 0x7D
16
+ VALTYPE_F64 = 0x7C
17
+
18
+ def initialize(bytes)
19
+ @bytes = bytes.b
20
+ @pos = 0
21
+ end
22
+
23
+ def decode
24
+ header!
25
+ mod = Module.new
26
+ until eof?
27
+ id = read_byte
28
+ size = read_u32
29
+ section_end = @pos + size
30
+ decode_section(mod, id, section_end)
31
+ @pos = section_end
32
+ end
33
+ mod
34
+ end
35
+
36
+ private
37
+
38
+ def header!
39
+ magic = take(4)
40
+ raise DecodeError, "bad magic: #{magic.inspect}" unless magic == MAGIC
41
+
42
+ version = take(4)
43
+ raise DecodeError, "bad version: #{version.inspect}" unless version == VERSION
44
+ end
45
+
46
+ def decode_section(mod, id, section_end)
47
+ case id
48
+ when 0 then skip_to(section_end) # custom
49
+ when 1 then decode_types(mod)
50
+ when 2 then decode_imports(mod)
51
+ when 3 then decode_functions(mod)
52
+ when 4 then decode_tables(mod)
53
+ when 5 then decode_memories(mod)
54
+ when 6 then decode_globals(mod)
55
+ when 7 then decode_exports(mod)
56
+ when 8 then mod.instance_variable_set(:@start, read_u32)
57
+ when 9 then decode_elements(mod)
58
+ when 10 then decode_code(mod)
59
+ when 11 then decode_data(mod)
60
+ else raise DecodeError, "unknown section id #{id}"
61
+ end
62
+ end
63
+
64
+ def decode_types(mod)
65
+ read_vec do
66
+ tag = read_byte
67
+ raise DecodeError, "bad functype tag #{tag}" unless tag == 0x60
68
+
69
+ params = read_vec { read_byte }
70
+ results = read_vec { read_byte }
71
+ mod.types << FuncType.new(params, results)
72
+ end
73
+ end
74
+
75
+ def decode_imports(mod)
76
+ read_vec do
77
+ mod_name = read_name
78
+ field = read_name
79
+ kind_byte = read_byte
80
+ kind, desc = case kind_byte
81
+ when 0x00 then [:func, read_u32]
82
+ when 0x01 then [:table, decode_tabletype]
83
+ when 0x02 then [:memory, decode_memtype]
84
+ when 0x03 then [:global, [read_byte, read_byte == 1]]
85
+ else raise DecodeError, "bad import kind #{kind_byte}"
86
+ end
87
+ mod.imports << Import.new(mod_name, field, kind, desc)
88
+ mod.function_type_indices << desc if kind == :func
89
+ end
90
+ end
91
+
92
+ def decode_functions(mod)
93
+ read_vec do
94
+ mod.function_type_indices << read_u32
95
+ end
96
+ end
97
+
98
+ def decode_tables(mod)
99
+ read_vec do
100
+ mod.tables << decode_tabletype
101
+ end
102
+ end
103
+
104
+ def decode_memories(mod)
105
+ read_vec do
106
+ mod.memories << decode_memtype
107
+ end
108
+ end
109
+
110
+ def decode_globals(mod)
111
+ read_vec do
112
+ valtype = read_byte
113
+ mutable = read_byte == 1
114
+ init_expr = decode_const_expr
115
+ mod.globals << Global.new(valtype, mutable, init_expr)
116
+ end
117
+ end
118
+
119
+ def decode_exports(mod)
120
+ read_vec do
121
+ name = read_name
122
+ kind_byte = read_byte
123
+ kind = case kind_byte
124
+ when 0x00 then :func
125
+ when 0x01 then :table
126
+ when 0x02 then :memory
127
+ when 0x03 then :global
128
+ else raise DecodeError, "bad export kind #{kind_byte}"
129
+ end
130
+ mod.exports << Export.new(name, kind, read_u32)
131
+ end
132
+ end
133
+
134
+ def decode_elements(mod)
135
+ read_vec do
136
+ flags = read_u32
137
+ # MVP form: flags == 0 means active, table 0, offset, funcref vec.
138
+ raise DecodeError, "unsupported element segment flags #{flags}" unless flags.zero?
139
+
140
+ offset_expr = decode_const_expr
141
+ funcs = read_vec { read_u32 }
142
+ mod.elements << Element.new(0, offset_expr, funcs)
143
+ end
144
+ end
145
+
146
+ def decode_code(mod)
147
+ read_vec do
148
+ size = read_u32
149
+ body_end = @pos + size
150
+ local_groups = read_vec { [read_u32, read_byte] } # [count, valtype]
151
+ locals = local_groups.flat_map { |count, vt| Array.new(count, vt) }
152
+ body = decode_instructions(body_end)
153
+ mod.code << Code.new(locals, body)
154
+ @pos = body_end
155
+ end
156
+ end
157
+
158
+ def decode_data(mod)
159
+ read_vec do
160
+ flags = read_u32
161
+ mem_idx = 0
162
+ offset_expr = nil
163
+ case flags
164
+ when 0
165
+ offset_expr = decode_const_expr
166
+ when 1
167
+ # passive — not used here
168
+ when 2
169
+ mem_idx = read_u32
170
+ offset_expr = decode_const_expr
171
+ else
172
+ raise DecodeError, "bad data flags #{flags}"
173
+ end
174
+ bytes = take(read_u32)
175
+ mod.data << Data.new(mem_idx, offset_expr, bytes)
176
+ end
177
+ end
178
+
179
+ def decode_tabletype
180
+ elem_type = read_byte
181
+ decode_limits.then { |init, max| TableType.new(elem_type, init, max) }
182
+ end
183
+
184
+ def decode_memtype
185
+ decode_limits.then { |init, max| MemoryType.new(init, max) }
186
+ end
187
+
188
+ def decode_limits
189
+ flag = read_byte
190
+ init = read_u32
191
+ max = flag == 1 ? read_u32 : nil
192
+ [init, max]
193
+ end
194
+
195
+ # A constant init expression: a single instruction followed by `end`.
196
+ # For our module it'll only be i32.const / i64.const / global.get.
197
+ def decode_const_expr
198
+ op = read_byte
199
+ val = case op
200
+ when 0x41 then [:i32_const, read_s32]
201
+ when 0x42 then [:i64_const, read_s64]
202
+ when 0x23 then [:global_get, read_u32]
203
+ else raise DecodeError, "unsupported const expr op 0x#{op.to_s(16)}"
204
+ end
205
+ end_op = read_byte
206
+ raise DecodeError, "const expr not terminated with end (got 0x#{end_op.to_s(16)})" unless end_op == 0x0B
207
+
208
+ val
209
+ end
210
+
211
+ # Decode the instruction stream of a function body up to its final
212
+ # `end`. Returns a flat array of [op_sym, *operands]. Block-introducing
213
+ # ops are post-processed to carry `end_pc` (and `else_pc` for `if`)
214
+ # so the interpreter can jump without scanning at runtime.
215
+ def decode_instructions(body_end)
216
+ instrs = []
217
+ stack = [] # of [kind_sym, instr_index]
218
+
219
+ until @pos >= body_end
220
+ op = read_byte
221
+ pc = instrs.size
222
+ case op
223
+ when 0x00 then instrs << [:unreachable]
224
+ when 0x01 then instrs << [:nop]
225
+ when 0x02
226
+ bt = decode_blocktype
227
+ instrs << [:block, bt, nil] # end_pc filled in below
228
+ stack << [:block, pc]
229
+ when 0x03
230
+ bt = decode_blocktype
231
+ instrs << [:loop, bt, nil]
232
+ stack << [:loop, pc]
233
+ when 0x04
234
+ bt = decode_blocktype
235
+ instrs << [:if, bt, nil, nil] # else_pc, end_pc
236
+ stack << [:if, pc]
237
+ when 0x05
238
+ if_kind, if_pc = stack.last
239
+ raise DecodeError, "else without matching if" unless if_kind == :if
240
+
241
+ instrs[if_pc][2] = pc # else_pc points at the `else` instruction
242
+ instrs << [:else, nil]
243
+ when 0x0B
244
+ instrs << [:end]
245
+ if stack.any?
246
+ kind, intro_pc = stack.pop
247
+ case kind
248
+ when :block, :loop then instrs[intro_pc][2] = pc
249
+ when :if
250
+ instrs[intro_pc][3] = pc # end_pc
251
+ instrs[intro_pc][2] ||= pc # else_pc defaults to end_pc
252
+ # if there was an `else`, fix it up
253
+ else_pc = instrs[intro_pc][2]
254
+ instrs[else_pc][1] = pc if else_pc < pc && instrs[else_pc][0] == :else
255
+ end
256
+ end
257
+ when 0x0C then instrs << [:br, read_u32]
258
+ when 0x0D then instrs << [:br_if, read_u32]
259
+ when 0x0E
260
+ labels = read_vec { read_u32 }
261
+ default = read_u32
262
+ instrs << [:br_table, labels, default]
263
+ when 0x0F then instrs << [:return_]
264
+ when 0x10 then instrs << [:call, read_u32]
265
+ when 0x11
266
+ type_idx = read_u32
267
+ table_idx = read_u32
268
+ instrs << [:call_indirect, type_idx, table_idx]
269
+ when 0x1A then instrs << [:drop]
270
+ when 0x1B then instrs << [:select]
271
+ when 0x20 then instrs << [:local_get, read_u32]
272
+ when 0x21 then instrs << [:local_set, read_u32]
273
+ when 0x22 then instrs << [:local_tee, read_u32]
274
+ when 0x23 then instrs << [:global_get, read_u32]
275
+ when 0x24 then instrs << [:global_set, read_u32]
276
+ when 0x28..0x3E
277
+ align = read_u32
278
+ offset = read_u32
279
+ instrs << [MEM_OPS[op], align, offset]
280
+ when 0x3F
281
+ read_byte # reserved
282
+ instrs << [:memory_size]
283
+ when 0x40
284
+ read_byte # reserved
285
+ instrs << [:memory_grow]
286
+ when 0x41 then instrs << [:i32_const, read_s32]
287
+ when 0x42 then instrs << [:i64_const, read_s64]
288
+ when 0x45..0x8A then instrs << [NUM_OPS.fetch(op)]
289
+ when 0xA7 then instrs << [:i32_wrap_i64]
290
+ when 0xAC then instrs << [:i64_extend_i32_s]
291
+ when 0xAD then instrs << [:i64_extend_i32_u]
292
+ when 0xC0 then instrs << [:i32_extend8_s]
293
+ when 0xC1 then instrs << [:i32_extend16_s]
294
+ when 0xC2 then instrs << [:i64_extend8_s]
295
+ when 0xC3 then instrs << [:i64_extend16_s]
296
+ when 0xC4 then instrs << [:i64_extend32_s]
297
+ else
298
+ raise DecodeError, "unsupported opcode 0x#{op.to_s(16)} at byte #{@pos - 1}"
299
+ end
300
+ end
301
+ instrs
302
+ end
303
+
304
+ # Block type: 0x40 = empty, single valtype byte, or signed LEB128 type
305
+ # index. Returns one of [:empty] / [:value, valtype] / [:typeidx, idx].
306
+ def decode_blocktype
307
+ b = peek_byte
308
+ if b == 0x40
309
+ read_byte
310
+ [:empty]
311
+ elsif [VALTYPE_I32, VALTYPE_I64, VALTYPE_F32, VALTYPE_F64].include?(b)
312
+ [:value, read_byte]
313
+ else
314
+ [:typeidx, read_s33]
315
+ end
316
+ end
317
+
318
+ MEM_OPS = {
319
+ 0x28 => :i32_load, 0x29 => :i64_load,
320
+ 0x2C => :i32_load8_s, 0x2D => :i32_load8_u,
321
+ 0x2E => :i32_load16_s, 0x2F => :i32_load16_u,
322
+ 0x30 => :i64_load8_s, 0x31 => :i64_load8_u,
323
+ 0x32 => :i64_load16_s, 0x33 => :i64_load16_u,
324
+ 0x34 => :i64_load32_s, 0x35 => :i64_load32_u,
325
+ 0x36 => :i32_store, 0x37 => :i64_store,
326
+ 0x3A => :i32_store8, 0x3B => :i32_store16,
327
+ 0x3C => :i64_store8, 0x3D => :i64_store16,
328
+ 0x3E => :i64_store32
329
+ }.freeze
330
+
331
+ NUM_OPS = {
332
+ 0x45 => :i32_eqz, 0x46 => :i32_eq, 0x47 => :i32_ne,
333
+ 0x48 => :i32_lt_s, 0x49 => :i32_lt_u, 0x4A => :i32_gt_s, 0x4B => :i32_gt_u,
334
+ 0x4C => :i32_le_s, 0x4D => :i32_le_u, 0x4E => :i32_ge_s, 0x4F => :i32_ge_u,
335
+ 0x50 => :i64_eqz, 0x51 => :i64_eq, 0x52 => :i64_ne,
336
+ 0x53 => :i64_lt_s, 0x54 => :i64_lt_u, 0x55 => :i64_gt_s, 0x56 => :i64_gt_u,
337
+ 0x57 => :i64_le_s, 0x58 => :i64_le_u, 0x59 => :i64_ge_s, 0x5A => :i64_ge_u,
338
+ 0x67 => :i32_clz, 0x68 => :i32_ctz, 0x69 => :i32_popcnt,
339
+ 0x6A => :i32_add, 0x6B => :i32_sub, 0x6C => :i32_mul,
340
+ 0x6D => :i32_div_s, 0x6E => :i32_div_u, 0x6F => :i32_rem_s, 0x70 => :i32_rem_u,
341
+ 0x71 => :i32_and, 0x72 => :i32_or, 0x73 => :i32_xor,
342
+ 0x74 => :i32_shl, 0x75 => :i32_shr_s, 0x76 => :i32_shr_u,
343
+ 0x77 => :i32_rotl, 0x78 => :i32_rotr,
344
+ 0x79 => :i64_clz, 0x7A => :i64_ctz, 0x7B => :i64_popcnt,
345
+ 0x7C => :i64_add, 0x7D => :i64_sub, 0x7E => :i64_mul,
346
+ 0x7F => :i64_div_s, 0x80 => :i64_div_u, 0x81 => :i64_rem_s, 0x82 => :i64_rem_u,
347
+ 0x83 => :i64_and, 0x84 => :i64_or, 0x85 => :i64_xor,
348
+ 0x86 => :i64_shl, 0x87 => :i64_shr_s, 0x88 => :i64_shr_u,
349
+ 0x89 => :i64_rotl, 0x8A => :i64_rotr
350
+ }.freeze
351
+
352
+ # ---- low-level reading -----------------------------------------------
353
+
354
+ def eof?
355
+ @pos >= @bytes.bytesize
356
+ end
357
+
358
+ def read_byte
359
+ b = @bytes.getbyte(@pos)
360
+ raise DecodeError, "unexpected eof" unless b
361
+
362
+ @pos += 1
363
+ b
364
+ end
365
+
366
+ def peek_byte
367
+ @bytes.getbyte(@pos) or raise(DecodeError, "unexpected eof")
368
+ end
369
+
370
+ def take(n)
371
+ s = @bytes.byteslice(@pos, n)
372
+ raise DecodeError, "unexpected eof (wanted #{n} bytes)" unless s && s.bytesize == n
373
+
374
+ @pos += n
375
+ s
376
+ end
377
+
378
+ def skip_to(end_pos)
379
+ @pos = end_pos
380
+ end
381
+
382
+ def read_u32
383
+ result = 0
384
+ shift = 0
385
+ loop do
386
+ b = read_byte
387
+ result |= (b & 0x7F) << shift
388
+ break if (b & 0x80).zero?
389
+
390
+ shift += 7
391
+ raise DecodeError, "u32 overflow" if shift > 35
392
+ end
393
+ result
394
+ end
395
+
396
+ def read_s32
397
+ read_signed(32)
398
+ end
399
+
400
+ def read_s33
401
+ read_signed(33)
402
+ end
403
+
404
+ def read_s64
405
+ read_signed(64)
406
+ end
407
+
408
+ def read_signed(bits)
409
+ result = 0
410
+ shift = 0
411
+ loop do
412
+ b = read_byte
413
+ result |= (b & 0x7F) << shift
414
+ shift += 7
415
+ if (b & 0x80).zero?
416
+ result -= (1 << shift) if shift < bits && (b & 0x40).positive?
417
+ return result
418
+ end
419
+ raise DecodeError, "signed overflow" if shift > bits + 7
420
+ end
421
+ end
422
+
423
+ def read_name
424
+ take(read_u32).force_encoding(Encoding::UTF_8)
425
+ end
426
+
427
+ def read_vec
428
+ count = read_u32
429
+ Array.new(count) { yield }
430
+ end
431
+ end
432
+ end
433
+ end
434
+ end