Porffor /ˈpɔrfɔr/ (poor-for)
一个从零开始的实验性 AOT 优化 JS/TS -> Wasm/C 引擎/编译器/运行时,用 JS 编写。研究项目,目前尚不适合严肃使用。
设计
Porffor 是一个非常独特的 JS 引擎,采用了许多完全不同的方法。它有严重的限制,但它能做的事情,做得相当不错。主要区别:
- 100% AOT 编译(无 JIT)
- 无常量运行时/前置代码
- 尽可能少的 Wasm 导入(仅 I/O)
Porffor 主要是从零开始构建的,唯一不是的部分是解析器(使用 Acorn)。不使用 Binaryen 等,我们自己制作最终的 wasm 二进制文件。你可以将其想象成编译一种是 JavaScript 的子集(某些功能不支持)和超集(新的/自定义 API)的语言。不基于任何特定的规范版本。
使用
不要期望一切都能正常工作!目前仅支持非常有限的 JS。请参见 bench
文件夹中的示例。
安装
npm install -g porffor@latest
。就是这么简单(希望如此):)
尝试 REPL
porf
。直接运行,不带脚本文件参数。
运行 JS 文件
porf path/to/script.js
编译为 Wasm
porf wasm path/to/script.js out.wasm
。目前它不使用像 WASI 这样的导入标准,所以单独使用基本上是不可用的。
编译为原生二进制文件
[!警告] 编译为原生二进制文件使用 2c,Porffor 自己的 Wasm -> C 编译器,这是实验性的。
porf native path/to/script.js out(.exe)
。你可以用 --compiler=clang|gcc|zig
指定编译器(默认为 clang
),用 --cO=Ofast|O3|O2|O1|O0
指定使用的优化级别(默认为 Ofast
)。输出的二进制文件默认也会被剥离。
编译为 C
[!警告] 编译为 C 使用 2c,Porffor 自己的 Wasm -> C 编译器,这是实验性的。
porf c path/to/script.js (out.c)
。当不包含输出文件时,将会打印到标准输出。
分析 JS 文件
[!警告] 非常实验性的进行中功能!
porf profile path/to/script.js
调试 JS 文件
[!警告] 非常实验性的进行中功能!
porf debug path/to/script.js
调试 JS 文件编译后的 Wasm
[!警告] 非常实验性的进行中功能!
porf debug-wasm path/to/script.js
选项
--parser=acorn|@babel/parser|meriyah|hermes-parser
(默认:acorn
)设置使用哪个解析器--parse-types
启用解析类型注解/typescript。如果未设置-parser
,默认更改为@babel/parser
。不进行类型检查--opt-types
使用类型注解作为编译器提示进行优化。不进行类型检查--valtype=i32|i64|f64
(默认:f64
)设置值类型-O0
禁用优化-O1
(默认)启用基本优化(简化指令,树摇 wasm 导入)-O2
启用高级优化(部分求值)。不稳定!
当前限制
- 有限的异步支持
- 作用域之间没有变量(除了参数和全局变量)
- 不支持
eval()
/Function()
等(因为是 AOT)
子引擎
Asur
Asur 是 Porffor 自己的 Wasm 引擎;它是一个用 JS 编写的有意简化的解释器。它还在开发中。更多详情请参见其自述文件。
Rhemyn
Rhemyn 是 Porffor 自己的正则表达式引擎;它将字面正则表达式 AOT 编译为 Wasm 字节码(想起什么了吗?)。它相当基础且还在开发中。更多详情请参见其自述文件。
2c
2c 是 Porffor 自己的 Wasm -> C 编译器,使用生成的 Wasm 字节码和内部信息来生成特定且高效/快速的 C 代码。很少的样板/前置代码或所需的外部文件,仅用于 CLI 二进制文件(与 wasm2c 非常不同)。
支持的功能
有关已实现/支持的优化,请参见优化部分。
提案
这些包括一些早期(阶段 1/0)和/或已停止(最后提交已多年)的提案,但我认为它们相当不错,所以。
Math
提案(阶段 1/0)
Math.clamp
提案:Math.clamp
(阶段 0 - 最后提交于 2023 年 4 月)Math
扩展提案:Math.scale
、Math.radians
、Math.degrees
、Math.RAD_PER_DEG
、Math.DEG_PER_RAD
(阶段 1 - 最后提交于 2020 年 9 月)Math.signbit
提案:Math.signbit
(阶段 1 - 最后提交于 2020 年 2 月)
语言
- 数字字面量
- 声明函数
- 调用函数
return
- 基本声明(
let
/const
/var
) - 一些基本的整数运算符(
+-/*%
) - 一些基本的整数位运算符(
&|
) - 相等运算符(
==
、!=
等) - 大于/小于运算符(
>
、<
、>=
等) - 一些一元运算符(
!
、+
、-
) - 逻辑运算符(
&&
、||
) - 一次声明多个变量(
let a, b = 0
) - 数组解构(
let [a, ...b] = foo
) - 全局变量(顶层作用域中的
var
/无修饰符) - 布尔值
if
和if ... else
- 匿名函数
- 使用变量设置函数(
const foo = function() { ... }
) - 箭头函数
undefined
/null
- 更新表达式(
a++
、++b
、c--
等) for
循环(for (let i = 0; i < N; i++)
等)- 基本对象(无原型)
console.log
while
循环break
和continue
- 命名导出函数
- IIFE支持
- 赋值运算符(
+=
、-=
、>>=
、&&=
等) - 条件/三元运算符(
cond ? a : b
) - 递归函数
- 裸返回(
return
) throw
(仅支持字面量,用于new Error
的hack)- 基本的
try { ... } catch { ... }
(不提供错误信息) - 调用参数不匹配的函数(如
f(a, b); f(0); f(1, 2, 3);
) typeof
- 未声明变量的运行时错误(
ReferenceError
),非函数的运行时错误(TypeError
) - 通过
[]
创建数组(如let arr = [ 1, 2, 3 ]
) - 通过
arr[ind]
访问数组成员(如arr[0]
) - 字符串字面量(
'hello world'
) - 通过
str[ind]
访问字符串成员(字符)(如str[0]
) - 字符串拼接(
+
)(如'a' + 'b'
) - 真值/假值(如
!'' == true
) - 字符串比较(如
'a' == 'a'
、'a' != 'b'
) - 空值合并运算符(
??
) for...of
(数组和字符串)for...in
- 数组成员赋值(
arr[0] = 2
、arr[0] += 2
等) - 数组构造函数(
Array(5)
、new Array(1, 2, 3)
) - 标签语句(
foo: while (...)
) do...while
循环- 可选参数(
(foo = 'bar') => { ... }
) - 剩余参数(
(...foo) => { ... }
) this
- 构造函数(
new Foo
) - 类(
class A {}
) - Await(
await promise
)
内置功能
NaN
和Infinity
isNaN()
和isFinite()
Number
的大部分功能(MAX_VALUE
、MIN_VALUE
、MAX_SAFE_INTEGER
、MIN_SAFE_INTEGER
、POSITIVE_INFINITY
、NEGATIVE_INFINITY
、EPSILON
、NaN
、isNaN
、isFinite
、isInteger
、isSafeInteger
)- 大多数
Math
函数(sqrt
、abs
、floor
、sign
、round
、trunc
、clz32
、fround
、random
、exp
、log
、log2
、log10
、pow
、expm1
、log1p
、sqrt
、cbrt
、hypot
、sin
、cos
、tan
、sinh
、cosh
、tanh
、asinh
、acosh
、atanh
、asin
、acos
、atan
、atan2
) - 基本的
globalThis
支持 - 基本的
Boolean
和Number
- 基本的
eval
用于字面量 - 使用自制的xorshift128+ PRNG实现
Math.random()
performance
的部分功能(now()
、timeOrigin
)Array.prototype
的大部分方法(at
、push
、pop
、shift
、fill
、slice
、indexOf
、lastIndexOf
、includes
、with
、reverse
、toReversed
、forEach
、filter
、map
、find
、findLast
、findIndex
、findLastIndex
、every
、some
、reduce
、reduceRight
、join
、toString
)Array
的大部分方法(of
、isArray
)String.prototype
的大部分方法(at
、charAt
、charCodeAt
、toUpperCase
、toLowerCase
、startsWith
、endsWith
、indexOf
、lastIndexOf
、includes
、padStart
、padEnd
、substring
、substr
、slice
、trimStart
、trimEnd
、trim
、toString
、big
、blink
、bold
、fixed
、italics
、small
、strike
、sub
、sup
、trimLeft
、trimRight
、trim
)crypto
的部分功能(randomUUID
)escape
btoa
Number.prototype
的大部分方法(toString
、toFixed
、toExponential
)parseInt
- 符合规范的
Date
- 正在开发的类型化数组(
Uint8Array
、Int32Array
等) - 同步
Promise
自定义功能
- 支持i32、i64和f64作为值类型
- 内部函数(见下文)
- 通过``asm`...```"宏"内联wasm
版本控制
Porffor使用独特的版本控制系统,示例如下:0.18.2+2aa3f0589
。让我们来分解一下:
0
- 主版本号,始终为0
,因为Porffor还未准备就绪18
- 次版本号,总Test262通过百分比(向下取整到最接近的整数)2
- 微版本号,该次版本的构建编号(每次发布/git推送时递增)2aa3f0589
- 提交哈希值
性能
对于大多数支持的功能,Porffor与大多数解释器和常见引擎(在不使用JIT的情况下)相比速度极快。对于那些使用JIT的引擎,默认情况下通常较慢,但可以通过编译器参数和类型化输入赶上,在编译为本机二进制文件时更是如此。
优化
主要用于减小体积。我不太关心编译器性能/时间,只要合理即可。我们不使用/依赖外部优化工具(如wasm-opt
等),而是在编译器内部进行优化,创建比wasm-opt
本身产生的代码体积更小的代码,因为我们拥有更多内部信息。
传统优化
- 内联函数(正在开发中,有限支持)
- 内联常量数学运算
- 尾调用(在
--tail-call
标志后面)
Wasm 转换
local.set
、local.get
->local.tee
i32.const 0
、i32.eq
->i32.eqz
i64.extend_i32_s
、i32.wrap_i64
-> ``f64.convert_i32_u
、i32.trunc_sat_f64_s
-> ``return
、end
->end
- 更改常量,转换为转换后的值类型的常量(例如
f64.const
、i32.trunc_sat_f64_s
->i32.const
) - 移除一些冗余的设置/获取操作
- 移除不需要的仅使用一次的变量
- 移除不需要的块(内部没有
br
指令) - 移除未使用的导入
- 使用数据段初始化数组/字符串
- (可能还有更多未记录的转换,待完成)
Wasm 模块
- 类型缓存/索引(无重复类型)
- 如果主函数为空则不包含(以及其他导出)
- 如果未使用或已优化掉,则不包含标签
Test262
Porffor 可以通过一些技巧/转换来运行 Test262,这些技巧/转换移除了不支持的特性,同时仍然执行相同的断言(例如使用仅包含字面量的简化错误消息)。目前通过率超过 14%(具体数据和详情请查看最新提交描述)。使用 node test262
进行测试,它还会显示上次提交和当前结果之间的整体差异。
代码库
-
compiler
:包含编译器本身2c.js
:porffor 的自定义 wasm-to-c 引擎allocators.js
:支持各种语言特性的静态和动态分配器assemble.js
:将 wasm 操作和元数据组装成 wasm 模块/文件builtins.js
:引擎的所有手动编写的内置功能(规范、自定义、变量、函数)builtins_object.js
:所有各种内置对象(如String
、globalThis
等)builtins_precompiled.js
:从builtins/
文件夹动态生成的内置功能codegen.js
:代码(wasm)生成,ast -> wasm。主要工作量在此cyclone.js
:wasm 部分常量求值器(快速且危险,因此称为"cyclone")decompile.js
:用于调试信息的基本 wasm 反编译器diagram.js
:生成 Mermaid 图表embedding.js
:嵌入常量的工具encoding.js
:将内容编码为 wasm 所需字节的工具expression.js
:将大多数运算符映射到操作码(高级运算符作为内置功能,如f64_%
)havoc.js
:wasm 重写库(它对 wasm 字节码造成破坏,因此称为"havoc")index.js
:执行所有编译器步骤,输入代码,输出 wasmopt.js
:自制的 wasm 字节码优化器parse.js
:简单封装 acorn 的解析器pgo.js
:基于概要引导的优化器precompile.js
:生成builtins_precompied.js
的工具prefs.js
:读取命令行参数的工具prototype.js
:一些内置原型函数types.js
:每个内置类型的定义wasmSpec.js
:来自 wasm 规范的"枚举"/信息wrap.js
:编译器的包装器,实例化并生成友好的导出
-
runner
:包含使用编译器运行 JS 的工具index.js
:主文件,您可能想使用这个info.js
:运行时打印额外信息repl.js
:基本的 repl(使用node:repl
)
-
rhemyn
:包含 Rhemyn - 我们的正则表达式引擎(由 Porffor 使用)compile.js
:将正则表达式 ast 编译为 wasm 字节码parse.js
:自己的正则表达式解析器
-
test
:包含许多测试文件,用于测试大多数支持的功能 -
test262
:test262 运行器和工具
用途
目前,Porffor 在功能和特性上有严重限制,但它有一些关键优势:
- 安全性。由于 Porffor 是用 JS 编写的,JS 是一种内存安全的语言*,并将 JS 编译为 Wasm,Wasm 是一个完全沙箱化的环境*,因此它相当安全。(* 这些依赖于底层实现的安全性。您也可以使用解释器而不是 JIT 来运行 Wasm,甚至是 Porffor 本身,以获得额外的安全性。)
- 将 JS 编译为本机二进制文件。这仍处于非常早期的阶段!
- 内联 Wasm,用于当您想在性能上超越编译器,或只是想要精细的功能时。
- SIMD 操作和其他低级概念的潜力。
- 未来可能会有更多用途?
待办事项
没有特定顺序,也不做保证,只是可能很快发生的事情™
- Asur
- 支持内存
- 支持异常
- 异常
- 在 catch 内重新抛出
- 优化
- 重写每个函数的局部索引,以获得最小的局部头部并删除未使用的索引
- 更智能的内联选择(快照?)
- 内存对齐
- 运行时
- WASI 目标
- 如果提供,运行预编译的 Wasm 文件
- 有趣的提案
- 文章
- 内联调查
- JS -> 原生
- 预编译 TS 内置函数
- Asur
escape()
优化- PGO
- 自托管测试?
VSCode 扩展
vscode-ext
中有一个 vscode 扩展,它调整了 JS 语法高亮,使其更适合 porffor 特性(例如,在内联汇编中高亮 wasm)。
使用的 Wasm 提案
Porffor 有意不使用尚未广泛实现的 Wasm 提案(例如 GC),以便它可以在尽可能多的地方使用。
- 多值返回 (必需)
- 非陷阱浮点数到整数的转换 (必需)
- 批量内存操作(可选,有时可以不使用)
- 异常处理(可选,仅用于错误)
- 尾调用(可选,默认关闭)
常见问题
1. 为什么叫这个名字?
威尔士语中的"紫色"是 porffor
。为什么选择紫色?
- 没有其他 JS 引擎使用紫色
- 紫色很酷
- 据说紫色代表"雄心",这是描述这个项目的一个词
2. 为什么要做这个项目?
是的!
3. 这与AssemblyScript或其他Wasm语言不是一样的吗?
不,它们在内部完全不同,并且有着非常不同的目标和理念:
- Porffor是作为一个通用的JS引擎开发的,并不专门针对Wasm
- Porffor主要处理JS
- Porffor是用纯JS编写的,并且能自我编译,不使用Binaryen等工具
- (而且说实话,我开始做这个项目时都不知道它的存在,哈哈)