Bindings, Contexts, and Object State
Canonical source:
docs/guide/01-bindings-contexts-and-state.mdat locked revisione99b712ecf658a527b1fe82c8beba2c1286d8dc9. This rendered page is non-normative; the applicable Protos specification remains authoritative.
Status: Non-normative programming guide
Normative owners:
spec/semantics/EXECUTION_AND_CONTROL.md,spec/semantics/OBJECT_MODEL.md,spec/semantics/CALLABLES.md, andspec/semantics/MODULES.md
One of the first surprising things a programmer may notice in Protos is the
absence of var, let, const, or a separate local-variable declaration
construct.
The important point is not that Protos lacks local state. It is that Protos does not create a second semantic category just for local variables.
The short answer
Section titled “The short answer”What programmers commonly call a local variable is a slot of an execution context.
greet: (name) => { message: "Hello " + name print(message)}Conceptually, invoking greet gives the activation an execution context with
local slots such as:
activation context├── name└── messagename is a parameter binding. message is created while the body executes.
Both are represented by the same language-level idea: a slot on an execution
context object.
Execution contexts are themselves ordinary Protos objects. The language does not need a separate hidden “local variable” universe.
Translating familiar concepts
Section titled “Translating familiar concepts”| Familiar concept | Protos model |
|---|---|
| local variable | local slot of an execution context |
| parameter | slot of a fresh activation context |
| captured local | slot in a captured lexical execution context |
| lexical scope chain | lexical-parent relation between execution contexts |
| global variable | no separate global-variable category |
| module-level binding | local slot of the module context |
| field / property / instance state | slot of an ordinary object |
| inheritance lookup | delegation |
| local declaration | bare slot creation with : |
| assignment | modification with = |
These translations are a learning aid, not new terminology that replaces the specification.
Two relations, not one
Section titled “Two relations, not one”The shared use of slots does not mean lexical scope and object delegation are the same relation.
An execution context has ordinary object delegation, like other objects, but lexical lookup follows a distinct lexical-parent relation between execution contexts.
Conceptually:
lexical lookup
current activation context ↓ lexical parentcaptured enclosing context ↓ lexical parent...Object-state lookup instead uses a receiver and its delegation chain:
member lookup
this ↓ delegation parentprototype ↓ delegation parent...ObjectOrdinary delegation of context itself is not secretly part of lexical lookup.
This distinction matters throughout the language.
: means create here
Section titled “: means create here”For a bare creation:
x: 10Protos does not search for an existing x.
After evaluating the value, it attempts to create a local slot named x on the
current execution context.
Inside a Closure body, the useful mental model is:
x: 10conceptually corresponding to:
context.x: 10Creation can therefore intentionally shadow an outer lexical binding, a prelude binding, or receiver state without modifying any of them.
Trying to create the same local slot twice is an error rather than silently turning creation into assignment.
= means modify something that already exists
Section titled “= means modify something that already exists”For a bare assignment:
x = 20Protos does not create x.
The assignment search is conceptually:
current execution context: local x? ↓ nolexical parent: local x? ↓ no... ↓ lexical chain exhaustedthis: local x?The nearest existing lexical binding wins. If no lexical binding exists, an
eligible local slot of this may be selected when the current execution has an
ordinary receiver.
The search does not follow ordinary delegation to find a write destination. An inherited slot can be readable without being writable through a descendant.
If no destination exists, assignment fails. = never silently becomes :.
A bare read has a different search
Section titled “A bare read has a different search”A bare read:
xfirst searches local slots through the lexical-context chain.
Only after that lexical chain is exhausted, when the current execution has an
ordinary receiver, lookup may fall back to ordinary receiver lookup starting at
this and continuing through delegation.
Conceptually:
current execution context ↓ lexical parentcaptured lexical contexts ↓standard prelude when present ↓ lexical chain exhaustedthis ↓ delegationreceiver parentsAt module top level there is no additional implicit global receiver namespace: the module context and its prelude parent provide the top-level lexical path.
If lookup fails, Protos signals a fresh SlotNotFound Error. Missing lookup does
not produce null.
Creation, modification, and reading therefore deliberately use related but different algorithms:
x: value // create herex = value // modify an existing eligible bindingx // read through lexical lookup, then receiver lookup when applicableWhy writes do not delegate
Section titled “Why writes do not delegate”Consider:
animal: { alive: true}
dog: animal { name: "Rex"}Reading inherited state is ordinary:
dog.aliveBut this is invalid:
dog.alive = falsealive belongs locally to animal, not dog. A write through dog does not
silently mutate the ancestor prototype.
To give dog its own state, say so explicitly:
dog.alive: falsedog.alive = trueThe first expression creates a local override. The second modifies that local slot.
This is one reason Protos distinguishes creation from modification instead of using one declaration/assignment form with context-dependent meaning.
Receiver state is not lexical state
Section titled “Receiver state is not lexical state”An object body is not automatically inserted into the lexical environment captured by methods created inside it.
Consider:
animal: { name: "animal"
speak: () => { print(name) }}
dog: animal { name: "Rex"}
dog.speak()The name inside speak is not captured as a lexical variable merely because
the Closure was created in the body of animal.
When genuine lexical contexts do not provide name, the bare read falls back to
the dynamic receiver. The call above therefore resolves name against dog
and prints "Rex".
That separation is essential:
- lexical bindings belong to execution contexts;
- receiver state belongs to objects;
- a method can still use bare-name syntax because lookup composes those two mechanisms in a defined order.
To refer explicitly to receiver state, use a member operation such as:
this.namethis.name = "Max"To create new receiver state explicitly:
this.nickname: "Rex"Closures capture contexts, not snapshots
Section titled “Closures capture contexts, not snapshots”Closures capture genuine lexical execution contexts by reference.
makeCounter: () => { n: 0
() => { n = n + 1 n }}The returned Closure does not receive a copied numeric snapshot of n. It
retains access to the lexical context that owns the n slot.
Repeated calls therefore observe and modify the same captured binding while that context remains alive.
This is another consequence of the same model rather than a special Closure-only variable system.
Parameters fit the same model
Section titled “Parameters fit the same model”Parameters do not require their own storage category either.
add: (a, b) => { result: a + b result}A fresh activation context contains the parameter slots a and b. result
is then created as another local slot in that activation context.
The syntactic roles differ, but lookup operates over context slots rather than over separate parameter and local-variable namespaces.
Module bindings fit the same model
Section titled “Module bindings fit the same model”A module executes in its own moduleContext.
Top-level creation:
answer: 42creates a local slot of that module context. The module context has the frozen standard prelude as its lexical parent.
There is no separate Process-wide or language-wide mutable “global variable” namespace hiding behind top-level syntax.
Cross-module access is explicit through module instances and ordinary member access.
context makes the model visible
Section titled “context makes the model visible”context is the intrinsic pseudo-identifier for the current execution-context
object.
It can be useful when code deliberately wants to talk about that context as an object. This does not mean every ordinary object operation participates in lexical lookup.
The standard Context object is the ordinary delegation prototype used by
execution-context objects. It is distinct from the intrinsic context
pseudo-identifier.
Keep the two relations separate in your mental model:
lexical parent relation determines bare lexical lookupordinary delegation determines ordinary member lookupPractical rules to remember
Section titled “Practical rules to remember”When writing ordinary Protos code:
- Use
name: valuewhen you mean create a binding here. - Use
name = valuewhen you mean modify an existing binding. - Use
this.namewhen you specifically mean receiver state. - Expect Closures to observe later changes to captured lexical bindings because contexts are captured by reference.
- Do not expect an object containing a method to become that method’s lexical scope.
- Do not expect a write through a descendant to mutate inherited prototype state.
- Treat missing lookup as an Error, not as
null.
These rules all follow from the context/slot/delegation model rather than from independent special cases.
Run the executable lessons
Section titled “Run the executable lessons”The corresponding executable tutorial material starts in:
../../protos/tutorials/01-values-and-slots/01-values.protos../../protos/tutorials/01-values-and-slots/02-create-slots.protos../../protos/tutorials/01-values-and-slots/03-assignment.protos
Continue with ../../protos/tutorials/README.md
for the progressive executable sequence.
Normative references
Section titled “Normative references”For exact behavior, consult:
../../spec/semantics/EXECUTION_AND_CONTROL.mdfor execution contexts, lexical parents, bare lookup, creation, assignment, and receiver fallback;../../spec/semantics/OBJECT_MODEL.mdfor slots, delegation, member reads/writes, and object state;../../spec/semantics/CALLABLES.mdfor Closure capture, methods, receiver binding, and invocation;../../spec/semantics/MODULES.mdfor module contexts, top-level bindings, imports, and module isolation.
Those documents define the language. This chapter only explains how their rules fit together for programmers.