Syntax Reference
Contents
- Source Structure
- Imports
- Comments
- Statement Terminators
- Line Continuation
- Blocks
- Identifiers
- Keywords
- Literals
- Operators
- Top-Level Declarations
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:
| Keyword | Purpose |
|---|---|
var | Mutable variable declaration |
let | Immutable binding |
buffer | Buffered value (delayed one tick) |
array | Array declaration |
chip | Chip declaration (anonymous or named) |
mod | Inline chip (expanded at call sites) |
fn | Function (compiled once, run by a Function Call at each call site) |
on | Event handler |
in | Input port declaration |
out | Output port binding |
emit | Emit a user-defined event |
if | Conditional (statement or expression) |
else | Else branch |
then | Used in if-then-else expressions |
match | Reserved; no expression form is implemented |
return | Early return from handler |
import | Import declarations from another file |
from | Used with import { } from "path" |
as | Alias in imports or namespace |
true | Boolean literal |
false | Boolean literal |
ref | Reference type or ref-of expression |
type | Record type declaration |
static | Persistent-variable modifier (inside handlers/mods) |
await | Suspend 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
| Escape | Character |
|---|---|
\\ | Backslash |
\" | Double quote (in double-quoted strings) |
\' | Single quote (in single-quoted strings) |
\n | Newline |
\t | Tab |
\r | Carriage return |
\$ | Literal dollar sign (prevents interpolation) |
\0 | Null 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 variablelet name = expr: Immutable bindingbuffer name = expr: Buffered valuearray name: type[]: Arrayin name: type: Input portout name = expr: Output portchip name(params) -> outputs { body }: Named chipchip { body }: Anonymous chipchip let name = expr: Anonymous chip with let bindingschip on trigger { body }: Anonymous chip with handlermod name(params) { body }: Inline chip (macro-like)fn name(params) -> outputs { body }: Function, compiled once into a function definition planeon trigger { body }: Event handlerlet name = on trigger { body }: Captured event with handlerimport "path": Import all declarations from fileimport { names } from "path": Selective importimport * as ns from "path": Namespace importif cond { body }: Conditional (in exec context)return: Early return from handlertarget = expr: Assignment (in exec context)expr: Expression statement