Parameter Passing Mechanisms

Consider a procedure `swap(a, b)` meant to exchange two variables, called as `swap(x, y)`. In some languages this works perfectly. In others it silently does nothing. In still others it corrupts memory if `x` and `y` happen to alias the same storage location. The difference is not a bug in any single implementation. It is a difference in parameter passing mechanism, the rule that governs how an actual argument at a call site is bound to a formal parameter inside the callee. Parameter passing sits at the boundary between a language's Evaluation Strategies: Call-by-Value, Name, and Need and its model of Values and Storage. It answers a deceptively simple question: when we write `f(x)`, does the callee receive a copy of `x`'s value, a handle to `x`'s storage location, or something more exotic that synchronizes copies at entry and exit? Each answer produces a distinct observable semantics for assignment inside the callee. It also affects aliasing between parameters, and whether a callee can affect its caller's variables at all. This matters enormously in practice. Fortran's historic call-by-reference (and IBM's call-by-value-result) shaped decades of scientific computing code. It produced genuine aliasing bugs when the same array was passed as two different parameters. C's exclusively call-by-value semantics forces C programmers to pass pointers explicitly to simulate reference behavior. This design choice ripples through the entire language's idiom of "pointer discipline." Ada deliberately offers `in`, `out`, and `in out` modes with value-result-style implementation-defined semantics. This lets programmers state intent while giving compilers freedom to optimize. Pascal's `var` parameters, Java's reference-semantics for objects but value-semantics for primitives, and C++'s explicit `&` reference parameters are all points in this same design space. This article gives a precise, unifying account of the major parameter passing mechanisms. These include call-by-value, call-by-reference, call-by-result, call-by-value-result, and (briefly) call-by-name/need as they interact with parameter passing. It provides an operational semantics for each, a worked example exposing where they diverge, and a catalogue of the aliasing and efficiency tradeoffs. These tradeoffs make this a genuinely load-bearing design decision in language design.

Key takeaways
  • Parameter passing mechanisms define how an actual argument is bound to a formal parameter: by value (copy in), by reference (alias), by result (copy out), or by value-result (copy in and copy out).
  • Call-by-value never lets the callee mutate the caller's variable; call-by-reference always does, immediately and visibly.
  • Call-by-reference and call-by-value-result agree when there is no aliasing between parameters or with non-local variables, but can diverge sharply when aliasing is present.
  • Call-by-result discards the argument's incoming value entirely — the parameter starts uninitialized inside the callee.
  • The choice interacts with Static vs Dynamic Scope and with array/pointer aliasing to determine whether two formal parameters can secretly refer to the same storage.
  • Compilers exploit value-result semantics to pass aggregates in registers efficiently, while reference semantics simplifies large-structure passing but complicates aliasing analysis.
The core insight

Parameter passing mechanisms differ only in when and how a value flows between argument storage and parameter storage — at call time, at return time, or continuously via a shared address — and this timing decision is exactly what determines aliasing behavior.

The idea

A procedure call `f(x)` involves two storage locations that may or may not coincide: the actual argument's storage (the caller's variable `x`) and the formal parameter's storage (a fresh location `a` local to the activation of `f`). A parameter passing mechanism is a precise rule specifying (1) what value or reference is computed at the call site, (2) how the formal parameter's storage is initialized from it, (3) what operations on the formal parameter do to the actual argument's storage during execution, and (4) what happens, if anything, at return. The classical taxonomy distinguishes five mechanisms. Call-by-value copies the argument's current value into fresh parameter storage at call time; the parameter is thereafter an independent variable, and assignments to it never affect the caller. Call-by-reference passes the address (l-value) of the argument; the formal parameter becomes an alias for the argument's storage, so every read or write to the parameter is a read or write to the caller's variable, visible immediately, mid-call. Call-by-result (sometimes called call-by-out) allocates fresh, uninitialized parameter storage at call time — the argument's current value is never read — and copies the final value of the parameter back into the argument's storage at return. Call-by-value-result (call-by-copy-restore) combines the two: it copies the argument's value in at call time (like call-by-value) and copies the parameter's final value back out at return (like call-by-result), with no aliasing during the call body itself. Call-by-name and call-by-need, covered under Evaluation Strategies: Call-by-Value, Name, and Need, instead substitute the unevaluated argument expression (or a memoized thunk) for every occurrence of the parameter, which is a fundamentally different, syntactic mechanism rather than a storage-copying one — it is mentioned here only to contrast its aliasing behavior with the four storage-based mechanisms above. The crucial technical distinction is when a copy happens versus when an alias exists. Call-by-value and call-by-result each involve exactly one copy (in, or out, respectively) and no aliasing at all. Call-by-reference involves no copies and permanent aliasing for the call's duration. Call-by-value-result involves two copies and no aliasing during execution — the parameter is a genuinely separate variable during the call body, but its final value overwrites the argument no matter what happened to the argument's own storage in between. This last point is exactly where value-result and reference semantics can diverge: if some other reference to the same storage — an alias, a global, a second parameter — changes the argument's location during the call, reference semantics sees that change immediately, while value-result semantics overwrites it at the end regardless.

How it works

Each mechanism can be given a precise operational semantics in terms of an environment (mapping names to locations) and a store (mapping locations to values), the same framework used for Values and Storage. Let `env` map identifiers to locations, and `store` map locations to values. A call `f(x)` inside caller environment `env_caller`, with formal parameter `a` in `f`'s body, proceeds as follows under each mechanism.

Call-by-value. At call time, evaluate the argument expression to a value `v = store(env_caller(x))`. Allocate a fresh location `ℓ_a`, set `store(ℓ_a) := v`, and bind `env_callee(a) := ℓ_a`. Execute the body in `env_callee`. On return, discard `ℓ_a`; no write-back occurs.

Call-by-reference. At call time, the argument must be an l-value (a variable, array element, or other addressable location); no fresh location is allocated. Bind `env_callee(a) := env_caller(x)` directly — `a` and `x` denote the same location. Execute the body; every `store` write through `a` is a write through `x`, visible even to other aliases of that location during the call. On return, no copy is needed because there was never a separate copy.

Call-by-result. At call time, allocate a fresh, uninitialized location `ℓ_a` and bind `env_callee(a) := ℓ_a`; the argument's current value is not read. Execute the body (any read of `a` before its first write is undefined behavior in a well-formed program). On return, copy `store(ℓ_a)` into `store(env_caller(x))` — this requires the argument to be an l-value, exactly as in call-by-reference.

Call-by-value-result. At call time, evaluate `v = store(env_caller(x))`, allocate fresh `ℓ_a`, set `store(ℓ_a) := v`, bind `env_callee(a) := ℓ_a`. Execute the body entirely against `ℓ_a`; the caller's location `env_caller(x)` is untouched during execution. On return, copy `store(ℓ_a)` into `store(env_caller(x))`. The pseudocode below makes the four mechanisms directly comparable by expressing the call/return protocol as explicit store operations, parameterized by a `mode` flag. ``` // env: identifier -> location. store: location -> value. // A call site CALL f(actual) inside caller, with formal parameter `a` in f. procedure CALL_BY_VALUE(f, actual, caller_env, store): v := store[ caller_env[actual] ] // read argument's value loc_a := fresh_location() store[loc_a] := v // copy IN callee_env := extend(f.env, a -> loc_a) EXECUTE(f.body, callee_env, store) // no write-back: loc_a simply becomes garbage return procedure CALL_BY_REFERENCE(f, actual, caller_env, store): loc_actual := caller_env[actual] // address, not value callee_env := extend(f.env, a -> loc_actual) // ALIAS, no fresh loc EXECUTE(f.body, callee_env, store) // writes to a hit loc_actual live return procedure CALL_BY_RESULT(f, actual, caller_env, store): loc_a := fresh_location() // UNINITIALIZED callee_env := extend(f.env, a -> loc_a) EXECUTE(f.body, callee_env, store) store[ caller_env[actual] ] := store[loc_a] // copy OUT return procedure CALL_BY_VALUE_RESULT(f, actual, caller_env, store): v := store[ caller_env[actual] ] loc_a := fresh_location() store[loc_a] := v // copy IN callee_env := extend(f.env, a -> loc_a) EXECUTE(f.body, callee_env, store) // isolated from caller during body store[ caller_env[actual] ] := store[loc_a] // copy OUT (overwrites unconditionally) return ``` The subtlety that every implementer must get right: multi-argument calls under value-result must decide an order for the copy-out step, because if two formal parameters alias the same caller location, the last copy-out wins and earlier ones are silently lost — a divergence from reference semantics that is entirely legal but easy to miss (worked out below). Also, under call-by-result and value-result, the evaluation of which location the copy-out targets happens at call time (aliasing of `actual`'s address is fixed then), even though the value being written is only known at return.

Figure
Four mechanisms, one call site: where copies and aliases occur
CALL f(x) — caller location Lx holds value 7 by-value Lx = 7 La = 7 copy in body sets La:=9 — Lx unaffected by-reference Lx = 7 a IS Lx (same box) alias, no copy body sets a:=9 → Lx becomes 9 immediately by-result Lx = 7 La = ? argument value NEVER read copy out, at return, La=9 → Lx=9 by-value-result Lx = 7 La = 7→9 in out, at return: Lx set to 9 unconditionally Key distinction by-value: Lx read once (in), never written back. by-reference: Lx and La are the SAME location — every write inside the call is visible instantly. by-result: Lx never read; written once (out) at return. by-value-result: Lx read once (in), written once (out) — isolated in between; last writer wins under aliasing.

Complexity & correctness

Time and space cost. For a scalar of size `k` machine words, call-by-value and call-by-result each cost exactly one word-copy of size `k` (in, or out, respectively): `Θ(k)` time, `Θ(k)` extra space for the parameter's storage. Call-by-value-result costs `Θ(k)` twice, i.e. still `Θ(k)` asymptotically but with roughly double the constant. Call-by-reference costs `Θ(1)` regardless of `k` — only an address (one word) is passed — which is exactly why languages that pass large aggregates (arrays, records) by reference or value-result rather than raw copy-by-value avoid an unacceptable `Θ(k)` copy for every call on a structure that might be megabytes large. This is the practical reason Fortran, Ada, and Pascal all support reference-like or value-result passing for arrays: call-by-value copy semantics on a large array would be correct but asymptotically wasteful compared to passing one address. Conversely, call-by-value on scalars is cheap and gives the strongest isolation guarantee, which is why C, restricted to call-by-value, remains efficient for `int`/`double` parameters while requiring explicit pointers for large structures.

Correctness as isolation vs. aliasing. The right way to state correctness here is not "does the algorithm terminate" but "does the mechanism produce the intended aliasing structure." Define two formal parameters (or a parameter and a non-local variable) as aliased during a call if they denote the same store location at some point during execution. The precise correctness claims are:

  • Call-by-value introduces no aliasing whatsoever between the callee's parameters and the caller's variables: the parameter's location `ℓ_a` is fresh and never equated with `env_caller(x)`. Proof: by construction, `fresh_location()` returns a location distinct from every location currently reachable from `env_caller`, and no operation in the semantics ever re-binds `a` to `env_caller(x)`. Hence any store update through `a` cannot be observed through `x`. This is why call-by-value functions are referentially transparent with respect to their arguments — a property essential for equational reasoning and for compiler optimizations like common-subexpression elimination across calls.
  • Call-by-reference introduces aliasing that is exactly the aliasing structure of the call site. If two actual arguments `x` and `y` denote the same location (e.g., `swap(v, v)`, or `x` and `y` are both aliases of a shared array element), then the corresponding formal parameters `a` and `b` are aliased too, by direct substitution `env_callee(a) = env_caller(x) = env_caller(y) = env_callee(b)`. This is not a bug — it is the defining, correct semantics of reference passing — but it means static reasoning about a callee's behavior requires knowing the call site's aliasing, which is precisely why alias analysis is a necessary and nontrivial compiler pass whenever reference or pointer parameters are in play.
  • Call-by-value-result and call-by-reference coincide exactly when no aliasing is present among the parameters and non-locals touched by the call. Proof sketch: with no aliasing, each parameter's location is written and read only through its own name during the call body; the sequence of store operations restricted to that location is identical whether the location is `env_caller(x)` itself (reference) or a fresh copy `ℓ_a` whose final value is copied back to `env_caller(x)` at the single point where control returns (value-result) — because no other name observes or mutates `env_caller(x)` in between, the two schedules of writes to that location (immediate vs. deferred-to-return) produce the same final value. When aliasing is present, this argument breaks precisely at the step "no other name observes... in between," which is exactly the source of the classical divergence illustrated below.
  • Call-by-result is correct only for write-only ("output") parameters. Because the incoming value is never read, any callee code path that reads `a` before writing it observes an unspecified value; a correctness obligation on the caller and language definition is that such a parameter is never read-before-written, which is why languages exposing this mode (Ada's `out` parameters) typically forbid or flag reads of an `out` parameter before assignment.

Honesty about tradeoffs. Reference and value-result semantics are asymptotically cheaper for large structures but sacrifice the referential transparency and easy equational reasoning that call-by-value provides; value-result additionally sacrifices the "always immediately visible" property of reference passing, trading it for isolation during the call body (useful for optimizing compilers that want to keep the parameter in a register without worrying about concurrent aliasing) at the cost of surprising last-writer-wins behavior under aliasing. There is no dominant mechanism — each is a genuinely different point in the time/space/aliasing tradeoff space, which is why real languages offer more than one (Ada's three modes; C++'s value vs. reference vs. const-reference parameters).

Worked example

The example that best exposes the differences is the classic aliasing scenario: a procedure with two formal parameters both bound, at the call site, to the same actual variable or overlapping storage — e.g., a global array `A` passed as both an "in" and an "out" parameter, or a swap called with the same variable twice. Consider: ``` procedure P(x, y): x := x + 1 y := y + 10 // global integer g := 5 CALL P(g, g) // both actual arguments are the SAME variable g ``` Trace the store contents of `g` (and, where relevant, the two parameter locations) under each mechanism, assuming left-to-right evaluation and copy-out order `x` then `y` for value-result.

MechanismStep 1 (bind)Step 2 (`x := x+1`)Step 3 (`y := y+10`)Final `g`Why
Call-by-value`Lx=5`, `Ly=5` (both fresh copies of `g`)`Lx=6``Ly=15`5Neither copy is ever written back; `g` never touched.
Call-by-reference`x` and `y` both are `g`'s location`g := g+1` → `g=6``g := g+10` → `g=16`16`x` and `y` alias the same store cell; both writes hit it in sequence, visibly.
Call-by-value-result`Lx=5`, `Ly=5` (copies, isolated)`Lx=6``Ly=15`15 (if copy-out order is x then y) or could differ if order is y then x — here y=15 copied lastBoth copies computed independently from the original `g=5`; whichever copy-out happens last wins, overwriting the other — `x`'s increment is lost.
Call-by-result`Lx=?`, `Ly=?` (uninitialized — reading `x` or `y` before write is undefined)`Lx := ? + 1` — undefined, since `x` was never given 5—undefinedThis mechanism is only well-formed for write-only parameters; this program violates that discipline.

The three well-defined rows already show three different final answers for `g` — 5, 16, 15 — from the same source program, differing only in the parameter-passing rule. This is the textbook demonstration of why the choice is not cosmetic: call-by-reference computes the "fully sequential" answer (16, as if `x` and `y` really were both names for `g` throughout), call-by-value-result computes an answer that depends on the arbitrary, language-defined copy-out order (15, or 6 if the order were reversed), and call-by-value simply never lets the callee see `g` mutate at all (5). Historically, this exact class of example — with real Fortran/Algol arrays passed twice — produced genuine, hard-to-debug numerical differences between compilers that chose reference semantics and compilers that chose value-result for the same source language, because the language standard left the mechanism implementation-defined for aggregate parameters. A second, non-aliased trace confirms the "they agree without aliasing" correctness claim from the previous section: call `P(g, h)` with distinct globals `g=5, h=100`. Under reference: `g` becomes 6, `h` becomes 110. Under value-result: `Lx=5→6` copied back to `g` (now 6), `Ly=100→110` copied back to `h` (now 110) — identical result, exactly as the no-aliasing argument predicted.

Common pitfalls & traps

PitfallWhy it happensConsequence
Assuming reference and value-result always agreeThey agree only absent aliasing; the proof's hypothesis is silently droppedDivergent results across compilers/platforms for the same source, as in the worked example
Reading a call-by-result parameter before writing itMechanism never initializes the parameter from the argumentUndefined / garbage value read, a bug invisible in mechanisms where it "happens to" hold the old value
Assuming call-by-value protects against mutation of pointed-to dataValue-passing copies the pointer, not the pointeeThe pointee is still mutable through the copied pointer — "pass by value" of a reference type is not full isolation
Ignoring copy-out order under value-result with aliased actualsLanguage standard leaves order unspecified or implementation-definedNondeterministic-looking final state, exactly reproducing the `P(g,g)` example
Passing a local variable by reference/value-result out of a returning frameConfusing lifetime of the callee's activation with the aliasing rule itselfDangling reference to a deallocated stack frame — a storage-lifetime bug layered on top of the aliasing mechanism
Treating "pass array by reference" as free of the value-vs-reference question for its elementsConflating the array handle's mechanism with element-level assignment semanticsConfusing bugs when array elements are themselves references (arrays of objects)

Pioneers & history

Origins · 1 minJohn Backus and how this was discovered
JB
John Backus
1924–2007 · Designed Fortran and its early parameter-passing conventions at IBM in the 1950s

Backus led the team that built Fortran, the first widely used high-level compiled language, whose subroutine-call convention passed arguments by reference (in practice, by address) for efficiency on machines where copying arrays was prohibitively expensive. This single implementation choice — later formalized and contrasted against value-result semantics in the Algol and Ada traditions — became one of the most consequential and most debated decisions in early language design, directly motivating the later, more disciplined treatment of parameter modes in Ada.

History
1954
— Fortran's subroutine mechanism passes arguments by reference (address), motivated by the cost of copying arrays on early hardware.
1960
— Algol 60 formalizes call-by-value and introduces call-by-name, prompting the first rigorous comparative study of parameter mechanisms.
1970
s — IBM's Fortran implementations and other compilers adopt call-by-value-result for arrays, exposing the aliasing divergence from pure reference semantics documented above.
1980
— Ada's design explicitly separates `in`, `out`, and `in out` parameter modes, with the underlying mechanism (reference vs. value-result) left implementation-defined for composite types.
1983
— C++ formalizes reference parameters (`&`) as a first-class, syntactically explicit alternative to C's pointer-based simulation of call-by-reference.

Where this matters

Compiler back ends for Fortran, Ada, and Pascal must choose (or the language standard must specify) reference vs. value-result semantics for array and record parameters, and this choice is directly visible whenever aliased arguments are passed — a recurring source of certified-compiler and formal-verification concern in safety-critical avionics and scientific-computing code, where Ada's `in`/`out`/`in out` modes are chosen specifically to let verification tools reason about data flow without committing to one mechanism. C and its descendants (C++, Rust) restrict the language to call-by-value and make reference-like behavior explicit via pointers or `&` references, a design that trades programmer convenience for the referential-transparency guarantees described above and that underlies static analyses (alias analysis, borrow checking) used throughout systems programming. Java and similar managed languages pass primitives by value and object references by value (the reference itself is copied, but the referent is shared), a hybrid that routinely confuses newcomers who expect "pass by reference" for objects; understanding the four mechanisms precisely is exactly what resolves that confusion. Scientific and numerical libraries (LAPACK/BLAS bindings, MPI buffer arguments) still document their parameter-passing assumptions explicitly, because the difference between reference and value-result semantics for large output arrays affects both correctness under aliasing and performance at scale — this article's isolation vs. aliasing framework is the standard vocabulary such documentation relies on.

Ask the tutor: Can you trace `swap(A[i], A[j])` under call-by-reference versus call-by-value-result when `i == j`?

Is call-by-reference the same as passing a pointer, as in C?

Not quite. C has no true call-by-reference — every C parameter is passed by value, including pointer-typed parameters. Passing `&x` gives the callee a value (an address), which it can dereference to mutate `x`'s storage, simulating reference behavior through an explicit extra level of indirection. True call-by-reference, as in Fortran or Pascal's `var` parameters, binds the formal parameter directly to the actual argument's storage location with no separate pointer value ever appearing as a first-class object; the aliasing is built into the calling convention itself, not simulated by the programmer.

Why would any language choose call-by-result instead of just call-by-value-result?

Call-by-result saves the cost of the "copy in" step, which matters when the argument's incoming value is provably irrelevant, as with a genuinely write-only output parameter (Ada's `out` mode is a documented example of this intent). It also lets a compiler avoid ever materializing an initial value for a large output structure. The tradeoff is that any accidental read of the parameter before it is written becomes undefined behavior, so this mechanism is only safe when the language or programmer guarantees write-before-read discipline, which is exactly why Ada pairs the `out` mode with static or dynamic checks against premature reads.

Does call-by-name interact with this taxonomy at all?

Call-by-name is a fundamentally different kind of mechanism: rather than copying or aliasing a storage location, it substitutes the unevaluated argument expression for every occurrence of the parameter, re-evaluating it each time (call-by-need memoizes this). It can simulate reference-like aliasing when the argument is itself a variable, which is precisely the mechanism behind Jensen's Device, a classic trick exploiting call-by-name in Algol 60. But because it operates on expressions rather than storage locations, it is best treated as an evaluation strategy from Evaluation Strategies: Call-by-Value, Name, and Need rather than a fifth entry in this storage-based taxonomy.

How does static vs dynamic scoping affect parameter passing?

Scoping determines which environment a callee's free (non-parameter) variables are resolved against, while parameter passing determines how bound parameters relate to caller storage — the two are largely orthogonal, but they interact under reference and value-result semantics. Under Static vs Dynamic Scope, a callee's free variables always resolve in its defining environment regardless of parameter mechanism, so aliasing between a parameter and a free variable is determined purely by whether the actual argument's location happens to coincide with that free variable's location, which is a call-site fact independent of the scoping rule itself.

Why do languages bother with value-result instead of just always using reference for efficiency?

Value-result gives the compiler freedom to keep the parameter in a register for the duration of the call, since no external code can observe or mutate it mid-call (unlike a true alias, which forces the compiler to assume the memory location might change unpredictably via other paths). This isolation is valuable for optimization and for reasoning about the callee in isolation, at the cost of the copy-out step and the aliasing-order surprise documented in the worked example. Reference semantics is cheaper when the parameter is large and rarely re-read from a register, but harder to optimize around due to potential external mutation.

What does "referential transparency with respect to arguments" mean for call-by-value?

It means that within a call-by-value callee, the formal parameter behaves as an ordinary local variable whose value can only change via explicit local assignment, never as a side effect of anything happening elsewhere in the program during the call. This lets a compiler or a programmer substitute the parameter's initial value for its every use (before any local reassignment) without changing the program's meaning, a form of equational reasoning that reference and value-result parameters do not support in general, because their storage can be affected by, or affect, code outside the immediate assignment statements that mention the parameter's name.

Can a single language mix multiple parameter-passing mechanisms?

Yes, and most modern languages do. Ada exposes `in`, `out`, and `in out` modes mapped (per implementation) onto value/result-style mechanisms; C++ lets a programmer choose value parameters, `&` reference parameters, or `const &` parameters explicitly per formal; Java fixes value semantics for primitives but effectively passes object references by value. The choice can even vary parameter-by-parameter within one call, which is precisely why understanding the four mechanisms individually — rather than assuming a single language-wide rule — is necessary to reason correctly about any specific procedure signature.

Before this