本文档描述了 WebAssembly 设计决策的基本原理,作为主要设计文本的脚注,使主要规范更易于阅读,并使以后更容易重新访问决策,而不必仔细研究所有问题和拉取请求。这份基本原理文档试图列出决策是如何做出的,以及为了语言工效学、可移植性、性能、安全性和完成任务而做出的权衡。
WebAssembly 是以增量方式设计的,同时进行多个实现。随着 MVP 的稳定,我们从真实世界的代码库中获得经验,我们将重新审视下面列出的备选方案,重新评估权衡,并在 MVP 最终确定之前更新design。
为什么不是 AST,或者基于寄存器或 SSA 的字节码?
- 我们从 AST 开始,然后推广到 A结构化堆栈机。AST 允许密集编码和高效解码、编译和解释。WebAssembly 的结构化堆栈机器是以前版本中允许的 AST 的泛化,同时允许在解释和基线编译中提高效率,以及对多返回函数的直接设计。
- 堆栈机器允许比寄存器或 SSAJSZap更小的二进制编码,Slim Binaries并且结构化控制流允许更简单且更有效的验证,包括直接解码为编译器的内部 SSA 形式。
- Polyfill prototype显示了到 ASM.JS 的简单而高效的转换。
WebAssembly 堆栈机器仅限于堆栈的结构化控制流和结构化使用。这极大地简化了一遍验证,避免了像其他堆栈机器(如 Java 虚拟机(之前stack maps))那样的定点计算。这也简化了其他工具对 WebAssembly 代码的编译和操作。计划在 MVP 之后进一步推广 WebAssembly 堆栈机器,例如添加来自控制流构造和函数调用的多个返回值。
WebAssembly 仅表示几种类型。
- 从这些基本类型中可以形成更复杂的类型。由源语言编译器根据基本机器类型来表示它自己的类型。这允许 WebAssembly 将其自身呈现为虚拟 ISA,并允许编译器将其作为任何其他 ISA 的目标。
- 这些类型在所有现代 CPU 体系结构上都可以直接表示。
- 较小的类型(如
i8
和i16
)通常不会更有效,并且在像 C/C++ 这样的语言中,只有在语义上对内存访问有意义,因为算术被扩展到i32
或i64
。至少对于 MVP 来说,避免使用它们可以更容易地实现 WebAssembly VM. - 其他类型(如
f16
、i128
)不受现有硬件的广泛支持,如果开发人员希望使用它们,可以由运行时库提供支持。硬件支持有时是不均衡的,例如,一些硬件仅支持的加载/存储f16
,而其他硬件也支持上的f16
标量算法,而其他硬件仅支持上的f16
SIMD 算法。稍后可以将它们添加到 WebAssembly 中,而不会影响 MVP. - 更复杂的对象类型在语义上对 MVP 没有用处:WebAssembly 试图提供基本的构建块,在此基础上可以构建更高级的构造。它们可能对支持其他语言很有用,尤其是在考虑垃圾收集。
加载/存储指令包括用于的addressing立即数偏移量。这是为了简化将偏移量合并到硬件中的复杂地址模式中,并简化边界检查优化。它将一些优化工作卸载到针对 WebAssembly 的编译器,在开发人员的机器上执行,而不是在用户机器上的 WebAssembly 编译器中执行该工作。
加载/存储指令包含对齐提示。这使得在某些硬件架构上生成高效代码变得更加容易。
无论是工具还是规范中的显式选择加入“调试模式”,都可能允许在对不对齐的访问抛出异常的模式下执行模块。在大多数平台上,这种模式会为分支带来一些运行时成本,这就是为什么它不是指定的默认模式。
理想的语义是 for越界访问to trap,但其含义还不完全清楚。
这种设计有几种可能的变化正在讨论和试验中。需要更多的测量来理解相关的权衡。
- 在越界访问之后,实例不能再执行代码,并且任何未完成的 JavaScriptArrayBuffer别名和线性内存都将被分离。
- 这将主要允许在有效操作员上方进行提升边界检查。
- 这可以被看作是一种温和的安全措施,假设当沙箱仍然确保安全时,实例的内部状态是不连贯的,并且进一步的执行可能导致不好的事情(例如,XSS 攻击)。
- 为了允许潜在的更有效的内存沙箱,当发生越界访问时,语义可以允许在以下选项之一之间进行不确定的选择。
- 理想陷阱语义学。
- 加载返回未指定的值。
- 存储要么被忽略,要么存储到线性存储器中未指定的位置。
- 无论是工具还是规范中的显式选择加入“调试模式”,都应该允许在对越界访问抛出异常的模式下执行模块。
为了允许高效引擎采用基于虚拟内存的边界检查技术,内存大小需要按页对齐。为了在一系列 CPU 架构和操作系统之间实现可移植性,WebAssembly 定义了一个固定的页面大小。程序可以依赖于这个固定的页面大小,并且仍然可以在所有 WebAssembly 引擎之间进行移植。64KiB 代表许多平台和 CPU 的最小公倍数。将来,WebAssembly 可能会提供在某些平台上使用更大页面大小的功能,以提高 TLB 效率。
grow_memory
操作符返回旧的内存大小。这对于在多个线程上独立使用 grow_memory
是可取的,以便每个线程可以知道它分配的区域从哪里开始。对于这样的线程,显而易见的替代方案是手动通信,但是 WebAssembly 实现可能已经在线程之间进行通信,以便正确地分配分配请求的总和,因此预计它们可以提供所需的信息,而不需要大量的额外工作。
可选最大尺寸旨在解决一系列相互竞争的限制:
- 允许 WebAssembly 模块在应用程序启动的早期,在虚拟地址空间因应用程序的执行而变得支离破碎之前,在 32 位地址空间中获取连续内存的大区域。
- 允许许多小型 WebAssembly 实例在单个 32 位进程中执行。(例如,单个 Web 应用程序使用数十个库是很常见的,随着时间的推移,每个库都可能包含 WebAssembly 模块作为实现细节。)
- 避免强迫每个开发人员使用 WebAssembly 来了解其精确的最大堆使用量。
- 将线程和共享内存添加到 WebAssemblypost-MVP时,设计不应要求内存增长到,
realloc
因为这意味着显著的实现复杂性、安全风险和优化挑战。
可选的最大值解决了以下限制:
- (1)通过指定较大的最大内存大小来解决。由于(3),简单地设置较大开始的的内存大小存在问题,并且分配初始值失败是一个致命错误,这使得选择“多大?”变得困难。
- (2)和(3)是通过使最大值可选与隐含的实现相结合来解决的,在 32 位上,引擎不会分配明显超过当前内存大小的空间,和编译器将初始大小设置为刚好足以保存静态数据。
- (4)假设在添加线程时,将新的可选“共享”标志添加到必须设置为启用共享内存的内存部分,并且共享标志强制指定最大值。在这种情况下,共享内存从不移动;唯一改变的是边界的增长,它没有上述所有的危险。特别地,别名线性存储器的任何现存
SharedArrayBuffer
的 S 在没有任何更新的情况下保持有效。
看吧#107。
结构化控制流提供简单且大小有效的二进制编码和编译。任何控制流(即使是不可简化的)都可以转换为结构化控制流Relooperalgorithm,保证低代码大小开销和通常最小的吞吐量开销(不可简化控制流的病态情况除外)。替代方法可以通过节点分裂生成可减少的控制流,这可以减少吞吐量开销,代价是增加代码大小(在病态情况下可能非常显著)。此外,将来可能会添加 [更具表现力的控制流构造:unicorn:][未来流控制]。
NOP 运算符不会产生值,也不会产生副作用。然而,它对于编译器和工具是有用的,它们有时需要将指令替换为 nop
。在没有 nop
指令的情况下,代码生成器将使用消耗模块中的空间并且可能具有运行时成本的替代什么都不做操作码模式。找到一个适当的操作码,它什么都不做,只是具有节点位置的适当类型,这并不是一件容易的事情。存在许多不同的编码 nop
方式(通常混合在同一模块中)会降低压缩算法的效率。
C/C++ 可以获取函数本地值的地址,并将此指针传递给被调用者或其他线程。由于 WebAssembly 的局部变量在地址空间之外,C/C++ 编译器通过在线性内存中创建单独的堆栈数据结构来实现获取地址的变量。该堆栈有时称为“别名”堆栈,因为它用于可由指针指向的变量。
由于别名堆栈在 WebAssembly 引擎中显示为普通内存,因此针对别名堆栈的 WebAssembly 优化需要更加通用,因此也更加复杂。我们观察到,在生成 WebAssembly 代码之前进行的常见编译器优化(如 LLVM 的全局值编号)有效地将获取地址的变量拆分为许多小范围,这些小范围通常可以分配为局部变量。因此,我们期望优化潜力的任何损失都是最小的。
相反,通常位于堆栈上的非地址取值则表示为函数内部的局部变量。这实际上意味着 WebAssembly 有一组无限的寄存器,并且可以选择以托管代码无法观察到的方式溢出它认为合适的值。这意味着有一个单独的堆栈,无法从托管代码中寻址,它也用于溢出返回值。这允许实施强大的安全属性,但这意味着要维护两个堆栈(一个由 VM 维护,另一个由以 WebAssembly 为目标的编译器维护),这可能会导致效率低下。
局部变量不是静态单赋值(SSA)形式,这意味着具有单独活性的多个传入 SSA 值可以通过 set_local
运算符“共享”由局部变量表示的存储。从 SSA 的角度来看,这意味着多个独立的值可以共享 WebAssembly 中的一个局部变量,这是一种有效的预着色,聪明的生产者可以使用它来对变量进行预着色,并为 WebAssembly VM 的寄存器分配算法提供提示,从而减轻 WebAssemblyVM 的一些优化工作。
C 和 C++ 编译器通过将参数存储在线性内存的缓冲区中并将指针传递到缓冲区来实现可变长度的参数列表。这极大地简化了 WebAssembly VM 实现,因为它将这一 ABI 考虑放在了前端编译器上。它确实会对性能产生负面影响,但是可变长度调用已经有些慢了。
WebAssembly 的 MVP 不支持函数的多个返回值,因为它们对于最早预期的用例并不是绝对必要的(而且它是一个最小量可行的产品),并且它们会给某些实现带来一些复杂性。然而,多个返回值是一个非常有用的特性,并且与 Abis 相关,因此它可能会在 MVP 之后不久添加。
基于表格的间接函数调用方案的动机是需要将函数指针表示为可以存储到线性内存中的整数值,以及强制执行基本安全属性,例如使用错误签名调用函数不会破坏 WebAssembly 的安全保证。特别是,精确签名匹配意味着内部机器级 ABI 匹配,某些引擎需要这种匹配来确保安全。间接还避免了通过原始代码地址可能的信息泄漏。
编译为 WebAssembly 的 C 和 C++ 等语言也提出了要求,例如函数指针的唯一性以及将函数指针与数据指针进行比较或将数据视为函数指针的能力。
考虑了具有异构间接函数表的直接索引的几种替代方案,从具有多个表的替代方案到可以来回映射到整数的静态类型函数指针。随着动态链接和动态代码生成复杂性的增加,这些替代方案都不能完全满足需求。
当前设计在调用函数指针时需要两个动态检查:针对间接函数表大小的边界检查,以及针对预期签名对该索引处函数的签名检查。一些动态优化技术(例如,内联缓存或单元素缓存)可以减少常见情况下的检查次数。将来可以考虑其他技术,例如将边界检查替换为掩码,或者将每个签名的表分离为只需要边界检查。此外,如果表足够小,引擎可以在内部使用填充了故障处理程序的每个签名表,以避免一次检查。
、 br_if
、 br_table
if
和 if-else
等 br
控制流指令可以在 WebAssembly 中传输堆栈值。这些原语是 WebAssembly 生产者的有用构建块,例如在编译表达式语言中。在只有一次直接使用的表达式的常见情况下,它通过避免需要 set_local
/ get_local
对来提供显著的大小缩减。然后,控制流指令可以使用结果值对表达式进行建模,从而允许更多的机会来进一步减少 set_local
/ get_local
使用(占中多边形填充原型总字节的 30-40%)。 br
-with-value 和 if
返回值的结构也可以建模出现在程序的 SSA 表示中的模型 phis
。
在一些明显的情况下,不确定性对 API 至关重要,例如随机数生成器、日期/时间函数或输入事件。当涉及到 OF 操作符的其他来源有限局部非决定论时,WebAssembly 规范是严格的:它指定了所有可能的极端情况,并在可以合理完成时指定了单一结果。
理想情况下,WebAssembly 应该是完全确定的,因为完全确定的平台更容易:
- 关于的理由。
- 实施。
- 便携式测试。
只有在没有其他可行方法时,非确定性才被指定为一种折衷方案:
- 实现portable本机性能。
- 降低资源使用率。
- 降低实现复杂性(WebAssembly 虚拟机以及生成 WebAssembly 二进制文件的编译器)。
- 允许使用新硬件功能。
- 允许实现安全加固某些用例。
当 WebAssembly 中允许非确定性时,它总是以有限和局部的方式完成。这可以防止整个程序无效,就像 C++ 未定义行为的情况一样。
随着 WebAssembly 在多个架构上使用多种语言进行实现和测试,我们可能会重新考虑一些设计决策:
- 当所有相关硬件以相同的方式实现操作时,WebAssembly 语义中就不需要不确定性。一个这样的例子是浮点:在高级别上,大多数运算符遵循 IEEE-754 语义,因此没有必要将 WebAssembly 的浮点运算符指定为与 IEEE-754 不同。
- 当不同的语言有不同的期望时,如果 WebAssembly 通过强制执行该语言不关心但另一种语言可能想要的决定论来显著地惩罚一个语言的性能,这是很不幸的。
在大多数情况下,WebAssembly 中的浮点指令生成的 Nan 具有不确定的位模式。Nan 的位模式通常并不重要,但有几种方法可以观察到它:
- 到整数类型的
reinterpret
转换 store
到线性内存,然后加载不同的类型或索引。- 存储到导入或导出的全局变量或线性存储器的 Nan 可以被外部环境观察到。
- 传递给
call
或call_indirect
导入函数的 Nan 可能会被外部环境观察到 - 输出函数的返回值可能会被外部环境观察到。
copysign
可用于将符号位复制到非 NaN 值上,然后在其中进行观察
NaN 位模式中的非确定性的动机是流行的平台具有不同的行为。IEEE 754-2008 提出了一些建议,但在这方面几乎没有硬性要求,并且在实践中存在明显的分歧,例如:
- 当没有 Nan 输入的指令产生 Nan 输出时,x86 产生设置了符号位的 Nan,而 ARM 和其他指令产生未设置符号位的 Nan.
- 当指令具有多个 Nan 输入时,x86 总是返回第一个 Nan(如果需要,则转换为安静 Nan),而 ARMv8 如果存在一个 Nan,则返回第一个信令 Nan(转换为安静 Nan),否则返回第一个安静的 Nan.
- 一些硬件体系结构已经发现,返回输入 Nan 之一是有代价的,并且更愿意返回具有固定比特模式的 Nan.
- LLVM(在某些 WebAssembly 实现中使用)并不保证它不会与其他指令交换
fadd
,fmul
因此不可能依赖于保留的“第一个”Nan. - IEEE 754-2008 本身建议体系结构使用 NaN 位来提供特定于体系结构的调试工具。
IEEE 754-2008 6.2 指出,返回 NaN应该的指令返回其输入 Nan 之一。在 WebAssembly 中,实现可以这样做,但不要求它们这样做。由于 IEEE 754-2008 规定这是“应该”(而不是“应该”),因此它不是 IEEE 754-2008 一致性的要求。
另一种设计是要求引擎在可以观察到 Nan 的位时始终“规范化”Nan.这将消除不确定性并提供更好的可移植性,因为它将隐藏特定于硬件的 Nan 传播行为。然而,从理论上讲,这将增加不可接受的开销,并且由于大多数程序不受此问题的影响,因此好处是微不足道的。
通常,WebAssembly 的浮点指令保证,如果传递给指令的所有 Nan 都是“规范的”,则结果是“规范的”,其中规范是指分数字段的最高有效位为 1,尾随位均为 0。
这是为了支持在使用 Nan-Boxing 的 WebAssembly 上运行的解释器,因为如果他们知道输入是规范的,他们就不必规范化算术指令的输出。
当指令的一个或多个输入是非规范 Nan 时,产生的 NaN 位模式是非确定性的。这是为了适应流行硬件架构中 Nan 行为的多样性。
注意,在规范的 Nan 中,符号位仍然是不确定的。这也是为了适应流行的硬件架构;例如,x86 生成的 Nan 的符号位设置为 1,而其他体系结构生成的 Nan 的符号位设置为 0. 如上所述,标准化 Nan 的成本被认为大于收益。
外部环境中的 JS 或其他实体生成的 Nan 不需要是规范的,因此导出的函数参数、导入的函数返回值以及存储在导出的变量或内存中的值可能是不规范的。
WebAssembly 的有符号整数除法将其结果舍入为零。这并不是因为缺乏同情心更好的选择,而是出于实用性。因为今天所有流行的硬件都实现了向零的舍入,并且因为 C 和许多其他语言现在都指定了向零的舍入,所以让 WebAssembly 在中间做一些不同的事情将意味着除法将变得更加复杂。
类似地,WebAssembly 的移位运算符将其移位计数屏蔽为移位值中的位数。令人困惑的是,这意味着将 32 位值移位 32 位是恒等运算,而左移位并不等同于乘以 2 的幂,因为溢出行为不同。然而,由于当今有几种流行的硬件架构实现了这种掩码行为,而那些没有实现的硬件架构通常可以通过一条额外的掩码指令来模拟它,并且由于几种流行的源语言(包括 JavaScript 和 C#)也开始指定这种行为,因此我们也不情愿地采用这种行为。
WebAssembly 有三类整数运算:有符号、无符号和符号不可知。有符号指令和无符号指令具有这样的特性:每当它们不能返回其数学期望值时(例如,当发生溢出时,或者当它们的操作数在其域之外时),它们就会捕获,以避免静默地返回不正确的值。
请注意, add
、 sub
和 mul
运算符被归类为符号不可知。由于 2 的补码表示的魔力,它们可以用于有符号和无符号的目的。请注意,这(非常方便!)意味着引擎不需要在最流行的硬件平台上为这些最常见的算术运算符添加额外的溢出检查代码。
- [后 MVP:unicorn:][未来将军],
引入了 [
i32.min_s
🦄][未来整数]。WebAssembly 开发人员更新他们的工具包,以便编译器可以利用i32.min_s
。开发人员的 WebAssembly 模块在 MVP 的执行环境以及支持i32.min_s
的环境中都能正常工作。
- 这是一个变体,其中有更多的新操作码可用,编译器 已更新为能够利用所有这些目标,但并非所有执行目标都支持所有这些目标。开发人员希望接触到尽可能多的客户,同时尽可能为他们提供最佳体验。开发者必须平衡由可能的特征配置的组合产生的测试矩阵的成本。
- Post-MVP:unicorn:,模块作者现在可以使用 浏览器中的ThreadingAPI.开发人员希望在他们的模块中利用多线程。
-
在该场景的一个变体中,我们的开发人员不想支付 开发和支持其代码的线程化和非线程化版本的工程成本。他们选择不支持 MVP 目标,只支持后 MVP 目标。最终用户(浏览器用户)收到一些消息,表明他们需要 MVP 支持。
-
在另一个变体中,我们的开发人员显式地编写仅 MVP 和后- MVP(带线程)代码。
-
SIMD支持不是普遍的 在所有目标上等效。虽然 SIMD API 的 polyfill 变体是可用的,但开发人员更喜欢编写其压缩算法的专用 SIMD 和非 SIMD 版本,因为与 SIMD polyfill 相比,非 SIMD 版本在没有 SIMD 支持的环境中执行得更好。他们将压缩代码打包以供第三方重用。
-
应用程序作者通过重用将应用程序组装在一起 例如在上述场景中开发的模块。应用程序作者的开发环境能够快速且正确地识别平台依赖性(例如,线程、SIMD),并将这些依赖性对终端应用程序的影响传达回应用程序作者。从线程感知模块中公开的一些 API 仅适用于支持线程的环境。因此,当线程不受支持时,应用程序作者需要编写专门的代码。(注意:我们应该理解当前设想的两种形式的 WebAssembly 重用:动态链接和静态导入。)
-
场景 3 中描述的压缩算法部署在 限制性执行环境,作为应用程序的一部分。在该环境中,进程可以不改变存储器页面访问保护标志(例如,某些游戏控制台,以调查服务器端部署情况)。压缩模块由 WebAssembly 环境编译,支持最特定于目标的配置(即使用/不使用线程、SIMD 等)。
- 这种情况的一种变体,其中环境是额外分离的 存储为系统可见和应用程序可见,后者不能包含机器可执行代码(某些电话,以调查游戏控制台或服务器端是否具有类似的沙箱机制)。
考虑到文本是如此的可压缩,并且众所周知,很难击败 gzipped 源代码,那么二进制格式是否比文本格式更好呢?是:
- 有效负载大小的大幅减少仍然可以显著降低压缩文件的大小。
- A多边形填充原型的实验结果表明,gzipped 二进制格式比相应的 gzipped ASM.JS 小大约 20-30%。
- 使用原始索引而不是字符串来表示变量和函数名称的二进制格式的解码速度要快得多:数组索引与字典查找相比。
- 二进制格式允许对代码大小和解码速度进行许多优化,这在源形式上是不可能的。
- 我们可以做得比一般压缩更好,因为我们知道代码结构和其他细节:
- 例如,与原则上需要查看
O(bytes*bytes)
实体的通用压缩相比,宏压缩对 AST 树进行重复数据删除可以关注 AST+ 其子级,因此O(nodes)
需要担心实体。这样的宏将允许的逻辑等价物#define ADD1(x) (x+1)
,即,被参数化。更简单的宏(#define ADDX1 (x+1)
)可以实现常量池等有用的功能。 - 另一个例子是函数和一些内部节点的重新排序,我们知道这不会改变语义,但是可以改进一般压缩。
- 例如,与原则上需要查看
- JIT 和简单的开发工具不能从压缩中受益,因此分层允许将相关的开发和维护负担转移到可重用的工具/库中。
- 每一层都尽其所能寻找压缩机会,而不会侵犯后续层的压缩机会。
- 现有的 Web 标准演示分层编码策略的许多优点。
对于堆栈机器,类型检查必须确保的基本属性是,对于控制流图(由各个指令形成)中的每一个_边缘_,关于堆栈的假设都是匹配的。同时,类型检查应该是快速的,因此可以通过线性遍历来表示。
从无条件控制转移指令(如 br
、 br_table
、 return
或 unreachable
)到正文中的下一条指令之间没有控制边--下一条指令是不可到达的。因此,关于堆栈,不必在两者之间施加约束。在线性类型检查算法中,这可以通过允许假设任何类型的堆栈来表示。因此,在类型系统术语中,可以说_多态的_指令处于其堆栈类型中。
该解决方案是规范的,因为它引入了确保可靠性所必需的最小类型约束,同时还保持了关于有效(即,类型良好)代码的以下有用的结构属性:
- 它是可组合的:如果指令序列 A 和 B 是有效的,则块(A,B)是有效的(例如,在空栈类型)。
- 它是可分解的:如果块(A,B)有效,则 A 和 B 都有效。
- 它概括了一种表达式语言:每个表达式都是可直接表达的,即使它包含控制转移。
- 可达性是不可知的:规范和生产者都不需要定义、检查或以其他方式关心可达性(除非他们选择这样做,例如执行死代码消除)。
- 它维护基本的转换:例如,(i32.const 1)(br_if$l) can always be replaced with (br $l)。
这些属性中的一些与支持某些编译技术有关,而不会使生产者变得复杂。作为一个小的但有代表性的例子,考虑一个教科书中的一遍编译器,它通常使用类似于下面的方案来编译表达式:
compile(A + B) = compile(A); compile(B); emit(i32.add)
compile(A / B) = compile(A); compile(B); emit(dup i32.eqz (br_if $div_zero) drop i32.div)
compile(A / 0) = compile(A); emit(br $div_zero)
第三种情况是第二种情况的专门化,即简单的优化。然而,如果没有指令的 br
多态类型,这种简单的方案将不起作用,因为 compile((x/0)+y)
将导致无效代码。更糟糕的是,它可能导致微妙的编译器错误。类似的情况出现在生产编译器中,例如,在一些 WebAssembly VM/工具正在实现的 ASM.JS-to-wasm 编译器中。
已经讨论过的其他解决方案将失去上述大部分特性。例如:
- 禁止某些形式的不可访问代码(无论是通过规则还是通过构造)将破坏除可分解性之外的所有功能。
- 允许但不允许类型检查不可达代码将破坏可分解性,并要求规范提供可达性的语法定义(这不太可能与生产者使用的定义相匹配)。
- 要求堆栈在跳转后为空仍然与可达性无关,并保持可分解性,但仍然会破坏所有其他属性。
值得注意的是,一般来说,这种类型检查并不罕见。例如,JVM 在跳转后也不会对堆栈类型施加任何约束(但是,在其现代形式中,它建议使用的形式的_堆栈贴图_类型注释,然后在跳转后需要使用这些注释来显式地实例化多态类型规则)。此外,允许控制转移_表情_的编程语言通常也是多态的(例如, throw
/ raise
,它是某些语言中的表达式)。