Skip to content

Bindings and assignment

Binding = ( "let" | "var" ) BindTarget [ ":" Type ] "=" Expression
| identifier ":" Type "=" Expression .
BindTarget = identifier | "(" identifier { "," identifier } ")" .
Assignment = Lvalue "=" Expression .
Compound = Lvalue ( "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^="
| "<<=" | ">>=" | ">>>=" ) Expression .
Lvalue = identifier { "." identifier | "[" Expression "]" } .

let name = value declares an immutable local and infers its type. var name = value declares a mutable local and infers its type. Both accept an explicit type after the name, which also supplies the expected type to target-typed expressions such as lambdas, method groups, default, and composite cases.

Mutability and addressability are separate facts. A local declaration selects one of three representations:

Source formReassignable& formpurpose
name: T = expressionyesnoordinary mutable value binding
let name[: T] = expressionnoreadonly *T onlycompiler-managed readonly location
var name[: T] = expressionyes*T or readonly *Tcompiler-managed writable location

A bare typed local is intentionally mutable—count: int = 1; count += 1 is ordinary code—but it is not a place whose address can be borrowed. &count is rejected at address formation, before escape analysis or pointer-direction checking. Choose var count: int = 1 when the local must be borrowed mutably, or let when a readonly borrow is the intended contract. This prevents a typed local from silently acquiring a more powerful storage representation merely because a later call happens to take its address.

namespace Test
func double(value: int) -> int = value * 2
func applyTwice(value: int) -> int {
let initial: int = value
let transform: Func<int, int> = double
return transform(transform(initial))
}

The same spelling in a method and a type body

Section titled “The same spelling in a method and a type body”

name: Type = expression is contextual. In a function body, it is the field-ordered local declaration form: the colon introduces a local, the explicit type is mandatory, and the result is a mutable local equivalent to var name: Type = expression only in reassignment behavior. It is not representation-equivalent: the bare typed form remains non-addressable, while var explicitly requests compiler-managed writable storage. It is a declaration, not an assignment.

Inside a class or struct, the same token sequence is instead a member declaration: it declares a direct mutable field and its construction-time default. It does not introduce a local for the enclosing method. The field is reached through self.name inside methods or object.name outside them.

namespace Test
class AppConfig {
// A direct mutable field.
environment: string = "development"
// Member let/var are properties, even without `{ }`.
let source: string = "built-in"
var displayName: string { }
init() {
priv self.reloadStorage: int = 0
}
var reloads: int {
loca => &self.reloadStorage
}
}
func addOne(value: *int) { value += 1 }
func selectEnvironment() -> int {
let config = AppConfig()
// This is the name-first mutable local form, created per call.
currentEnv: string = config.environment
// The same explicit type can accompany either keyword.
// `let` preserves the configuration that this call observed.
let configuredEnv: string = currentEnv
// `var` names a separately mutable staging location.
var selectedEnv: string = configuredEnv
selectedEnv = "production"
currentEnv = selectedEnv
// `&` borrows two real locations: the compiler-managed local and an
// explicitly location-aware property.
var localReloads: int = 0
addOne(&localReloads)
addOne(&config.reloads)
// This writes the object's direct field, not the local binding.
config.environment = currentEnv
return config.environment.Length * 100 + localReloads * 10 + config.reloads // 1011
}

The superficial similarity is intentional—E# reads declarations name-first—but scope determines the thing being introduced: a local execution-time binding in a function, or a per-instance member slot in a type. In the function, currentEnv: string = ... is the concise typed mutable-but-non-addressable local form; let configuredEnv: string is an immutable addressable snapshot, while var selectedEnv: string is an explicitly mutable addressable local. At type level, bare name: Type is the ordinary direct-field form, while member let and var always select properties: stored getter/init and getter/set properties respectively, unless their => or accessor body customizes the protocol. The example also shows why that distinction matters to &: &localReloads borrows compiler-managed local storage and &config.reloads uses the property’s explicit location contract. A direct class field is not a source *T target. Read the detailed contracts in Properties and object initialization and By-ref calls and method sets; see Pointer values and allocation for & versus new.

Every brace-delimited block is a scope. A binding introduced in a block is visible to the statements that follow it within that block and to nested blocks, and is not visible after the block ends. The bindings a construct introduces for its own body — a for variable, a catch binding, a match arm binding — belong to that body’s scope. A letelse guard is the exception by design: its binding belongs to the enclosing scope, which is the point of the form.

defer registers against the scope it appears in, and letelse must leave that scope; both are stated in terms of this rule.

A name may be reused, and the two ways of reusing one are different facts:

ReuseMeaningReport
Same scopeThe earlier binding is unreachable from the later declaration onward.ES2284, error
Enclosing scope, same callableThe inner binding hides the outer one; the outer is readable again after the inner scope ends.ES4001, warning
Across a callable boundaryA distinct callable’s own binding. Closures still capture the enclosing scope.none

Shadowing is well-formed: the inner binding is a separate binding with its own type and storage, and the outer one is unaffected. It is reported because it is the shape a reader misattributes, not because it is ambiguous — so it is a warning, suppressible, and never blocks a build. The report names the line being hidden so the fix is a rename rather than a search.

namespace Test
func shadowing() -> int {
let x = 1
var acc = 0
if true {
let x = 100 // ES4001: shadows the binding declared at line 4
acc = acc + x // the inner x
}
return acc + x // the outer x is readable again — 101
}

A callable boundary is a function body or a function-literal body. Reusing an enclosing local’s name for a lambda parameter is ordinary code and is not reported; the lambda body still sees the enclosing scope, which is how captures work.

namespace Test
func boundary() -> int {
let v = 1
let scale = func(v: int) -> int { // not reported — a new callable
return v * 10
}
return scale(4) + v // 41
}

Redeclaring within one scope is an error because no reading of it is useful — the first binding cannot be read after the second declaration:

namespace Test
func redeclared() -> int {
let x = 1
let x = 2 // ES2284: 'x' is already declared in this scope
return x
}

Assignment is the intended form when the existing binding is meant to change; declaration introduces a new one. Two sibling scopes may each bind the same name freely — neither encloses the other, so neither hides anything, and the bindings may differ in type.

An assignment requires an assignable lvalue: a mutable local, parameter, mutable field/property, namespace var, or indexer location. A let local/field and a computed or get-only property are not assignable. Mutability concerns the binding/location, not whether a referenced object itself has mutable members.

Plain = evaluates the target location and right-hand expression, then stores the value. Compound assignment reads the target once, evaluates the right side once, applies its operator, and stores the result back. For a member/index target, receiver and index expressions are evaluated only once and before any asynchronous right side resumes; this preserves ordinary left-to-right expression sequencing.

The following complete program demonstrates ordinary local, field, and index targets. totals is a value type, so it is a var binding before its mutable field can be updated. slots is an immutable array reference, but its elements are independent assignable locations.

namespace Test
struct Totals { var value: int }
func update() -> int {
var retries = 1
retries += 2
var totals = Totals { value: 10 }
totals.value += retries
let slots = int[](2)
slots[0] = totals.value
slots[1] = retries * 10
slots[0] += slots[1]
return slots[0] // 43
}

This complete program makes the single-evaluation rule observable. The index and right-hand helper each run once, so updateOnce() returns 511: the stored value is 5, with one index call and one right-side call.

namespace Test
struct Calls {
var indexCalls: int
var valueCalls: int
}
func nextIndex(calls: *Calls) -> int {
calls.indexCalls += 1
return 0
}
func nextValue(calls: *Calls) -> int {
calls.valueCalls += 1
return 5
}
func updateOnce() -> int {
var calls: *Calls = new Calls { indexCalls: 0, valueCalls: 0 }
let slots = int[](1)
slots[nextIndex(calls)] += nextValue(calls)
return slots[0] * 100 + calls.indexCalls * 10 + calls.valueCalls
}

= is a statement form, never an expression. It cannot be nested in a call, condition, or another assignment. This keeps binding, mutation, and value production distinct: introduce a name with a binding, mutate a location with a statement, and obtain values through expressions.

An explicitly typed *T local is a first-class, nullable heap-pointer representation because its lifetime can escape the immediate frame. Assignment does not silently turn a T into *T or the reverse: use &place to borrow a location or new T { ... } to allocate pointed-to storage. The pointer model is specified in Pointers and Memory model.