Skip to main content

lib/quickbeam/wasm/parser.ex

defmodule QuickBEAM.WASM.Parser do
  @moduledoc false

  alias QuickBEAM.WASM.{Function, Module}

  @wasm_magic <<0x00, 0x61, 0x73, 0x6D>>
  @wasm_version_1 <<0x01, 0x00, 0x00, 0x00>>

  @section_custom 0
  @section_type 1
  @section_import 2
  @section_function 3
  @section_table 4
  @section_memory 5
  @section_global 6
  @section_export 7
  @section_start 8
  @section_element 9
  @section_code 10
  @section_data 11
  @section_data_count 12
  @section_tag 13

  # ── Public API ─────────────────────────────────────────

  @spec parse(binary()) :: {:ok, Module.t()} | {:error, String.t()}
  def parse(<<@wasm_magic, @wasm_version_1, rest::binary>>) do
    mod = %Module{version: 1}
    parse_sections(rest, mod)
  end

  def parse(<<@wasm_magic, v1, v2, v3, v4, _::binary>>),
    do: {:error, "unsupported WASM version: #{v1}.#{v2}.#{v3}.#{v4}"}

  def parse(_), do: {:error, "not a WASM binary (missing magic header)"}

  @spec validate(binary()) :: boolean()
  def validate(binary) do
    case parse(binary) do
      {:ok, _} -> true
      {:error, _} -> false
    end
  end

  # ── Section dispatch ───────────────────────────────────

  defp parse_sections(<<>>, mod), do: {:ok, finalize(mod)}

  defp parse_sections(<<id, rest::binary>>, mod) do
    {size, rest} = decode_u32(rest)
    <<section_data::binary-size(^size), rest::binary>> = rest

    mod = parse_section(id, section_data, mod)
    parse_sections(rest, mod)
  rescue
    MatchError -> {:error, "truncated section (id=#{id})"}
  end

  defp parse_section(@section_custom, data, mod) do
    {name, data} = decode_name(data)
    custom = %{name: name, data: data}
    %{mod | custom_sections: mod.custom_sections ++ [custom]}
  end

  defp parse_section(@section_type, data, mod) do
    {types, <<>>} = decode_vec(data, &decode_functype/1)
    %{mod | types: types}
  end

  defp parse_section(@section_import, data, mod) do
    {imports, <<>>} = decode_vec(data, &decode_import/1)
    %{mod | imports: imports}
  end

  defp parse_section(@section_function, data, mod) do
    {type_indices, <<>>} = decode_vec(data, &decode_u32/1)
    funcs = Enum.with_index(type_indices, fn type_idx, i -> {i, type_idx} end)
    Map.put(mod, :_func_type_indices, funcs)
  end

  defp parse_section(@section_table, data, mod) do
    {tables, <<>>} = decode_vec(data, &decode_table/1)
    %{mod | tables: tables}
  end

  defp parse_section(@section_memory, data, mod) do
    {memories, <<>>} = decode_vec(data, &decode_memory/1)
    %{mod | memories: memories}
  end

  defp parse_section(@section_global, data, mod) do
    {globals, <<>>} = decode_vec(data, &decode_global/1)
    %{mod | globals: globals}
  end

  defp parse_section(@section_export, data, mod) do
    {exports, <<>>} = decode_vec(data, &decode_export/1)
    %{mod | exports: exports}
  end

  defp parse_section(@section_start, data, mod) do
    {func_idx, <<>>} = decode_u32(data)
    %{mod | start: func_idx}
  end

  defp parse_section(@section_element, data, mod) do
    {elements, <<>>} = decode_vec(data, &decode_element/1)
    %{mod | elements: elements}
  end

  defp parse_section(@section_code, data, mod) do
    {bodies, <<>>} = decode_vec(data, &decode_code_body/1)
    Map.put(mod, :_code_bodies, bodies)
  end

  defp parse_section(@section_data, data, mod) do
    {segments, <<>>} = decode_vec(data, &decode_data_segment/1)
    %{mod | data: segments}
  end

  defp parse_section(@section_data_count, data, mod) do
    {count, <<>>} = decode_u32(data)
    Map.put(mod, :_data_count, count)
  end

  defp parse_section(@section_tag, data, mod) do
    {tags, <<>>} = decode_vec(data, &decode_tag/1)
    %{mod | tags: tags}
  end

  defp parse_section(_id, _data, mod), do: mod

  # ── Finalization ───────────────────────────────────────

  defp finalize(mod) do
    func_type_indices = Map.get(mod, :_func_type_indices, [])
    code_bodies = Map.get(mod, :_code_bodies, [])
    names = extract_names(mod.custom_sections)

    num_imports = Enum.count(mod.imports, &(&1.kind == :func))

    functions =
      Enum.zip(func_type_indices, code_bodies)
      |> Enum.map(fn {{local_idx, type_idx}, {locals, opcodes}} ->
        func_idx = num_imports + local_idx
        type = Enum.at(mod.types, type_idx, %{params: [], results: []})

        %Function{
          index: func_idx,
          name: Map.get(names, func_idx),
          type_idx: type_idx,
          params: type.params,
          results: type.results,
          locals: locals,
          opcodes: opcodes
        }
      end)

    mod =
      mod
      |> Map.delete(:_func_type_indices)
      |> Map.delete(:_code_bodies)
      |> Map.delete(:_data_count)

    imports = Enum.map(mod.imports, &enrich_import(&1, mod.types))
    exports = Enum.map(mod.exports, &enrich_export(&1, mod, func_type_indices))

    %{mod | functions: functions, imports: imports, exports: exports}
  end

  defp extract_names(custom_sections) do
    case Enum.find(custom_sections, &(&1.name == "name")) do
      nil -> %{}
      %{data: data} -> parse_name_section(data)
    end
  end

  defp parse_name_section(data), do: parse_name_subsections(data, %{})

  defp parse_name_subsections(<<>>, names), do: names

  defp parse_name_subsections(<<id, rest::binary>>, names) do
    {size, rest} = decode_u32(rest)
    <<subsection::binary-size(^size), rest::binary>> = rest

    names =
      case id do
        1 -> parse_name_map(subsection)
        _ -> names
      end

    parse_name_subsections(rest, names)
  rescue
    _ -> names
  end

  defp parse_name_map(data) do
    {entries, _} =
      decode_vec(data, fn bin ->
        {idx, bin} = decode_u32(bin)
        {name, bin} = decode_name(bin)
        {{idx, name}, bin}
      end)

    Map.new(entries)
  end

  defp enrich_import(%{kind: :func, type_idx: type_idx} = import, types) do
    type = Enum.at(types, type_idx, %{params: [], results: []})
    Map.merge(import, %{params: type.params, results: type.results})
  end

  defp enrich_import(import, _types), do: import

  defp enrich_export(%{kind: :func, index: index} = export, mod, func_type_indices) do
    type_idx = function_type_idx(mod.imports, func_type_indices, index)
    type = Enum.at(mod.types, type_idx || -1, %{params: [], results: []})
    Map.merge(export, %{params: type.params, results: type.results})
  end

  defp enrich_export(%{kind: :memory, index: index} = export, mod, _func_type_indices) do
    Map.merge(export, Enum.at(mod.memories, index, %{}))
  end

  defp enrich_export(%{kind: :table, index: index} = export, mod, _func_type_indices) do
    Map.merge(export, Enum.at(mod.tables, index, %{}))
  end

  defp enrich_export(%{kind: :global, index: index} = export, mod, _func_type_indices) do
    global = mod.globals |> Enum.at(index, %{}) |> Map.drop([:init])
    Map.merge(export, global)
  end

  defp enrich_export(export, _mod, _func_type_indices), do: export

  defp function_type_idx(imports, func_type_indices, index) do
    import_type_indices =
      imports
      |> Enum.filter(&(&1.kind == :func))
      |> Enum.map(& &1.type_idx)

    if index < length(import_type_indices) do
      Enum.at(import_type_indices, index)
    else
      local_idx = index - length(import_type_indices)

      case Enum.at(func_type_indices, local_idx) do
        {_, type_idx} -> type_idx
        _ -> nil
      end
    end
  end

  # ── Type decoders ──────────────────────────────────────

  defp decode_functype(<<0x60, rest::binary>>) do
    {params, rest} = decode_vec(rest, &decode_valtype/1)
    {results, rest} = decode_vec(rest, &decode_valtype/1)
    {%{params: params, results: results}, rest}
  end

  defp decode_valtype(<<0x7F, rest::binary>>), do: {:i32, rest}
  defp decode_valtype(<<0x7E, rest::binary>>), do: {:i64, rest}
  defp decode_valtype(<<0x7D, rest::binary>>), do: {:f32, rest}
  defp decode_valtype(<<0x7C, rest::binary>>), do: {:f64, rest}
  defp decode_valtype(<<0x7B, rest::binary>>), do: {:v128, rest}
  defp decode_valtype(<<0x70, rest::binary>>), do: {:funcref, rest}
  defp decode_valtype(<<0x6F, rest::binary>>), do: {:externref, rest}

  defp decode_valtype(<<byte, rest::binary>>),
    do: {:unknown_valtype, <<byte, rest::binary>>}

  # ── Import decoders ────────────────────────────────────

  defp decode_import(data) do
    {module_name, data} = decode_name(data)
    {field_name, data} = decode_name(data)
    {desc, data} = decode_import_desc(data)
    {Map.merge(%{module: module_name, name: field_name}, desc), data}
  end

  defp decode_import_desc(<<0x00, rest::binary>>) do
    {type_idx, rest} = decode_u32(rest)
    {%{kind: :func, type_idx: type_idx}, rest}
  end

  defp decode_import_desc(<<0x01, rest::binary>>) do
    {table, rest} = decode_table_type(rest)
    {Map.put(table, :kind, :table), rest}
  end

  defp decode_import_desc(<<0x02, rest::binary>>) do
    {mem, rest} = decode_limits(rest)
    {Map.put(mem, :kind, :memory), rest}
  end

  defp decode_import_desc(<<0x03, rest::binary>>) do
    {global, rest} = decode_global_type(rest)
    {Map.put(global, :kind, :global), rest}
  end

  defp decode_import_desc(<<0x04, rest::binary>>) do
    {tag, rest} = decode_tag_type(rest)
    {Map.put(tag, :kind, :tag), rest}
  end

  # ── Export decoders ────────────────────────────────────

  defp decode_export(data) do
    {name, data} = decode_name(data)
    <<kind_byte, data::binary>> = data
    {index, data} = decode_u32(data)

    kind =
      case kind_byte do
        0x00 -> :func
        0x01 -> :table
        0x02 -> :memory
        0x03 -> :global
        0x04 -> :tag
      end

    {%{name: name, kind: kind, index: index}, data}
  end

  # ── Table / Memory / Global decoders ───────────────────

  defp decode_table(data), do: decode_table_type(data)

  defp decode_table_type(data) do
    {elem_type, data} = decode_valtype(data)
    {limits, data} = decode_limits(data)
    {Map.put(limits, :element, elem_type), data}
  end

  defp decode_memory(data), do: decode_limits(data)

  defp decode_limits(<<0x00, rest::binary>>) do
    {min, rest} = decode_u32(rest)
    {%{min: min, max: nil}, rest}
  end

  defp decode_limits(<<0x01, rest::binary>>) do
    {min, rest} = decode_u32(rest)
    {max, rest} = decode_u32(rest)
    {%{min: min, max: max}, rest}
  end

  defp decode_global(data) do
    {type, data} = decode_global_type(data)
    {init_opcodes, data} = decode_expr(data)
    {Map.put(type, :init, init_opcodes), data}
  end

  defp decode_global_type(data) do
    {valtype, data} = decode_valtype(data)
    <<mutability, data::binary>> = data
    {%{type: valtype, mutable: mutability == 1}, data}
  end

  defp decode_tag_type(<<0x00, rest::binary>>) do
    {type_idx, rest} = decode_u32(rest)
    {%{type_idx: type_idx}, rest}
  end

  defp decode_tag(data), do: decode_tag_type(data)

  # ── Element decoders ───────────────────────────────────

  defp decode_element(<<0x00, rest::binary>>) do
    {offset, rest} = decode_expr(rest)
    {func_indices, rest} = decode_vec(rest, &decode_u32/1)
    {%{mode: :active, table_idx: 0, offset: offset, init: func_indices}, rest}
  end

  defp decode_element(<<kind, rest::binary>>) when kind in 1..7 do
    {%{mode: :unhandled_element_kind, kind: kind, data: rest}, <<>>}
  end

  # ── Data segment decoders ──────────────────────────────

  defp decode_data_segment(<<0x00, rest::binary>>) do
    {offset, rest} = decode_expr(rest)
    {size, rest} = decode_u32(rest)
    <<bytes::binary-size(^size), rest::binary>> = rest
    {%{memory_idx: 0, offset: offset, bytes: bytes}, rest}
  end

  defp decode_data_segment(<<0x01, rest::binary>>) do
    {size, rest} = decode_u32(rest)
    <<bytes::binary-size(^size), rest::binary>> = rest
    {%{memory_idx: nil, offset: nil, bytes: bytes}, rest}
  end

  defp decode_data_segment(<<0x02, rest::binary>>) do
    {mem_idx, rest} = decode_u32(rest)
    {offset, rest} = decode_expr(rest)
    {size, rest} = decode_u32(rest)
    <<bytes::binary-size(^size), rest::binary>> = rest
    {%{memory_idx: mem_idx, offset: offset, bytes: bytes}, rest}
  end

  # ── Code body decoder ──────────────────────────────────

  defp decode_code_body(data) do
    {body_size, data} = decode_u32(data)
    <<body::binary-size(^body_size), rest::binary>> = data
    {locals, body} = decode_locals(body)
    opcodes = decode_instructions(body)
    {{locals, opcodes}, rest}
  end

  defp decode_locals(data) do
    {groups, data} =
      decode_vec(data, fn bin ->
        {count, bin} = decode_u32(bin)
        {type, bin} = decode_valtype(bin)
        {{count, type}, bin}
      end)

    locals = Enum.flat_map(groups, fn {count, type} -> List.duplicate(type, count) end)
    {locals, data}
  end

  # ── Expression decoder (for init exprs) ────────────────

  defp decode_expr(data) do
    {opcodes, rest} = decode_instructions_until_end(data, 0, [])
    {opcodes, rest}
  end

  # ── Instruction decoder ────────────────────────────────

  defp decode_instructions(data) do
    {opcodes, _} = decode_instructions_until_end(data, 0, [])
    opcodes
  end

  defp decode_instructions_until_end(<<>>, _offset, acc), do: {Enum.reverse(acc), <<>>}

  defp decode_instructions_until_end(<<0x0B, rest::binary>>, offset, acc) do
    {Enum.reverse([{offset, :end} | acc]), rest}
  end

  defp decode_instructions_until_end(data, offset, acc) do
    {opcode, rest} = decode_one_instruction(data, offset)
    next_offset = offset + (byte_size(data) - byte_size(rest))
    decode_instructions_until_end(rest, next_offset, [opcode | acc])
  end

  # Control
  defp decode_one_instruction(<<0x00, rest::binary>>, off), do: {{off, :unreachable}, rest}
  defp decode_one_instruction(<<0x01, rest::binary>>, off), do: {{off, :nop}, rest}

  defp decode_one_instruction(<<0x02, rest::binary>>, off) do
    {bt, rest} = decode_blocktype(rest)
    {{off, :block, bt}, rest}
  end

  defp decode_one_instruction(<<0x03, rest::binary>>, off) do
    {bt, rest} = decode_blocktype(rest)
    {{off, :loop, bt}, rest}
  end

  defp decode_one_instruction(<<0x04, rest::binary>>, off) do
    {bt, rest} = decode_blocktype(rest)
    {{off, :if, bt}, rest}
  end

  defp decode_one_instruction(<<0x05, rest::binary>>, off), do: {{off, :else}, rest}
  defp decode_one_instruction(<<0x0B, rest::binary>>, off), do: {{off, :end}, rest}

  defp decode_one_instruction(<<0x0C, rest::binary>>, off) do
    {label, rest} = decode_u32(rest)
    {{off, :br, label}, rest}
  end

  defp decode_one_instruction(<<0x0D, rest::binary>>, off) do
    {label, rest} = decode_u32(rest)
    {{off, :br_if, label}, rest}
  end

  defp decode_one_instruction(<<0x0E, rest::binary>>, off) do
    {labels, rest} = decode_vec(rest, &decode_u32/1)
    {default, rest} = decode_u32(rest)
    {{off, :br_table, labels, default}, rest}
  end

  defp decode_one_instruction(<<0x0F, rest::binary>>, off), do: {{off, :return}, rest}

  defp decode_one_instruction(<<0x10, rest::binary>>, off) do
    {idx, rest} = decode_u32(rest)
    {{off, :call, idx}, rest}
  end

  defp decode_one_instruction(<<0x11, rest::binary>>, off) do
    {type_idx, rest} = decode_u32(rest)
    {table_idx, rest} = decode_u32(rest)
    {{off, :call_indirect, table_idx, type_idx}, rest}
  end

  defp decode_one_instruction(<<0x12, rest::binary>>, off) do
    {idx, rest} = decode_u32(rest)
    {{off, :return_call, idx}, rest}
  end

  defp decode_one_instruction(<<0x13, rest::binary>>, off) do
    {type_idx, rest} = decode_u32(rest)
    {table_idx, rest} = decode_u32(rest)
    {{off, :return_call_indirect, table_idx, type_idx}, rest}
  end

  # Parametric
  defp decode_one_instruction(<<0x1A, rest::binary>>, off), do: {{off, :drop}, rest}
  defp decode_one_instruction(<<0x1B, rest::binary>>, off), do: {{off, :select}, rest}

  defp decode_one_instruction(<<0x1C, rest::binary>>, off) do
    {types, rest} = decode_vec(rest, &decode_valtype/1)
    {{off, :select, types}, rest}
  end

  # Variable
  defp decode_one_instruction(<<0x20, rest::binary>>, off) do
    {idx, rest} = decode_u32(rest)
    {{off, :local_get, idx}, rest}
  end

  defp decode_one_instruction(<<0x21, rest::binary>>, off) do
    {idx, rest} = decode_u32(rest)
    {{off, :local_set, idx}, rest}
  end

  defp decode_one_instruction(<<0x22, rest::binary>>, off) do
    {idx, rest} = decode_u32(rest)
    {{off, :local_tee, idx}, rest}
  end

  defp decode_one_instruction(<<0x23, rest::binary>>, off) do
    {idx, rest} = decode_u32(rest)
    {{off, :global_get, idx}, rest}
  end

  defp decode_one_instruction(<<0x24, rest::binary>>, off) do
    {idx, rest} = decode_u32(rest)
    {{off, :global_set, idx}, rest}
  end

  # Table
  defp decode_one_instruction(<<0x25, rest::binary>>, off) do
    {idx, rest} = decode_u32(rest)
    {{off, :table_get, idx}, rest}
  end

  defp decode_one_instruction(<<0x26, rest::binary>>, off) do
    {idx, rest} = decode_u32(rest)
    {{off, :table_set, idx}, rest}
  end

  # Memory load/store
  defp decode_one_instruction(<<opcode, rest::binary>>, off) when opcode in 0x28..0x3E do
    {align, rest} = decode_u32(rest)
    {mem_offset, rest} = decode_u32(rest)
    name = memory_op_name(opcode)
    {{off, name, align, mem_offset}, rest}
  end

  # Memory size/grow
  defp decode_one_instruction(<<0x3F, 0x00, rest::binary>>, off),
    do: {{off, :memory_size, 0}, rest}

  defp decode_one_instruction(<<0x40, 0x00, rest::binary>>, off),
    do: {{off, :memory_grow, 0}, rest}

  # Numeric constants
  defp decode_one_instruction(<<0x41, rest::binary>>, off) do
    {val, rest} = decode_i32(rest)
    {{off, :i32_const, val}, rest}
  end

  defp decode_one_instruction(<<0x42, rest::binary>>, off) do
    {val, rest} = decode_i64(rest)
    {{off, :i64_const, val}, rest}
  end

  defp decode_one_instruction(<<0x43, val::little-float-32, rest::binary>>, off),
    do: {{off, :f32_const, val}, rest}

  defp decode_one_instruction(<<0x44, val::little-float-64, rest::binary>>, off),
    do: {{off, :f64_const, val}, rest}

  # i32 comparison/arithmetic
  defp decode_one_instruction(<<0x45, rest::binary>>, off), do: {{off, :i32_eqz}, rest}
  defp decode_one_instruction(<<0x46, rest::binary>>, off), do: {{off, :i32_eq}, rest}
  defp decode_one_instruction(<<0x47, rest::binary>>, off), do: {{off, :i32_ne}, rest}
  defp decode_one_instruction(<<0x48, rest::binary>>, off), do: {{off, :i32_lt_s}, rest}
  defp decode_one_instruction(<<0x49, rest::binary>>, off), do: {{off, :i32_lt_u}, rest}
  defp decode_one_instruction(<<0x4A, rest::binary>>, off), do: {{off, :i32_gt_s}, rest}
  defp decode_one_instruction(<<0x4B, rest::binary>>, off), do: {{off, :i32_gt_u}, rest}
  defp decode_one_instruction(<<0x4C, rest::binary>>, off), do: {{off, :i32_le_s}, rest}
  defp decode_one_instruction(<<0x4D, rest::binary>>, off), do: {{off, :i32_le_u}, rest}
  defp decode_one_instruction(<<0x4E, rest::binary>>, off), do: {{off, :i32_ge_s}, rest}
  defp decode_one_instruction(<<0x4F, rest::binary>>, off), do: {{off, :i32_ge_u}, rest}

  # i64 comparison
  defp decode_one_instruction(<<0x50, rest::binary>>, off), do: {{off, :i64_eqz}, rest}
  defp decode_one_instruction(<<0x51, rest::binary>>, off), do: {{off, :i64_eq}, rest}
  defp decode_one_instruction(<<0x52, rest::binary>>, off), do: {{off, :i64_ne}, rest}
  defp decode_one_instruction(<<0x53, rest::binary>>, off), do: {{off, :i64_lt_s}, rest}
  defp decode_one_instruction(<<0x54, rest::binary>>, off), do: {{off, :i64_lt_u}, rest}
  defp decode_one_instruction(<<0x55, rest::binary>>, off), do: {{off, :i64_gt_s}, rest}
  defp decode_one_instruction(<<0x56, rest::binary>>, off), do: {{off, :i64_gt_u}, rest}
  defp decode_one_instruction(<<0x57, rest::binary>>, off), do: {{off, :i64_le_s}, rest}
  defp decode_one_instruction(<<0x58, rest::binary>>, off), do: {{off, :i64_le_u}, rest}
  defp decode_one_instruction(<<0x59, rest::binary>>, off), do: {{off, :i64_ge_s}, rest}
  defp decode_one_instruction(<<0x5A, rest::binary>>, off), do: {{off, :i64_ge_u}, rest}

  # f32 comparison
  defp decode_one_instruction(<<0x5B, rest::binary>>, off), do: {{off, :f32_eq}, rest}
  defp decode_one_instruction(<<0x5C, rest::binary>>, off), do: {{off, :f32_ne}, rest}
  defp decode_one_instruction(<<0x5D, rest::binary>>, off), do: {{off, :f32_lt}, rest}
  defp decode_one_instruction(<<0x5E, rest::binary>>, off), do: {{off, :f32_gt}, rest}
  defp decode_one_instruction(<<0x5F, rest::binary>>, off), do: {{off, :f32_le}, rest}
  defp decode_one_instruction(<<0x60, rest::binary>>, off), do: {{off, :f32_ge}, rest}

  # f64 comparison
  defp decode_one_instruction(<<0x61, rest::binary>>, off), do: {{off, :f64_eq}, rest}
  defp decode_one_instruction(<<0x62, rest::binary>>, off), do: {{off, :f64_ne}, rest}
  defp decode_one_instruction(<<0x63, rest::binary>>, off), do: {{off, :f64_lt}, rest}
  defp decode_one_instruction(<<0x64, rest::binary>>, off), do: {{off, :f64_gt}, rest}
  defp decode_one_instruction(<<0x65, rest::binary>>, off), do: {{off, :f64_le}, rest}
  defp decode_one_instruction(<<0x66, rest::binary>>, off), do: {{off, :f64_ge}, rest}

  # i32 arithmetic
  defp decode_one_instruction(<<0x67, rest::binary>>, off), do: {{off, :i32_clz}, rest}
  defp decode_one_instruction(<<0x68, rest::binary>>, off), do: {{off, :i32_ctz}, rest}
  defp decode_one_instruction(<<0x69, rest::binary>>, off), do: {{off, :i32_popcnt}, rest}
  defp decode_one_instruction(<<0x6A, rest::binary>>, off), do: {{off, :i32_add}, rest}
  defp decode_one_instruction(<<0x6B, rest::binary>>, off), do: {{off, :i32_sub}, rest}
  defp decode_one_instruction(<<0x6C, rest::binary>>, off), do: {{off, :i32_mul}, rest}
  defp decode_one_instruction(<<0x6D, rest::binary>>, off), do: {{off, :i32_div_s}, rest}
  defp decode_one_instruction(<<0x6E, rest::binary>>, off), do: {{off, :i32_div_u}, rest}
  defp decode_one_instruction(<<0x6F, rest::binary>>, off), do: {{off, :i32_rem_s}, rest}
  defp decode_one_instruction(<<0x70, rest::binary>>, off), do: {{off, :i32_rem_u}, rest}
  defp decode_one_instruction(<<0x71, rest::binary>>, off), do: {{off, :i32_and}, rest}
  defp decode_one_instruction(<<0x72, rest::binary>>, off), do: {{off, :i32_or}, rest}
  defp decode_one_instruction(<<0x73, rest::binary>>, off), do: {{off, :i32_xor}, rest}
  defp decode_one_instruction(<<0x74, rest::binary>>, off), do: {{off, :i32_shl}, rest}
  defp decode_one_instruction(<<0x75, rest::binary>>, off), do: {{off, :i32_shr_s}, rest}
  defp decode_one_instruction(<<0x76, rest::binary>>, off), do: {{off, :i32_shr_u}, rest}
  defp decode_one_instruction(<<0x77, rest::binary>>, off), do: {{off, :i32_rotl}, rest}
  defp decode_one_instruction(<<0x78, rest::binary>>, off), do: {{off, :i32_rotr}, rest}

  # i64 arithmetic
  defp decode_one_instruction(<<0x79, rest::binary>>, off), do: {{off, :i64_clz}, rest}
  defp decode_one_instruction(<<0x7A, rest::binary>>, off), do: {{off, :i64_ctz}, rest}
  defp decode_one_instruction(<<0x7B, rest::binary>>, off), do: {{off, :i64_popcnt}, rest}
  defp decode_one_instruction(<<0x7C, rest::binary>>, off), do: {{off, :i64_add}, rest}
  defp decode_one_instruction(<<0x7D, rest::binary>>, off), do: {{off, :i64_sub}, rest}
  defp decode_one_instruction(<<0x7E, rest::binary>>, off), do: {{off, :i64_mul}, rest}
  defp decode_one_instruction(<<0x7F, rest::binary>>, off), do: {{off, :i64_div_s}, rest}
  defp decode_one_instruction(<<0x80, rest::binary>>, off), do: {{off, :i64_div_u}, rest}
  defp decode_one_instruction(<<0x81, rest::binary>>, off), do: {{off, :i64_rem_s}, rest}
  defp decode_one_instruction(<<0x82, rest::binary>>, off), do: {{off, :i64_rem_u}, rest}
  defp decode_one_instruction(<<0x83, rest::binary>>, off), do: {{off, :i64_and}, rest}
  defp decode_one_instruction(<<0x84, rest::binary>>, off), do: {{off, :i64_or}, rest}
  defp decode_one_instruction(<<0x85, rest::binary>>, off), do: {{off, :i64_xor}, rest}
  defp decode_one_instruction(<<0x86, rest::binary>>, off), do: {{off, :i64_shl}, rest}
  defp decode_one_instruction(<<0x87, rest::binary>>, off), do: {{off, :i64_shr_s}, rest}
  defp decode_one_instruction(<<0x88, rest::binary>>, off), do: {{off, :i64_shr_u}, rest}
  defp decode_one_instruction(<<0x89, rest::binary>>, off), do: {{off, :i64_rotl}, rest}
  defp decode_one_instruction(<<0x8A, rest::binary>>, off), do: {{off, :i64_rotr}, rest}

  # f32 arithmetic
  defp decode_one_instruction(<<0x8B, rest::binary>>, off), do: {{off, :f32_abs}, rest}
  defp decode_one_instruction(<<0x8C, rest::binary>>, off), do: {{off, :f32_neg}, rest}
  defp decode_one_instruction(<<0x8D, rest::binary>>, off), do: {{off, :f32_ceil}, rest}
  defp decode_one_instruction(<<0x8E, rest::binary>>, off), do: {{off, :f32_floor}, rest}
  defp decode_one_instruction(<<0x8F, rest::binary>>, off), do: {{off, :f32_trunc}, rest}
  defp decode_one_instruction(<<0x90, rest::binary>>, off), do: {{off, :f32_nearest}, rest}
  defp decode_one_instruction(<<0x91, rest::binary>>, off), do: {{off, :f32_sqrt}, rest}
  defp decode_one_instruction(<<0x92, rest::binary>>, off), do: {{off, :f32_add}, rest}
  defp decode_one_instruction(<<0x93, rest::binary>>, off), do: {{off, :f32_sub}, rest}
  defp decode_one_instruction(<<0x94, rest::binary>>, off), do: {{off, :f32_mul}, rest}
  defp decode_one_instruction(<<0x95, rest::binary>>, off), do: {{off, :f32_div}, rest}
  defp decode_one_instruction(<<0x96, rest::binary>>, off), do: {{off, :f32_min}, rest}
  defp decode_one_instruction(<<0x97, rest::binary>>, off), do: {{off, :f32_max}, rest}
  defp decode_one_instruction(<<0x98, rest::binary>>, off), do: {{off, :f32_copysign}, rest}

  # f64 arithmetic
  defp decode_one_instruction(<<0x99, rest::binary>>, off), do: {{off, :f64_abs}, rest}
  defp decode_one_instruction(<<0x9A, rest::binary>>, off), do: {{off, :f64_neg}, rest}
  defp decode_one_instruction(<<0x9B, rest::binary>>, off), do: {{off, :f64_ceil}, rest}
  defp decode_one_instruction(<<0x9C, rest::binary>>, off), do: {{off, :f64_floor}, rest}
  defp decode_one_instruction(<<0x9D, rest::binary>>, off), do: {{off, :f64_trunc}, rest}
  defp decode_one_instruction(<<0x9E, rest::binary>>, off), do: {{off, :f64_nearest}, rest}
  defp decode_one_instruction(<<0x9F, rest::binary>>, off), do: {{off, :f64_sqrt}, rest}
  defp decode_one_instruction(<<0xA0, rest::binary>>, off), do: {{off, :f64_add}, rest}
  defp decode_one_instruction(<<0xA1, rest::binary>>, off), do: {{off, :f64_sub}, rest}
  defp decode_one_instruction(<<0xA2, rest::binary>>, off), do: {{off, :f64_mul}, rest}
  defp decode_one_instruction(<<0xA3, rest::binary>>, off), do: {{off, :f64_div}, rest}
  defp decode_one_instruction(<<0xA4, rest::binary>>, off), do: {{off, :f64_min}, rest}
  defp decode_one_instruction(<<0xA5, rest::binary>>, off), do: {{off, :f64_max}, rest}
  defp decode_one_instruction(<<0xA6, rest::binary>>, off), do: {{off, :f64_copysign}, rest}

  # Conversions
  defp decode_one_instruction(<<0xA7, rest::binary>>, off), do: {{off, :i32_wrap_i64}, rest}
  defp decode_one_instruction(<<0xA8, rest::binary>>, off), do: {{off, :i32_trunc_f32_s}, rest}
  defp decode_one_instruction(<<0xA9, rest::binary>>, off), do: {{off, :i32_trunc_f32_u}, rest}
  defp decode_one_instruction(<<0xAA, rest::binary>>, off), do: {{off, :i32_trunc_f64_s}, rest}
  defp decode_one_instruction(<<0xAB, rest::binary>>, off), do: {{off, :i32_trunc_f64_u}, rest}
  defp decode_one_instruction(<<0xAC, rest::binary>>, off), do: {{off, :i64_extend_i32_s}, rest}
  defp decode_one_instruction(<<0xAD, rest::binary>>, off), do: {{off, :i64_extend_i32_u}, rest}
  defp decode_one_instruction(<<0xAE, rest::binary>>, off), do: {{off, :i64_trunc_f32_s}, rest}
  defp decode_one_instruction(<<0xAF, rest::binary>>, off), do: {{off, :i64_trunc_f32_u}, rest}
  defp decode_one_instruction(<<0xB0, rest::binary>>, off), do: {{off, :i64_trunc_f64_s}, rest}
  defp decode_one_instruction(<<0xB1, rest::binary>>, off), do: {{off, :i64_trunc_f64_u}, rest}
  defp decode_one_instruction(<<0xB2, rest::binary>>, off), do: {{off, :f32_convert_i32_s}, rest}
  defp decode_one_instruction(<<0xB3, rest::binary>>, off), do: {{off, :f32_convert_i32_u}, rest}
  defp decode_one_instruction(<<0xB4, rest::binary>>, off), do: {{off, :f32_convert_i64_s}, rest}
  defp decode_one_instruction(<<0xB5, rest::binary>>, off), do: {{off, :f32_convert_i64_u}, rest}
  defp decode_one_instruction(<<0xB6, rest::binary>>, off), do: {{off, :f32_demote_f64}, rest}
  defp decode_one_instruction(<<0xB7, rest::binary>>, off), do: {{off, :f64_convert_i32_s}, rest}
  defp decode_one_instruction(<<0xB8, rest::binary>>, off), do: {{off, :f64_convert_i32_u}, rest}
  defp decode_one_instruction(<<0xB9, rest::binary>>, off), do: {{off, :f64_convert_i64_s}, rest}
  defp decode_one_instruction(<<0xBA, rest::binary>>, off), do: {{off, :f64_convert_i64_u}, rest}
  defp decode_one_instruction(<<0xBB, rest::binary>>, off), do: {{off, :f64_promote_f32}, rest}

  defp decode_one_instruction(<<0xBC, rest::binary>>, off),
    do: {{off, :i32_reinterpret_f32}, rest}

  defp decode_one_instruction(<<0xBD, rest::binary>>, off),
    do: {{off, :i64_reinterpret_f64}, rest}

  defp decode_one_instruction(<<0xBE, rest::binary>>, off),
    do: {{off, :f32_reinterpret_i32}, rest}

  defp decode_one_instruction(<<0xBF, rest::binary>>, off),
    do: {{off, :f64_reinterpret_i64}, rest}

  # Sign extension
  defp decode_one_instruction(<<0xC0, rest::binary>>, off), do: {{off, :i32_extend8_s}, rest}
  defp decode_one_instruction(<<0xC1, rest::binary>>, off), do: {{off, :i32_extend16_s}, rest}
  defp decode_one_instruction(<<0xC2, rest::binary>>, off), do: {{off, :i64_extend8_s}, rest}
  defp decode_one_instruction(<<0xC3, rest::binary>>, off), do: {{off, :i64_extend16_s}, rest}
  defp decode_one_instruction(<<0xC4, rest::binary>>, off), do: {{off, :i64_extend32_s}, rest}

  # Reference instructions
  defp decode_one_instruction(<<0xD0, rest::binary>>, off) do
    {ht, rest} = decode_valtype(rest)
    {{off, :ref_null, ht}, rest}
  end

  defp decode_one_instruction(<<0xD1, rest::binary>>, off), do: {{off, :ref_is_null}, rest}

  defp decode_one_instruction(<<0xD2, rest::binary>>, off) do
    {idx, rest} = decode_u32(rest)
    {{off, :ref_func, idx}, rest}
  end

  # 0xFC prefix — saturating truncation + bulk memory + table ops
  defp decode_one_instruction(<<0xFC, rest::binary>>, off) do
    {sub, rest} = decode_u32(rest)
    decode_fc_instruction(sub, rest, off)
  end

  # Catch-all for unknown opcodes
  defp decode_one_instruction(<<byte, rest::binary>>, off),
    do: {{off, :unknown, byte}, rest}

  # ── 0xFC sub-opcodes ───────────────────────────────────

  defp decode_fc_instruction(0, rest, off), do: {{off, :i32_trunc_sat_f32_s}, rest}
  defp decode_fc_instruction(1, rest, off), do: {{off, :i32_trunc_sat_f32_u}, rest}
  defp decode_fc_instruction(2, rest, off), do: {{off, :i32_trunc_sat_f64_s}, rest}
  defp decode_fc_instruction(3, rest, off), do: {{off, :i32_trunc_sat_f64_u}, rest}
  defp decode_fc_instruction(4, rest, off), do: {{off, :i64_trunc_sat_f32_s}, rest}
  defp decode_fc_instruction(5, rest, off), do: {{off, :i64_trunc_sat_f32_u}, rest}
  defp decode_fc_instruction(6, rest, off), do: {{off, :i64_trunc_sat_f64_s}, rest}
  defp decode_fc_instruction(7, rest, off), do: {{off, :i64_trunc_sat_f64_u}, rest}

  defp decode_fc_instruction(8, rest, off) do
    {data_idx, rest} = decode_u32(rest)
    <<0x00, rest::binary>> = rest
    {{off, :memory_init, data_idx, 0}, rest}
  end

  defp decode_fc_instruction(9, rest, off) do
    {data_idx, rest} = decode_u32(rest)
    {{off, :data_drop, data_idx}, rest}
  end

  defp decode_fc_instruction(10, <<0x00, 0x00, rest::binary>>, off),
    do: {{off, :memory_copy, 0, 0}, rest}

  defp decode_fc_instruction(11, <<0x00, rest::binary>>, off),
    do: {{off, :memory_fill, 0}, rest}

  defp decode_fc_instruction(12, rest, off) do
    {elem_idx, rest} = decode_u32(rest)
    {table_idx, rest} = decode_u32(rest)
    {{off, :table_init, table_idx, elem_idx}, rest}
  end

  defp decode_fc_instruction(13, rest, off) do
    {elem_idx, rest} = decode_u32(rest)
    {{off, :elem_drop, elem_idx}, rest}
  end

  defp decode_fc_instruction(14, rest, off) do
    {dst, rest} = decode_u32(rest)
    {src, rest} = decode_u32(rest)
    {{off, :table_copy, dst, src}, rest}
  end

  defp decode_fc_instruction(15, rest, off) do
    {table_idx, rest} = decode_u32(rest)
    {{off, :table_grow, table_idx}, rest}
  end

  defp decode_fc_instruction(16, rest, off) do
    {table_idx, rest} = decode_u32(rest)
    {{off, :table_size, table_idx}, rest}
  end

  defp decode_fc_instruction(17, rest, off) do
    {table_idx, rest} = decode_u32(rest)
    {{off, :table_fill, table_idx}, rest}
  end

  defp decode_fc_instruction(sub, rest, off),
    do: {{off, :unknown_fc, sub}, rest}

  # ── Block type decoder ─────────────────────────────────

  defp decode_blocktype(<<0x40, rest::binary>>), do: {:void, rest}
  defp decode_blocktype(<<0x7F, rest::binary>>), do: {:i32, rest}
  defp decode_blocktype(<<0x7E, rest::binary>>), do: {:i64, rest}
  defp decode_blocktype(<<0x7D, rest::binary>>), do: {:f32, rest}
  defp decode_blocktype(<<0x7C, rest::binary>>), do: {:f64, rest}
  defp decode_blocktype(<<0x7B, rest::binary>>), do: {:v128, rest}
  defp decode_blocktype(<<0x70, rest::binary>>), do: {:funcref, rest}
  defp decode_blocktype(<<0x6F, rest::binary>>), do: {:externref, rest}

  defp decode_blocktype(data) do
    {idx, rest} = decode_s33(data)
    {{:type, idx}, rest}
  end

  # ── Memory op name lookup ──────────────────────────────

  @memory_ops %{
    0x28 => :i32_load,
    0x29 => :i64_load,
    0x2A => :f32_load,
    0x2B => :f64_load,
    0x2C => :i32_load8_s,
    0x2D => :i32_load8_u,
    0x2E => :i32_load16_s,
    0x2F => :i32_load16_u,
    0x30 => :i64_load8_s,
    0x31 => :i64_load8_u,
    0x32 => :i64_load16_s,
    0x33 => :i64_load16_u,
    0x34 => :i64_load32_s,
    0x35 => :i64_load32_u,
    0x36 => :i32_store,
    0x37 => :i64_store,
    0x38 => :f32_store,
    0x39 => :f64_store,
    0x3A => :i32_store8,
    0x3B => :i32_store16,
    0x3C => :i64_store8,
    0x3D => :i64_store16,
    0x3E => :i64_store32
  }

  defp memory_op_name(opcode), do: Map.fetch!(@memory_ops, opcode)

  # ── LEB128 decoders ────────────────────────────────────

  defp decode_u32(data), do: decode_uleb128(data, 0, 0)

  defp decode_uleb128(<<byte, rest::binary>>, result, shift) do
    value = Bitwise.bor(result, Bitwise.bsl(Bitwise.band(byte, 0x7F), shift))

    if Bitwise.band(byte, 0x80) == 0 do
      {value, rest}
    else
      decode_uleb128(rest, value, shift + 7)
    end
  end

  defp decode_i32(data), do: decode_sleb128(data, 0, 0, 32)
  defp decode_i64(data), do: decode_sleb128(data, 0, 0, 64)
  defp decode_s33(data), do: decode_sleb128(data, 0, 0, 33)

  defp decode_sleb128(<<byte, rest::binary>>, result, shift, size) do
    value = Bitwise.bor(result, Bitwise.bsl(Bitwise.band(byte, 0x7F), shift))
    shift = shift + 7

    if Bitwise.band(byte, 0x80) == 0 do
      value =
        if shift < size and Bitwise.band(byte, 0x40) != 0 do
          Bitwise.bor(value, Bitwise.bsl(-1, shift))
        else
          value
        end

      {value, rest}
    else
      decode_sleb128(rest, value, shift, size)
    end
  end

  # ── Vec / Name decoders ────────────────────────────────

  defp decode_vec(data, decoder) do
    {count, data} = decode_u32(data)
    decode_n(data, count, decoder, [])
  end

  defp decode_n(data, 0, _decoder, acc), do: {Enum.reverse(acc), data}

  defp decode_n(data, n, decoder, acc) do
    {item, data} = decoder.(data)
    decode_n(data, n - 1, decoder, [item | acc])
  end

  defp decode_name(data) do
    {len, data} = decode_u32(data)
    <<name::binary-size(^len), data::binary>> = data
    {name, data}
  end
end