Bindings and assignment
Grammar
Section titled “Grammar”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.
Local representation and addressability
Section titled “Local representation and addressability”Mutability and addressability are separate facts. A local declaration selects one of three representations:
| Source form | Reassignable | & form | purpose |
|---|---|---|---|
name: T = expression | yes | no | ordinary mutable value binding |
let name[: T] = expression | no | readonly *T only | compiler-managed readonly location |
var name[: T] = expression | yes | *T or readonly *T | compiler-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.
Scope, shadowing, and redeclaration
Section titled “Scope, shadowing, and redeclaration”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 let
… else 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 let … else 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:
| Reuse | Meaning | Report |
|---|---|---|
| Same scope | The earlier binding is unreachable from the later declaration onward. | ES2284, error |
| Enclosing scope, same callable | The inner binding hides the outer one; the outer is readable again after the inner scope ends. | ES4001, warning |
| Across a callable boundary | A 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.
Mutability and assignment
Section titled “Mutability and assignment”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.
Typed pointer bindings
Section titled “Typed pointer bindings”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.
See also
Section titled “See also”- Properties and object initialization — direct fields, write-once fields, and properties in a type body.
- By-ref calls and method sets —
&,*T,readonly *T, andoutat a call boundary. - Pointer values and allocation — the lifetime and
representation distinction between borrowing with
&and allocating withnew.