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 — 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.

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.

Statement Terminators

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.

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

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 – 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)
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
openModifier for anonymous chips (start expanded)
typeRecord type declaration
staticPersistent-variable modifier (inside handlers/mods)
awaitSuspend 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.

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

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)
  • 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