Using the compiler | The AssemblyScript Book
Similar to TypeScript's tsc transpiling to JavaScript, AssemblyScript's asc compiles to WebAssembly.
# Compiler options
The compiler supports various options available on the command line, in a configuration file and programmatically. On the command line, it looks like this:
# Entry file(s)
Non-option arguments are treated as the names of entry files. A single program can have multiple entries, with the exports of each entry becoming the exports of the WebAssembly module. Exports of imported files that are not entry files do not become WebAssembly module exports.
# General
# Optimization
The compiler can optimize for both speed (-Ospeed) and size (-Osize), as well as produce a debug build (--debug).
Optimization levels can also be tweaked manually: --optimizeLevel (0-3) indicates how much the compiler focuses on optimizing the code with --shrinkLevel (0-2, 1=s, 2=z) indicating how much it focuses on keeping the size low during code generation and while optimizing. A shorthand for both is -O[optimizeLevel][shrinkLevel] , with shrink level indicated by optionally appending the letter s (1) or z (2).
# Output
Typical output formats are WebAssembly binary (.wasm, --outFile) and/or text format (.wat, --textFile). Often, both are used in tandem to run and also inspect generated code.
# Debugging
For easier debugging during development, a source map can be emitted alongside the WebAssembly binary, and debug symbols can be embedded:
# Features
There are several flags that enable or disable specific WebAssembly or compiler features. By default, only the bare minimum is exposed, and fully standardized WebAssembly features will be used.
# Linking
Specifying the base offsets of compiler-generated memory respectively the table leaves some space for other data in front. In its current form this is mostly useful to link additional data into an AssemblyScript binary after compilation, be it by populating the binary itself or initializing the data externally upon initialization. One good example is leaving some scratch space for a frame buffer.
# API
To integrate with the compiler, for example to post-process the AST, one or multiple custom transforms can be specified.
# Other
Other options include those forwarded to Binaryen and various flags useful in certain situations.
# Binaryen
# And the kitchen sink
# Configuration file
Instead of providing the options outlined above on the command line, a configuration file typically named asconfig.json can be used. It may look like in the following example, excluding comments:
Per-target options, e.g. targets.release, add to and override top-level options. Options provided on the command line override any options in the configuration file. Usage is, for example:
# Programmatic usage
The compiler API can also be used programmatically:
The compiler runs in browsers as well. The simplest way to set it up is to include the generated web.js (opens new window) so the compiler can be used with an import on the Web:
Here, x.x.x must be replaced with the respective version to use (opens new window), or latest to always use the latest version (not recommended in production). By default, the script installs the necessary import map (opens new window) and, for browsers that do not yet support import maps, an import map shim (opens new window). It also accepts the following options in case there is a need to only perform part of the setup:
| Script URL | Effect |
|---|---|
web.js?noinstall | Does not install the import map. |
web.js?noshim | Does not install the import map shim. |
web.js?noinstall,noshim | Does not install the import map or shim. |
Regardless of the options used, the script always declares the following global variables:
| Variable | Description |
|---|---|
ASSEMBLYSCRIPT_VERSION | Version string of the compiler release used |
ASSEMBLYSCRIPT_IMPORTMAP | The respective import map of the release as JSON |
# Host bindings
WebAssembly alone cannot yet transfer higher level data types like strings, arrays and objects over module boundaries, so for now some amount of glue code is required to exchange these data structures with the host / JavaScript.
The compiler can generate the necessary bindings using the --bindings command line option (either as an ES module or a raw instantiate function), enabling exchange of:
| Type | Strategy | Description |
|---|---|---|
| Number | By value | Basic numeric types except 64-bit integers. |
| BigInt | By value | 64-bit integers via js-bigint-integration. |
| Boolean | By value | Coerced to true or false. |
| Externref | By reference | Using reference-types. |
| String | Copy | |
| ArrayBuffer | Copy | |
| TypedArray | Copy | Any Int8Array, Float64Array etc. |
| Array | Copy | Any Array<T> |
| StaticArray | Copy | Any StaticArray<T> |
| Object | Copy | If a plain object. That is: Has no constructor or non-public fields. |
| Object | By reference | If not a plain object. Passed as an opaque reference counted pointer. |
Note the two different strategies used for Object: In some situations, say when calling a Web API, it may be preferable to copy the object as a whole, field by field, which is the strategy chosen for plain objects with no constructor or non-public fields:
However, copying may not be desirable in every situation, say when individual object properties are meant to be modified externally where serializing/deserializing the object as a whole would result in unnecessary overhead. To support this use case, the compiler can pass just an opaque reference to the object, which can be enforced by providing an empty constructor (not a plain object anymore):
Also note that exporting an entire class has no effect at the module boundary (yet), and it is instead recommended to expose only the needed functionality as shown in the example above. Supported elements at the boundary are globals, functions and enums.
# Using ESM bindings
Bindings generated with --bindings esm perform all the steps from compilation over instantiation to exporting the final interface. To do so, a few assumptions had to be made:
The WebAssembly binary is located next to the JavaScript bindings file using the same name but with a
.wasmextension.JavaScript globals in
globalThiscan be accessed directly via theenvmodule namespace. For example,console.logcan be manually imported through:Note that this is just an example and
console.logis already provided by the standard library when called from an AssemblyScript file. Other global functions not already provided by the standard library may require an import as of this example, though.Imports from other namespaces than
env, i.e.(import "module" "name"), become animport { name } from "module"within the binding. Importing a custom function from a JavaScript file next to the bindings file can be achieved through:Similarly, importing a custom function from, say, a Node.js dependency can be achieved through:
These assumptions cannot be intercepted or customized since, to provide static ESM exports from the bindings file directly, instantiation must start immediately when the bindings file is imported. If customization is required, --bindings raw can be used instead.
# Using raw bindings
The signature of the single instantiate function exported by --bindings raw is:
Note that the function does not make any assumptions on how the module is to be compiled, but instead expects a readily compiled WebAssembly.Module as in this example:
Unlike --bindings esm, raw bindings also do not make any assumptions on how imports are resolved, so these must be provided manually as part of the imports object. For example, to achieve a similar result as with ESM bindings, but now customizable:
# Debugging
The debugging workflow is similar to debugging JavaScript since both Wasm and JS execute in the same engine, and the compiler provides various options to set up additional WebAssembly-specific debug information. Note that any sort of optimization should be disabled in debug builds.
# Debug symbols
When compiling with the --debug option, the compiler appends a name section to the binary, containing names of functions, globals, locals and so on. These names will show up in stack traces.
# Source maps
The compiler can generate a source map alongside a binary using the --sourceMap option. By default, a relative source map path will be embedded in the binary which browsers can pick up when instantiating a module from a fetch response. In environments that do not provide fetch or an equivalent mechanism, like in Node.js, it is alternatively possible to embed an absolute source map path through --sourceMap path/to/source/map.
# Breakpoints
Some JavaScript engines also support adding break points directly in WebAssembly code. Please consult your engine's documentation: Chrome (opens new window), Firefox (opens new window), Node.js (opens new window), Safari (opens new window).
# Transforms
AssemblyScript is compiled statically, so code transformation cannot be done at runtime but must instead be performed at compile-time. To enable this, the compiler frontend (asc) provides a mechanism to hook into the compilation process before, while and after the module is being compiled.
Specifying --transform ./myTransform.js on the command line will load the node module pointed to by ./myTransform.js.
# Properties
A transform is an ES6 class/node module with the following inherited properties:
Reference to the
Programinstance.Base directory used by the compiler.
Output stream used by the compiler.
Error stream uses by the compiler.
Logs a message to console.
Writes a file to disk.
Reads a file from disk.
Lists all files in a directory.
# Hooks
The frontend will call several hooks, if present on the transform, during the compilation process:
Called when parsing is complete, before a program is initialized from the AST. Note that types are not yet known at this stage and there is no easy way to obtain them.
Called once the program is initialized, before it is being compiled. Types are known at this stage, respectively can be resolved where necessary.
Called with the resulting module before it is being emitted. Useful to modify the IR before writing any output, for example to replace imports with actual functionality or to add custom sections.
Transforms are a very powerful feature, but may require profound knowledge of the compiler to utilize them to their full extent, so reading through the compiler sources is a plus.
# Portability
With AssemblyScript being very similar to TypeScript, there comes the opportunity to compile the same code to JavaScript with tsc and WebAssembly with asc. The AssemblyScript compiler itself is portable code. Writing portable code is largely a matter of double-checking that the intent translates to the same outcome in both the strictly typed AssemblyScript and the types-stripped-away TypeScript worlds.
# Portable standard library
Besides the full standard library, AssemblyScript provides a portable variant of the functionality that is present in both JavaScript and WebAssembly. In addition to that, the portable library lifts some of the functionality that is only available with asc to JavaScript, like the portable conversions mentioned below.
Also note that some parts of JavaScript's standard library function a little more loosely than how they would when compiling to WebAssembly. While the portable definitions try to take care of this, one example where this can happen is Map#get returning undefined when a key cannot be found in JavaScript, while resulting in an abort in WebAssembly, where it is necessary to first check that the key exists using Map#has.
To use the portable library, extend assemblyscript/std/portable.json instead of assemblyscript/std/assembly.json within tsconfig.json and add the following somewhere along your build step so the portable features are present in the environment:
Note that the portable standard library is still a work in progress and so far focuses on functionality useful to make the compiler itself portable, so if you need something specific, feel free to improve its definitions and feature set (opens new window).
# Portable conversions
While asc understands the meaning of
and then inserts the correct conversion steps, tsc does not because all numeric types are just aliases of number. Hence, when targeting JavaScript with tsc, the above will result in
which is obviously wrong. To account for this, portable conversions can be used, resulting in actually portable code. For example
will essentially result in
which is correct. The best way of dealing with this is asking yourself the question: What would this code do when compiled to JavaScript?
# Portable overflows
Likewise, again because asc knows the meaning but tsc does not, overflows must be handled explicitly:
essentially resulting in
# Non-portable code
In JavaScript, all numeric values are IEEE754 doubles that cannot represent the full range of values fitting in a 64-bit integer (max. safe integer (opens new window) is 2^53 - 1). Hence i64 and u64 are not portable and not present in std/portable. There are several ways to deal with this. One is to use an i64 polyfill like in this example (opens new window).
Other than that, portable code (JavaScript) does not have a concept of memory, so there are no load and store implementations in the portable standard library. Technically this can be polyfilled in various ways, but no default is provided since actual implementations are expected to be relatively specific (for instance: the portable compiler accesses Binaryen's memory).