Control Instructions
撰写时间:2025-01-24
修订时间:2025-01-28
概述
程序流程控制指令包含以下指令:
- nop
- unreachable
- block
- loop
- if
- br
- br_if
- br_table
- return
- call
- call_indirect
block, loop及if指令为结构化指令 (structured instructions)。用在语句块中。语句块以end或else伪指令来结束或分隔。
根据语句块的类型,结构化指令可以消耗栈中数据,并在操作数栈中产生结果。
每个结构化指令产生一个隐式的标签。标签是分支指令 (branch instructions)的目标。可使用label 0
来引用第0个标签,其中0表示最里层的语句块。
br或br_if是直接跳转(类似于C语言中的break
语句),而loop则是跳到循环的开始位置(类似于C语言中的continue
语句)。
br执行无条件分支,br_if执行有条件分支,br_table执行一个间接分支,return是一个无条件分支的快捷指令 (shortcut),跳转到默认为当前函数的最外层语句块。
向前指令 (forward branches) 消耗整个语句块最后所生成的结果,而向后指令 (backward branches) 消耗整个语句块最初所依赖的在栈中的操作数。
call指令调用另一个函数,消耗栈中的参数,返回调用的结果。call_indirect通过table索引值来间接调用另一函数。
return 指令
Wasm自带检验功能,包括自动检查函数内Result Stack的状态是否与返回值一致。
而这又分为几种情况。
第一种情况,函数没有返回值,但Result Stack非空。此时将抛出校验异常。
let watSrc = `
(module
(func
i32.const 5
)
(start 0)
)
`;
WabtUtils.RunWat(watSrc)
.catch(e => {
pc.error('%s', e.message);
});
异常信息为,函数没有返回值,却在Result Stack中发现了一个i32的元素。
return指令将Result Stack中的所有元素依序弹出。因此可解决上面出现异常的问题。
let watSrc = `
(module
(func
i32.const 15
i32.const 25
i32.const 35
return
)
(start 0)
)
`;
WabtUtils.RunWat(watSrc)
.then(() => pc.log('pass'))
.catch(e => {
pc.error('%s', e.message);
});
栈中原本有3个元素,一个return指令将所有元素都弹出栈,栈变空,通过了检验。
当特定函数没有返回值时,可调用start指令来自动运行该函数。
第二种情况,函数通过result指令声明有具体数量的返回值,且Result Stack中的元素数量正好与所声明的返回值的数量相等。
let watSrc = `
(module
(func (export "checkReturnValue") (result i32 i32)
i32.const 15
i32.const 25
)
)
`;
const {checkReturnValue} = await WabtUtils.RunWat(watSrc);
pc.log(checkReturnValue());
返回值的顺序为依序进栈的顺序。
因为函数带有result指令,函数体内实际上是隐式地调用了return指令。因此也上面代码也可编写为:
let watSrc = `
(module
(func (export "checkReturnValue") (result i32 i32)
i32.const 15
i32.const 25
return
)
)
`;
const {checkReturnValue} = await WabtUtils.RunWat(watSrc);
pc.log(checkReturnValue());
也可改写为一行语句的方式:
let watSrc = `
(module
(func (export "checkReturnValue") (result i32 i32)
(return (i32.const 15) (i32.const 25))
)
)
`;
const {checkReturnValue} = await WabtUtils.RunWat(watSrc);
pc.log(checkReturnValue());
这种方式更加直观。
第三种情况,如果return实际返回的数量大于result所声明的数量,则前面多余的返回值将被舍弃。
let watSrc = `
(module
(func (export "checkReturnValue") (result i32 i32)
(return (i32.const 15) (i32.const 25) (i32.const 35))
)
)
`;
const {checkReturnValue} = await WabtUtils.RunWat(watSrc);
pc.log(checkReturnValue());
第四种情况,如果return实际返回的数量小于result所声明的数量,则抛出校验异常。
let watSrc = `
(module
(func (export "checkReturnValue") (result i32 i32)
(return (i32.const 15))
)
)
`;
const {checkReturnValue} = await WabtUtils.RunWat(watSrc);
pc.log(checkReturnValue());
if 指令
条件语句的基本语法如下:
let watSrc = `
(module
(func (export "judge") (result i32)
(if (i32.const 1)
(then (return (i32.const 25)))
(else (return (i32.const 35)))
)
(return (i32.const 0))
)
)
`;
const { judge } = await WabtUtils.RunWat(watSrc);
pc.log(judge());
默认情况下,if
语句将对Result Stack的栈顶元素进行判断。如果调用if
语句时,栈为空,则抛出检验错误的异常。
上面代码i32.const 1
先将数值1压进栈顶,以供if
语句使用(消耗)。当此值为非零值时,执行then
语句分支;当此值为零值时,执行else
语句分支。
当有result声明时,为通过检验,函数体内最后一条语句需显式地返回相应的值,尽管上面的else指令已实际达成此效果。因此,上面代码也可改写为:
let watSrc = `
(module
(func (export "judge") (result i32)
(if (i32.const 0)
(then (return (i32.const 25)))
)
(return (i32.const 35))
)
)
`;
const { judge } = await WabtUtils.RunWat(watSrc);
pc.log(judge());
block 指令
block指令为一个语句块附上一个名称,以供br等指令跳出此段代码。block指令只能用于func中。其基本使用方法如下:
let watSrc = `
(module
(func
(block $block_a ;; define a named block
br $block_a ;; jump out of the block
unreachable ;; trap
)
;; codes here would be run
)
(start 0)
)
`;
await WabtUtils.RunWat(watSrc);
定义了一个名为$block_a
的语句块,然后使用br指令跳出该语句块,转到执行函数中该语句块后面的语句。
当br指令后面的标签是一个block时,程序流程跳转到该语句块的后面,形成了跳出
的效果。相对应的,当br指令后面的标签是一个loop时,程序流程跳转到该语句块的前面,形成了循环
的效果。
unreachable指令用于定义一个不应被执行到的陷阱 (trap),如果该处的语句被执行到,则会抛出一个异常。因此下面代码将抛出异常:
let watSrc = `
(module
(func
(block $block_a
unreachable ;; fall off trap
)
)
(start 0)
)
`;
await WabtUtils.RunWat(watSrc);
loop 指令
loop指令为一个语句块附上一个名称,以供br等指令跳到此段代码的起始处,从而形成循环执行语句块代码的效果。loop指令只能用于func中。其基本使用方法如下:
let watSrc = `
(module
(func
(loop $loop_a
(if (...)
(then (br $loop_a))
)
)
)
(start 0)
)
`;
await WabtUtils.RunWat(watSrc);
loop指令自身不会自动循环执行语句,需配合br等跳转分支语句,才会形成循环语句的效果。
在使用loop指令时,最重要的一点是需保证确实存在不再重复执行语句块的条件,以免程序流程陷入进入无尽的循环。
br 指令
br指令,branch
的缩写,直接跳转至相应的block语句块或loop语句块。
跳转至block分支
const importObject = {
js: {log: pc.log.bind(pc)}
};
let watSrc = `
(module
(import "js" "log" (func $log (param i32)))
(func (export "leaveTen") (param $arg i32)
(block $block_a
local.get $arg
i32.const 10
i32.eq
(if
(then br $block_a)
)
(local.set $arg (i32.const 5))
)
(call $log (local.get $arg))
)
)
`;
const { leaveTen } = await WabtUtils.RunWat(watSrc, importObject);
leaveTen(2);
leaveTen(10);
leaveTen函数将值不为10的参数值统一设置为5。
在代码中,如果参数值等于10,则跳转到$block_a
语句块的结尾;否则,继续执行$block_a
语句块中后续代码,将参数值统一设置为5。
跳转至loop分支
const importObject = {
js: {log: pc.log.bind(pc)}
};
let watSrc = `
(module
(import "js" "log" (func $log (param i32)))
(func (export "stopAtTen") (param $arg i32)
(loop $loop_a
local.get $arg
i32.const 10
i32.lt_u
(if
(then
local.get $arg
i32.const 1
i32.add
local.set $arg
br $loop_a
)
)
)
(call $log (local.get $arg))
)
)
`;
const { stopAtTen } = await WabtUtils.RunWat(watSrc, importObject);
stopAtTen(2);
stopAtTen(10);
stopAtTen函数将参数值持续加1,直至其值等于10为止。
在代码中,如果参数值小于10,则加1后,跳转到$loop_a
语句块的开始处重复执行该语句块;否则,退出循环。
小结
br指令实际跳转,而block及loop指令规范了跳转的目的地。
br_table 指令
br_table可从嵌套的blocks中直接跳转到指定的block。
const importObject = {
js: {log: pc.log.bind(pc)}
};
let watSrc = `
(module
(import "js" "log" (func $log (param i32)))
(func (export "nestedBlock") (param $arg i32)
(block
(block
(block
(call $log (i32.const 5))
local.get $arg
br_table 0 1 2
)
(call $log (i32.const 10))
)
(call $log (i32.const 15))
)
)
)
`;
const { nestedBlock } = await WabtUtils.RunWat(watSrc, importObject);
pc.group('block 0');
nestedBlock(0);
pc.groupEnd();
pc.group('block 1');
nestedBlock(1);
pc.groupEnd();
pc.group('block 2');
nestedBlock(2);
pc.groupEnd();
pc.group('block 7');
nestedBlock(7);
pc.groupEnd();
定义了3个blocks,每一个block都分别打印了一个数值。
最里层的代码:
(block
(call $log (i32.const 5))
local.get $arg
br_table 0 1 2
)
根据所传进的参数,决定跳转到第几层block。
首先,代码br_table 0 1 2
必须放置在最里层中。其次,从最里层到最外层,分别用数字0, 1, 2表示。第三,在调用br_table
指令前,需在栈中设置一个值而成为其立即参数,上面的local.get
即为此作用。当此立即参数超出层数的范围时,则跳转至最外层。
br_if 指令
br_if指令,branch if
的缩写,可在满足特定条件下跳转至相应的loop语句块或block语句块。
先看下面的JavaScript代码:
function iter(num) {
let tmp = num;
do {
pc.log(tmp);
tmp++;
} while (tmp <= 5)
pc.log(-999);
}
iter(3);
调用do ... while
语句,只要参数num的值小于等于5,就打印其值;否则,其值加1后重复循环。循环结束后,打印-999以示程序结束。
编写相对应的Wat代码如下:
const importObject = {
js: {log: pc.log.bind(pc)}
};
let watSrc = `
(module
(import "js" "log" (func $log (param i32)))
(func (export "iter") (param $arg i32)
(local $tmp i32) ;; store the increasing value
local.get $arg ;; update $tmp
local.set $tmp
(loop $iter_loop ;; define looping label
local.get $tmp ;; print current $temp value
call $log
local.get $tmp ;; increase the value of $tmp by 1
i32.const 1
i32.add
local.set $tmp ;; and update
local.get $tmp
i32.const 5
i32.le_u ;; $tmp <= 5 ?
br_if $iter_loop ;; if true, jump to branch $iter_loop
)
i32.const -999 ;; $tmp > 5, jump out of branch $iter_loop
call $log
)
)
`;
const { iter } = await WabtUtils.RunWat(watSrc, importObject);
iter(3);
end 指令
当以块指令 (block instructions) 的方式来编写if, loop, block等语句块代码时,须用end指令来标记该语句快的结束。
标记 if 代码块
const importObject = {
js: {
log: pc.log.bind(pc),
prtZero() {
pc.log('Zero.');
},
prtNonZero() {
pc.log('Non zero.');
},
prtEnd() {
pc.log('End of testing.');
}
}
};
let watSrc = `
(module
(import "js" "log" (func $log (param i32)))
(import "js" "prtZero" (func $prtZero))
(import "js" "prtNonZero" (func $prtNonZero))
(import "js" "prtEnd" (func $prtEnd))
(func (export "isZero") (param i32)
local.get 0
i32.eqz
if
call $prtZero
else
call $prtNonZero
end
call $prtEnd
)
)
`;
const { isZero } = await WabtUtils.RunWat(watSrc, importObject);
isZero(0);
isZero(1);
这种方式的if语句块中不能再调用then指令,但更接近于其他高级语言的形式。
标记 block 代码块
const importObject = {
js: {log: pc.log.bind(pc)}
};
let watSrc = `
(module
(import "js" "log" (func $log (param f32)))
(func (export "nonNeg") (param f32)
block $block_a
local.get 0
f32.const 0
f32.ge
br_if $block_a
local.get 0
f32.neg
local.set 0
end
local.get 0
call $log
)
)
`;
const { nonNeg } = await WabtUtils.RunWat(watSrc, importObject);
nonNeg(-5);
nonNeg(5);
当函数参数大于等于0时,跳出$block_a
语句块,否则将其符号反转,从而得到一个非负数。
标记 loop 代码块
const importObject = {
js: {log: pc.log.bind(pc)}
};
let watSrc = `
(module
(import "js" "log" (func $log (param i32)))
(func (export "prtToTen") (param i32)
loop $loop_a
local.get 0
call $log
local.get 0
i32.const 10
i32.lt_u
if
local.get 0
i32.const 1
i32.add
local.set 0
br $loop_a
end
end
)
)
`;
const { prtToTen } = await WabtUtils.RunWat(watSrc, importObject);
prtToTen(8);
打印函数参数值,直至其值等于10为止。
nop 指令
nop指令是一个空指令,不会执行任何操作。
let watSrc = `
(module
(func
nop
)
)
`;
await WabtUtils.RunWat(watSrc);
call 指令
call指令用以调用函数。
const importObject = {
js: {log: pc.log.bind(pc)}
};
let watSrc = `
(module
(import "js" "log" (func $log (param i32)))
(func
i32.const 10
call $log
)
(start 1)
)
`;
await WabtUtils.RunWat(watSrc, importObject);
所导入的$log函数的索引值为0,所定义的匿名函数的索引值为1。
call_indirect 指令
call_indirect指令用以调用存储在函数表中的函数,可用于实现函数指针。
let watSrc = `
(module
(func $add (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add
)
(func $sub (param i32 i32) (result i32)
local.get 0
local.get 1
i32.sub
)
(table $funcTable 2 funcref)
(elem $funcTable (i32.const 0) $add $sub)
(type $funcType (func (param i32 i32) (result i32)))
(func (export "funcPointer") (param $funcIdx i32) (param $num1 i32) (param $num2 i32) (result i32)
local.get $num1
local.get $num2
(call_indirect $funcTable (type $funcType) (local.get $funcIdx))
)
)
`;
const { funcPointer } = await WabtUtils.RunWat(watSrc);
pc.log(funcPointer(0, 2, 3)); // invoke the 0th function
pc.log(funcPointer(1, 5, 2)); // invoke the 1st function