网页汇编接口
标准库宿主服务 闲块数 接受有值参数,返回豫言内核服务 45 的空闲物理块数,每块 2 MiB;原生与节点宿主没有该内核池时返回负一。执行器将带类型导入映射到原语 194,再调用共用底层平台查询。
网一:唯一的目标
编译器只有一个目标:WasmGC。它使用引擎引用、类型数组、结构及 Wasm 调用栈,不依赖任何“裸指针 ABI”。本篇各节编号保持稳定,便于引用;原第二节已随旧目标删除。
网三:WasmGC 值型
所有普通表达式结果统一为 (ref null eq),允许以下实际值型:
| 值 | WasmGC 表示 |
|---|---|
| 单位/擦除类型 | ref.null eq |
| -2^30 至 2^30-1 整数 | ref.i31,按有符号 i31 解码 |
| 其余 i64 整数 | $big = struct(field i64) |
| 爻 | i31 的 0/1;非任意对象真值 |
| 小数 | $float = struct(field f64) |
| 元组/引用/闭包/数组 | $tuple = array(mut (ref null eq)) |
| 字符串/字节串/字节缓冲 | $bytes = array(mut i8),长度为逻辑字节数,不附隐藏的末尾零字节;字节缓冲是可写的,只在类型上与字节串区分 |
| 构造器实例 | 专用结构体 $变形 = struct (field i32) (field 各字段):形是构造器各显式字段的表示:声明为整数的字段是“整”(i64),其余(含声明为小数的字段与类型变量字段)是“任”((ref null eq));“小”(f64)是给小数字段留的表示,现在不生成;一实例一个对象;首字段是标签(构造器在所属类型内的序数,从一起),第 i 个字段在 i+1 号;全部字段不可变,类型 final、无超类型 |
| 资源(接口文件里没有构造器的类型) | $外 = struct(field (ref null extern)),字段不可变;装着宿主交来的对象。字段可空只为让原型全局能用 struct.new_default 构造 |
| 函数代码引用 | i31 的函数表索引 |
闭包首项仍是代码引用,随后为捕获值。普通源码函数沿用擦除后的逐参闭包链;下述多参数函数型对应内部 IR,不能直接由源箭头数量推定。取函数引用须 ref.cast (ref i31)、i31.get_u,然后以对应参数数目的函数类型 call_indirect。不能拿宿主地址填 i31。
同形的构造器(不论属于哪个类型)共用同一个结构体,同一类型里同形的构造器靠标签区分。匹配构造器时,按本次匹配各构造器分支的形依次 ref.test(字段多的形在前),命中就转换并取标签;都不中得零,落到默认支或报错支。进入分支后,被匹配者只转换一次,存进带类型的局部,取字段都读这个局部。
网四:调用、模块、异常
本基线总是生成 $t0…$t16;模块里的函数或动态调用用到更多参数时,按用到的最大参数数目 N 接着生成 $t17…$tN。每种类型有相应数量的 (ref null eq) 参数及一个同型结果。闭包 self 算在运行时参数数目内。尾调用用 return_call_indirect。正常分支合并点用带结果块及 br,不分配续延帧。
确知函数(见《函数应用形状分析》“去装箱”)另有快体 $m号,纯整函数的快体是 $n号。快体的参数按确知签名是 i64、f64、(ref $tuple) 或 (ref null eq),结果是 i64、f64 或 (ref null eq),类型内联写出,不进函数表。公开入口 $f号 保持上面的 $tN 约定:拆箱后调用快体,结果不是 eqref 时再装箱。编译器直接调用确知函数时,直接调用快体。函数局部除了通用局部,还可以有 i64、i32、f64 局部,存放未装箱的整数、爻与小数;它们在元组、闭包、全局等通用使用点才装箱。
函数表按收集的闭包函数顺序分配索引;全局以 路径#定义名 作键分配 eq 引用全局:普通模块的顶层写成不可变全局,初值是常量指令(struct.new、array.new_fixed、ref.i31 等,被几处共用的对象另设不可变全局 $常号),测试、脚本的程序体所写的全局可变、初值为空引用;本基线该键未包含定义序号。生成器输出一个合并模块,不提供可任意拼接的独立模块 ABI。要换掉单个函数,必须保持表索引、类型和相应全局映射,或显式适配。
入口导出 _start。它在捕获 $error 的 try_table 里先把超过一万字节的字符串常量从 data 段拷入,再按收集顺序调用测试、脚本、黄金样例的程序体并丢弃结果,最后调用入口函数。顶层数据用到的字符串字面量写成常量全局,函数里用到的在第一次执行时从 data 段造出。未被接住的 $error 落到 _start,以异常字符串调用 $exception 全局里的顶层未捕获处理器(闭包);处理器返回则执行 unreachable,处理器为空时类型转换失败而陷入。字节常量按 \hh 生成,不能先假定是有效文本而改变原始字节。
字符串异常采用 $error 异常标签,携带一个 eq 引用(异常字符串)。抛出即 throw $error,模式缺支同样如此。尝试运行调用模块内函数 $try(操作, 处理):在 (try_table (catch $error …)) 里以 元 调用操作闭包,正常结束直接返回其结果;捕获后离开 try_table,以异常字符串尾调用处理闭包。两个闭包都按公开约定间接调用(类型 $t2,首参为闭包自身)。$exception 可变全局只保存顶层未捕获处理器,初值是标准库默认异常处理所在的全局,可经 豫言_设置当前异常处理器 更换。
提前返回采用 $escape 异常标签,携带两个 eq 引用(标识与值);每次动态入口新分配一个空 $tuple 作标识,按 ref.eq 比较,没有次数上限。捕获时比较标识,不相等则重抛;离开动态范围的续延恢复不受支持。两个标签都在模块内定义;$error 以 yy_错误 之名导出,宿主的导入实现可用它抛出能被尝试运行捕获的异常,载荷是本模块的 $bytes 错误消息(JS 宿主写 new WebAssembly.Exception(exports.yy_错误, [消息字节]));$escape 只在模块内部使用。普通算术、索引越界、类型 cast 失败的 Wasm trap 不是自动可捕获的语言字符串异常。
网五:宿主边界
客体经带类型的导入调用宿主,宿主经导出调用客体。
带类型的导入
。接口。豫 文件里的无体声明(构建时没有被适配替换的)编成带类型的 Wasm 函数导入。导入的模块名是声明所在包的短名,字段名是声明名,都不带版本;同一模块的(模块,字段)唯一,同一导入两次出现时签名必须相同。导入在函数编号里排在定义的函数之前,次序由编译器决定、每次相同,宿主按(模块,字段)绑定。一个接口模块只要被链进程序,它声明的全部函数都会成为导入;宿主没有实现的,给调用时才报“接口函数未绑定:模块.字段”的桩。
标准库的宿主服务声明在 库/豫章/东道。接口。豫 与 宿主带列。接口。豫(用到列的八个函数单独成文,免得模块成环),导入模块都是“标准库”,共六十八个函数;构建基础的工具服务声明在 库/营造/营东道。接口。豫,导入模块“构建基础”,六个函数(存放包上下文、面板可用性、绘制监视面板、当前程序 SHA-256、指定文件 SHA-256、字节串页散列 SHA-256)。标准库的公开函数改调这些声明,公开名与参数次序不变。编译器与豫构因此只导入这两个模块、不再导入 call,运行编译器的宿主都要提供它们;Node 工具宿主提供全部,只有读取终端按键、进入与退出终端原始输入模式、两个拼音转换与绘监视面板是桩。
类型检查按参数与结果的类型算出边界签名,写作“参数形,参数形→结果形”。参数里的“元”不写出,所以签名的参数与 Wasm 参数一一对应。形的文法:
签名 = [形 {"," 形}] "→" 果
果 = 形 | "异〔" 形 "〕"
形 = "整" | "小" | "爻" | "串" | "元" | "资"
| "组〔" [形 {"," 形}] "〕"
| "列〔" 形 "〕"
| "变〔" 支 {"|" 支} "〕"
支 = [形 {"," 形}]
| 豫言类型 | 形 | 顶层参数、结果的 Wasm 类型 | 嵌在元组里的表示 |
|---|---|---|---|
| 整数 | 整 | i64 |
规范化的 i31 或 $big(与客体 $box 相同) |
| 小数 | 小 | f64 |
$float |
| 爻 | 爻 | i32,只许 0、1 |
i31 的 0、1 |
| 字符串、字节串、字节缓冲 | 串 | (ref $bytes) |
$bytes |
| 有 | 元 | 参数省去;结果没有 | 空引用 |
| 接口文件里没有构造器的类型(资源) | 资 | (ref extern) |
$外 |
| 元组、命名元组(按字段次序) | 组〔…〕 | (ref $tuple) |
$tuple |
列 于 T |
列〔T〕 | (ref $tuple),元素逐项是 T 的嵌套表示 |
$tuple |
| 有构造器的类型 | 变〔支|…〕 | (ref $tuple):首项是支序(构造器在类型声明里的次序,从零起)的 i31,其后是载荷各项 |
$tuple |
异步返回 于 T(只作结果的最外层) |
异〔T 的形〕 | 与 T 相同 | — |
- 顶层引用一律非空,只有元组元素可以为空。
- 结果类型为
异步返回 于 T的导入(诺在标准库语言核心/异步原语。豫里声明),签名的结果记作“异〔T 的形〕”,Wasm 类型与转换都与 T 相同;“异”只作结果的最外层,出现在参数里或结果里层时类型检查报错。宿主可以在这类导入里挂起:JS 胶水只给签名带“异”、实现又标了“异步”的导入套 JSPI 的WebAssembly.Suspending,签名带“异”而实现同步的照常同步调用,签名不带“异”的导入返回 Promise 时报错;原生执行器与 Node 工具宿主照常阻塞完成。客体用「坐待」取值,「即得」「既而」在客体内组合;三者都在运行时擦除(「即得」「坐待」是类型转换,「既而」是一次调用)。应用接口暂不支持异步返回。 - 转换都在客体内完成:整数拆箱、装箱,小数取出、装进
$float,爻转 i31,资源拆出、装进$外(参数拆箱后先ref.as_non_null)。列、变体与含列变的组,由编译器按形去重生成的转换函数处理;只含整、小、爻、串、元、资的组原样直传,宿主只读,不得改写客体传来的元组,只往自己新分配的对象里写引用。 - 变体各支在客体里的专用结构体形由类型检查算出,只写进内部导入名(签名里变〔…〕后接 客〔形|形…〕,全是“任”时省去),转换函数据此把整、小字段装箱成边界上的整数、小数,回来时再拆箱;自定义段「豫言边界」里的签名不带它。
- 不能过边界的类型在类型检查时报错:函数(回调请用应用接口)、泛型函数与类型变量、除列以外的递归类型、引用与数组引用等非接口文件里没有构造器的类型、动态分类值。
自定义段「豫言边界」
提案 00002 第四步开发稿的列表身份检查:内建列应用按元素类型得到列边界形;旧多态列核对所有者“豫言”、包名“标准库”及包内 数据结构/多态列。豫 的列声明。包目录迁移保持该身份。此开发稿仍在验证,标准库别名迁移与内建列表运行时转换尚未完成。
每个 WasmGC 模块都带一个名为 豫言边界 的自定义段,内容是 UTF-8 文本,一行一项,每行以换行结尾,字段以制表符分隔:
导入⇥模块⇥字段⇥签名:每个带类型的导入恰有一行,次序与导入段相同;导出⇥导出名⇥签名:每个应用接口导出恰有一行,签名里已去掉类型为“有”的参数。
没有带类型的导入与导出时内容为空。读者遇到不认识的首字段,整行跳过。JS 宿主据此认出豫言程序,并按签名转换值;原生执行器可以不读,按 Wasm 类型核对。
应用接口导出
入口模块(编译顺序的最后一个)或任一链进程序的普通模块定义了某个 。应用接口。豫 模块所声明的同名顶层函数时(先查入口,再按编译顺序从后往前查其余模块),编译器把它编成导出;所以库也能提供应用接口函数(如云工东道库的回调入口「行云工回音」)。导出名为 包名/函数名:源码里定义「理来书」,导出名是 豫寓坐堂/理来书。导出函数的参数是各非“有”参数的顶层 Wasm 类型,结果是结果形的顶层类型(“有”则没有结果)。包装函数把参数转成客体值(“有”参数补空引用),有顶层直接调用的工函数时直接调用它,否则经全局槽里的闭包逐个参数调用,再把结果转回边界表示;未捕获的 $error 交给与 _start 相同的顶层处理器(打印“未捕捉的豫言异常”并以退出码 1 结束)。没有模块实现、程序却调用了的应用接口(豫言代码直接调用应用接口声明时也走这里),运行时抛出可捕获的豫言异常“应用接口函数没有由应用实现:包名/函数名”。
宿主先调 _start,需要时再调应用接口导出。_start 跑完程序体就调用入口函数,宿主不另调入口。
入口函数(提案 00006):入口文件不是 。测试。豫、。脚本。豫 或黄金样例 名其编号。豫 时,须在本文件定义入口函数(默认名「入口」,类型 化「有」而「有」;包描述「类型」的「入口函数」或编译器选项 --入口函数 名 可另指名字)。_start 先按依赖次序做各模块的静态初始化,把标准库「常处祸」的全局槽写进 $exception,再调用入口函数。包装函数 $入口调用 与应用接口导出的包装相同,不导出。缺少入口函数或其元数不是一时编译报错。测试、脚本与黄金样例的顶层就是程序体,_start 按次序运行它们,不调用入口函数。JS 宿主经胶水 读边界导出、造边界导出(JS 值按形转成 Wasm 值,结果转回 JS 值;异步时经 WebAssembly.promising)调用应用接口导出。
原型全局与值桥
模块向宿主暴露不可变的类型原型全局 yy_bytes、yy_tuple、yy_big、yy_float、yy_extern,另导出异常标签 yy_错误(见网四)。宿主从这些实例取得引擎实际的类型,构造返回引用;不能仅凭相同字段形状另造一个无法通过 cast 的类型。原型与宿主所用的引擎属于同一目标配置,未经验证不能跨版本混用。
JS 宿主经值桥模块(与编译器同代生成,类型定义与客体运行时逐字相同)读写嵌在元组里的值:kind 判别种类(空 0、小整数 1、字节串 2、元组 3、大整数 4、小数 5、资源 6),int/new_int、float/new_float、ext_get/new_ext、tuple_len/tuple_get/tuple_set/new_tuple、bytes_len/bytes_out/bytes_in 读写各种值,字节经值桥的线性内存交换。三家 JS 宿主共用胶水 豫言操作系统/宿主/网页汇编/边界.mjs:它读「豫言边界」段,按签名把 Wasm 值与 JS 值互转(整为 BigInt,小为 Number,爻为布尔,串为 Uint8Array,资为宿主对象本身,组与列为数组,变为 [支序, …载荷]),没有实现的导入给桩;只给标了“异步”的实现套 JSPI。各 JS 宿主提供标准库、构建基础的哪些函数,以及退出、标准错误、小数文字等的统一语义,见仓库的 豫言操作系统/宿主/标准库宿主.汉语.md。
编译器就地生成的原语
外部调用只由无体声明合成:宿主服务一律编成带类型导入(见上文),原语由编译器就地生成指令或调用客体内的运行时函数;代码生成遇到没有登记或没有生成规则的内建原语就报“未实现的内建原语”。
已在 guest 内实现的核心原语包括:引用及数组新建/读写、抛出异常与尝试运行、登记顶层未捕获处理器、不安全恒等转换、整数加减乘除/相等、串与字节拼接、串比较、字节长度、取字节与截取字节串、字节缓冲的新建、读写单字节与小端四字节、填充、复制与转字节串、若干 UTF-8/JSON 字符串操作,以及字节转字符串(只收一至二百五十五)、字节串单字节(收零至二百五十五;两者越界都陷阱)、字节串空与字符串转整数(语义见执行与编译的编四)。它们必须遵循对应参数次序,经「。内建。豫」内建声明使用(见类型与模块类六)。
小数加、减、乘、除与整数转小数、小数转整数也在 guest 内完成:编译器按名字就地生成 f64.add、f64.sub、f64.mul、f64.div、f64.convert_i64_s、i64.trunc_f64_s,参数从 $float 或整数拆箱,结果再装箱,语义见执行与编译的编四。宿主仍保留这六个名字的实现,供旧编译器编出的产物使用。超出 i31 的整数字面量与小数字面量各登记为一个可变 eq 引用全局 $n号,_start 在初始化字符串全局之后把它们各装箱一次存入,用处读全局;直接作上述运算或整数运算参数的字面量写成 i64.const、f64.const,不装箱。
第二批数值原语由编译器按代码生成里的原语登记表就地生成,声明名与粗形签名列在原语表里:整数取余(i64.rem_s);整数位与、位或、位异或(i64.and/or/xor)与位非(与 −1 异或);整数左移、右移、无符号右移、循环左移、循环右移(i64.shl/shr_s/shr_u/rotl/rotr);整数前导零数、末尾零数、置位数(i64.clz/ctz/popcnt);整数小等、大等、无符号小于、无符号大于(i64.le_s/ge_s/lt_u/gt_u);整数无符号除与无符号余(i64.div_u/rem_u);小数等、不等、小于、大于、小等、大等(f64.eq/ne/lt/gt/le/ge);小数最小、最大、复号(f64.min/max/copysign);小数绝对值、取负、向上取整、向下取整、截断、就近取整、开方(f64.abs/neg/ceil/floor/trunc/nearest/sqrt);小数饱和转整数(i64.trunc_sat_f64_s)、无符号整数转小数(f64.convert_i64_u)、小数位与位转小数(i64.reinterpret_f64、f64.reinterpret_i64)。参数照类别从整数或 $float 拆箱,字面量直接写常量;结果照类别装箱(整数调 $box,小数 struct.new $float,爻 ref.i31);比较的结果放进 i32 局部,作条件时不装箱。语义见执行与编译的编四。这一批只经「。内建。豫」声明使用,宿主没有对应实现。
网六:兼容声明
本篇根据 Wasm 后端源码说明目标契约。完整实现需独立通过数值边界、尾递归、异常动态范围、嵌零字节、宿主失败及引擎 GC 压力测试。