Курс › Лекція 8
▶ СлайдиСРС 15+16⌨️ команди 4 · 3 нових
Конспект · Лекція 8

WEBASSEMBLY ЯК ПЕРЕНОСНИЙ АСЕМБЛЕР

Один код — дві машини

Ключова ідея. WebAssembly — це набір інструкцій вигаданої машини, однакової в кожному браузері й рушії: найдешевший спосіб побачити межу між кодом і набором інструкцій живим.
Демо. Той самий .wat-модуль з функцією додавання компілюється у wat2wasm і запускається у браузері через WebAssembly.instantiateStreaming — виклик з DevTools console дає результат миттєво.
🧩 СРС ДО ЦІЄЇ ЛЕКЦІЇ · 2-га година заняття

Після презентації теми — самостійна робота в аудиторії над двома темами. Вони ж готують до захисту та розширення ЛР8. Матеріали публікуються в Google Classroom разом із лекцією.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Вступ: та сама задача, інша машина

x86-64, який ми розберемо наступного разу (Лекція 9), — набір інструкцій конкретного фізичного процесора: те, що вміє виконати кристал Intel чи AMD, і більше нічого. WebAssembly (WASM) розв'язує іншу задачу: дати переносний, компактний, безпечний байт-код, що однаково виконується в Chrome, Firefox, Node.js, Wasmtime чи на мікроконтролері, незалежно від того, ARM це, x86 чи RISC-V під капотом. WASM успадкував головну ідею асемблера — «мова, близька до заліза, без прихованої магії», — але заради переносності відмовився від найінтимнішого атрибута фізичного процесора: регістрів. Замість них — операційний стек.

Важливо одразу розвіяти поширену помилку: WASM — це не заміна JavaScript і не «повільніший JS, що виглядає як асемблер». Це окремий формат байт-коду з власною віртуальною машиною, і рушій браузера компілює його в нативні інструкції (JIT/AOT) так само, як компілює JS — тільки з набагато меншим простором для здогадок про типи, бо типи в WASM статичні й перевірені ще до першого виконання.

Стекова модель виконання

Питання, на яке відповідає будь-який набір інструкцій: де лежать проміжні результати обчислення. Справжні процесори тримають їх у регістрах — кількох іменованих комірках всередині кристала (як саме це виглядає, розберемо наступного тижня, на Л9). WASM обрав іншу відповідь, простішу й переносну: регістрів загального призначення в нього немає, а в кожної функції є операційний стек значень (i32, i64, f32, f64), і кожна інструкція знімає з нього стільки значень, скільки їй треба, а результат кладе назад:

i32.const 2   ;; стек: [2]
i32.const 3   ;; стек: [2, 3]
i32.add       ;; зняти 2 і 3, покласти 5 -> стек: [5]

Це запис у постфіксній (зворотній польській) нотації: порядок інструкцій буквально є порядком обчислення виразу, без потреби в дужках чи пріоритеті операцій. local.get/local.set — це доступ до параметрів і локальних змінних функції за індексом у її таблиці локальних змінних (local.get 0 — перший параметр). На Л9 ти побачиш, як те саме роблять справжні процесори — там локальна змінна адресується зміщенням від бази кадру стека, і запис виглядає як -4(%rbp); ідея та сама, форма інша функції.

Типи значень і валідація

WASM MVP (базова специфікація) визначає чотири числові типи: i32, i64 (цілі, без розрізнення знаку — знак визначає інструкція, наприклад i32.div_s проти i32.div_u), f32, f64 (IEEE-754). Сучасні розширення (reference types, SIMD v128, GC) додають funcref, externref, v128, але базова модель курсу — саме чотири числові типи.

Головна відмінність від x86-64: модуль перевіряється (validation) ще до виконання. Рушій статично проходить кожну функцію й переконується, що на кожному кроці стек містить рівно ті типи, яких очікує наступна інструкція. Якщо .wat намагається викликати i32.add, маючи на стеку f64, модуль не завантажиться — на відміну від x86-64, де невідповідність типів на асемблерному рівні просто не існує (там усе — байти, а тип — питання інтерпретації компілятором вищого рівня). Ця валідація — одна з причин, чому WASM безпечний за замовчуванням: неможливо «випадково» виконати довільний код через переплутаний тип, як це можливо через переповнення буфера в C.

Анатомія .wat-модуля

.wat (WebAssembly Text Format) — текстове, людинозрозуміле представлення бінарного .wasm; кожна бінарна конструкція має точний текстовий відповідник, і wat2wasm/wasm2wat конвертують між ними без втрат.

(module
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add)
  (export "add" (func $add)))
  • (module ...) — корінь дерева; усе, що є в модулі, вкладено сюди у вигляді S-виразів (та сама дужкова нотація, що в Lisp — кожен вузол дерева синтаксису явний);
  • (func $ім'я (param ...) (result ...) тіло) — функція з обов'язковою типізованою сигнатурою; $ім'я — символічний ідентифікатор, який компілюється в числовий індекс у бінарному форматі;
  • (export "назва" (func $ім'я)) — робить функцію видимою ззовні модуля, для виклику з JavaScript чи іншого хоста; без експорту функція існує, але недосяжна;
  • (import "модуль" "ім'я" (func ...)) — дзеркальна конструкція: імпорт функції (чи пам'яті, таблиці, глобальної змінної) з хост-середовища всередину модуля.

Керування потоком: структурованість замість довільних стрибків

x86-64 має jmp/jcc на довільну мітку — будь-яку адресу коду. WASM свідомо цього не дозволяє: керування потоком структуроване, вкладене, як дужки в мові програмування.

(func $sum_to_n (param $n i32) (result i32)
  (local $i i32) (local $acc i32)
  (block $break
    (loop $continue
      local.get $i
      local.get $n
      i32.ge_s
      br_if $break        ;; вихід з block, якщо i >= n
      local.get $acc
      local.get $i
      i32.add
      local.set $acc
      local.get $i
      i32.const 1
      i32.add
      local.set $i
      br $continue))       ;; повернутися на початок loop
  local.get $acc)
  • block ... end — іменована ділянка коду; br $мітка виходить із неї одразу (аналог break);
  • loop ... end — на відміну від block, br всередині нього повертається на початок (аналог continue), а не виходить назовні;
  • br_if — умовний варіант: знімає i32 зі стека, і якщо він не нуль — виконує br;
  • є ще if ... else ... end для структурованого розгалуження без явних міток.

Ця структурованість — не стилістичний вибір, а гарантія: неможливо стрибнути в середину чужої функції чи на довільне зміщення в пам'яті коду, як це технічно можливо (хоч і небезпечно) у x86-64 через самомодифікований код чи пошкоджену адресу повернення. Компілятори (LLVM для Rust/C/C++) генерують ці конструкції автоматично з for/while/loop вихідної мови — вручну писати їх, як ми зробимо сьогодні в ЛР8, потрібно лише для навчання самій моделі.

Лінійна пам'ять

Окремо від операційного стека кожен інстанс модуля має лінійну пам'ять (linear memory) — суцільний, адресований з нуля масив байтів, спільний для всіх функцій модуля:

(memory (export "mem") 1)      ;; 1 сторінка = 64 КіБ, зростає динамічно

(func $store_and_load (result i32)
  i32.const 0        ;; адреса
  i32.const 42        ;; значення
  i32.store            ;; memory[0..4] = 42 (little-endian)
  i32.const 0
  i32.load)            ;; читає ті самі 4 байти назад

Пам'ять росте сторінками по 64 КіБ через memory.grow (повертає попередній розмір у сторінках або -1 при відмові — типова точка, де забувають перевірити помилку). Доступ — за i32-адресою через i32.load/i32.store (і 8-, 16-бітні варіанти з суфіксами 8/16). Рядки, масиви, будь-які складні структури живуть саме тут, а не в локальних змінних чи операційному стеку — тобто адресуються числом-зміщенням від нуля, як у звичайному масиві байтів. У справжніх процесорів роль такої бази грає регістр (Л9); у WASM база завжди нуль, а простір — єдиний і спільний на весь модуль.

Критична відмінність від C/асемблера: вихід за межі виділеної пам'яті — це trap (контрольоване переривання виконання з помилкою), а не невизначена поведінка (UB). Рушій завжди перевіряє межі при кожному load/store; заплатити за це доводиться швидкістю (перевірка на кожен доступ), натомість отримуємо гарантію, що модуль фізично не може прочитати чи записати пам'ять поза власною лінійною областю — навіть якщо код усередині модуля містить логічну помилку.

Порівняння з нативним x86-64

x86-64 (Л9) WebAssembly
Обчислення іменовані регістри операційний стек значень
Керування потоком довільний jmp/jcc на мітку структуровані block/loop/br/if
Пам'ять адресний простір процесу, будь-яка адреса ізольована лінійна пам'ять, з нуля, з перевіркою меж
Виклик функції ABI-угода про регістри/стек типізована сигнатура, перевірена валідатором
Перевірка коректності немає — байти є байти валідація типів при завантаженні модуля
Ціль виконання конкретний фізичний CPU будь-який рушій WASM (браузер, Node.js, Wasmtime…)

Пісочниця (sandbox): модель безпеки за замовчуванням

WASM-модуль не може виконати системний виклик, відкрити файл чи звернутися до мережі напряму — у нього немає інструкції «syscall». Єдиний спосіб взаємодіяти зі світом поза власною лінійною пам'яттю — явно оголошені імпорти, які надає хост (host — браузер чи Node.js) під час інстанціювання. Це капабіліті-модель: модуль отримує рівно ті можливості, які йому явно передали, і жодних інших. У поєднанні зі структурованим керуванням потоком (неможливо стрибнути в довільне місце) і перевіркою меж лінійної пам'яті (неможливо прочитати чужу пам'ять) це робить WASM природним кандидатом для виконання недовіреного коду — саме тому браузер може безпечно запустити модуль, завантажений з довільного сайту.

Живий приклад: .wat у браузері

wat2wasm add.wat -o add.wasm
<script type="module">
  const { instance } = await WebAssembly.instantiateStreaming(fetch('add.wasm'));
  console.log(instance.exports.add(2, 3));   // 5
</script>

instantiateStreaming компілює модуль паралельно з завантаженням байтів по мережі — рушій не чекає, доки файл довантажиться повністю, щоб почати компіляцію перших функцій.

Підводні камені

  • Забути export — функція існує в модулі, компілюється, валідується, але недосяжна з JS, доки не оголошена в (export ...).
  • Плутати i32.add з i32.div_s/i32.div_u — цілі типи WASM не мають вбудованого знаку; знак визначає інструкція, а не сам тип значення.
  • Розраховувати на UB при виході за межі пам'яті, як у C, — WASM завжди робить trap, а не «читає щось випадкове»; це інша модель помилки, яку треба явно ловити на боці хоста.
  • Не перевіряти результат memory.grow — при відмові повертається -1 (сторінок), а не викид виключення; код, що ігнорує це, продовжує роботу з пам'яттю, яка насправді не виросла.
  • Ігнорувати вирівнювання (align=) як обов'язкову вимогу — це лише підказка рушію для оптимізації, доступ спрацює й без правильного вирівнювання, просто повільніше.
  • Намагатися стрибнути на довільну мітку — так уміють справжні процесори (інструкція jmp, Л9), а WASM цього структурно не дозволяє; переписувати логіку доведеться через block/br, а не через мітки-адреси.

Розширення специфікації: SIMD, reference types, exception handling

Базова специфікація (WASM MVP, 2017) визначала лише чотири числові типи й структурований потік керування, описані вище. Відтоді специфікація розширилась кількома важливими доповненнями, які станом на 2026 рік підтримує кожен сучасний рушій: reference types (funcref, externref — типізовані посилання на функції та host-об'єкти, потрібні, зокрема, для wasm-bindgen), SIMD (v128 — 128-бітний вектор для паралельної обробки чисел, критичний для кодеків і графіки), tail calls (оптимізація рекурсії без росту стека) і exception handling (нативні try/catch на рівні байт-коду, а не емуляція через коди повернення). Для навчальної моделі курсу достатньо чотирьох базових числових типів — саме вони формують фундамент, на якому побудовані всі розширення.

Підсумок

WebAssembly переносить головну ідею асемблера — пряме, без прихованої магії відображення обчислення на інструкції машини — у формат, незалежний від фізичного процесора. Замість регістрів — операційний стек; замість довільних переходів — структурований block/loop/br; замість «будь-яка адреса процесу» — ізольована, перевірена на межі лінійна пам'ять; замість «довіряй байтам» — валідація типів при завантаженні. Це той самий словник, яким говорить асемблер справжнього процесора (Лекція 9), перекладений на мову, яку розуміє кожен браузер, сервер і мікроконтролер одночасно — і саме ця переносність, разом із вбудованою пісочницею, робить WASM природним місцем призначення для коду, скомпільованого з Rust, який ми розглянемо в Лекції 10.

Розбір за слайдами

План лекції

Фокус: та сама логіка обчислень, інша модель виконання

Перше входження: що таке інструкція машини

Це третя межа курсу: перша була між процесом і ОС, друга — між програмою й пам'яттю, ця — між кодом і набором інструкцій.

Перше входження: що таке WebAssembly

Головна відмінність від справжнього процесора — модель обчислень: замість регістрів тут стек. Наступний слайд.

Яку проблему це вирішує

Для нас важливий саме другий бік: WASM — це найдешевший спосіб побачити набір інструкцій живим, просто в DevTools.

Чим WebAssembly НЕ є

Третій пункт — це та сама межа з Модуля 1, лише ще вужча: WASM ізольований навіть від того, що бачить контейнер.

Стек замість регістрів: у чому відмінність моделі

Відмова від регістрів — головна ціна переносності: код WASM трохи довший, зате однаковий на всіх машинах

WebAssembly у цифрах

4
типи значень
i32, i64, f32, f64 — і жодних рядків
0
регістрів
замість них — операційний стек
64 КіБ
сторінка лінійної пам'яті
росте через memory.grow
2017
рік специфікації MVP
одна VM у Chrome, Firefox, Node.js, Wasmtime

Кожна функція має явну сигнатуру; невідповідність типів на стеку — помилка ще під час завантаження, не виконання

Стекова машина: як це працює

2 + 3 записується як: i32.const 2; i32.const 3; i32.add — покажіть стан стека після кожної інструкції: [2] → [2, 3] → [5]

WEBASSEMBLY — СТЕКОВА МАШИНА i32.const 2 i32.const 3 i32.add 3 2 стек операндів 5 результат інструкції беруть операнди зі стека й кладуть результат назад типізовано й валідується перед виконанням → пісочниця

Модуль .wat: функція додавання

wat
(module
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add)
  (export "add" (func $add)))

Той самий add — але для стекової машини; з x86-64 порівняємо на Л9

Анатомія .wat модуля

🌳
(module …)корінь: усе з бінарного .wasm має текстовий відповідник
ƒ
(func $ім'я …)оголошення функції; типи параметрів і результату обов'язкові
🔢
(param $a i32) (result i32)сигнатура — перевіряється валідатором до запуску
📥
local.get / local.setлокальні змінні й параметри — аналог того, що в x86-64 пишуть як -4(%rbp) (Л9)
📤
(export "add" (func $add))що видно ззовні модуля, з JavaScript
🔌
(import "env" "log" …)що модуль отримує від хоста — дзеркало export

S-вирази ((…)) — та сама нотація, що в Lisp: кожен вузол дерева синтаксису явний. wat = WebAssembly Text format

Життєвий цикл модуля

  1. 1
    wat2wasm add.watтекст → бінарний .wasm без втрат (wasm2wat — назад)
  2. 2
    fetch('add.wasm')байти прибувають по мережі — компіляція починається одразу
  3. 3
    Валідаціярушій перевіряє типи на стеку кожної функції — або відмова
  4. 4
    Інстанціюванняпідставити import-об'єкт, виділити лінійну пам'ять
  5. 5
    exports.add(2, 3)виклик із JS — звичайна функція, результат 5

instantiateStreaming суміщає кроки 2–4: компілює, доки файл ще довантажується

Керування потоком: цикл у .wat

wat
(func $sum_to_n (param $n i32) (result i32)
  (local $i i32) (local $acc i32)
  (block $break
    (loop $continue
      local.get $i
      local.get $n
      i32.ge_s
      br_if $break
      local.get $acc
      local.get $i
      i32.add
      local.set $acc
      local.get $i
      i32.const 1
      i32.add
      local.set $i
      br $continue))
  local.get $acc)

block/loop замінюють мітки .L_cond/.L_end з x86-64

Керування потоком: x86-64 ↔ WebAssembly

Конструкціяx86-64 (Л9)WebAssemblyАналог у мові
Вихід із ділянкиjmp .L_endblock $b … br $bbreak
Повторjmp .L_condloop $c … br $ccontinue
Умовний перехідcmp + jgei32.ge_s + br_ifif (…) break
Розгалуженняjcc на міткуif (result T) … else … endif / else
Довільний стрибокjmp на будь-яку адресунеможливийgoto

br_if знімає i32 зі стека: не нуль — перехід. Структурованість — не стиль, а гарантія: у чужу функцію стрибнути не можна; компілятор генерує ці конструкції з for/while сам

Лінійна пам'ять: перший погляд

Місток до ЛР8, рівень 3: там студенти вручну пишуть i32.store8/i32.load8_u у лінійну пам'ять і бачать trap при виході за межі

Лінійна пам'ять: запис і читання 4 байтів

(memory 1)1 сторінка = 65 536 байтів
→
i32.const 0 · i32.const 42стек: [адреса, значення]
→
i32.storemem[0..4] = 2a 00 00 00
→
i32.const 0 · i32.load→ 42 назад на стек
→
адреса ≥ 65 536trap, а не UB

Little-endian, як і в x86-64 (Л9); «база + зсув» тут — фіксована база 0 і i32-адреса. Межі перевіряються при кожному load/store

Пам'ять модуля: чому за межу вийти неможливо

ЛІНІЙНА ПАМ'ЯТЬ: STORE, LOAD, МЕЖА → TRAP i32.store addr 0 · val 42 i32.load addr 0 → 42 addr ≥ 65 536 вихід за межу 2a 0 00 1 00 2 00 3 00 4 00 5 00 6 00 7 00 8 00 9 … trap кінець пам'яті memory.size × 64 КіБ 42 = 0x2a → little-endian: молодший байт за меншою адресою запис 4 байтівчитанняtrap, не UB вказівників «назовні» немає — лише зсув у власному масиві байтів

Той самий приклад, що у флоу вище: 42 лягає як 2a 00 00 00, за межею — trap, а не чужа пам'ять

Живе демо: .wat у браузері

  1. wat2wasm add.wat -o add.wasm — компілюємо текст у бінарний формат
  2. Відкрити мінімальну HTML-сторінку з <script type=module>
  3. fetch('add.wasm') → WebAssembly.instantiateStreaming(...)
  4. У DevTools console: instance.exports.add(2, 3)
  5. Показати вкладку Network — розмір .wasm-файлу в байтах, набагато менший за еквівалентний JS-бандл

Очікувано: Console друкує 5; той самий .wat відкривається і виконується без змін у Chrome, Firefox, Node.js

x86-64 проти WebAssembly

x86-64 (Л9)WebAssembly
Обчисленняіменовані регістриопераційний стек значень
Керування потокомдовільний jmp/jcc на міткуструктуровані block/loop/br/if
Пам'ятьувесь адресний простір процесуізольована лінійна пам'ять з перевіркою меж
Виклик функціїABI-угода про регістри/стектипізована сигнатура, перевірена валідатором
Перевірка коректностінемає — байти є байтивалідація типів при завантаженні
Ціль виконанняконкретний фізичний CPUбудь-який рушій: браузер, Node.js, Wasmtime

Обидва — «мова, близька до заліза»; різниця в тому, чиє залізо і скільки йому довіряють

Пісочниця WebAssembly: модель безпеки

🚫
Немає syscallжодного прямого доступу до файлів, мережі, ОС
🔌
Тільки importєдиний вихід назовні — функції, які хост передав явно
🎟️
Капабіліті-модельмодуль має рівно ті можливості, що йому дали, і жодних інших
🧱
Потік + межі пам'ятіне стрибнути й не прочитати поза дозволеним

Пісочниця (sandbox) — ізольоване середовище, з якого код не дістає до системи; хост (host) — браузер, Node.js, Wasmtime. Тому браузер безпечно запускає WASM з недовіреного сайту

Trap замість UB: інша філософія помилок

C / асемблер: undefined behavior
  • Вихід за межі — будь-який наслідок можливий
  • Читає «щось випадкове» або пише в чуже
  • Перевірки немає — швидко, але наосліп
vs
WebAssembly: trap
  • Виконання зупиняється з чіткою помилкою
  • Межі перевіряються при кожному load/store
  • Плата — трохи повільніше за «сирий» asm

Виняток із правила: memory.grow при відмові повертає -1, а не trap — перевірка на совісті коду

Межа хост ↔ модуль: import і export

JS-хостinstantiate(…, {env: {log}})
→
import env.logмодуль отримує $log
→
func $reportlocal.get · call $log
→
export reportвидно з JS
→
exports.report(42)→ console.log(42)

Модуль сам нічого не друкує: без явного import виклику console.log просто не існувало б

Імпорт функції хоста у .wat

wat
(module
  (import "env" "log" (func $log (param i32)))
  (func $report (param $code i32)
    local.get $code
    call $log)
  (export "report" (func $report)))

Модуль сам нічого не друкує — 'log' надає хост через import

Два способи розгалуження у WASM

block + br_if — перехід
  • Умова (i32) знімається зі стека
  • Не нуль — вихід із block, нуль — далі
  • Аналог break / continue
  • Так компілятор реалізує цикли
vs
if (result T) … else … end — вираз
  • Умова так само знімається зі стека
  • Обидві гілки лишають значення типу T
  • Аналог тернарного виразу ? :
  • Так компілятор реалізує кожен if-вираз

Спільне: жодних «голих» міток — лише вкладені структуровані блоки; валідатор перевіряє тип на виході з кожної гілки

Живе демо: імпорт хост-функції в консоль браузера

  1. Написати add.wat з import env.log(i32) та export report
  2. wat2wasm add.wat -o add.wasm
  3. У JS: WebAssembly.instantiateStreaming(fetch('add.wasm'), { env: { log: console.log } })
  4. instance.exports.report(42) — переконатися, що консоль надрукувала число

Очікувано: Студенти бачать живий приклад капабіліті-моделі: без явного import виклику console.log просто не існувало б

WebAssembly — це безпечний, переносний, низькорівневий формат коду, розроблений для ефективного виконання й компактного представлення. — WebAssembly Core Specification (W3C), вступ

Підсумок

Джерела

🔗 ДЖЕРЕЛА
  • Understanding the WebAssembly text format — MDN EN ~1.5 год — s-вирази, `func/param/result`, `local.get/set`, розділ «Stack machines», export/import, розділи про пам'ять і data sections. Базовий текст лекції
  • WebAssembly Concepts — MDN EN ~30 хв — Module/Instance/Memory/Table, «safe, sandboxed execution environment»
  • Text Format: Modules EN ~2 год — нормативний опис неявного операційного стека, labels, call frames, traps
  • Control flow: block EN ~1 год — з опкодами й повним прикладом лічильника
  • loop EN ~1 год — з опкодами й повним прикладом лічильника
  • br_if EN ~1 год — з опкодами й повним прикладом лічильника
  • if…else EN ~1 год — з опкодами й повним прикладом лічильника
  • Memory: load EN ~1 год
  • store EN ~1 год
  • grow EN ~1 год
  • size EN ~1 год
  • WebAssembly Security EN ~40 хв — захист користувача й розробника, CFI, недосяжний стек викликів, bounds-checking, трапи; **і чесно перелічені обмеження** (side channels, code reuse)
  • WebAssembly.instantiateStreaming — MDN EN ~50 хв
  • Loading and running EN ~50 хв
  • wat2wasm online demo EN ~30 хв — головний інструмент для лабораторних без установки
  • Exercism — WebAssembly track EN — 87 задач; задачі з циклами (Clock, Difference of Squares) саме на `loop`/`br_if`
🥚 ПАСХАЛКА
У демо-терміналі wasmtime --invoke pixel pixel.wasm 2 3 повертає 5. Спробуйте аргументи 3000 і 101: стекова машина поверне 3101 — 0xC1D. Це число не випадкове: у курсі воно трапляється не раз.
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 4 · 3 нових▾

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

wat2wasm
З WABT: перекладає текстовий формат WAT у бінарний модуль .wasm (wat2wasm m.wat -o m.wasm).
wasmtimeнове
Самостійний рантайм WebAssembly: wasmtime --invoke f m.wasm 2 3 викликає функцію з аргументами.
wasm2watнове
З WABT: зворотне — бінарний .wasm назад у читабельний WAT.
wasm-bindgenнове
Генерує JS-обгортку для модуля .wasm із Rust (його викликає wasm-pack; версія CLI має збігатися з крейтом wasm-bindgen).

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.