ตัวแปร Local และ Stack
WebAssembly เป็น stack machine ไม่มี register file เหมือนใน x86 หรือ ARM — แทนที่จะเป็นแบบนั้น คำสั่งสื่อสารกันด้วยการ push และ pop ค่าบน value stack แบบโดยปริยาย การเข้าใจแบบจำลองนี้คือกุญแจสำคัญในการอ่าน WAT ได้อย่างคล่องแคล่ว
Value stack
หัวข้อที่มีชื่อว่า “Value stack”ทุกคำสั่งทำงานบนยอดสุดของ stack คำสั่งหนึ่งจะ pop อินพุตของตัวเองและ push เอาต์พุตของตัวเอง ตัวอย่างเช่น i32.add จะ pop ค่า i32 สองค่าและ push ผลลัพธ์ i32 หนึ่งค่า
flowchart LR A["Stack: []\ni32.const 10"] --> B["Stack: [10]\ni32.const 3"] B --> C["Stack: [10, 3]\ni32.add"] C --> D["Stack: [13]"]
เมื่อสิ้นสุดฟังก์ชัน สิ่งที่เหลืออยู่บน stack ต้องตรงกับชนิด result ที่ประกาศไว้อย่างแน่ชัด หากเหลือมากเกินไปหรือน้อยเกินไป module จะ validation ไม่ผ่าน
ตัวแปร local
หัวข้อที่มีชื่อว่า “ตัวแปร local”นอกจาก stack แล้ว ฟังก์ชันสามารถประกาศตัวแปร local ได้โดยใช้ (local $name type) สิ่งเหล่านี้เป็นช่องเก็บค่าที่เปลี่ยนแปลงได้ ซึ่งมีชีวิตอยู่ตลอดระยะเวลาที่เรียกฟังก์ชัน
(module (func (export "example") (param $x i32) (result i32) (local $tmp i32) local.get $x ;; push $x i32.const 1 i32.add ;; pop two, push sum local.set $tmp ;; pop and store in $tmp local.get $tmp)) ;; push $tmp backพารามิเตอร์ก็เป็น local เช่นกัน — คุณอ่านค่าด้วย local.get ได้เหมือน local ที่ประกาศไว้ แต่ถูกกำหนดค่าเริ่มต้นด้วยอาร์กิวเมนต์ที่ถูกเรียก
local.tee: เก็บค่าแล้วเก็บไว้บน stack
หัวข้อที่มีชื่อว่า “local.tee: เก็บค่าแล้วเก็บไว้บน stack”local.tee คล้ายกับ local.set แต่ทิ้งสำเนาของค่าไว้บน stack หลังจากเก็บแล้ว มีประโยชน์เมื่อคุณต้องการทั้งบันทึกค่าและใช้ค่านั้นทันที:
(module (func (export "storeAndDouble") (param $n i32) (result i32) (local $saved i32) local.get $n local.tee $saved ;; stores $n in $saved AND leaves the value on stack i32.const 2 i32.mul)) ;; doubles the value that tee left behindหากไม่มี local.tee คุณจะต้องใช้ local.set ตามด้วย local.get ที่เป็นสองคำสั่งแทนที่จะเป็นหนึ่ง
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| เขียน WAT เอง | บังคับให้เข้าใจ value stack และ local จริง ๆ เพราะไม่มี syntax sugar มาบัง เหมาะมากสำหรับเรียนรู้ | ต้อง track stack depth เองตลอด แค่ลืม local.set หนึ่งบรรทัดก็ทำให้ validation พังได้ |
| ให้ compiler generate Wasm | compiler จัดการ stack allocation และ register-to-local mapping ให้อัตโนมัติ productive กว่ามากสำหรับแอปจริง | ควบคุม lifetime ของ local แบบละเอียดเองไม่ได้ ต้องเชื่อใจ compiler |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- จบ block หรือฟังก์ชันโดยมีค่าค้างอยู่บน stack มากกว่าที่ signature ประกาศไว้ เช่นลืม pop ค่าที่ push ไว้ระหว่างทาง ทำให้ module validation ไม่ผ่าน
- ใช้
local.teeทั้งที่ตั้งใจจะใช้local.setทำให้ค่าที่ไม่ต้องการค้างอยู่บน stack เกินมาโดยไม่รู้ตัว - ลืมว่า branch อย่าง
br/br_ifที่กระโดดออกจาก block ต้องทิ้ง stack ไว้ในสภาพที่ target ของ branch คาดหวังพอดี ไม่ใช่แค่ปล่อยให้ไหลไปตามลำดับปกติ
💡 ตัวอย่างจากของจริง
วิศวกรที่ debug production
.wasmblob ที่บริษัทอย่าง Cloudflare Workers มักใช้wasm2watแปลง binary กลับเป็น WAT ที่อ่านได้ เพื่อไล่ดู stack ทีละคำสั่งตอนหา bug ที่ compiler สร้างโค้ดผิดพลาด