ข้ามไปยังเนื้อหา

Calls และ Recursion

ฟังก์ชันใน WebAssembly เรียกกันด้วยคำสั่ง call argument จะถูก push ขึ้น stack ก่อนเรียก และค่าที่คืนกลับ (ถ้ามี) จะถูกฝากไว้บน stack เมื่อ callee return Wasm รองรับ direct recursion อย่างสมบูรณ์ — ฟังก์ชันสามารถเรียกตัวเองได้ — และ mutual recursion ระหว่างฟังก์ชันที่กำหนดใน module เดียวกัน

call $funcname เรียกฟังก์ชันที่มีชื่อนั้น สามารถเรียกด้วย index ได้ด้วย (call 0) แต่ named reference อ่านง่ายกว่ามาก ก่อนรัน call ให้ push argument ทั้งหมดตามลำดับจากซ้ายไปขวา:

(module
(func $double (param $n i32) (result i32)
local.get $n
i32.const 2
i32.mul)
(func (export "quadruple") (param $x i32) (result i32)
local.get $x
call $double ;; passes $x; result replaces it on stack
call $double)) ;; doubles again

quadruple(3) เรียก $double สองครั้ง ได้ผลลัพธ์ 3 → 6 → 12

WAT อนุญาตให้ฟังก์ชันเรียกตัวเองได้ รูปแบบเหมือนทุกภาษา: push argument, call self, รวม result base case ใช้ if เพื่อ return ตรงๆ โดยไม่มี recursive call

(module
(func $factorial (export "factorial") (param $n i32) (result i32)
local.get $n
i32.const 1
i32.le_s ;; n <= 1?
if (result i32)
i32.const 1 ;; base case: 0! = 1! = 1
else
local.get $n
local.get $n
i32.const 1
i32.sub
call $factorial ;; factorial(n-1)
i32.mul ;; n * factorial(n-1)
end))

ฟังก์ชันนี้มีทั้ง internal name ($factorial) เพื่อเรียกตัวเองได้ และ export name เพื่อให้ JavaScript เรียกได้

การมี recursive call สองครั้งใน else arm ทำงานในลักษณะเดียวกัน — push argument, call, แล้วรวมผลลัพธ์ทั้งสอง:

(module
(func $fib (export "fib") (param $n i32) (result i32)
local.get $n
i32.const 1
i32.le_s ;; n <= 1?
if (result i32)
local.get $n ;; fib(0)=0, fib(1)=1
else
local.get $n
i32.const 1
i32.sub
call $fib ;; fib(n-1)
local.get $n
i32.const 2
i32.sub
call $fib ;; fib(n-2)
i32.add
end))
WebAssembly
ตัวเลือกBenefitCost
Structured call (ไม่มี arbitrary jump ข้าม stack frame)engine validate ได้ว่า call graph ถูกต้องตั้งแต่ compile time ปลอดภัยกว่าการกระโดดข้าม address ตรงๆไม่มี manual stack manipulation แบบ assembly ทำให้ pattern บางอย่าง (เช่น coroutine แบบเดิม) ต้องเขียนใหม่
Recursion ผ่าน call ธรรมดาเขียนง่าย เข้าใจง่าย เหมือน recursive function ในภาษาอื่นWasm ไม่มี automatic stack growth เหมือน native thread บางตัว recursion ลึกเกินไปจะ trap ด้วย stack exhaustion
  • เขียน recursive function โดยไม่มี base case ที่ครอบคลุมครบ (เช่นลืมเช็ค n < 0) ทำให้ recursion ไม่มีวันจบ
  • คาดหวังว่า Wasm จะทำ tail-call optimization ให้อัตโนมัติเหมือนบางภาษา แต่ทุก call เพิ่ม frame เข้า call stack เสมอ ทำให้ recursion ลึกๆ เสี่ยง stack exhaustion
  • push argument ผิดลำดับก่อน call (WAT ไม่มี named argument) ทำให้ parameter ไปผูกกับตำแหน่งผิดโดยไม่มี error ตอน compile

💡 ตัวอย่างจากของจริง

Wasmtime ใช้ structured call stack ของ Wasm ในการ verify และ compile function call ให้เร็วโดยไม่ต้องวิเคราะห์ arbitrary jump และยัง enforce stack size limit เพื่อจับ unbounded recursion ก่อนที่จะ crash process จริง

วิธีเรียกฟังก์ชันชื่อ `$add` ใน WAT คืออะไร?
สิ่งที่ต้องอยู่บน stack ก่อน `call $fn` ที่รับ parameter i32 สองตัวคืออะไร?
WebAssembly พื้นฐานปรับแต่ง tail call โดยอัตโนมัติหรือไม่?