# 概念

AssemblyScript 基本概念概述。

# 类 TypeScript

AssemblyScript 与 TypeScript 非常相似,具有很大程度上兼容的语法和语义。因此,从 TypeScript 中已知的许多概念也适用于 AssemblyScript,但并非所有 TypeScript 特性都与提前编译或 WebAssembly 目前支持的功能集相匹配。有些功能已被省略,有些尚未实现,而另外一些概念则需要添加,从技术上讲,使 AssemblyScript 部分成为子集,部分成为超集 - 一个变体。因此,现有的 TypeScript 代码不太可能由 AssemblyScript 编译器编译,但有可能将足够严格的代码移植到几乎没有努力的情况下。

支持的 TypeScript 特性详细概述可以在 实现状态 中找到。

# 严格类型化

虽然 TypeScript 有类型,但其类型系统能够描述 JavaScript 的许多动态特性。毕竟,TypeScript 是 JavaScript 之上的一个超集/类型检查器。相反,AssemblyScript 是在运行时静态提前编译的,这使得支持非常动态的 JavaScript 特性变得不可行,以避免进入解释器领域,或者需要更严格的类型检查来保证在运行时正确性,而 TypeScript 在这种情况下不会报错。

因此,没有 anyundefined

// 😢
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 {
}

对象必须是类型化的,例如使用 Mapclass

// 😢
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 覆盖 aborttraceseed 的相应实现,这里将对 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 { ... }
运算符 描述 说明
"++" 后缀递增 实例重载重新赋值
"--" 后缀递减 实例重载重新赋值

重载的后缀操作不会自动保留原始值。