语义

本文档主要介绍WebAssembly代码的高级设计:类型,结构和语义。WebAssembly代码可以被认为是一个结构化堆栈机器; 大多数计算仅使用栈中存储的数值内容,但是控制流使用结构化构造表示,如区块,条件判断ifs 和循环等。 实践中, 具体实现无须为控制流维护实际值栈和数据结构;它们只须遵循as if规则 。 全部详情请参阅the formal Specification, 想要了解文件级编码详情,请阅读Binary Encoding, 人类可读的文本表示请参阅文本格式请阅读Text Format.

[注]

as-if rule 只要不改变程序的 可观察行为(observable behavior),就可以任意改变程序代码。在as-if规则下,编译器可以对代码做最大限度的优化,尽可能提升应用程序的执行效率。

函数体由指令序列组成, 也要以把它看作一个隐式块。 同传统程序计数器(PC)一样,指令执行过程通过向前移动的方式处理指令序列。 指令分为两类:控制指令和简单指令。控制指令组成了控制结构和简单指令, 它从栈顶弹出参数值,根据情况决定是否更改PC的内容,然后将结果值压入堆栈。 简单指令从堆栈中弹出一个或多个参数值,批量对这些值应用一个运算符操作后,将结果值压入堆栈,随后自动递增 PC 的内容。

WebAssembly中所有指令和运算符都是显式类型的,不支持重载。 WebAssembly代码验证需要在一个常数时间内同时通过类型检查和合法性检查。

WebAssembly提供了一组语言无关的运算符,它们与许多编程语言中的运算符紧密匹配,并且可在所有现代计算机上高效地实现。 每个运算符都有相应的简单指令。

理论基础 文档详细解释了WebAssembly被设计成这样的原因。

:unicorn: = 未来功能 计划

陷阱指令

一些运算符在某些情况下可能会产生_陷阱指令_,下面会提到具体情况。 在MVP中,陷阱指令意味着WebAssembly实例中的执行将被终止, 将异常终止报告给外部环境。 在JavaScript环境中,比如浏览器,陷阱指令会导致JavaScript异常。如果找开了开发者工具,在终止前添加一个debugger调试会是非常明智的。

栈溢出

调用堆栈空间受到未指定和动态变化约束的限制,它们也带来了 不确定性.。 任何时候程序调用堆栈使用的空间超出了可用的调用堆栈空间,都会终止WebAssembly实例中的执行,异常终止将被报告给外部环境。

实现必须设定一个内部的最大调用堆栈空间尺寸,每次调用必须占用一些资源, 来消耗该值(当然,动态资源可能会更快耗尽)。 这个规则是为了避免观察行为间的差异; 如果某些实现设定了内部最大调用堆栈空间尺寸,有些实则没有,那么在这些没设定最大调用堆栈空间尺寸的实现上成功运行的程序可能会无限制地消耗资源,将这些程序放在其他实现中运行将会失败。 此外,预期WebAssembly未来将添加某种形式的堆栈内省功能,在这种情况下,优化会是直接可见的。

显式尾调用计划在未来 :unicorn: 支持。 介时会提供一个显式的尾调用运算符, 它具有明确的栈内省功能定义。

类型

WebAssembly 具有以下 值类型:

  • i32: 32-bit 整型

  • i64: 64-bit 整型

  • f32: 32-bit 浮点型

  • f64: 64-bit 浮点型

每个参数和局部变量都必须是以上四种值类型之一 . 函数签名由0或多个参数的类型序列及0或多个返回值的类型序列组成。 (注意:在MVP中,一个函数最多可以有一个返回类型)。

需要注意的是, 值类型i32i64不是固有有符号或无符号的。 这些类型的解释取决于某个具体的运算符。

线性内存

线性内存 是一段 按字节寻址的连续存储区间, 范围从0 到一个可变 内存容量 。 这个容量通常是WebAssembly页容量的倍数, WebAssembly的页容量被固定为64KiB(尽量未来可能添加可选的更大页容量支持)。模块文档的线性内存数据部分中给出了线性内存的初始状态定义。可以用 grow_memory 运算符动态增加线性内存容量。

线性内存可以被认为是一个无类型的字节数组,嵌入器是如何把这个数组映射到进程自己的 虚拟内存 中并没给出明确说明。线性内存是沙盒隔离的,它不依赖其它的线性内存、 执行引擎的内部数据结构 , 执行堆栈、局部变量、其它进程的内存。

每个WebAssembly 实例都有特别指定的 默认 线性内存, 这段内存可被所有的内存运算符访问。在MVP中,只有默认的线性内存,但在MVP之后可能会添加可以访问非默认内存的 新内存运算符 :unicorn:

线性内存(默认或其它) 要么是被 导入 , 要么是 模块内定义的。这两种方式对后续的线性内存访问没有区别。

在MVP中,线性内存不能在执行线程间共享。未来将会允许线程间共享, 见线程 :unicorn: .

线性内存访问

线性内存访问通过显式调用 loadstore 运算符完成。 对值和字节进行转换时, 所有的loadstore 运算符都采用小端字节序。加载整数可以指定_存储大小_和符号, 存储大要小小于结果类型的大小, 符号决定结果类型中的字节是符号扩展的还是零扩展的。

[注]

符号扩展: 二进制中的有符号数,符号位总是位于数的第一位,如果向方位较大的数据类型进行扩展,符号位也应该位于第一位才对,所以当一个负数被扩展时,其扩展的高位全被置位为1;对于整数,因为符号位是0,所以其扩展的位仍然是0

零扩展: 不管要转换成什么整型类型,不要最初值的符号位是什么,扩展的高位都被置位0.

见《深入理解计算机系统》 原书第3版 第2章 信息的表示和处理 2.2.6节 扩展一个数字的位表示

  • i32.load8_s: 加载1字节, 将8位整数零扩展为32位整数

  • i32.load8_u: 加载1字节, 将8位整数零扩展为32位整数

  • i32.load16_s: 加载2字节, 将16位整数符号扩展为32位整数

  • i32.load16_u: 加载2字节, 将16位整数零扩展为32位整数

  • i32.load: 加载4字节,转换为32位整数

  • i64.load8_s: 加载1字节, 将8位整数符号扩展为64位整数

  • i64.load8_u: 加载1字节, 将8位整数零扩展为64位整数

  • i64.load16_s: 加载2字节, 将16位整数符号扩展为64位整数

  • i64.load16_u: 加载2字节, 将16位整数零扩展为64位整数

  • i64.load32_s: 加载4字节, 将32位整数符号扩展为64位整数

  • i64.load32_u: 加载4字节, 将32位整数零扩展为64位整数

  • i64.load: 加载8字节,转换为64位整数

  • f32.load: 加载4字节,转换为32位浮点数

  • f64.load: 加载8字节,转换为64位浮点数

存储有一个额外的输入操作数,这个操作数就是要存储到内存中的value, 同加载一样,整数存储可以指定比操作数更小的_存储大小_, 这种情况下隐含了整数包装。

  • i32.store8: 将32位整数包装为8位整数, 存储1字节

  • i32.store16: 将32位整数包装为16位整数, 存储2字节

  • i32.store: (不转换) 存储4字节

  • i64.store8: 将64位整数包装为8位整数, 存储1字节

  • i64.store16: 将64位整数包装为16位整数, 存储2字节

  • i64.store32: 将64位整数包装为32位整数, 存储4字节

  • i64.store: (不转换) 存储8字节

  • f32.store: (不转换) 存储4字节

  • f64.store: (不转换) 存储8字节

存储运算符不产生value

以上运算符都在默认线性内存上进行操作。

寻址

每个线性内存访问运算符都有一个地址操作数和无符号的整数字节偏移立即数。 对地址操作数值与偏移值进行无限精度无符号求和得到的值, 称为_有效地址_,这被解释为线性内存中的无符号字节索引。

线性内存运算符从有效地址开始访问字节,一直访问到存储大小指定的字节数。 如果任何访问的字节超出了当前的内存大小,则访问被认为_越界_。

在有效地址计算中使用无限精度意味着向地址添加偏移量绝对不会导致包装,因此,如果访问地址超出范围,则有效地址将始终为越界。

在wasm32中, 地址操作数和偏移量属性的数据类型为i32, 线性内存大小限制为4 GiB(当然,实际大小进一步受 可用资源的限制)。 在wasm64中,地址操作数和偏移量的数据类型为i64。 MVP只包括wasm32; 后续版本将增加对wasm64的支持,从而支持 >4 GiB 的线性内存 :unicorn:

对齐

每个线性内存访问运算符还有一个必须是 2 的正整数幂的立即数,对齐属性必须小于等于可访问内存大小。对齐值与内存访问值的大小一致时, 这个值被认为是自然对齐的。对齐不仅适用于地址操作数, 也适用于有效地址。 即考虑对齐时会想到立即数偏移量

对齐具有和地址操作数、偏移量操作数一致的类型(取决于wasm32/wasm64, 如上所述)。

如果内存访问的有效地址是对齐属性值的倍数, 则内存访问被认为是对齐的, 否则被认为未对齐。对齐和不对齐的访问具有相同的行为。

对齐会影响以下性能:

  • 只有自然对齐的对齐访问速度也是很快的

  • 小于自然对齐的对齐访问可能会稍微慢一点( 考虑下:实现可以在软件中或在硬件中进行多次访问)。

  • 任何形式的未对齐访问都可能会非常慢(考虑下:实施需要一个信号修复问题)

因些, 建议WebAssembly 生产者对齐频繁使用的数据以允许自然对齐访问, 并且尽可能使用具有最大对齐值的加载和存储,同时始终避免未对齐访问。

越界

越界产生中断。 访问存储时,如果访问的字节中任何一个发生越界,则所有访问到的字节都不会被修改。

调整

MVP中, 可以用grow_memory 运算符调整线性内存的大小。grow_memory运算符的操作数以WebAssembly页面大小为单位, 页大小被固定为64KiB (未来 :unicorn:会添加大页支持)

  • grow_memory : 通过给定的页量增量来增加线性内存。返回原来的内存大小(以页为单位),执行失败时返回-1

如果线性内存 声明了最大内存大小,且增长超过该最大值时,grow_memory必须失败。 但是,如果无法预留空间,或者如果启用预留内存失败,grow_memory可能会在到达最大值之前失败。 当没有声明最大内存大小时,grow_memory 将会执行系统分配, 也可能会失败。

当前的线性内存大小可以用下面的运算符查询

  • current_memory : 返回当前内存大小, 以页为单位

如上所述, 线性内存是连续的, 也就是说, 线性地址空间中没有”洞”。 MVP以后, 未来功能 :unicorn: 提议允许在连续线性内存中设置保护和创建映射 。

MVP中, 内存只能增长 。MVP以后, 可能会添加内存缩小运算符。然而, 由于正常的分段,应用程序反而期望未来的discard :unicorn: 功能, 从工作集中释放未使用的物理页面.

以上运算符都在默认线性内存上进行操作。

表_类似于线性内存,表元素是不透明值,一种特殊的_表元素类型, 而不是字节。 这允许表包含值—类似GC引用、原始OS句柄、或原生指针—WebAssembly 代码可以通过整数索引直接获取这些值。 该功能以边界检查表为代价,间接弥补了底层的不受信的线性内存和高层的不透明句柄/引用之间的差距。

表的元素类型限制了存储在表中的元素类型,为引擎避免了一些表使用中的类型检查。 当WebAssembly值存储在表中时,值的类型必须与元素类型精确匹配。 取决于用来存储值的运算符/ API,检查可能是静态也可能动态的。 就像线性内存一样,引用该表的所有实例都会立即观察到表的更新。 宿主环境还可以允许在表中存储非WebAssembly值,在这种情况下,与导入 一样,由宿主环境定义该值的使用含义。

每个WebAssembly实例都有一个特别指定的缺省表,可以被call_indirect 和其他未来的表运算符索引。表要么是被 导入 , 要么是 模块内定义的。这两种方式对后续的表内调用访问没有区别。

在MVP中,表的主要目的是实现C / C ++中的间接函数调用(把整数索引当作函数指针使用),并且用表来保存间接函数调用的数组。 因此,在MVP中:

  • WebAssembly 代码只能用 call_indirect 访问表

  • anyfunc (任意签名的函数)是唯一允许的表元素类型

  • WebAssembly 代码不能直接改变或调整表; 这些操作只能在宿主环境中进行(如, WebAssembly JavaScript API

局部变量

每个函数都有一些固定的,预声明的局部变量,它们占用函数内部的单个索引空间。 参数被当作局部变量处理。 局部变量没有地址和线性内存别名。 局部变量具有 值类型 ,函数开头将局部变量的类型初始化为相应的零值(整型值初始为0,浮点数初始为+0),形参则被初始化为传递给函数的实参值。

  • get_local: 获取局部变量当前值

  • set_local: 设置局部变量当前值

  • tee_local: 类似 set_local, 设置局部变量当前值后返回被设置的新值

关于局部变量索引空间和类型的细节将进一步明确, 例如, 类型为i32和i64的局部变量必须是相邻的,还是被其它变量分开的等等。

全局变量

_全局变量_存储固定值类型的单个值,并且可以声明为可变的或不可变的。 这为WebAssembly提供了与任何线性存内存都不相交的内存位置,因此不能任意地作为位来别名。

全局变量只能从模块内定义的 全局索引空间 中通过整数索引的方式获取。全局变量要么是被 导入 , 要么是 模块内定义的。这两种方式对后续的全局变量访问没有区别。

  • get_global: 获取全局变量的当前值

  • set_global: 设置全局变量的当前值

set_global 设置不可变全局变量索引会导致验证异常

MVP中,全局变量的主要用例是将实例化不可变值表示为 动态链接的有用构建块。

MVP之后,当引用类型 被添加到值类型集合中时,需要使用全局变量来允许 线程 :unicorn: 间共享引用类型,因为共享线性内存不能加载或存储引用。

控制结构和指令

WebAssembly提供基本的结构化控制流结构,如_块_,循环_和 _ifs。 所有结构都由以下控制指令组成:

  • nop: 无操作, 无效果

  • block: 一个块结构的开始,以一个标记结尾的指令序列

  • loop: 一个开头有标记的块,可以用来形成循环

  • if: 一个具有隐式_then_块的if结构的开始

  • else: 标记if指令的else块

  • br: 到封闭结构中给定标记的分支

  • br_if: 到封闭结构中给定标记的条件分支

  • br_table: 跳表, 跳转到封闭结构中的标记

  • return: 函数返回0或多个值

  • end: 标记块、循环,if或函数的结束的指令

块的组成包括匹配的blockend 指令对、 匹配的loopend 指令对、 ifend 或者 ifelseend 序列的条件语句 。每个结构中的省略号中的指令被称作被_封闭_在结构中。

分支和嵌套

br, br_if, br_table 指令表示底层分支,后续统一简称为分支。 分支仅能引用包围在它们结构内的定义标记。例如,只能在block内引用block标记。

实际上,除了由于嵌套限制使得不能从循环外部分支进入循环中间, 外部block可以用于为任何给定的分支模式放置标记. 这种限制通过构造确保所有控制流程图都具有良好的结构,如高级语言(Java,JavaScript和Go)。 请注意,一个分支到block的标记相当于高级语言的标记break; 简单地从block中跳出,进入循环的声明是continue

控制指令的执行语义

执行return从值栈弹出返回值(1或多个),并从当前函数返回。

执行block 或者loop指令对值栈没有影响。

执行 block or loopend 指令,(包括隐式块,如if块或函数体)对值堆栈没有影响。

执行函数体隐式块的end指令, 会从栈顶弹出返回值(1或多个)(如果有的话), 并从函数返回。

执行if指令从堆栈中弹出一个i32条件, 然后要么转到下一条指令, 要么设置程序计数器指向if指令的elseend

执行ifelse指令设置程序计数器指向if相应的end

退出blockif的分支可能产生结构的值 。分支从堆栈中弹出结果值,值类型必须与目标结构声明的类型相同。如果采取条件或无条件的分支,则丢弃在构造和分支的开始之间被推到堆栈上的值,结果值被推回堆栈,更新程序计数器指向构造的结尾。

转向loop的分支不会产生值;它们从堆栈中弹出所有从循环开始时进入堆栈中的值,更新程序计数器指向循环的开始位置。

drop运算符可以用来显示的从栈顶弹出一个值。

与显式分支相关联的隐式弹出使编译表达式语言简单明了,甚至更少的出栈操作也能实现非局部的控制流传递。

注意,在MVP中,所有的控制结构和控制指令,限制return最多返回一个值。

br_table

br_table由一个下标从零开始的标记数组,一个_default_标记,和一个索引操作数组成。br_table跳转到数组中的索引标记,如果索引越界,则跳转到默认标记。

调用

每个函数都有签名,签名的组成包括:

  • 返回类型, 一系列值类型

  • 参数类型, 一系列值类型

WebAssembly 不支持变长参数列表(亦称 varargs). webassembly编译器可以通过显式访问线性内存的方式提供变通支持。

MVP中,返回类型序列的长度可能只有0或1。未来可能取消这一限制。

直接调用函数通过 函数索引空间中的索引来指定被调用者。

  • call: 直接函数调用

直接调用签名不匹配的函数的直接调用会引发一个模块验证异常。

间接函数调用用table中的一个i32索引 来表示被调用者。目标函数的_期望_签名(由 类型部分中的索引指定)作为第二个立即数签名。

  • call_indirect: 间接函数调用

作为验证的一部分,call 静态地检查调用者和被调用者的签名是否匹配。不同于callcall_indirect _动态_地对比调用者期待的签名和被调用者的函数签名 ,如果不匹配则产生中断。由于被调用者可能在不同的模块中, 那个模块肯定有独立的类型区间 、类型索引空间, 因此签名匹配必须对比底层的 func_type. 如上面 部分所述, 表元素也可以是在宿主环境定义的值 , 这种情况意味着调用(签名的检查方式)也是宿主环境定义的,就像调用import一样。

MVP中, 唯一的 call_indirect 访问了缺省表.

多个返回值的调用将成为可能,尽管MVP中不可以。 多个返回值调用的细节需要明确。 调用返回多个值的函数可能必须是一个指定多个局部变量的语句,以分配相应的返回值。

常量

这些运算符具有其相关类型的立即操作数,它们是作为结果值生成的。所有类型的所有可能值都被支持(包括所有可能的位模式的NaN值)。

  • i32.const: 产生一个i32类型的立即数

  • i64.const: 产生一个i64类型的立即数

  • f32.const: 产生一个f32类型的立即数

  • f64.const: 产生一个f64类型的立即数

32位整型运算符

整数运算符包括有符号的,无符号的或符号不可知的。 有符号运算符使用二进制补码有符号整数表示法。

当结果不能用结果类型表示时,有符号和无符号运算符都会产生中断。 包括对零求余、有符号除法溢出(INT32_MIN / -1)。带有非零分母的有符号余数总是返回正确值, 即使对应的除法产生中断。 符号不可知的运算符静默地将溢出结果包装成结果类型。

  • i32.add: 符号不可知加法

  • i32.sub: 符号不可知减法

  • i32.mul: 符号不可知乘法 (lower 32-bit)

  • i32.div_s: 有符号除法 (结果截断为零)

  • i32.div_u: 无符号除法 (结果 向下求整)

  • i32.rem_s: 有符号求余 (符号与被除数相同)

  • i32.rem_u: 无符号求余

  • i32.and: 符号不可知的的按位与

  • i32.or: 符号不可知的的按位或

  • i32.xor: 符号不可知的的按位异或

  • i32.shl: 符号不可知左移

  • i32.shr_u: 无符号右移(逻辑右移)

  • i32.shr_s: 有符号右移(算术右移)

  • i32.rotl: 符号不可知左循环

  • i32.rotr: 符号不可知右循环

  • i32.eq: 符号不可知的相等比较

  • i32.ne: 符号不可知的不相等比较

  • i32.lt_s: 有符号的小于

  • i32.le_s: 有符号的小于等于

  • i32.lt_u: 无符号的小于

  • i32.le_u: 无符号的小于等于

  • i32.gt_s: 有符号的大于

  • i32.ge_s:有符号的大于等于

  • i32.gt_u: 无符号的大于

  • i32.ge_u: 无符号的大于等于

  • i32.clz: 符号不可知的前导零位计数(如果值为零,所有零位都被认为是前导)

  • i32.ctz:符号不可知的拖尾零位计数(如果值为零,所有零位都被认为是的拖尾)

  • i32.popcnt: :符号不可知的一位计数

  • i32.eqz: 零值相等比较(如果操作数是0, 则返回1, 否则均返回0)

移位计数被当作无符号数包装成小于以2为底被右移值位数的对数。 例如,在32位移位中,只有计数的最低位的5位有效位会影响结果。在64位移位中,只有计数的最低位的6位有效位会影响结果。

循环计数被当作无符号的。对计数值大于等于值位数的值进行循环计数,会产生相同的结果 ,就像计数被包装成它的最低N位有效位, 计数值为i32类型时N等于5, 计数值为i64类型时N为6。

所有的对比运算符产生32位整数的结果, 1 代表 true0代表 false.

64-位整型运算符

适用于32位的整型运算符同样适用于64位整数

浮点运算符

浮点运算遵循IEEE 754-2008标准,除了:

  • IEEE 754-2008 6.2节建议操作从其操作数扩大NaN位, 允许这种操作,但不是必需的。

  • WebAssembly使用“不间断”模式,浮点异常则不可观察。 特别是,不支持可替代的浮点异常处理属性,也不支持状态标志上的非计算运算符。 静默和信号NaN之间没有明显的差异。 然而,如IEEE 754-2008所规定,当发生总是产生正无穷大,负无穷大和NaN的结果值,以指示溢出,无效和除以零的情况。

  • WebAssembly使用舍入到最接近,在一样接近的情况下偶数优先的舍入属性。除非另有说明。不支持非默认的定向舍入属性。

未来可能会解除这些限制,实现 完整的IEEE 754-2008支持 :unicorn:.

请注意,WebAssembly并没有直接提供IEEE 754-2008要求的所有运算符。 但是,WebAssembly包括足够的功能支持剩余所需运算符的合理库实现。

当除negabscopyinvalidsign之外的任何算术运算的结果是NaN时,NaN的符号位和小数字段 fraction field (不包括尾数significand 的隐含前导位)计算如下:

  • 如果指令的所有NaN输入的小数字段最高有效位中为1, 剩余位全为0,或者没有NaN输入,则结果是具有非确定性符号位的NaN,小数字段最高有效位为1,剩余有效位全部为零。

  • 否则,结果是具有非确定性符号位的NaN,小数字段的最高有效位为1,剩余有效位为非确定值。

32位浮点运算符包括如下这些:

  • f32.add: 加法

  • f32.sub: 减法

  • f32.mul: 乘法

  • f32.div: 除法

  • f32.abs: 绝对值

  • f32.neg:非

  • f32.copysign: copysign(x, y)返回的值由x的不带符号的部分和y的符号组成。

  • f32.ceil: 向上取整运算符

  • f32.floor: 向下取整运算符

  • f32.trunc: 朝零方向舍入到最接近的整数

  • f32.nearest: 向偶数舍入到最接近的整数

  • f32.eq: 有序和相等比较

  • f32.ne:无序或不等比较

  • f32.lt: 有序和小于比较

  • f32.le: 有序和小于等于比较

  • f32.gt: 有序和大于比较

  • f32.ge: 有序和大于等于比较

  • f32.sqrt: 平方根

  • f32.min: 最小值(位运算符); 任何一个操作数是NaN, 则返回NaN

  • f32.max: 最大值(位运算符); 任何一个操作数是NaN, 则返回NaN

64位浮点运算符:

  • f64.add: 加法

  • f64.sub: 减法

  • f64.mul: 乘法

  • f64.div: 除法

  • f64.abs: 绝对值

  • f64.neg:非

  • f64.copysign: copysign(x, y)返回的值由x的不带符号的部分和y的符号组成。

  • f64.ceil: 向上取整运算符

  • f64.floor: 向下取整运算符

  • f64.trunc: 朝零方向舍入到最接近的整数

  • f64.nearest: 向偶数舍入到最接近的整数

  • f64.eq: 有序和相等比较

  • f64.ne:无序或不等比较

  • f64.lt: 有序和小于比较

  • f64.le: 有序和小于等于比较

  • f64.gt: 有序和大于比较

  • f64.ge: 有序和大于等于比较

  • f64.sqrt: 平方根

  • f64.min: 最小值(位运算符); 任何一个操作数是NaN, 则返回NaN

  • f64.max: 最大值(位运算符); 任何一个操作数是NaN, 则返回NaN

minmax运算符把t -0.0 视为小于 0.0.

浮点数比较中,如果任何一个操作数是NaN, 则操作数被当作是_无序的_, 否则就是_有序的_

数据类型转换, 截断, 重解释, 提升和降级

  • i32.wrap/i64: 将64位整数包装成32位整数

  • i32.trunc_s/f32:将32位浮点数截断成有符号32位整数

  • i32.trunc_s/f64: 将64位浮点数截断成有符号32位整数

  • i32.trunc_u/f32:将32位浮点数截断成无符号32位整数

  • i32.trunc_u/f64: 将64位浮点数截断成无符号32位整数

  • i32.reinterpret/f32: 将32位浮点数重解释为32位整数

  • i64.extend_s/i32: 将有符号32位整数扩展为64位整数

  • i64.extend_u/i32: 将无符号32位整数扩展为64位整数

  • i64.trunc_s/f32: 将32位浮点数截断为有符号64位整数

  • i64.trunc_s/f64: 将64位浮点数截断为有符号64位整数

  • i64.trunc_u/f32: 将32位浮点数截断为无符号64位整数

  • i64.trunc_u/f64: 将64位浮点数截断为无符号64位整数

  • i64.reinterpret/f64: 将64位浮点数重解释为64位整数

  • f32.demote/f64: 将64位浮点数降级为32位浮点数

  • f32.convert_s/i32: 将有符号32位整数转换为32位浮点数

  • f32.convert_s/i64: 将有符号64位整数转换为32位浮点数

  • f32.convert_u/i32: 将无符号32位整数转换为32位浮点数

  • f32.convert_u/i64: 将无符号64位整数转换为32位浮点数

  • f32.reinterpret/i32: 将32位整数重解释为32位浮点数

  • f64.promote/f32: 将32位浮点数提升为64位浮点数

  • f64.convert_s/i32: 将有符号32位整数转换为64位浮点数

  • f64.convert_s/i64: 将有符号64位整数转换为64位浮点数

  • f64.convert_u/i32: 将无符号32位整数转换为64位浮点数

  • f64.convert_u/i64: 将无符号64位整数转换为64位浮点数

  • f64.reinterpret/i64: 将64位整数重解释为64位浮点数

整数值的包装和扩展、浮点值的提升和降级都总会成功。 如IEEE 754-2008所规定,浮点值的降级使用舍入到最近,偶数优先的舍入规则,可能溢出到无穷大或负无穷大。

如果提升或降级的操作数是NaN,结果是具有以下符号位和小数字段(不包括尾数Significand的隐含前导位)的NaN:

  • 如果操作数的小数字段的最高有效位是1, 剩余位为0,则结果是具有非确定性符号位的NaN,小数字段最高有效位为1,剩余有效位全部为零。

  • 否则,结果是具有非确定性符号位的NaN,小数字段的最高有效位为1,剩余有效位为非确定值。

重解释总是会成功。

使用舍入到最近, 偶数优先的传入规则将整数转换为浮点数时, 转换总是会成功。

从浮点数到整数的截断,IEEE 754-2008期望无效的运行符异常(例如,当浮点值为NaN, 或对超过整数范围值进行舍入操作)产生中断。

类型参数运算符

  • drop: 一元运算符,放弃操作数的值

  • select: 三元运行符,有两个相同类型的操作数,再加上一个布尔(I32)条件判断。如果条件操作数不为零,则返回第一个操作数,否则返回第二个操作数。

不可达

  • unreachable: 一个用于主动中断的指令。它应该被用在调用者明确知道不会有返回值的函数后面。用户代码无法捕捉 或处理这个中断, 即使未来有可能处理一些其它类别的中断或异常。

[注]

参考 LLVM Unreachable

UnreachableInst - This function has undefined behavior.

In particular, the presence of this instruction indicates some higher level knowledge that the end of the block cannot be reached.

http://llvm.org/docs/doxygen/html/classllvm_1_1UnreachableInst.html#details

校验

模块二进制文件必须在编译之前进行_校验_。 校验可以确保模块定义明确,代码中不能有任何未定义的行为。 特别是,连同一些运行时检查,可以确保任何程序都不能访问或破坏它不拥有的内存。

代码校验大多是根据类型检查、操作数堆栈的使用来定义的。 它依次检查每个指令,从栈中弹出指令预期的操作数,跟踪哪些新的操作数被推进栈中。 在函数开始时,堆栈为空; 末尾处它必须匹配函数的返回类型。 此外,block(或者是lookif)内的指令不能消耗block(或者是lookif)外部推入栈中的操作数。 在块的末尾,剩余的内部操作数必须与块签名相匹配。

一种特殊情况是无条件控制转移(br, br_table, return, unreachable),因为执行后不会继续。 这样的指令之后的堆栈是不受约束的,因此被认为是_多态的_。 后续指令依然必须要进行类型检查,但在概念上,为了检查连续的指令,任何类型的值都可以弹出多态堆栈。 多态堆栈也匹配块或函数末尾的任何可能的签名。 在块结束之后,堆栈由块签名和块之前的堆栈确定。

有关校验的详情在解释器/规范中定义。 ​