Runtime Values, Types and Environments

Inside an interpreter every value needs a label saying what it is — "this is a number", "this is a string" — so operators can check what they are working with.

Learning Objectives

Represent runtime values: primitives and compound values, tagged values and unions, boxing and unboxing, dynamic type tags.

Explain type errors, truthiness, equality versus identity, numeric types and coercion, string representation and null-like values, and missing versus null values.

Model environments: global, local, nested, chains; lexical versus dynamic scope; shadowing; assignment semantics; copy versus reference; mutability; and inspect values for debugging.

Plain-Language Explanation

Inside an interpreter every value needs a label saying what it is — "this is a number", "this is a string" — so operators can check what they are working with. Variables are names in tables, and tables are nested: a lookup searches the innermost table first, then the enclosing ones. Whether two variables share the same object or have separate copies decides whether changing one affects the other.

Precise Technical Explanation

Runtime value representation A runtime value must carry enough information for every operation: its type and its payload. MiniLang (in TypeScript) uses the host's values directly: JavaScript numbers, strings, booleans, null for nil, and objects for functions — the host's own tags provide the type.

Primitive values and compound values Primitives (numbers, Booleans, nil) are atomic; compound values (strings in many runtimes, lists, objects, functions with captured environments) have parts and usually live on the heap.

Tagged values and tagged unions C implementations commonly use a struct { type tag; union { double number; bool boolean; Obj* object; } as; } — the tag says which union member is valid. Every operation switches on the tag.

Boxing and unboxing Boxing wraps a primitive in a heap object so it can be treated uniformly (Java Integer,.NET boxing an int to object); unboxing extracts it. Boxing costs an allocation; runtimes avoid it with tagged pointers or NaN-boxing (Module 25).

Dynamic typing and runtime type tags In dynamically typed languages each value's tag is checked at run time (typeof in JavaScript, type() in Python, the type checks in MiniLang's operators).

Static typing and runtime checks Statically typed runtimes still check some things at run time: array bounds, casts (ClassCastException), null dereferences, integer division by zero.

Operations applied to values of the wrong type: MiniLang reports them with the operator and both types.

Truthiness and falsiness Which values count as false in conditions: MiniLang nil, false; Python also 0, "", empty containers; JavaScript also 0, -0, NaN, "", undefined.

Equality, identity, and object equivalence Identity: the same object (is in Python, === on objects in JavaScript, ReferenceEquals in.NET). Equality: equal by some definition (==/__eq__, equals). Structural equivalence compares contents.

Numeric types and coercion MiniLang has one numeric type (double). Others distinguish integers and floats, fixed and arbitrary precision (Python ints are unbounded), and may coerce implicitly (1 + 2.5, JavaScript's "3" * 2 === 6).

Strings may be UTF-8, UTF-16 (Java, JavaScript, .NET) or flexible (CPython's PEP 393); they are often immutable and may be interned.

Null / nil / None / undefined concepts A value meaning "no value": nil (MiniLang, Lua), None (Python), null (Java, C#), and in JavaScript both null and undefined.

Missing values versus null values A *missing* binding (undeclared variable, absent dictionary key, undefined property) differs from a binding *holding* null. MiniLang: reading an undeclared name is an error; a variable may hold nil.

Environments and variable bindings A binding maps a name to a storage location holding a value.

Bindings visible everywhere; in MiniLang, top-level let and fun, plus the native str.

Created for blocks and function calls; destroyed (or left for the garbage collector) when they end — unless captured by a closure.

Blocks inside functions inside blocks: each environment points to its parent.

Lookup walks the chain from innermost to outermost:

A lookup of x from the local environment misses locally and finds x in the global environment.

The chain follows where code is *written*: a function's environment links to the environment where the function was defined.

Dynamic scope as a contrast Under dynamic scope the chain follows where code is *called from*. Emacs Lisp (by default in older code), early Lisps and Bash variables behave this way; it makes behaviour depend on callers and is rare in modern languages.

An inner declaration with the same name hides the outer one inside its scope.

Assignment changes the binding found by lookup; it does not create one (MiniLang). Python's assignment creates a local unless declared global/nonlocal; JavaScript's sloppy mode creates a global on assignment to an undeclared name (strict mode forbids it).

Copy versus reference behavior Assigning a value type copies it; assigning a reference copies the reference, so both names reach the same object (aliasing). MiniLang has no mutable compound values, so aliasing is visible only through closures sharing a captured variable.

Immutable and mutable values Immutable values (numbers, MiniLang strings) can be shared freely; mutable objects require reasoning about who else holds a reference.

Runtime representation versus source-language abstraction A source-level "integer" may be a tagged pointer, a boxed object or a register; the language promises behaviour, not layout.

Value debugging and inspection Debuggers print values with types; MiniLang's visualizers show strings quoted ("1") so they are not confused with numbers (1).

global: { x → 10 }
    ↓
local:  { y → 20 }

Why It Matters in Real Systems

Value representation drives performance (boxing, allocation), correctness (equality bugs, null handling) and security (type confusion).

Key Terms

Runtime marker of a value's type Wrapping a primitive in a heap object Identity / equality Same object / equal by a definition Linked environments searched for a name Inner declaration hiding an outer one

Syntax or Instruction Format

No dedicated source syntax; this is a runtime or architecture concept. Tagged value (illustrative C):

typedef enum { VAL_NIL, VAL_BOOL, VAL_NUMBER, VAL_OBJ } ValueType;
typedef struct { ValueType type; union { bool boolean; double number; struct Obj* obj; } as; } Value;

Complete Example

MiniLang's value rules — truthiness, equality without conversion, shadowing and an environment chain (both back ends):

The environment chain while the block runs (treetrace mode — innermost environment first):

Identity, equality, boxing and aliasing in production runtimes — Python (CPython 3.13.1):

…and Java's boxing (Temurin JDK 21):

print(0 == false);          // different types: false
print("1" == 1);            // no conversion: false
print(nil == nil);
if (0) print("0 is truthy in MiniLang");
if ("") print("so is the empty string");
let x = "global x";
{
  let x = "block x";        // shadows the global inside the block
  let y = "block y";
  print(x + " / " + y);
}
print(x);
print(str(10 / 4) + " is a number turned into a string");
false
false
true
0 is truthy in MiniLang
so is the empty string
block x / block y
global x
2.5 is a number turned into a string
let x = 10;
{
  let y = 20;
  print(x + y);
}
define  define x = 10 in global scope   env: global{x=10}
enter   enter block — new scope   env: block{} → global{x=10}
define  define y = 20 in block scope   env: block{y=20} → global{x=10}
lookup  look up 'x' → 10   env: block{y=20} → global{x=10}
lookup  look up 'y' → 20   env: block{y=20} → global{x=10}
print   print → 30   env: block{y=20} → global{x=10}
exit    leave block — its variables go out of scope
a = [1, 2]
b = a                 # copies the reference: both names reach one list
b.append(3)           # mutation through one alias is visible through the other
c = [1, 2, 3]         # an equal but distinct list
print(a, a is b, a == c, a is c)
print(type(1).__name__, type(1.0).__name__, type(None).__name__, 2 ** 100)
print(bool(0), bool(""), bool([]), 0 == False)    # Python's truthiness and int/bool equality differ from MiniLang's
[1, 2, 3] True True False
int float NoneType 1267650600228229401496703205376
False False False True
public class Boxing {
    public static void main(String[] args) {
        Integer small1 = 127, small2 = 127;
        Integer big1 = 1000, big2 = 1000;
        System.out.println(small1 == small2);      // identity: the Integer cache covers -128..127
        System.out.println(big1 == big2);          // identity of two distinct boxes
        System.out.println(big1.equals(big2));     // value equality
        Object boxed = 42;                         // autoboxing int -> Integer
        System.out.println(boxed.getClass().getName());
    }
}
true
false
true
java.lang.Integer

Line-by-Line Explanation

MiniLang: 0 == false and "1" == 1 are false because types differ; 0 and "" are truthy; the block's x hides the global only inside the block; str converts explicitly.

Trace: inside the block the environment chain is block{y=20} → global{x=10}; the lookup of x misses the block and finds the global.

Python: a is b (same object) but a is c is false although a == c; ints are unbounded (2 ** 100); 0 == False is True in Python because bool is a subclass of int.

Java: == on Integer compares references; 127 boxes come from the Integer cache (guaranteed for −128…127 by the JLS), 1000 boxes are distinct objects here — never use == on boxed numbers.

Expected Output

As shown; the Java result for 1000 depends on caching beyond the guaranteed range (configurable with -XX:AutoBoxCacheMax), so equals is the only correct comparison.

Step-by-Step Runtime Behavior

On block entry a new environment links to the global one; on exit it is dropped and lookups find the global x again.

Common Misconceptions

"== means the same thing everywhere." Each language defines its own equality.

"Boxed and unboxed numbers behave the same." Identity, nullability and performance differ.

"Variables hold objects." In reference-based runtimes they hold references.

Common Bugs and Failure Modes

Comparing boxed numbers with == (Java), truthiness surprises (if (count) false for 0 in Python/JS), mutation through an alias, null versus undefined confusion, shadowing a variable you meant to update.

Corrected or Safer Patterns

Use value equality methods; compare explicitly (count is not None); copy before mutating shared data; avoid shadowing in nested scopes; enable linters that flag them.

Debugging and Inspection Techniques

Print types with values; check identity explicitly (id() in Python, System.identityHashCode in Java); inspect environment chains in debuggers (scope panes) or MiniLang's tracer.

Performance Considerations

Boxing allocates; hash-map environments are slower than indexed slots; Module 8 compiles locals to slot indices.

Security Considerations

Type confusion (treating a value as the wrong type) is a classic memory-safety vulnerability in runtimes written in unsafe languages — tag checks and verifiers prevent it.

Portability and Version Caveats

Integer caching ranges, string interning and small-int caches are implementation details (CPython caches −5…256; the JLS mandates boxes for −128…127 only).

Real-World Use Cases

Debuggers' variable panes, serialisers deciding how to encode values, ORMs mapping nulls, configuration systems distinguishing "unset" from "null".

Best Practices

Define truthiness and equality explicitly; separate missing from null; prefer immutable values; avoid identity comparisons on values.

Exercises

E1 · Beginner. What does MiniLang print for print(nil == false);?

Answer: false — different types.

E2 · Intermediate. Show the environment chain at print(a + b) in let a = 1; { let b = 2; { print(a + b); } }.

Answer: block{} → block{b=2} → global{a=1}; b is found one level up, a two levels up.

E3 · Advanced challenge. Implement dynamic scope in the Python interpreter of Module 6, and give a program whose output differs from lexical scope.

Answer: Look up names in the *caller's* environment chain at call time (pass the current environment to function bodies instead of the defining one). Program: let x = "global"; fun show() { print(x); } fun caller() { let x = "caller"; show(); } caller(); prints global under lexical scope and caller under dynamic scope.

Quiz

? In MiniLang, which values are falsy?
+ Only nil and false :: 0 and "" are truthy.
- nil, false, 0 and "" :: That is closer to Python or JavaScript.
- Only false :: nil is also falsy.
- Every value except true :: Most values are truthy.

? Why does Java print false for `big1 == big2` with two boxed 1000s here?
+ `==` compares references, and these are two distinct Integer objects. :: Use equals for values.
- 1000 overflows an Integer. :: It fits easily.
- Autoboxing is disabled. :: It is enabled.
- The JIT optimised it away. :: Unrelated.

? Which statements about environments are correct? (select all that apply)
+ Lookup searches the innermost environment first. :: Then its parents.
+ Lexical scope links a function to where it was defined. :: Not where it is called.
- Assignment always creates a new binding. :: MiniLang assignment updates an existing one.
+ A block's bindings disappear after the block unless captured. :: Closures can keep them alive.

Summary and Key Takeaways

Runtime values carry types (tags) and payloads; equality, identity, truthiness and coercion are language decisions to make explicitly. Environments form chains searched innermost-first; lexical scope follows the program text, and aliasing follows references.

Further Reading

*Crafting Interpreters*, ch. 18 "Types of Values".

*The Java Language Specification*, §5.1.7 "Boxing Conversion".

Python Language Reference, §4.2 "Naming and binding".