- Source Structure
- Imports
- Comments
- Statement Terminators
- Line Continuation
- Blocks
- Identifiers
- Keywords
- Literals
- Operators
- Top-Level Declarations
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
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 — 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.
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 none to select. No error, no warning: the receiver gate is
never emitted, so the events it would have caught go nowhere.
The trap is the 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 robust arrangement is to keep on handlers in the entry file regardless:
expose the logic from the library as a mod, and let each entry file declare the
thin handler that calls it. That way 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 WireGraphPseudo_CustomEvent
Paths are resolved relative to the importing file. Circular imports are an error.
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 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 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.
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.
Statements are terminated by newlines or semicolons. Both are interchangeable -- 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.
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
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 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 -- blank lines are fine.
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.
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) |
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 |
open |
Modifier for anonymous chips (start expanded) |
type |
Record type declaration |
static |
Persistent-variable modifier (inside handlers/mods) |
await |
Suspend an exec chain until a signal fires |
Using a reserved word as an identifier (eg. from) as a variable or parameter
name produces a cascade of confusing WSP001 expected Ident, got '<word>' (Kw) parse errors that mask the real cause.
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
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 are delimited by double quotes " or single quotes '.
"hello world"
'hello world'
| 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 |
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.
true
false
Operators are listed here for reference. See Expressions for full details on precedence and behavior.
+, -, *, /, %, ** (power)
==, !=, <, <=, >, >=
&&, ||, !
&, |, ^, ~, <<, >>
.. (concatenation)
= (assignment), -> (return type / outputs), => (fat arrow, reserved)
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)on 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