Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Syntax Reference

Contents

Source Structure

A Wirescript file (.ws) is a sequence of top-level declarations. There is no required entry point or wrapper. Declarations appear at the top level of the file.

in trigger: exec
var count: int = 0

on trigger {
  count = count + 1
}

out total = count

Imports

Import declarations bring symbols from other .ws files into scope. The .ws extension is implicit.

import "lib"                            // import all exportable declarations
import { swap, clamp } from "lib"       // selective import
import { swap as mySwap } from "lib"    // aliased import
import * as utils from "lib"            // namespace import, called as utils.swap()

Importable: mod, chip, fn, let, const, type, and a module’s root-level var, array, map, buffer, in and out declarations. Imported state is shared, not copied: every importer reads and writes the same storage gate, so a library module can own state that several entry files drive.

An enum of a namespace import is the type ns.Name, and its variants are ns.Name.Variant (var d: ns.Dir = ns.Dir.S); inside the module it is Name. A match arm names such a variant by the variant alone (S =>). A program has one enum of a name, whichever way it is imported, so a namespace import of a module declaring an enum the program declares too is WS013.

A selective import drops the module’s handlers. import "lib" and import * as lib from "lib" both carry a module’s top-level on handlers. import { helper } from "lib" does not, because it selects declarations by name and a handler has no name to select. The compiler warns WS014, naming the handlers left out, and still compiles. The receiver gate is never emitted, so the events it would have caught go nowhere.

This usually happens through an edit that narrows an import. Turning import "lib" into import { helper } from "lib" to quiet an unused-name warning drops every handler that module declared, and neither just check nor just compile says so. Narrowing to import * as lib from "lib" keeps them.

The safer arrangement is to keep on handlers in the entry file in every case. Expose the logic from the library as a mod, and let each entry file declare the thin handler that calls it. Then no import form can silently remove them:

// lib.ws: the logic, reachable by importers
mod onPing(v: int) { total = total + v }

// main.ws: the handler must be declared here
import { onPing } from "lib"
on CustomEvent("ping") -> (v: int) { onPing(v) }

To confirm the receivers survived, count them in the lowered graph. This is the only check that sees the problem:

just ir main.ws | grep -c Event_Custom

A file calls only the mods it can name. Code in a file calls, as f(x) or as x.f(), only the mods and chips that file declares or that its own imports name. A module’s private helper, present because an imported body calls it, is not callable from the importer, and an importer’s own mods are not callable from the bodies it imports. A library that needs one declares it, or imports it from a module both files import, since a module can’t import the file that imports it. A plain call to a mod the file can’t name is WS002, and a method call is WS036. Where a builtin of the same name exists, the builtin is called instead.

Paths are resolved relative to the importing file. Circular imports are an error.

Comments

Line Comments

Line comments start with // and extend to the end of the line.

// This is a line comment
var x: int = 0  // inline comment

Block Comments

Block comments are delimited by /* and */. They may be nested.

/* This is a block comment */

/* Block comments
   can span
   multiple lines */

/* And they /* can be */ nested */

Doc Comments

Doc comments start with /// (three slashes) and are attached to the declaration that immediately follows them. They are preserved by the compiler for documentation generation.

/// The player's current score.
/// Resets to zero each round.
var score: int = 0

Multiple consecutive doc comment lines are joined together. A single space after /// is consumed automatically.

Diagnostic Directives

A // comment containing ws-ignore-line:WS014 or ws-ignore-file:WS014 turns that warning off for the line or the file. See Silencing a warning.

Statement Terminators

Statements are terminated by newlines or semicolons. Both are interchangeable, so you can use whichever style you prefer.

// Newline-terminated (typical style)
var x: int = 0
var y: int = 1

// Semicolon-terminated (compact style)
var x: int = 0; var y: int = 1

// Mixed
var x: int = 0; var y: int = 1
var z: int = 2

Multiple consecutive newlines and semicolons are consumed as a single statement boundary.

Line Continuation

Expressions can span multiple lines when split at an operator. The parser skips newlines when it encounters an infix operator, allowing natural line wrapping:

let total = a +
  b +
  c

let check = condition1 &&
  condition2 &&
  condition3

A line that starts with * is the exception: it begins a new statement (a deref, such as *h = 5), never the continuation of the line above. To continue a multiplication, end the first line with *. A bare deref line whose value nothing uses gets a WSP001 warning, since it is almost always a split multiplication:

let area = width *
  height

Newlines are also allowed inside delimited groups (call arguments ( ... ), array literals [ ... ], and record literals { ... }) after the opener, around commas, and before the closer, with an optional trailing comma:

var names: string[] = [
  "alice",
  "bob",
]

let point = {
  x: 1,
  y: 2,
}

Blocks

Blocks are enclosed in curly braces { } and contain a sequence of statements. They are used for handler bodies, chip bodies, if/else branches, and named chip declarations.

on RoundStart() {
  count = count + 1
  score = 0
}

Newlines inside blocks are consumed freely, so blank lines are fine.

Identifiers

Identifiers start with a letter or underscore and continue with letters, digits, or underscores.

valid_name
_private
counter2
myVar

Identifiers are case-sensitive. count and Count are different names.

Keywords

The following words are reserved and cannot be used as identifiers:

KeywordPurpose
varMutable variable declaration
letImmutable binding
bufferBuffered value (delayed one tick)
arrayArray declaration
chipChip declaration (anonymous or named)
modInline chip (expanded at call sites)
fnFunction (compiled once, run by a Function Call at each call site)
onEvent handler
inInput port declaration
outOutput port binding
emitEmit a user-defined event
ifConditional (statement or expression)
elseElse branch
thenUsed in if-then-else expressions
matchReserved; no expression form is implemented
returnEarly return from handler
importImport declarations from another file
fromUsed with import { } from "path"
asAlias in imports or namespace
trueBoolean literal
falseBoolean literal
refReference type or ref-of expression
typeRecord type declaration
staticPersistent-variable modifier (inside handlers/mods)
awaitSuspend an exec chain until a signal fires

Using a reserved word (e.g. from) as a variable or parameter name produces a series of WSP001 expected Ident, got '<word>' (Kw) parse errors that hide the real cause.

Literals

Integer Literals

Decimal, hexadecimal, binary, and octal integer literals are supported. Underscores may be used as digit separators.

42
1_000_000
0xff          // hexadecimal
0b1010        // binary
0o77          // octal
0xFF
0B1100_0011

An integer constant in a place typed float is that float. An integer constant here is a literal, a name bound to an integer constant such as const N = 3, or an expression the compiler computes from them, such as N + 2, -N, an element of a constant array, a field of a constant record or a const mod call. A place typed float is a float var, parameter (of a mod, chip, namespaced call or enum payload), return value, output, record or tuple field, container element, or an annotated let/const. An annotated let/const then holds a float in constant evaluation too. So with mod half(v: float) -> float { return v / 2 }, half(3), half(N) and half(N + 2) are 1.5, 1.5 and 2.5. Each read decides on its own: N read into an int place beside them stays an integer. A runtime value is not converted: an int variable, input or computed value passed to a float place keeps its integer wire, so half(n) with n: int divides as integers. An if or match whose arms join to float makes its integer arms floats ((if c then 3 else 2.5) / 2 is 1.5 or 1.25), unless the whole value goes into an int place, which keeps them integers. A record, tuple, array or map literal fits a target whose fields or elements are float where its own are integers. A builtin math call such as clamp(n, 0, 8) takes its literals as written. min, max, clamp, abs, sign, floor, ceil and round of integers are integers, and are float once an operand is a float.

Float Literals

Floating-point literals use decimal notation with an optional exponent.

3.14
0.5
1e10
2.5e-3
1_000.0

A float literal requires a digit after the decimal point: 1. alone is not a float literal (it would be parsed as integer 1 followed by a dot).

String Literals

String literals are delimited by double quotes " or single quotes '.

"hello world"
'hello world'

Escape Sequences

EscapeCharacter
\\Backslash
\"Double quote (in double-quoted strings)
\'Single quote (in single-quoted strings)
\nNewline
\tTab
\rCarriage return
\$Literal dollar sign (prevents interpolation)
\0Null character

String Interpolation

Both single- and double-quoted strings support ${expr} interpolation. Any expression can be embedded:

"Hello, ${name}!"
"Score: ${score + bonus}"
'Position: ${pos.x}, ${pos.y}'

Interpolated expressions are converted to strings. Use \$ to include a literal dollar sign.

Boolean Literals

true
false

Operators

Operators are listed here for reference. See Expressions for full details on precedence and behavior.

Arithmetic

+, -, *, /, %, ** (power)

Comparison

==, !=, <, <=, >, >=

Logical

&&, ||, !

Bitwise

&, |, ^, ~, <<, >>

String

.. (concatenation)

Other

= (assignment), -> (return type / outputs), => (fat arrow, reserved)

Top-Level Declarations

The following forms are valid at the top level of a script:

  • var name: type = expr: Mutable variable
  • let name = expr: Immutable binding
  • buffer name = expr: Buffered value
  • array name: type[]: Array
  • in name: type: Input port
  • out name = expr: Output port
  • chip name(params) -> outputs { body }: Named chip
  • chip { body }: Anonymous chip
  • chip let name = expr: Anonymous chip with let bindings
  • chip on trigger { body }: Anonymous chip with handler
  • mod name(params) { body }: Inline chip (macro-like)
  • fn name(params) -> outputs { body }: Function, compiled once into a function definition plane
  • on trigger { body }: Event handler
  • let name = on trigger { body }: Captured event with handler
  • import "path": Import all declarations from file
  • import { names } from "path": Selective import
  • import * as ns from "path": Namespace import
  • if cond { body }: Conditional (in exec context)
  • return: Early return from handler
  • target = expr: Assignment (in exec context)
  • expr: Expression statement