# 概念
AssemblyScript 基本概念概述。
# 类 TypeScript
AssemblyScript 与 TypeScript 非常相似,具有很大程度上兼容的语法和语义。因此,从 TypeScript 中已知的许多概念也适用于 AssemblyScript,但并非所有 TypeScript 特性都与提前编译或 WebAssembly 目前支持的功能集相匹配。有些功能已被省略,有些尚未实现,而另外一些概念则需要添加,从技术上讲,使 AssemblyScript 部分成为子集,部分成为超集 - 一个变体。因此,现有的 TypeScript 代码不太可能由 AssemblyScript 编译器编译,但有可能将足够严格的代码移植到几乎没有努力的情况下。
支持的 TypeScript 特性详细概述可以在 实现状态 中找到。
# 严格类型化
虽然 TypeScript 有类型,但其类型系统能够描述 JavaScript 的许多动态特性。毕竟,TypeScript 是 JavaScript 之上的一个超集/类型检查器。相反,AssemblyScript 是在运行时静态提前编译的,这使得支持非常动态的 JavaScript 特性变得不可行,以避免进入解释器领域,或者需要更严格的类型检查来保证在运行时正确性,而 TypeScript 在这种情况下不会报错。
因此,没有 any
或 undefined
// 😢
function foo(a?) {
var b = a + 1
return b
}
// 😊
function foo(a: i32 = 0): i32 {
var b = a + 1
return b
}
目前还没有联合类型,但可以使用泛型实现类似的效果
// 😢
function foo(a: i32 | string): void {
}
// 😊
function foo<T>(a: T): void {
}
对象必须是类型化的,例如使用 Map
或 class
// 😢
var a = {}
a.prop = "hello world"
// 😊
var a = new Map<string,string>()
a.set("prop", "hello world")
// 😊
class A {
constructor(public prop: string) {}
}
var a = new A("hello world")
并且空值检查仅限于局部变量,以确保在 TypeScript 不会 (在新窗口中打开) 诊断 (在新窗口中打开) 一个问题 (在新窗口中打开)
function doSomething(foo: Foo): void {
// 😢
if (foo.something) {
foo.something.length // fails
}
}
function doSomething(foo: Foo): void {
// 😊
var something = foo.something
if (something) {
something.length // works
}
}
# 沙盒执行
WebAssembly 的一个独特功能是,模块不能在没有显式导入的情况下访问外部资源,默认情况下提供了强大的安全保证。因此,要使用 WebAssembly 做一些有用的事情,必须将模块连接到宿主环境,例如 JavaScript 和 DOM。
# 模块导入
在 AssemblyScript 中,可以使用环境上下文导入宿主功能,即使用 declare
语句
// assembly/env.ts
export declare function logInteger(i: i32): void
// assembly/index.ts
import { logInteger } from "./env"
logInteger(42)
AssemblyScript 文件中的环境声明将产生一个 WebAssembly 模块导入,使用文件的内部路径(不带文件扩展名)作为模块名称(此处:assembly/env
),以及声明元素的名称作为模块元素(此处为 logInteger
)。在上面的示例中,可以通过在实例化时提供以下导入对象来满足导入
WebAssembly.instantiateStreaming(fetch(...), {
"assembly/env": {
logInteger(i) { console.log("logInteger: " + i) }
}
})
如果需要,可以使用 @external
装饰器覆盖相应的外部模块和元素名称,并相应地修改导入对象
// assembly/index.ts
@external("log", "integer")
declare function logInteger(i: i32): void // { "log": { "integer"(i) { ... } } }
logInteger(42)
# 模块导出
类似地,入口文件的 export
将产生 WebAssembly 模块导出
// assembly/index.ts
export function add(a: i32, b: i32): i32 {
return a + b
}
然后可以从宿主环境调用模块导出
const { instance: { exports } } = await WebAssembly.instantiateStreaming(...)
console.log(exports.add(1, 2))
另请参阅:宿主绑定 自动执行了大部分连接工作。
# 特殊导入
某些语言特性需要宿主环境的支持才能正常工作,这会产生一些特殊的模块导入,具体取决于模块中使用的特性集。生成的绑定会在必要时自动提供这些导入。
function env.abort?(message: usize, fileName: usize, line: u32, column: u32): void
在不可恢复的错误时调用。通常在启用断言或抛出错误时出现。
function env.trace?(message: usize, n: i32, a0..4?: f64): void
在用户代码中调用
trace
时调用。仅当调用它时才会出现。function env.seed?(): f64
在随机数生成器需要播种时调用。仅当使用它时才会出现。
可以使用 --use abort=assembly/index/myAbort
覆盖 abort
、trace
和 seed
的相应实现,这里将对 abort
的调用重定向到 assembly/index.ts
中的自定义 myAbort
函数。如果环境没有提供兼容的实现,或者当不需要相应的导入而自定义实现就足够时,这很有用。
# 在实例化期间访问内存
需要注意的一个重要边缘情况是,顶级语句默认情况下作为 WebAssembly 模块的隐式 (start ...)
函数的一部分执行,这会导致在顶级语句已经调用需要访问模块内存实例的外部功能(例如,读取已记录字符串的内容)时出现循环依赖问题。由于实例化尚未完成,因此模块的导出,包括导出的内存,还没有可用,访问将失败。
一个解决方案是使用 --exportStart
命令行选项强制导出启动函数,而不是将其设为隐式函数。然后,实例化将在任何代码执行之前返回。但是请注意,导出的启动函数必须始终先于任何其他导出调用,否则将会发生未定义的行为。
# 树状摇晃
AssemblyScript 不会线性编译模块,而是从模块的导出开始,只编译从导出可到达的部分,这通常被称为 树状摇晃 (在新窗口中打开)。因此,死代码总是会进行语法验证,但不一定检查语义正确性。虽然这种机制有助于显着缩短编译时间,对于熟悉执行 JavaScript 的人来说几乎感觉很自然,但它最初可能会让人感到有些奇怪,不仅对于那些具有传统编译器背景的人来说,例如因为发出的诊断不是线性发生的,而且对于那些具有 TypeScript 背景的人来说,因为即使类型注释仍然被视为死代码的一部分而没有进行检查。一个例外是顶级代码,包括顶级变量声明及其初始化器,这些代码必须在相应的代码第一次执行时进行评估。
# 分支级树状摇晃
除了模块级树状摇晃之外,编译器还会忽略它可以证明不会执行的分支。适用于常量、编译为常量的内置函数、可以预先计算为常量的表达式,以及以下全局变量以检测特定的编译器标志或特性
const ASC_TARGET: i32
指示编译目标。可能的值为 0 = JS(可移植)、1 = WASM32、2 = WASM64。
const ASC_NO_ASSERT: bool
是否已设置
--noAssert
。const ASC_MEMORY_BASE: usize
--memoryBase
的值。const ASC_OPTIMIZE_LEVEL: i32
--optimizeLevel
的值。可能的取值是 0、1、2 和 3。const ASC_SHRINK_LEVEL: i32
--shrinkLevel
的值。可能的取值是 0、1 和 2。const ASC_LOW_MEMORY_LIMIT: i32
--lowMemoryLimit
的值。const ASC_EXPORT_RUNTIME: i32
是否设置了
--exportRuntime
。const ASC_FEATURE_SIGN_EXTENSION: bool const ASC_FEATURE_MUTABLE_GLOBALS: bool const ASC_FEATURE_NONTRAPPING_F2I: bool const ASC_FEATURE_BULK_MEMORY: bool const ASC_FEATURE_SIMD: bool const ASC_FEATURE_THREADS: bool const ASC_FEATURE_EXCEPTION_HANDLING: bool const ASC_FEATURE_TAIL_CALLS: bool const ASC_FEATURE_REFERENCE_TYPES: bool const ASC_FEATURE_MULTI_VALUE: bool const ASC_FEATURE_GC: bool const ASC_FEATURE_MEMORY64: bool
相应的功能是否启用。
例如,如果一个库支持 SIMD,但也希望在没有 SIMD 支持的情况下编译时提供一个回退。
if (ASC_FEATURE_SIMD) { // compute with SIMD operations } else { // fallback without SIMD operations }
# 代码注解
在 AssemblyScript 中,装饰器更像是编译器注解,在编译时进行评估。
注解 | 描述 |
---|---|
@inline | 请求内联常量或函数。 |
@final | 将一个类注解为 final,即它不能被子类化。 |
@unmanaged | 将一个类注解为不被 GC 跟踪,有效地成为一个类似 C 的结构体。 |
@external | 更改导入元素的外部名称。@external(module, name) 更改模块和元素名称,@external(name) 仅更改元素名称。 |
自定义装饰器会被忽略,除非使用 转换 赋予其意义。
还有一些其他内置装饰器,它们可能会随着时间的推移而发生重大变化,甚至可能完全被移除。虽然它们目前对标准库实现很有用,但不建议在自定义代码中使用它们,因为工具无法识别它们。
无论如何显示给我看
注解 | 描述 |
---|---|
@global | 注册一个元素作为全局范围的一部分。 |
@lazy | 请求变量的延迟编译。有助于避免不必要的全局变量。 |
@operator | 将一个方法注解为二元运算符重载。见下文。 |
@operator.binary | @operator 的别名。 |
@operator.prefix | 将一个方法注解为一元前缀运算符重载。 |
@operator.postfix | 将一个方法注解为一元后缀运算符重载。 |
# 二元运算符重载
@operator(OP)
static __op(left: T, right :T): T { ... }
@operator(OP)
__op(right: T): T { ... }
运算符 | 描述 |
---|---|
"[]" | 带检查的索引获取 |
"[]=" | 带检查的索引设置 |
"{}" | 无检查的索引获取 |
"{}=" | 无检查的索引设置 |
"==" | 相等(也适用于 === ) |
"!=" | 不相等(也适用于 !== ) |
">" | 大于 |
">=" | 大于或等于 |
"<" | 小于 |
"<=" | 小于或等于 |
">>" | 算术右移 |
">>>" | 逻辑右移 |
"<<" | 左移 |
"&" | 按位与 |
"|" | 按位或 |
"^" | 按位异或 |
"+" | 加法 |
"-" | 减法 |
"*" | 乘法 |
"/" | 除法 |
"**" | 指数 |
"%" | 余数 |
# 一元运算符重载
@operator.prefix(OP)
static __op(self: T): T { ... }
@operator.prefix(OP)
__op(): T { ... }
运算符 | 描述 | 说明 |
---|---|---|
"!" | 逻辑非 | |
"~" | 按位非 | |
"+" | 一元正 | |
"-" | 一元负 | |
"++" | 前缀递增 | 实例重载重新赋值 |
"--" | 前缀递减 | 实例重载重新赋值 |
请注意,递增和递减重载可能具有略微不同的语义。如果重载声明为实例方法,则在 ++a
上,编译器会发出重新将结果值赋值给 a
的代码,而如果重载声明为静态方法,则重载的行为就像任何其他重载一样,跳过否则隐式的赋值。
# 一元后缀操作
@operator.postfix(OP)
static __op(self: T): T { ... }
@operator.postfix(OP)
__op(): T { ... }
运算符 | 描述 | 说明 |
---|---|---|
"++" | 后缀递增 | 实例重载重新赋值 |
"--" | 后缀递减 | 实例重载重新赋值 |
重载的后缀操作不会自动保留原始值。