Функции, параметры и локальные переменные

Учимся писать функции на WAT с параметрами и локальными переменными.

Функция Wasm — это (func ...) с параметрами (param), результатом (result) и телом из стековых инструкций.

Анатомия функции

Объявление функции состоит из имени, входных параметров, типа результата и тела. Вот функция, возводящая число в квадрат:

(func $square (param $n i32) (result i32)
  local.get $n
  local.get $n
  i32.mul)

Читаем по шагам: (param $n i32) объявляет вход — целое $n. (result i32) обещает вернуть целое. В теле local.get $n дважды кладёт значение параметра на стек, а i32.mul перемножает два верхних значения. То, что осталось на стеке в конце, и есть результат.

Локальные переменные

Кроме параметров, функция может объявить свои локальные переменные через (local $имя тип). Они инициализируются нулём. Записать в локаль — local.set, прочитать — local.get. Вычислим (a + b) * 2, сохранив сумму в локаль:

(func $sum_times_two (param $a i32) (param $b i32) (result i32)
  (local $sum i32)
  local.get $a
  local.get $b
  i32.add
  local.set $sum     ;; $sum = a + b
  local.get $sum
  i32.const 2
  i32.mul)           ;; (a + b) * 2

Параметры — это тоже локали

Важная деталь: параметры и локали живут в одном пространстве индексов. К ним обоим обращаются через local.get и local.set. Разница лишь в том, что параметры получают значения от вызывающего, а собственные локали стартуют с нуля.

Смоделируем логику на JS

Чтобы убедиться, что логика верна, повторим тот же расчёт на JavaScript — здесь видно, что именно делает Wasm-функция.

function sumTimesTwo(a, b) {
  let sum = a + b;     // local.set $sum
  return sum * 2;      // i32.mul на 2
}
console.log(sumTimesTwo(3, 4));
console.log(sumTimesTwo(10, 5));

Вывод:

14
30

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

При вызове функции движок создаёт «кадр» (frame), где хранятся значения параметров и локалей. Стек операндов работает поверх этого кадра. Когда функция доходит до конца тела, движок проверяет: на стеке должно остаться ровно столько значений, сколько объявлено в result (для нашей $square — одно i32). Если форма стека не сходится, модуль не пройдёт валидацию.

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

  • Несоответствие result и стека — обещали (result i32), но в конце на стеке пусто или два значения. Ошибка валидации.
  • Путаница типовlocal.get кладёт значение объявленного типа; нельзя сложить i32 и f64.
  • Забыли local.get после local.setlocal.set снимает значение со стека, и оно «исчезает» из вычисления, пока вы снова не вызовете local.get.

Итоги

  • Функция: (func имя (param ...) (result ...) тело).
  • Локали объявляются через (local ...) и стартуют с нуля.
  • Параметры и локали — единое пространство, доступ через local.get/set.
  • В конце на стеке должно остаться ровно столько значений, сколько в result.
Проверьте себя
1. Как прочитать значение параметра $n внутри функции?
Aparam.read $n
Blocal.get $n
Cget $n
Dload $n
2. Чем инициализируются объявленные local-переменные?
AМусором из памяти
BНулём
CЗначением последнего параметра
DИх нужно инициализировать вручную, иначе ошибка
3. Что проверяет валидатор в конце функции с (result i32)?
AЧто функция короче 100 строк
BЧто на стеке осталось ровно одно значение типа i32
CЧто есть комментарий
DЧто использована память