# 概念

¥Concepts

基本 AssemblyScript 概念的概述。

¥An overview of basic AssemblyScript concepts.

# 类似 TypeScript

¥TypeScript-like

AssemblyScript 与 TypeScript 非常相似,具有很大程度上兼容的语法和语义。因此,TypeScript 中已知的许多概念也适用于 AssemblyScript,但并非所有 TypeScript 功能都能很好地映射到提前编译或 WebAssembly 迄今为止支持的功能集。一些功能已被省略,其他功能尚未实现,并且需要添加一些额外的概念,从技术上讲,使 AssemblyScript 部分成为子集,部分成为超集 - 一个变体。因此,AssemblyScript 编译器不太可能编译现有的 TypeScript 代码,但很可能可以轻松移植足够严格的代码。

¥AssemblyScript is very similar to TypeScript with largely compatible syntax and semantics. As such, many of the concepts known from TypeScript apply to AssemblyScript as well, but not all TypeScript features map well to ahead of time compilation or WebAssembly's so far supported feature set. Some features have been omitted, others not yet implemented, and a few additional concepts needed to be added, technically making AssemblyScript part a subset, part a superset - a variant. As such it is unlikely that existing TypeScript code can be compiled by the AssemblyScript compiler, but likely that sufficiently strict code can be ported with little effort.

实现情况 中提供了受支持的 TypeScript 功能的详细概述。

¥A detailed overview of supported TypeScript features is available within Implementation status.

# 严格打字

¥Strictly typed

虽然 TypeScript 有类型,但它的类型系统能够描述 JavaScript 的许多动态功能。毕竟,TypeScript 是 JavaScript 之上的超集/类型检查器。相反,AssemblyScript 是提前静态编译的,这使得支持非常动态的 JavaScript 功能不进入解释器字段是不可行的,分别需要更严格的类型检查来保证运行时的正确性,而 TypeScript 不会抗诉。

¥While TypeScript has types, its type system is able to describe many of JavaScript's dynamic features. TypeScript is a superset / a type checker on top of JavaScript after all. On the contrary, AssemblyScript is compiled statically ahead of time, making it infeasible to support very dynamic JavaScript features to not enter interpreter territory, respectively requires stricter type checking to guarantee correctness at runtime where TypeScript would not complain.

结果,没有 anyundefined

¥As a result, there is no any or undefined:

// 😢
function foo(a?) {
  var b = a + 1
  return b
}

// 😊
function foo(a: i32 = 0): i32 {
  var b = a + 1
  return b
}

目前还没有联合类型,但使用泛型可以实现类似的效果:

¥There are no union types yet, but a similar effect can be achieved with generics:

// 😢
function foo(a: i32 | string): void {
}

// 😊
function foo<T>(a: T): void {
}

对象必须键入,例如使用 Mapclass

¥Objects must be typed, say using a Map or 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 不会 (opens new window) diagnose (opens new window) 一个问题 (opens new window) 的健全性:

¥And nullability checks are limited to locals to guarantee soundness where TypeScript would not (opens new window) diagnose (opens new window) a problem (opens new window):

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
  }
}

# 沙盒执行

¥Sandboxed execution

WebAssembly 的独特功能之一是模块在未显式导入的情况下无法访问外部资源,默认情况下提供了强大的安全保证。因此,要使用 WebAssembly 做任何有用的事情,有必要将模块连接到主机环境,例如 JavaScript 和 DOM。

¥One of WebAssembly's unique features is that a module cannot access external resources without explicitly importing them, providing strong security guarantees by default. As such, to do anything useful with WebAssembly, it is necessary to wire a module to the host environment, like for example JavaScript and the DOM.

# 模块导入

¥Module imports

在 AssemblyScript 中,可以通过利用环境上下文(即使用 declare 语句)导入主机功能:

¥In AssemblyScript, host functionality can be imported by utilizing the ambient context, that is using a declare statement:

// assembly/env.ts
export declare function logInteger(i: i32): void
// assembly/index.ts
import { logInteger } from "./env"

logInteger(42)

AssemblyScript 文件中的环境声明将生成 WebAssembly 模块导入,使用文件的内部路径(不带文件扩展名)作为模块名称(此处:assembly/env),并将声明元素的名称作为模块元素(此处为 logInteger) 。在上面的示例中,可以通过在实例化时提供以下导入对象来完成导入:

¥Ambient declarations in an AssemblyScript file will yield a WebAssembly module import, using the internal path of the file, without file extension, as the module name (here: assembly/env), and the name of the declared element as the module element (here logInteger). In the example above, the import can be fulfilled by providing the following imports object upon instantiation:

WebAssembly.instantiateStreaming(fetch(...), {
  "assembly/env": {
    logInteger(i) { console.log("logInteger: " + i) }
  }
})

如有必要,还可以使用 @external 装饰器覆盖相应的外部模块和元素名称并相应地修改导入对象:

¥If necessary, the respective external module and element names can also be overridden using the @external decorator and modifying the imports object accordingly:

// assembly/index.ts
@external("log", "integer")
declare function logInteger(i: i32): void // { "log": { "integer"(i) { ... } } }

logInteger(42)

# 模块导出

¥Module exports

同样,入口文件中的 export 将生成 WebAssembly 模块导出:

¥Similarly, exports from an entry file will yield WebAssembly module exports:

// assembly/index.ts
export function add(a: i32, b: i32): i32 {
  return a + b
}

然后可以从主机环境调用模块导出:

¥Module exports can then be called from the host environment:

const { instance: { exports } } = await WebAssembly.instantiateStreaming(...)

console.log(exports.add(1, 2))

也可以看看:主机绑定 使大部分接线自动化。

¥See also: Host bindings automate much of the wiring.

# 特殊导入

¥Special imports

某些语言功能需要主机环境的支持才能发挥作用,从而根据模块中使用的功能集生成一些特殊的模块导入。生成的绑定会在必要时自动提供这些。

¥Some language features need support from the host environment to function, yielding a few special module imports depending on the feature set used within the module. Generated bindings provide these automatically where necessary.

  • function env.abort?(message: usize, fileName: usize, line: u32, column: u32): void
    

    调用不可恢复的错误。通常在启用断言或抛出错误时出现。

    ¥Called on unrecoverable errors. Typically present if assertions are enabled or errors are thrown.

  • function env.trace?(message: usize, n: i32, a0..4?: f64): void
    

    当用户代码中调用 trace 时调用。仅当存在时才存在。

    ¥Called when trace is called in user code. Only present if it is.

  • function env.seed?(): f64
    

    当随机数生成器需要播种时调用。仅当存在时才存在。

    ¥Called when the random number generator needs to be seeded. Only present if it is.

aborttraceseed 的各自实现可以被覆盖,例如 --use abort=assembly/index/myAbort,这里将对 abort 的调用重定向到 assembly/index.ts 中的自定义 myAbort 函数。如果环境不提供兼容的实现,或者不需要相应的导入并且自定义实现就足够了,则非常有用。

¥The respective implementations of abort, trace and seed can be overridden with, for example, --use abort=assembly/index/myAbort, here redirecting calls to abort to a custom myAbort function in assembly/index.ts. Useful if an environment does not provide compatible implementations, or when the respective imports are not desired and custom implementations are sufficient.

# 实例化期间访问内存

¥Accessing memory during instantiation

需要注意的一个重要边缘情况是,默认情况下,顶层语句作为 WebAssembly 模块隐式 (start ...) 函数的一部分执行,当顶层语句已经调用需要调用的外部功能时,这会导致先有鸡还是先有蛋的问题。 访问模块的内存实例(例如,读取记录字符串的内容)。由于实例化尚未完成,模块的导出(包括导出的内存)尚不可用,访问将失败。

¥One important edge case to be aware of is that top-level statements are executed as part of the WebAssembly module's implicit (start ...) function by default, which leads to a chicken and egg problem when top-level statements already call out to external functionality that needs to access the module's memory instance (say, reading the contents of a logged string). Since instantiation did not yet complete, the module's exports, including exported memory, are not available yet and the access will fail.

解决方案是利用 --exportStart 命令行选项强制导出启动函数,而不是使其隐式。然后,在执行任何代码之前,实例化首先返回。但请注意,在任何其他导出之前,必须始终首先调用导出的启动函数,否则将发生未定义的行为。

¥A solution is to utilize the --exportStart command line option to force exporting the start function instead of making it implicit. Then, instantiation first returns before any code is executed. Note, however, that the exported start function must always be called first, before any other exports, or undefined behavior will occur.

# 摇树优化

¥Tree-shaking

AssemblyScript 不会线性编译模块,而是从模块的导出开始,仅编译可从它们访问的内容,通常称为 tree-shaking (opens new window)。因此,死代码总是在语法上进行验证,但不一定检查语义正确性。虽然这种机制显着有助于减少编译时间,并且对于那些熟悉执行 JavaScript 的人来说几乎是自然的,但它最初可能会感觉有点奇怪,不仅对于那些具有传统编译器背景的人来说,例如,因为触发的诊断不是线性发生的,而且也不是线性发生的。 对于那些有 TypeScript 背景的人来说,因为即使类型注释也作为死代码的一部分而未被检查。该规则的例外是顶层代码,包括顶层变量声明及其初始值设定项,必须在相应文件首次执行时立即对其进行求值。

¥AssemblyScript does not compile a module linearly, but starts at the module's exports and only compiles what's reachable from them, often referred to as tree-shaking (opens new window). As such, dead code is always validated syntactically, but not necessarily checked for semantic correctness. While this mechanism significantly helps to reduce compile times and feels almost natural to those familiar with executing JavaScript, it may initially feel a little strange not only to those with a background in traditional compilers, for example because emitted diagnostics do not happen linearly, but also to those with a background in TypeScript, because even type annotations remain unchecked as part of dead code. The exception to the rule is top-level code, including top-level variable declarations and their initializers, that must be evaluated as soon as the respective file would first execute.

# 分支级树摇动

¥Branch-level tree-shaking

除了模块级树摇动之外,编译器还会忽略它可以证明不会被采用的分支。使用常量、编译为常量的内置函数、可预先计算为常量的表达式以及以下全局变量来检测特定的编译器标志或功能:

¥In addition to module-level tree-shaking, the compiler ignores branches that it can prove won't be taken. Works with constants, built-ins that compile to a constant, expressions that can be precomputed to a constant, plus the following globals to detect specific compiler flags or features:

  • const ASC_TARGET: i32
    

    表示编译目标。可能的值为 0 = JS(可移植)、1 = WASM32、2 = WASM64。

    ¥Indicates the compilation target. Possible values are 0 = JS (portable), 1 = WASM32, 2 = WASM64.

  • const ASC_NO_ASSERT: bool
    

    --noAssert 是否已设置。

    ¥Whether --noAssert has been set.

  • const ASC_MEMORY_BASE: usize
    

    --memoryBase 的值。

    ¥The value of --memoryBase.

  • const ASC_OPTIMIZE_LEVEL: i32
    

    --optimizeLevel 的值。可能的值为 0、1、2 和 3。

    ¥The value of --optimizeLevel. Possible values are 0, 1, 2 and 3.

  • const ASC_SHRINK_LEVEL: i32
    

    --shrinkLevel 的值。可能的值为 0、1 和 2。

    ¥The value of --shrinkLevel. Possible values are 0, 1 and 2.

  • const ASC_LOW_MEMORY_LIMIT: i32
    

    --lowMemoryLimit 的值。

    ¥The value of --lowMemoryLimit.

  • const ASC_EXPORT_RUNTIME: i32
    

    --exportRuntime 是否已设置。

    ¥Whether --exportRuntime has been set.

  • 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
    

    是否启用相应的功能。

    ¥Whether the respective feature is enabled.

    例如,如果一个库支持 SIMD,但还希望在不支持 SIMD 的情况下进行编译时提供后备:

    ¥For example, if a library supports SIMD but also wants to provide a fallback when being compiled without SIMD support:

    if (ASC_FEATURE_SIMD) {
      // compute with SIMD operations
    } else {
      // fallback without SIMD operations
    }
    

# 代码注释

¥Code annotations

装饰器的工作方式更像是 AssemblyScript 中的编译器注释,并在编译时进行评估。

¥Decorators work more like compiler annotations in AssemblyScript and are evaluated at compile time.

注解 描述
@inline 请求内联常量或函数。
@final 将一个类注释为 final,即它不能被子类化。
@unmanaged 将类注释为不被 GC 跟踪,从而有效地成为类似 C 的结构。
@external 更改导入元素的外部名称。@external(module, name) 同时更改模块和元素名称,@external(name) 仅更改元素名称。

自定义装饰器将被忽略,除非使用 transform

¥Custom decorators are ignored, unless given meaning by using a transform.

还有一些内置装饰器可能会随着时间的推移而发生显着变化,甚至可能被完全删除。虽然目前对于标准库实现很有用,但不建议在自定义代码中使用它们,因为工具无法识别它们。

¥There are a few more built-in decorators that are likely going to change significantly over time, or may even be removed entirely. While useful for standard library implementation currently, it is not recommend to utilize them in custom code since tooling does not recognize them.

Show me anyway
注解 描述
@global 将元素注册为全局范围的一部分。
@lazy 请求变量的延迟编译。有助于避免不必要的全局变量。
@operator 将方法注释为二元运算符重载。见下文。
@operator.binary @operator 的别名。
@operator.prefix 将方法注释为一元前缀运算符重载。
@operator.postfix 将方法注释为一元后缀运算符重载。

# 二元运算符重载

¥Binary operator overloads

@operator(OP)
static __op(left: T, right :T): T { ... }

@operator(OP)
__op(right: T): T  { ... }
OP 描述
"[]" 检查索引获取
"[]=" 检查索引集
"{}" 未检查的索引获取
"{}=" 未检查的索引集
"==" 平等(也适用于 ===
"!=" 不平等(也适用于 !==
">" 比...更棒
">=" 大于或等于
"<" 少于
"<=" 小于或等于
">>" 算术右移
">>>" 逻辑右移
"<<" 左移
"&" 按位与
`" "` 按位或
"^" 按位异或
"+" 添加
"-" 减法
"*" 乘法
"/" 分配
"**" 求幂
"%"

# 一元运算符重载

¥Unary operator overloads

@operator.prefix(OP)
static __op(self: T): T { ... }

@operator.prefix(OP)
__op(): T { ... }
OP 描述 注意
"!" 逻辑非
"~" 按位非
"+" 一元加
"-" 一元否定
"++" 前缀增量 实例过载重新分配
"--" 前缀减量 实例过载重新分配

请注意,递增和递减重载的语义可能略有不同。如果重载被声明为实例方法,则在 ++a 上,编译器会触发将结果值重新分配给 a 的代码,而如果重载被声明为静态,则重载的行为与任何其他重载类似,跳过其他隐式赋值。

¥Note that increment and decrement overloads can have slightly different semantics. If the overload is declared as an instance method, on ++a the compiler does emit code that reassigns the resulting value to a while if the overload is declared static, the overload behaves like any other overload, skipping the otherwise implicit assignment.

# 一元后缀运算

¥Unary postfix operations

@operator.postfix(OP)
static __op(self: T): T { ... }

@operator.postfix(OP)
__op(): T { ... }
OP 描述 注意
"++" 后缀增量 实例过载重新分配
"--" 后缀递减 实例过载重新分配

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

¥Overloaded postfix operations do not preserve the original value automatically.