Первый модуль вручную: сумма

Собираем свой первый полный WAT-модуль руками.

Цель урока — написать рабочий модуль (module ...) с функцией сложения и понять каждую его строку.

Полный модуль

Вот наш первый завершённый модуль. Он объявляет функцию сложения двух целых и экспортирует её под именем "add":

(module
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a      ;; кладём a на стек     -> [a]
    local.get $b      ;; кладём b на стек     -> [a, b]
    i32.add)          ;; складываем          -> [a+b]
  (export "add" (func $add)))

Каждая строка прокомментирована состоянием стека справа. После i32.add на стеке ровно одно значение — оно и станет результатом, как обещано в (result i32).

Расширяем: складываем три числа

Усложним — функция трёх аргументов. Складываем первые два, результат остаётся на стеке, добавляем третий:

(module
  (func $add3 (param $a i32) (param $b i32) (param $c i32) (result i32)
    local.get $a
    local.get $b
    i32.add           ;; на стеке: [a+b]
    local.get $c
    i32.add)          ;; на стеке: [a+b+c]
  (export "add3" (func $add3)))

Заметьте: после первого i32.add промежуточная сумма уже лежит на стеке, и второй i32.add просто складывает её с $c. Стек естественно накапливает результат — в этом красота стековой модели.

Проверим логику на JS

Прежде чем компилировать WAT в браузере, удобно проверить ожидаемые числа. Повторим логику обеих функций на JavaScript:

function add(a, b) { return a + b; }
function add3(a, b, c) { return a + b + c; }
console.log("add(2, 3) =", add(2, 3));
console.log("add3(10, 20, 30) =", add3(10, 20, 30));

Вывод:

add(2, 3) = 5
add3(10, 20, 30) = 60

Как это скомпилировать

Сохраните WAT в файл add.wat и прогоните через wat2wasm:

wat2wasm add.wat -o add.wasm
# теперь add.wasm можно загрузить из JS

Как работает под капотом

Когда wat2wasm читает наш текст, он сначала разбирает s-выражения в дерево, затем превращает имена ($add, $a) в числовые индексы, проверяет, что типы на стеке сходятся, и наконец записывает компактный бинарь с секциями: тип функции (i32, i32) -> i32, тело [local.get 0, local.get 1, i32.add] и таблицу экспортов с записью "add". Имена-псевдонимы в бинаре не нужны — там работают индексы.

Частые ошибки

  • Лишняя инструкция на стеке — если случайно положить значение и не использовать его, форма стека не совпадёт с result.
  • Перепутали порядок аргументов — для сложения это незаметно, но для вычитания i32.sub порядок важен: снимается a - b в порядке push.
  • Забыли экспорт — модуль скомпилируется, но из JS функция будет недоступна.

Итоги

  • Полный модуль: (module (func ...) (export ...)).
  • Стек естественно накапливает промежуточные результаты.
  • wat2wasm add.wat -o add.wasm компилирует текст в бинарь.
  • В бинаре имена становятся индексами; экспорт хранит публичное имя.
Проверьте себя
1. Что лежит на стеке сразу после первого i32.add в функции add3?
AНичего
BПромежуточная сумма a+b
CВсе три параметра
DТолько c
2. Почему в бинарном модуле не хранятся имена вроде $add?
AЧтобы запутать читателя
BИмена-псевдонимы заменяются числовыми индексами; для машины имена не нужны
CБинарь не поддерживает функции
DИмена хранятся, но шифруются