Skip to main content

Declarations

A declaration introduces a symbol into the current scope. Epsil has two declaration keywords:

  • let declares a mutable symbol.
  • const declares an immutable symbol.
let x = 5
const c = 6.28

A type annotation also implies a declaration, even without a keyword:

x: real = 5

is a declaration of x with type real, exactly as if it had been written let x: real = 5. The keyword is only mandatory for an untyped declaration — that's what distinguishes a declaration from a plain reassignment (see below).

Destructuring declarations

A let or const may bind the components of a tuple in one statement:

divmod(a, b) = (Floor(a / b), a % b)
let (q, r) = divmod(17, 5)
(q, r)
// ➔ (3, 2)

The pattern is a parenthesized list of at least two elements, each a bare symbol, a _ (which skips that position), or a nested tuple pattern:

let ((a, b), _, c) = ((1, 2), 99, 5)
a + b + c
// ➔ 8

The pattern is irrefutable in form — no literals, pins, or guards (use match for conditional destructuring). The value is evaluated once; it must be a tuple of the same shape, otherwise the declaration yields an incompatible-type error value and binds nothing. With const, every bound name is a constant. An initializer is required, and a type annotation is not accepted on a pattern. Duplicate names anywhere in one pattern are a diagnostic.

Destructuring lowers to the same Declare primitive with the pattern in the name position: ["Declare", ["Tuple", "q", "r"], ["Dictionary", ["KeyValuePair", "value", …]]].

Destructuring assignment

The same pattern may appear on the left of an assignment, to write bindings that already exist instead of declaring new ones:

let a = 1
let b = 2
(a, b) := (b, a)
(a, b)
// ➔ (2, 1)

The right side is evaluated once, in full, before any target is written, so a swap means what it reads — (a, b) := (b, a) exchanges the two values rather than assigning b to both. The same holds for a rotation ((a, b, c) := (c, a, b)) and for the pair-carrying loop step that is the usual reason to want this:

let a = 0
let b = 1
for k in 1..10 {
(a, b) := (b, a + b)
}
a
// ➔ 55

The pattern grammar is exactly the one above — at least two elements, each a bare symbol, a _ skipping that position, or a nested tuple pattern — and a shape mismatch is the same incompatible-type error value, which writes nothing: the whole pattern is matched before any target is written, so a mismatch nested under a position that would have bound leaves that one alone too.

The differences from a destructuring let are the ones assignment always has: the targets keep their identity and their declared type (a value that does not fit a target's type is an error value), and assigning to a const fails. Those two failures are found only by attempting the write, so unlike a shape mismatch they are not atomic — targets earlier in the pattern have already been written and stay written.

The assignment operator must be spelled :=. A statement-leading (a, b) = … is a comparison, not an assignment — a parenthesized left side is not a binding target, so the bare = reads as Equal. Because that is almost always a typo for the destructuring assignment, it is diagnosed.

Destructuring assignment lowers to Assign with the pattern in the target position: ["Assign", ["Tuple", "a", "b"], ["Tuple", "b", "a"]].

Declaring a type

A third declaration keyword, type, introduces a type name rather than a symbol — and, with it, a constructor of the same name:

type point = tuple<x: number, y: number>
type alias pair = tuple<number, number>
let p = point(1, 2)
let a: pair = (1, 2)

type declares a new, distinct type; type alias declares another name for an existing one, and takes a type-parameter clause if it needs one (type alias Pair<T> = tuple<T, T>). Unlike let and const, type is not a reserved word — only these statement shapes claim it. See Declaring a type for the whole story.

Reassignment vs. declaration

A bare x = 5 — no let/const keyword, no type annotation — is not declaration syntax: it is an assignment and lowers to Assign:

x = 5
["Assign", "x", 5]

The Compute Engine permits Assign to establish a value for a previously unbound symbol, but let is the explicit and idiomatic way to introduce a mutable binding.

Reassigning a symbol that was declared const produces an error value, not a parse error or a thrown exception:

const c = 1
c = 2

c = 2 still parses and lowers to ["Assign", "c", 2]; it's the engine, at evaluation time, that rejects the assignment and produces an ["Error", …] value.

Encoding

Declarations lower to the engine's Declare operator — not an Epsil-specific Let/Const head. Declare takes the declared symbol, an optional type (positional, when present), and a trailing attributes Dictionary carrying value and, for const, constant: True. const is a binding attribute (constant: True → the engine's isConstant), not a type — the engine, not Epsil, enforces it.

let x = 5
["Declare", "x", ["Dictionary", ["KeyValuePair", "value", 5]]]

The type is inferred (integer, here) when no annotation is given. With an annotation, the type appears as a positional argument before the attributes dictionary:

let x: real = 5
["Declare", "x", {"str": "real"},
["Dictionary", ["KeyValuePair", "value", 5]]]

A declaration with no initializer omits the attributes dictionary entirely:

let x: real
["Declare", "x", {"str": "real"}]
let x
["Declare", "x"]

const adds a constant key alongside value:

const c = 6.28
["Declare", "c", ["Dictionary", ["KeyValuePair", "value", 6.28], ["KeyValuePair", "constant", "True"]]]

Because declarations lower directly to the engine's own Declare primitive, there is no separate Epsil-side declaration logic at execution time — the program evaluates the Declare expression exactly like any other expression.

Scoping

Declarations live in the current scope. A program (a notebook cell or a chain of cells sharing one engine scope) declares at the top level; a block introduced by if/else/while/for, or a function body, pushes its own lexical scope, so a let/const inside a block does not leak into the enclosing scope.

let and const are the binding keywords. There is currently no compound assignment (+=); destructuring declarations (let (x, y) = t) and destructuring assignments ((x, y) := t) are described above.