# Concepts
An overview of basic AssemblyScript concepts.
# TypeScript-like
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.
A detailed overview of supported TypeScript features is available within Implementation status.
# Strictly typed
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.
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 {
}
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")
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
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
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)
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) }
}
})
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
Similarly, export
s 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
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.
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
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.
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 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
Indicates the compilation target. Possible values are 0 = JS (portable), 1 = WASM32, 2 = WASM64.
const ASC_NO_ASSERT: bool
Whether
--noAssert
has been set.const ASC_MEMORY_BASE: usize
The value of
--memoryBase
.const ASC_OPTIMIZE_LEVEL: i32
The value of
--optimizeLevel
. Possible values are 0, 1, 2 and 3.const ASC_SHRINK_LEVEL: i32
The value of
--shrinkLevel
. Possible values are 0, 1 and 2.const ASC_LOW_MEMORY_LIMIT: i32
The value of
--lowMemoryLimit
.const ASC_EXPORT_RUNTIME: i32
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.
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
Decorators work more like compiler annotations in AssemblyScript and are evaluated at compile time.
Annotation | Description |
---|---|
@inline | Requests inlining of a constant or function. |
@final | Annotates a class as final, that is it cannot be subclassed. |
@unmanaged | Annotates a class as not tracked by GC, effectively becoming a C-like struct. |
@external | Changes the external name of an imported element. @external(module, name) changes both the module and element name, @external(name) changes the element name only. |
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
Annotation | Description |
---|---|
@global | Registers an element to be part of the global scope. |
@lazy | Requests lazy compilation of a variable. Useful to avoid unnecessary globals. |
@operator | Annotates a method as a binary operator overload. See below. |
@operator.binary | Alias of @operator . |
@operator.prefix | Annotates a method as a unary prefix operator overload. |
@operator.postfix | Annotates a method as a unary postfix operator overload. |
# Binary operator overloads
@operator(OP)
static __op(left: T, right :T): T { ... }
@operator(OP)
__op(right: T): T { ... }
OP | Description |
---|---|
"[]" | Checked indexed get |
"[]=" | Checked indexed set |
"{}" | Unchecked indexed get |
"{}=" | Unchecked indexed set |
"==" | Equality (also applies on === ) |
"!=" | Inequality (also applies on !== ) |
">" | Greater than |
">=" | Greater than or equal |
"<" | Less than |
"<=" | Less than or equal |
">>" | Arithmetic right shift |
">>>" | Logical right shift |
"<<" | Left shift |
"&" | Bitwise AND |
"|" | Bitwise OR |
"^" | Bitwise XOR |
"+" | Addition |
"-" | Subtraction |
"*" | Multiplication |
"/" | Division |
"**" | Exponentiation |
"%" | Remainder |
# Unary operator overloads
@operator.prefix(OP)
static __op(self: T): T { ... }
@operator.prefix(OP)
__op(): T { ... }
OP | Description | Notes |
---|---|---|
"!" | Logical NOT | |
"~" | Bitwise NOT | |
"+" | Unary plus | |
"-" | Unary negation | |
"++" | Prefix increment | Instance overload reassigns |
"--" | Prefix decrement | Instance overload reassigns |
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 | Description | Notes |
---|---|---|
"++" | Postfix increment | Instance overload reassigns |
"--" | Postfix decrement | Instance overload reassigns |
Overloaded postfix operations do not preserve the original value automatically.