boyan
boyan

Reputation: 31

pre-parsing and execution context

I am trying to better understand what exactly happens when, for example, the V8 engine parses code and defines an execution context for a function.

What most JS courses and books typically discuss is what happens during a "creation phase" and an "execution phase". The language used for JS engines is different: pre-parsing, parsing, AST, baseline and optimized compiling.

My understanding based on my reading so far, although I may be very wrong, is that the "creation phase" is the same as pre-parsing the source code, where the parser does the bare minimum in terms of variable and function declaration so that the code can run. For example, it would not attempt to build an AST for a function before it is invoked in the code. When a function is invoked from the global scope, it will then be pre-parsed (creation phase) and then fully parsed (execution phase). A new execution context with lexical environment, etc. will be created during pre-parsing. This process would then be repeated for each nested function.

Question 1: Are my assumptions correct? Is pre-parsing===creating phase. Question 2: This is the bit that I am mostly unclear about. Does pre-parsing create a function object for a function that is not yet invoked. Is the function object shown in Chrome dev tools at the debugger below, actually created based on pre-parsing alone. Are the argument and caller properties then just updated during the subsequent execution phase.

let fun1 = function(name){
    let newname="Tom";
    console.log(name, newname);
    };
debugger
fun1("John");

//fun1 function as it appears in the dev tools before it is invoked (and presumably before it is added to the execution stack) Script fun1: ƒ (name) arguments: null caller: null length: 1 name: "fun1" prototype: {constructor: ƒ} proto: ƒ ()

//fun1 function after it is invoked and added to the execution stack. Local name: "John" newname: undefined this: Window Script fun1: ƒ (name) arguments: Arguments ["John", callee: ƒ, Symbol(Symbol.iterator): ƒ] caller: null length: 1 name: "fun1" prototype: {constructor: ƒ} proto: ƒ ()

Upvotes: 0

Views: 559

Answers (1)

jmrk
jmrk

Reputation: 40581

What most JS courses and books typically discuss is ...

I don't know "most JS courses and books", and that reference is not specific enough to look it up. If you want a specific comparison to a specific usage of terms somewhere, please specify the source you are referring to.

When a function is invoked from the global scope, it will then be pre-parsed (creation phase) and then fully parsed (execution phase).

Pre-parsing is only useful when it happens (long) before the function is invoked or fully parsed -- that's why it's called pre- parsing. Parsing is not part of execution. Parsing happens as the first step of compiling (or you could say: "right before compiling"; same thing), which must happen before execution can begin. (At least that's the case in modern high-performance engines; you could build an engine that doesn't compile anything but directly interprets source code, which would mean that effectively parsing and executing would be the same thing, but that would be much slower for everything except trivial scripts.)

Question 1: Are my assumptions correct? Is pre-parsing===creating phase.

Generally speaking, there is no close correspondence between high-level conceptual phases of JS execution, and engine-internal implementation details.

Pre-parsing is (part of) an entirely optional performance-enhancing implementation strategy. You can turn it off if you want -- things will be slower, but observable behavior will be the same. The basic idea is that the engine will generate code "lazily", i.e. when a function is invoked the first time (because generating all code "eagerly" up front would make for annoyingly slow startup). In order to be able to do that, it must solve the problem "here's a couple hundred kilobytes of JS code, compile function fun1 please", so it must know where "fun1" is. That's the information that the "pre-parsing" step generates: it basically creates an index mapping function names to the place in the source text where the respective function is defined.

(As another entirely optional performance-enhancing implementation strategy, modern engines make sophisticated choices about internal variable representation/allocation, which in order to produce the correct behavior require pre-parsing to generate additional data about variables referenced from other functions; so if you look at the implementation, you'll see that it is quite a bit more complicated than just identifying function <name> { ... } ranges. You're correct that pre-parsing doesn't build an AST, but it does build scope chains for this reason.)

Question 2: Does pre-parsing create a function object for a function that is not yet invoked.

Pre-parsing, full parsing, and compiling all only affect the code for a function. They are unrelated to the function object itself. When you have a function object, you generally can't tell whether it has been pre-parsed or not, whether its code has been compiled or not, or whether its code has been optimized (=compiled again, later, taking type feedback into account) or not. The function object is created by executing the respective outer function (which may be the code in the global scope). It has internal, hidden references to metadata (code and otherwise) required for its execution.

To emphasize this again: all the details about when-does-the-engine-parse/compile-what are internal implementation details that are different between engines and change over time (as the respective teams improve their engines), so they generally must not affect how your program is executed.

Upvotes: 4

Related Questions