completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Scope analysis & variable allocation

Learn how JavaScript engines bind names before execution, choose stack or Context storage, and why closures, eval, and with change allocation.

By the end, you can
  • 01
    Read a scope treeExplain how the engine links each name to the variable it means, before the code runs.
  • 02
    Predict where a variable is keptSay whether a variable stays on the stack, goes into a Context, or is dynamic because of eval or with.
  • 03
    Spot the engine's cluesRecognize V8's Context and ScopeInfo, the bytecode that reads Context slots, and the DevTools Scope pane.

Where variables live is decided early

The engine is the program inside a browser or Node.js that runs your JavaScript. Before it runs a function, the engine reads the code once. During that first read, it does a job called scope analysis:

  • It finds every variable the code makes. Making a variable is called a declaration, like const currency = "₹".
  • It finds every place the code uses a name. Each use is called a reference.
  • It links each reference to the declaration it means.
  • It decides where each variable will be kept while the code runs.

A part of the code where a name can be used is called a scope. A function is one scope. A block in curly braces, { }, is another. Scopes sit inside each other, like boxes inside boxes. Together they form a scope tree.

Earlier lessons covered Scope, Closures, Execution contexts, Hoisting, Stack & heap, eval, and Strict mode. Those lessons showed what JavaScript does. This lesson shows how the engine plans for it before the code runs.

The main idea

Scope analysis answers two questions before a function runs. First: which variable does each name mean? Second: can each variable be thrown away when the call ends, or must it be kept for later? A kept variable goes into a heap object that V8 calls a Context. You'll meet it below.

Real-life analogyA notebook on your desk and a shared class notebook

While you work, you write quick notes in your own notebook. You clear it after class. If a classmate needs one of your notes after you leave, you copy that note into the shared class notebook. If the class list keeps changing, you can't plan who will need what. So you keep everything in the shared notebook.

In real life: Your desk notebook, cleared after class
In JavaScript: A variable that is thrown away when the call ends
In real life: The shared class notebook anyone can reopen
In JavaScript: A Context slot that a closure can use later
In real life: A class list that keeps changing
In JavaScript: Names that eval or with can change while the code runs

Linking names before the code runs

LIVE ANALYZER

JavaScript uses lexical scope. That means a name's meaning comes from where the code is written, not from when it runs. So the engine can link most names just by reading the code.

The part of the engine that reads code text is called the parser. It makes a scope for each of these:

  • a function
  • a block in curly braces
  • a catch (error) part of a try...catch
  • a module, which is a file that uses import or export
  • a class

Then it takes each name the code uses. It starts in the scope where the name is used and looks outward, one scope at a time, until it finds the declaration. If it finds none, it marks the name as global or not found.

Every name here has a declarationPop out in the code editor (opens in a new tab)JavaScript
function priceLabel(price) {  const currency = "₹";  const rounded = Math.round(price);  return currency + rounded;} console.log(priceLabel(19.4));
  • Line 1 makes the function priceLabel with one parameter, price.
  • Line 2 makes currency.
  • Line 3 makes rounded by rounding price. Math.round(19.4) is 19.
  • Line 4 joins currency and rounded.
  • Line 7 calls the function and prints ₹19.

The engine doesn't have to guess what price, currency, and rounded mean. It links each one to its declaration before the first call.

The code return currency + rounded. An arrow goes from currency to an empty box called currency, made on line 2. Another arrow goes from rounded to an empty box called rounded, made on line 3.return currency + rounded;currencyroundedmade on line 2made on line 3
Before anything runs, JavaScript links each name on line 4 to the variable made on line 2 or line 3. The boxes are still empty: only the place is decided, not the value.
Scope analyzer: where is each variable kept?
Each name, colored by where it is kept
function makeCounter() {
  let count = 0;
  const label = "jobs";
  return function next() {
    count += 1;
    return label + ":" + count;
  };
}
stackContextdynamictop level
Scopes
  • script script
  • function makeCounter function
  • function next function
Variables and where they are kept
  • makeCounter top levela top-level variable; engines keep the top level of a script or module in a special place
  • count Contextan inner function uses it, so it needs a slot in a heap Context
  • label Contextan inner function uses it, so it needs a slot in a heap Context
  • next stackno inner function uses it, so it can stay in the call's frame or in registers
Try it yourself

The analyzer reads your code with acorn, builds the scopes, links each name to its variable, and colors each variable by where it is kept. It never runs your code.

This is a small analyzer made for this lesson, not V8 itself. It handles most everyday JavaScript, and it never runs your code.

This playground is a small analyzer made for this lesson. It is much simpler than V8, the engine inside Chrome and Node.js, but it is real code. It uses two libraries, acorn and acorn-walk, to read your code, build the scope tree, link each name, and sort the variables. It never runs your code.

Two places to keep a variable

SORTER

Once every name is linked, the engine decides where each variable lives. There are two main places:

  • The stack is fast, short-lived memory. Each function call gets its own area on it, called a frame. V8 also uses registers: small, fast slots for the values a call is working on. Everything in a frame is thrown away when the call returns.
  • The heap is memory for things that must last longer. V8 keeps such variables in a heap object called a Context. Each kept variable gets its own numbered place in it, called a slot.

A variable used only during the call can stay on the stack. A variable that an inner function uses needs a Context slot, because the inner function might run after the outer call has returned. Putting a variable in a Context is called context allocation.

The V8 team calls this the "variable allocation problem" in the blog post Blazingly fast parsing, part 2: lazy parsing. For each variable an inner function uses, the engine must know: is it on the stack, or in a Context? V8's runtime scope docs name three objects that take part:

  • Scope: what the parser builds while it reads the code.
  • ScopeInfo: a short saved description of a scope and its slots.
  • Context: the heap object that holds the kept values while the code runs.

The table lists the four kinds of decision. Read each row as: "when this is true, the engine picks this place, and this is what to picture."

Where the engine keeps a variable
Where it goesWhen this happensWhat to picture
StackThe variable belongs to one function call, and no inner function uses it.Fast space in the call's frame. It is thrown away when the call returns.
ContextAn inner function can use the variable after the outer call returns.A heap object called a Context, with one slot per kept variable. The inner function links to it.
DynamicDirect eval can read names from a string at run time, or with can change what a name means.The engine keeps those names findable by name, and skips some speed-ups.
Top levelThe variable is at the top of a script or module, outside every function.It lasts as long as the page or program, so engines keep it in a special place, not in one call's frame.

Some names can't be planned ahead. When eval or with is involved, the engine only finds out while the code runs what a name means. This lesson calls those names dynamic. You'll see why in the section on eval and with.

Stack, Context, or dynamic?
  • const rounded = Math.round(price), used only before the function returns
  • let count = 0, read and changed by a returned increment function
  • A local variable next to a strict direct eval("var hidden = 1")
  • A local variable that a non-strict direct eval(code) can see
  • answer read inside with (box) { return answer; }
  • The step parameter, used by a function inner made inside middle
Try it yourself
0 of 6 correct

Sort each card by where the engine has to keep the variable.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

How closures change where variables live

STEP THROUGH

A local variable normally goes away when its function returns. A closure changes that. A closure is a function made inside another function that still uses the outer function's variables. If the inner function can read or change a variable later, that variable must be kept.

So V8 gives the inner function two links: one to its code, and one to the outer call's Context. The Context holds only the variables that must be kept.

Replay: which counter variables are kept
Step 0 of 6Ready
Your turn: follow the blue line

This replay is recorded from the lesson's own analyzer. Step through how each variable is put on the stack or in a Context before the program runs.

Running in
  1. script
Next: line 1
  const scratch = label.trim();  let count = 0;  return function increment() {    count += 1;    return scratch + ":" + count;  };} const next = makeCounter(" jobs ");console.log(next());console.log(next());
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Step through the replay above. It shows what the analyzer decides for each variable:

  • label is used only on line 2, inside makeCounter. It can stay on the stack.
  • scratch is used by increment on line 6. It goes into the Context.
  • count is read and changed by increment on line 5. It goes into the Context too.
Left: a box called label holding " jobs ". It is used only during the call, so it is gone after return. Right: the function increment() points to a cart called Context holding scratch and count. They are kept after return.used only in the call" jobs "labelgone after returnused later by increment()increment()scratchcountContextkept after return
label is used only while makeCounter runs, so it can go when the call ends. increment() uses scratch and count later, so they move into a Context that stays.

Now run the same code. The function you get back uses the same kept variables on every call.

The counter keeps its count between callsPop out in the code editor (opens in a new tab)JavaScript
function makeCounter(label) {  const scratch = label.trim();  let count = 0;  return function increment() {    count += 1;    return scratch + ":" + count;  };} const next = makeCounter(" jobs ");console.log(next());console.log(next());
  • Line 10 calls makeCounter once. label.trim() on line 2 removes the spaces, so scratch is "jobs". The returned increment function is saved as next.
  • Line 11 prints jobs:1.
  • Line 12 prints jobs:2.

Both calls change the same count, because it lives in the Context, not in one call's frame.

V8 doesn't keep every outer variable just because a closure exists. It keeps only the variables the inner functions use. But all closures made by the same outer call share one Context. If one slot holds a big object, and any of those closures is still in use, the Context and the big object stay in memory.

Two functions made by one call share one ContextJavaScript
function buildWidgets() {  const bigConfig = { theme: "dark", cache: new Array(1000).fill("data") };  let clicks = 0;   function track() {    clicks += 1;    return clicks;  }   function readTheme() {    return bigConfig.theme;  }   return { track, readTheme };}
  • Line 2 makes bigConfig, which holds an array of 1000 strings.
  • track, on lines 5 to 8, uses only clicks.
  • readTheme, on lines 10 to 12, uses only bigConfig.
  • Line 14 returns both functions.

Both functions come from the same call to buildWidgets, so they share one Context. That Context holds clicks and bigConfig. While track is still in use, bigConfig stays in memory too, even after readTheme is gone.

This is a common cause of a memory leak: memory your program keeps but no longer needs. An event listener, a timer, or a cache keeps one small closure in use, and the shared Context keeps a big object with it. The later Memory management and Finding memory leaks lessons show how to find what is being kept.

Why eval and with get in the way

DYNAMIC

Scope analysis works best when the written code tells the whole story. Two features break that rule:

  • eval(text) runs a string as JavaScript code. When you call it by its own name, eval(...), it is a direct eval. The string can then use the variables around the call.
  • with (object) { ... } makes the object's properties act like variables inside the block.

Code without "use strict" is called non-strict code. In non-strict code, a direct eval string can read any variable it can see, and it can even make a new var variable in the calling function. The engine can't know any of that until the string arrives at run time.

A with statement has the same problem. An object property can hide a variable with the same name, and the engine only finds out while the code runs. In both cases the engine must play safe: it keeps the names findable by name and skips some speed-ups.

Replay: eval and with make names dynamic
Step 0 of 4Ready
Your turn: follow the blue line

eval and with are the cases where reading the code isn't enough. Step through why the engine has to play safe.

Running in
  1. script
Next: line 1
  let local = 1;  eval("var surprise = local");  return local;} function readWith(box) {  const answer = "lexical";  with (box) {    return answer;  }} console.log(readWith({ answer: "object wins" }));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The next example tries three ways to call eval:

  • sloppyDirect uses direct eval in non-strict code.
  • strictDirect uses direct eval in strict code, after "use strict".
  • indirectEval writes (0, eval). The comma hands back the plain eval function, so the call is no longer direct. An indirect eval runs the string at the top level of the script, outside every function.
Direct, strict, and indirect evalJavaScript
function sloppyDirect() {  const x = "local";  eval("var leaked = x");  return x + " | " + leaked;} function strictDirect() {  "use strict";  const x = "strict local";  eval("var hidden = 1");  return x + " | " + typeof hidden;} globalThis.x = "global";function indirectEval() {  const x = "local";  return (0, eval)("x");} console.log(sloppyDirect());console.log(strictDirect());console.log(indirectEval());
What the code prints

The lesson tests run this code as a normal script. It prints three lines. First, local | local: direct eval can see local variables. Second, strict local | undefined: strict direct eval keeps its var hidden to itself. Third, global: indirect eval runs at the top level and sees the global x.

Strict mode stops eval from adding variables to the calling function. It doesn't stop eval from reading them by name: the string can still name any local variable it can see. So in the tested version of V8 (12.4), that local variable is kept in a Context slot. Other engines may store it in a different way.

with lets an object answer a nameJavaScript
function readWith(box) {  const answer = "lexical";  with (box) {    return answer;  }} console.log(readWith({ answer: "object wins" }));
  • Line 2 makes a variable called answer, holding "lexical".
  • Line 3 starts a with (box) block.
  • On line 4, answer is looked up as a property of box first.
  • Line 8 passes an object that has an answer property, so it prints object wins.
The name answer is looked up. First in the box object, which holds "object wins", so that value is used. The const answer box holding "lexical" is never checked.with (box) { return answer; }answername to find"object wins"box1. found here"lexical"answer2. never checked
Inside with (box), the name answer is looked up in the box object first. The object has answer, so the function returns "object wins" and never reaches the variable.

Strict mode doesn't allow with at all: it is a syntax error. Even in non-strict code, write the property access yourself, like box.answer. People can read it more easily, and the engine can plan for it.

Context objects and context chains

V8 TERMS

The specification is the document that defines JavaScript. It describes each scope as a lexical environment: a list of variables plus a link to the scope outside it. Following those links outward gives a chain of scopes.

V8 does the same work with its own objects:

  • While it reads the code, it builds a Scope for each scope.
  • It saves what it needs later in a ScopeInfo.
  • While the code runs, it makes a Context for each scope that has kept variables.
  • Each Context links to the Context outside it. Those links form the context chain.
Which variables inner keeps
Functions inside functions make a context chainPop out in the code editor (opens in a new tab)JavaScript
function outer() {  let total = 0;  return function middle(step) {    let label = "count";    return function inner() {      total += step;      return label + ":" + total;    };  };} const inner = outer()(2);console.log(inner());console.log(inner());
Variables kept for inner
totalfunction outer

an inner function uses it, so it needs a slot in a heap Context

stepfunction middle

an inner function uses it, so it needs a slot in a heap Context

labelfunction middle

an inner function uses it, so it needs a slot in a heap Context

Try it yourself
The analyzer found 3 variables kept in a Context

inner uses variables from both middle and outer. So it reaches them through a chain of Context objects.

This list comes from the same analyzer as the playground above. It reads the code; it doesn't run it.
  • inner uses total, which outer makes on line 2.
  • It also uses step and label, which middle makes on lines 3 and 4.
  • Line 12 calls outer(), then calls the returned middle with 2.
  • Lines 13 and 14 print count:2 and then count:4.
The function inner() points to a Context made by middle(2), holding step and label. That Context points to a Context made by outer(), holding total.inner()the closuresteplabelContextfrom middle(2)totalContextfrom outer()
inner() keeps a link to the Context made by middle(2), which holds step and label. That Context links on to the one made by outer(), which holds total.

The table puts the specification's words next to V8's words. Read each row as: "this term, what it is, and why it matters."

Specification words and V8 words
TermWhat it isWhy it matters
Lexical environment (specification)The specification's model of a scope: a list of variables plus a link to the scope outside it.It says what JavaScript must do when it looks up a name.
Scope (V8)What V8's parser builds while it reads the code. It lists declarations, uses of names, and where each variable goes.V8 uses it before it makes any bytecode.
ScopeInfo (V8)A short saved description of a scope: its kind, its names, and its Context slots.V8 uses it when the code runs, when you debug, and when it undoes a speed-up.
Context (V8)A heap object, made while the code runs, with one slot per kept variable.Closures link to it, so kept variables last after the call returns.
Registers and stack slotsFast space in a call's frame, for variables that aren't kept and for in-between values.Bytecode such as Star and Ldar stores and loads these values.

The V8 blog post on faster class features uses the same words for private methods. V8 follows the context chain, reads a slot whose number it already knows, and uses that value. Variables kept for closures are found the same way.

You can see these choices in V8's bytecode. Bytecode is the list of small instructions V8 makes from your code before it runs it. Each instruction has a name, called an opcode:

  • Opcodes with ContextSlot in the name read or write a Context slot.
  • PushContext makes a Context the current one.
  • Star0 stores a value in register 0, on the stack side.

The table lists what to look for. Read each row as: "for this kind of variable, look for these opcodes; they tell you this."

Opcodes to look for in Node 22 (V8 12.4)
Kind of variableOpcodes to look forWhat they tell you
A variable no inner function usesStar0, Ldar, and math opcodesThe value stays in registers, in the call's frame.
A variable kept for a closureCreateFunctionContext, PushContext, LdaCurrentContextSlot, StaCurrentContextSlotThe outer function made a Context, and the inner function reads and writes its slots.
A variable next to strict direct evalCreateFunctionContext, StaCurrentContextSlotIn the tested V8 version, the variable goes into a Context, so the eval string can read it by name.
A variable from further out on the chainLdaContextSlot and StaContextSlot (or the read-only versions), with a depthThe closure goes a known number of steps out the context chain, then reads a known slot.

To print the bytecode yourself, save one of the probe programs below as a file, then run this command in a terminal. --print-bytecode-filter=inner prints only the function called inner.

Print V8 bytecode for one functionShell
node --print-bytecode --print-bytecode-filter=inner captured.js
# Node 22 / V8 12.4: look for LdaCurrentContextSlot,
# StaCurrentContextSlot, LdaContextSlot, StaContextSlot, and PushContext.
The tests check opcode names only

Bytecode positions and memory addresses change between runs. So the lesson tests only check that some opcode names appear. For kept variables they look for PushContext, LdaCurrentContextSlot, StaCurrentContextSlot, LdaContextSlot, and StaContextSlot. For a variable that isn't kept, they look for Star0. The strict direct-eval probe also makes a Context, so the eval string can read local by name.

Each probe is a tiny program for one case.

Probe 1: a variable kept for a closureJavaScript
function captured() {
  let x = 0;
  return function inner() {
    x += 1;
    return x;
  };
}
const f = captured();
f();
Probe 2: a variable that isn't keptJavaScript
function uncaptured() {
  let x = 0;
  x += 1;
  return x;
}
uncaptured();
Probe 3: a variable next to strict direct evalJavaScript
function strictEval() {
  "use strict";
  let local = 1;
  eval("local");
  return local;
}
strictEval();
Probe 4: variables from two outer functionsJavaScript
function outer() {
  let x = 1;
  return function middle() {
    let y = 2;
    return function inner() {
      x += 1;
      y += 1;
      return x + y;
    };
  };
}
const inner = outer()();
inner();

Seeing this in DevTools

Chrome DevTools lets you look at these ideas. When your code is paused at a breakpoint, the Scope pane lists the variables you can see, in groups:

  • Local: the variables of the function that is running.
  • Closure: outer variables kept in a Context for this function.
  • Block: let and const variables of the current block.
  • Script: let and const variables at the top level of the script.
  • Global: the global variables.

Some variables may not show a value. A variable that was never kept, or that the engine removed to save work, may show as unavailable or "optimized out". The exact words depend on the Chrome version.

How to use DevTools here

Use the Scope pane to ask: "Which variables are really kept here?" For memory questions, use a heap snapshot: a list of everything in memory at one moment. The Scope pane shows what the engine can show you. It doesn't promise a value for every variable in your code.

In real projects, avoiding eval and with matters for more than style:

  • Names the engine can link early let it make your code faster.
  • Keeping closures small stops a long-lived closure from holding a big object by accident.

Common misconceptions

"Every local variable lives on the heap."

No. A local that no inner function uses can stay in the call's frame or in registers.

"A closure keeps every outer variable alive."

No. V8 keeps only the variables inner functions use. But closures made by the same outer call share one Context, so one closure can keep another's variables alive.

"eval is slow only because reading the string takes time."

Reading the string does take time. But non-strict direct eval also forces the engine to play safe with every name it could reach.

"Strict eval and indirect eval do the same thing."

No. Strict direct eval can read the caller's local variables but keeps its own variables to itself. Indirect eval runs at the top level and can't see local variables at all.

"The DevTools Scope pane is an exact map of memory."

No. It shows what the engine can show you, after its speed-ups. It is not a list of where every value is stored.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict the price label

Work through the function in your head and type what it prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function priceLabel(price) {
  const currency = "₹";
  const rounded = Math.round(price);
  return currency + rounded;
}

console.log(priceLabel(19.4));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeFind the variable that goes into the Context

    Which variable must be kept in a Context?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function makeCounter() {
      let count = 0;
      return function next() {
        count += 1;
        return count;
      };
    }
    const next = makeCounter();
    console.log(next(), next());

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeStrict eval keeps its var

      Which word in the output shows that strict direct eval didn't add its var to the function?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function sloppyDirect() {
        const x = "local";
        eval("var leaked = x");
        return x + " | " + leaked;
      }
      
      function strictDirect() {
        "use strict";
        const x = "strict local";
        eval("var hidden = 1");
        return x + " | " + typeof hidden;
      }
      
      globalThis.x = "global";
      function indirectEval() {
        const x = "local";
        return (0, eval)("x");
      }
      
      console.log(sloppyDirect());
      console.log(strictDirect());
      console.log(indirectEval());

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFollow the context chain

        Type the two printed lines, with a comma between them.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function outer() {
          let total = 0;
          return function middle(step) {
            let label = "count";
            return function inner() {
              total += step;
              return label + ":" + total;
            };
          };
        }
        
        const inner = outer()(2);
        console.log(inner());
        console.log(inner());

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeMake the lookup plain again

          Which feature should you remove so the engine can link every name before the code runs?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          function readAnswer(box) {
            const fallback = "lexical";
            with (box) {
              return answer || fallback;
            }
          }

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz

            8 QUESTIONS
            Scope analysis quiz · 8 questionsScore: first tries count
            1. Question 1 of 8What does scope analysis do before a function runs?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function priceLabel(price) {
                const currency = "₹";
                const rounded = Math.round(price);
                return currency + rounded;
              }
              
              console.log(priceLabel(19.4));

              Choose an answer to see the explanation.

            3. Question 3 of 8When does V8 need a heap Context slot for a local variable?

              Choose an answer to see the explanation.

            4. Question 4 of 8What does this counter print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function makeCounter() {
                let count = 0;
                return function next() {
                  count += 1;
                  return count;
                };
              }
              const next = makeCounter();
              console.log(next(), next());

              Choose an answer to see the explanation.

            5. Question 5 of 8Which sentence about strict direct eval is true?

              Choose an answer to see the explanation.

            6. Question 6 of 8What does indirect eval read here?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              globalThis.x = "global";
              function demo() {
                const x = "local";
                return (0, eval)("x");
              }
              console.log(demo());

              Choose an answer to see the explanation.

            7. Question 7 of 8In Node 22 (V8 12.4), which bytecode names show that a closure's variable is kept in a Context?

              Choose an answer to see the explanation.

            8. Question 8 of 8Why can one closure keep a big object in memory, even if only another closure from the same call uses it?

              Choose an answer to see the explanation.

            Key takeaways

            • Because of lexical scope, the engine can link most names to their variables just by reading the code.
            • A local variable that no inner function uses can stay on the stack, in the call's frame or in registers.
            • A variable that an inner function uses is kept in a slot of a heap object called a Context.
            • Non-strict direct eval and with make names unknown until run time, so the engine has to play safe.
            • ScopeInfo describes which slots a Context has; the Context holds the values while the code runs.

            Final definition: scope analysis is the engine's plan, made before the code runs, for what each name means and where each variable is kept.

            Up next: Bytecode & interpreters, where you'll see these choices turn into bytecode instructions.

            CompleteFrontend Clear concepts. Working examples.