Values and Storage

Consider a beginner's confusion: after `y = x`, why does changing `x` sometimes change `y`, and sometimes not? The answer is not a quirk of any one language. It is the distinction between a value, the memory location that holds a value, and the store that maps locations to values. This distinction is the quiet machinery underneath every assignment statement, every function call, every pointer, and every "reference vs. value" debate. It trips up programmers moving between languages like Python, C, and Java. This idea matters because it is the semantic foundation on which Names, Binding, and Scope is built. A name is bound to something. Understanding whether that something is a value directly, or a location that in turn holds a value, matters greatly. It determines the entire behavior of assignment, aliasing, mutation, and parameter passing. Compiler writers must model this precisely to generate correct code. Language designers must choose it deliberately, because it shapes whether two variables can "see" each other's updates. The big picture is a three-layer model. Values are the abstract mathematical objects a language computes with — integers, booleans, closures, records. Locations (sometimes called l-values, addresses, or references) are storage cells that can hold a value. They can also be overwritten. The store (or memory, or heap-and-stack) is a function from locations to values at a given moment. It changes over time as the program executes. Names are bound either to values directly, or to locations. This single choice cascades into everything from "is my language pass-by-value or pass-by-reference" to "why does mutating a list inside a function affect the caller." This article delivers a precise vocabulary for values, locations, and stores. It offers a step-by-step model of how assignment and dereference actually work as store transformations. It includes a worked trace through both a value-semantics and a reference-semantics example. It also maps the classic bugs — aliasing surprises, dangling references, shared mutable state — that arise when this distinction is misunderstood. It sets up the machinery needed for Environments and for reasoning formally about Expressions and Statements.

Key takeaways
  • A value is an abstract, immutable mathematical object (like the integer `5`); a location is a mutable storage cell; the store maps locations to values at a point in time.
  • Binding a name can mean binding it directly to a value, or binding it to a location that holds a value — this choice defines a language's variable semantics.
  • Assignment (`x := e`) does not change a value; it changes the store, replacing what location `x` denotes points to.
  • Aliasing happens when two names denote the same location; mutating through one name is then visible through the other.
  • Pass-by-value copies a value into a fresh location; pass-by-reference (or reference-semantics for objects) shares a location — this is the root of most parameter-passing confusion.
  • The store model gives a rigorous foundation for reasoning about mutation, aliasing, and memory safety, and is the prerequisite for formalizing Environments and operational semantics of statements.
The core insight

A variable's behavior under assignment is determined entirely by whether its name is bound to a value or to a mutable location in a store that can be updated and shared.

The idea

In everyday speech we say "the variable `x` is `5`," blurring together three distinct things a language semantics must keep separate. A value is a self-contained, timeless piece of data — the integer `5`, the string `"cat"`, a function closure, a tuple. Values do not change; the value `5` is always `5`. A location (also called a cell, an address, or an l-value in C terminology) is a place where a value can be stored, and — critically — overwritten. A location has an identity that persists even as the value inside it changes. The store (sometimes called the memory or, in denotational semantics, a function `Store = Location → Value`) records, at each instant of execution, which value currently occupies which location. With this vocabulary, a variable is not a value — it is a name bound to something, and that something is either a value directly or a location. Languages differ starkly here. In a purely functional setting with no mutable variables, binding a name to a value is enough: `let x = 5` associates the name `x` with the value `5`, full stop, and there is no store to speak of (or the store is write-once). But as soon as a language has mutable variables — `x := x + 1` — a name must be bound to a location, because the whole point of assignment is that the same name can denote different values at different times. The location is the stable thing; the value it holds is what changes. This gives us the classic environment–store picture used throughout operational semantics: an environment maps names to locations (or, in value-oriented languages, to values), and a store maps locations to values. Reading a variable means looking up its location in the environment, then looking up that location's current value in the store — a two-step indirection. Assignment updates only the store, leaving the environment (the name-to-location binding) untouched — this is precisely why a `for` loop can reassign a counter without "re-binding" the variable each iteration. This decomposition also explains aliasing: if two names, perhaps in different scopes or under different parameter names, are bound to the same location, then mutating through one name is observable through the other, because they share one entry in the store. Understanding this store-sharing is the single most important prerequisite for reasoning about pointers, references, object mutation, and pass-by-reference parameter passing.

How it works

The cleanest way to see the mechanism is to model program state as a pair of maps and watch how each construct transforms them.

  • Environment `ρ : Name → Location` — where does a name's storage live?
  • Store `σ : Location → Value` — what value currently sits in a given storage cell?

Evaluating an expression that reads a variable `x` is a two-hop lookup: `σ(ρ(x))`. Executing an assignment `x := e` evaluates `e` to a value `v` using the current store, then produces a new store identical to the old one except that location `ρ(x)` now maps to `v`. The environment `ρ` is untouched — assignment never changes what location a name refers to, only what value that location holds. (Rebinding a name to a different location is a distinct operation — e.g., declaring a new local variable that shadows an outer one — and belongs to the theory of Environments.) Declaring a fresh mutable variable is the operation that actually grows the store: it allocates a brand-new location (one not already in the domain of `σ`), installs an initial value there, and extends the environment so the name maps to that fresh location. ``` // State: a pair (ρ, σ) // ρ : Name -> Location (environment: name to storage) // σ : Location -> Value (store: storage to current value) function EVAL(expr, ρ, σ): match expr: case Literal(v): return v case Variable(x): loc = ρ[x] // step 1: name -> location return σ[loc] // step 2: location -> value case BinOp(op, e1, e2): v1 = EVAL(e1, ρ, σ) v2 = EVAL(e2, ρ, σ) return APPLY(op, v1, v2) function EXEC(stmt, ρ, σ): match stmt: case Declare(x, initExpr): v0 = EVAL(initExpr, ρ, σ) loc = FRESH_LOCATION(σ) // pick l not in dom(σ) ρ' = ρ with x -> loc // extend environment σ' = σ with loc -> v0 // extend store return (ρ', σ') case Assign(x, rhsExpr): v = EVAL(rhsExpr, ρ, σ) loc = ρ[x] // location is unchanged σ' = σ with loc -> v // only the store changes return (ρ, σ') // environment unchanged case Seq(s1, s2): (ρ1, σ1) = EXEC(s1, ρ, σ) (ρ2, σ2) = EXEC(s2, ρ1, σ1) return (ρ2, σ2) case If(cond, sThen, sElse): if EVAL(cond, ρ, σ) is true: return EXEC(sThen, ρ, σ) else: return EXEC(sElse, ρ, σ) case While(cond, body): if EVAL(cond, ρ, σ) is true: (ρ1, σ1) = EXEC(body, ρ, σ) return EXEC(While(cond, body), ρ1, σ1) else: return (ρ, σ) ``` Two design points deserve emphasis. First, `Declare` extends both maps, while `Assign` extends neither — it only overwrites one entry of `σ`. This single asymmetry is the formal statement of "declaring makes a new box; assigning fills a box." Second, `FRESH_LOCATION` is what models allocation: every `Declare` (or, in a heap-based language, every object construction) manufactures a new identity in the store that nothing else aliases — until a reference to it is copied, at which point aliasing becomes possible. The same machinery explains aggregate values (arrays, records, objects). A language can treat a compound value as a single indivisible value copied wholesale (value semantics — as with C structs or Python tuples of immutables), or it can treat a variable holding a compound value as actually holding a reference: the store maps the variable's location to an address, and that address in turn is the base of a separate region of the store holding the object's fields. Assignment and parameter passing then only copy the reference (the address), not the object's fields — so two names can end up pointing at the same underlying object. This is exactly why Python lists and Java objects exhibit aliasing on assignment while Python ints and Java primitive `int`s do not.

Figure
Environment, store, and aliasing: two names sharing one location
Environment ρ (name → location) x → L1 y → L1 z → L2 Store σ (location → value) L1 → 42 L2 → 7 x and y both map to L1: they are ALIASES. Executing x := x + 1 changes L1 to 43 — reading y now also gives 43. After executing x := 43 : x → L1 y → L1 L1 → 43 Environment ρ is unchanged — only σ(L1) was overwritten.

Complexity & correctness

Complexity. In a real implementation the environment is typically a symbol table (hash map or array indexed by compile-time-resolved offset) and the store is the runtime memory (stack frame slots plus heap). Variable lookup `σ(ρ(x))` is `O(1)` amortized with hashing, or genuinely `O(1)` worst-case when the compiler resolves names to fixed stack/frame offsets at compile time (as in C, Java locals, or Scheme with lexical addressing) — this is one of the main reasons compilers perform static Names, Binding, and Scope resolution ahead of execution rather than doing dynamic name lookup at every access. Assignment is `O(1)`: it is a single write into one store cell, `σ' = σ[loc ↦ v]`, regardless of how large the rest of the program's state is, because the store update is a localized, persistent-map-style overwrite (or, in a real machine, a literal memory write). Declaring a new variable is `O(1)` amortized for allocation (bump-pointer or free-list allocation on stack/heap), though heap allocation can incur `O(1)` amortized but occasionally `O(n)` costs when a garbage collector runs — a cost tied to storage reclamation, not to the value/location distinction itself. None of the core operations (read, write, declare) are asymptotically expensive; the model's cost is dominated by whichever allocation strategy (stack vs. heap) a language chooses, not by the conceptual store itself.

Correctness. The correctness property this model must guarantee is referential transparency of locations under non-aliasing, and predictable sharing under aliasing — informally: "a location's value only changes when an assignment targets that exact location (or aliases of it), and never as a side effect of unrelated operations." This can be stated as an invariant on the operational semantics above:

  • Invariant (store locality): For any execution step that is not `Assign(x, …)` targeting location `ℓ = ρ(x)`, the value `σ(ℓ)` is unchanged across that step.
  • Invariant (aliasing coherence): If `ρ(x) = ρ(y) = ℓ` at some point in execution, then for every subsequent step that does not rebind `x` or `y` to a different location, `EVAL(x) = EVAL(y)` always holds, because both expressions resolve to `σ(ℓ)`.

These invariants are exactly what the `EXEC` rules preserve by construction: `Assign` is the only rule that mutates `σ`, and it mutates precisely one entry, `ρ(x)`; every other rule (`Seq`, `If`, `While`, `EVAL`) either leaves `σ` untouched or threads it through unchanged except via nested calls to `EXEC` on `Assign`/`Declare`. A simple structural induction on the derivation tree of `EXEC` establishes that no rule can write to a location other than the one explicitly denoted by the assignment target, so the two invariants hold by construction, not merely by testing. This is precisely why the value/location/store separation is the correctness tool for reasoning about mutation: bugs from unintended sharing are, formally, violations of the aliasing-coherence invariant caused by two names unexpectedly denoting the same location.

Tradeoffs. Value semantics (copy on assignment/pass) buys safety — no spooky-action-at-a-distance mutation — at the cost of copying overhead for large aggregates, which is why languages default to reference semantics for big objects and value semantics for small scalars. Reference/aliasing semantics buys efficiency (`O(1)` sharing instead of `O(n)` copying) at the cost of surprising bugs when programmers forget two names share a location. No choice is universally "correct"; the store model simply makes the consequences of each choice precise enough to prove things about.

Worked example

Trace the following program under the environment/store model, contrasting a value-semantics variable (`int`) with a reference-semantics variable (a list/array bound by reference), the exact situation that trips up learners moving from C-like scalars to Python/Java objects. ``` 1: a := 10 2: b := a // value semantics: copy 3: a := 99 4: L1 := [1, 2, 3] // list literal allocates heap object O1 5: L2 := L1 // reference semantics: copy the reference 6: L1[0] := 777 ```

stepoperationenvironment ρ (relevant entries)store σ (relevant entries)observed values
after 1`a := 10``a → ℓa``ℓa → 10``a = 10`
after 2`b := a``a → ℓa, b → ℓb``ℓa→10, ℓb→10``a = 10, b = 10` (independent copies)
after 3`a := 99`unchanged`ℓa→99, ℓb→10``a = 99, b = 10` — no aliasing, `b` unaffected
after 4`L1 := [1,2,3]``L1 → ℓ1``ℓ1 → addr(O1)`; heap: `O1 = [1,2,3]``L1 = [1,2,3]`
after 5`L2 := L1``L1 → ℓ1, L2 → ℓ2``ℓ1 → addr(O1), ℓ2 → addr(O1)``L1` and `L2` are aliases of O1
after 6`L1[0] := 777`unchangedheap: `O1 = [777,2,3]``L1 = [777,2,3]` and `L2 = [777,2,3]`

The key contrast is visible in the last two rows: `b := a` copies the value `10` into a fresh location `ℓb`, so subsequent mutation of `a` cannot touch `b` — this is the "no aliasing" invariant holding because `ρ(a) ≠ ρ(b)`. But `L2 := L1` copies only the reference (`addr(O1)`) — both `ℓ1` and `ℓ2` store the same heap address, so `L1` and `L2` become aliases not of each other's variable location, but of the same underlying object. Mutating through `L1[0] := 777` writes into the shared heap object `O1`, which is why the change is visible through `L2` as well, even though line 6 never mentions `L2`. This is exactly the aliasing-coherence invariant from the previous section, instantiated one level deeper: the aliasing is at the object store, not the variable store, but the reasoning is identical — two names denote one storage cell, so mutation through either is visible through both.

Common pitfalls & traps

PitfallWhy it happensFix / mental model
Assuming `y = x` always aliasesConflates value-copy languages (ints, structs-by-value) with reference-copy languages (objects, lists)Ask: does this type carry a location by reference, or is it copied wholesale?
"Pass-by-reference" vs. "pass-by-value of a reference" confusionJava/Python copy the object reference by value; this is not the same as C++'s true `&` pass-by-referenceTrace whether reassigning the parameter itself (not its contents) is visible to the caller — it is only visible under true reference passing
Dangling reference / use-after-freeA location is deallocated (stack frame popped, `free`d) but a name/pointer still denotes itTrack the lifetime of a location separately from the lifetime of names bound to it
Mutating a "copy" that was never actually copiedShallow copy duplicates the top-level location but shares nested object referencesDistinguish shallow copy (new outer location, shared inner locations) from deep copy (fresh locations all the way down)
Treating immutable value types as if declaring aliases themRebinding a name to a new value (e.g., reassigning a Python variable to a new int) looks like mutation but is a fresh binding, not a store write to a shared objectImmutable values force “assignment” to mean “rebind name to a new value,” never “mutate shared state”

Pioneers & history

The value/location/store distinction crystallized as programming languages moved from pure mathematical function evaluation toward stateful, imperative computation, and it became a central concern of formal semantics in the 1960s–70s.

Origins · 1 minChristopher Strachey and how this was discovered
CS
Christopher Strachey
1916–1975 · Pioneer of denotational semantics and the mathematical treatment of assignment and the store

Strachey, working with Dana Scott at Oxford, developed the mathematical framework in which the environment (names to locations) and the store (locations to values) are treated as formal mathematical functions, giving assignment and aliasing a rigorous denotational meaning rather than an informal operational description.

History
1936
— Turing formalizes computation via the Turing machine, whose tape cells are an early model of addressable, mutable storage.
1958–1960
— Algol 60 committee work formalizes variables as denoting storage locations distinct from the values they hold.
1969
— C.A.R. Hoare's axiomatic semantics for imperative programs treats assignment as a store-updating operation with a formal proof rule.
1971
— Strachey and Scott's denotational semantics gives environments and stores a precise mathematical (domain-theoretic) foundation.
1977
— Backus's "Can Programming Be Liberated from the von Neumann Style?" critiques store-based, assignment-centric computation and champions value-only functional models.

Where this matters

Every general-purpose compiler and virtual machine implements this model concretely: stack frames and heaps are the store; symbol tables and register/offset allocation are the environment. Garbage collectors exist because the store must eventually reclaim locations no name can reach — a direct consequence of separating "location" from "name bound to it." Memory-safety bugs in C/C++ (dangling pointers, use-after-free, buffer overruns) are, formally, violations of the store-locality invariant: a name or pointer denotes a location whose lifetime has ended or whose bounds it has escaped. Languages like Rust encode ownership and borrowing rules specifically to enforce, at compile time, that aliasing of mutable locations is either forbidden or provably safe — a direct, modern engineering response to the aliasing-coherence problem described here. Debuggers, memory profilers, and "reference vs. value" documentation in Python, Java, and JavaScript all exist to help programmers reason correctly about exactly this distinction. It is also the conceptual seed for Environments (formalizing the name-to-location map as execution enters and leaves scopes) and for the operational semantics of Expressions and Statements (where statements are precisely the language constructs that transform the store).

Ask the tutor: How does this value/location/store model extend to explain closures capturing variables by reference versus by value?

Is a "value" the same thing as a "variable"?

No. A value is an abstract piece of data, like the integer `5` or the string `"hi"` — it has no notion of being changed or having an address. A variable is a name, and depending on the language, that name is bound either directly to a value or to a mutable location that currently holds a value. Conflating the two is the source of most beginner confusion about assignment: assignment does not change a value (values are immutable mathematical objects), it changes what value a location holds, or in value-only languages, it rebinds a name to a different value entirely.

What exactly is the "store," concretely, in a real running program?

In an actual implementation, the store is the union of the call stack (holding local variables and parameters in stack frames) and the heap (holding dynamically allocated objects). A location is a memory address or a stack offset; the store is the full contents of RAM addressable by the program at a given moment. The environment, correspondingly, is realized by the compiler's symbol table at compile time (mapping names to stack offsets or heap-reference slots), so that at runtime, variable access compiles down to direct, fast address arithmetic rather than a name lookup.

How does this relate to "pass by value" vs. "pass by reference"?

Pass-by-value means the callee gets a fresh location initialized with a copy of the argument's value — mutating the parameter cannot affect the caller's location. Pass-by-reference means the callee's parameter name is bound to the same location as the caller's argument, so mutation is visible to the caller. Many languages (Java, Python, JavaScript) are pass-by-value of a reference: the reference itself is copied, so reassigning the parameter doesn't affect the caller, but mutating the object the reference points to does, because both references share one heap location.

Why do garbage collectors exist, given this model?

Because `Declare` allocates fresh locations but nothing in the model above ever removes them, real stores would grow without bound. A garbage collector periodically scans the environment (and reachable structures) to find which locations are still reachable from some name, and reclaims all others. This is only meaningful because of the location/value split: a garbage collector reasons about reachability of locations, which is a completely different question from what values those locations happen to hold.

What's the difference between a shallow copy and a deep copy in terms of the store?

A shallow copy allocates one fresh top-level location but copies the references stored inside unchanged, so nested objects remain aliased between the original and the copy. A deep copy recursively allocates fresh locations for every nested object reachable from the original, so no location is shared between original and copy at any depth. Whether a copy needs to be shallow or deep depends entirely on whether downstream code relies on independence of nested mutable state — a shallow copy is cheaper (`O(1)` extra allocation) but can silently reintroduce aliasing bugs at depth.

Does this model apply to purely functional languages with no mutable variables?

Only partially — a purely functional language with no assignment can bind names directly to values, collapsing the environment-then-store indirection into a single environment mapping names to values, since there is nothing to "overwrite." However, even functional languages often have a heap-allocated store for compound data (cons cells, closures) to enable structure sharing, and languages with explicit mutable references (like ML's `ref` or Haskell's `IORef`) reintroduce the full three-layer model deliberately, in an otherwise value-oriented language.

How does this connect to undefined behavior like use-after-free in C?

Use-after-free happens when a name (a pointer) still denotes a location whose lifetime the program has ended, e.g., by calling `free()`. The store's invariant that "a location is meaningful only within its allocated lifetime" is violated: the pointer still resolves to an address, but that address may have been reallocated to something else, so reading or writing through it produces meaningless or dangerous results. This is precisely why the value/location/store model must be paired with a lifetime discipline (stack scoping, garbage collection, or ownership systems like Rust's) to be safe in practice.

Before this

Where this leads