By Xulun, from F(x) Team
Various operation methods at the AST level of JS were introduced in the previous articles. After mastering AST, we are one step away from execution, which is the conversion into the intermediate code, the interpreted bytecode, or IR prepared for a compiler.
Let's take v8 as an example. First is the operating architecture:
There are three unfamiliar proper nouns in this diagram: Ignition, Crankshaft, and TurboFan.
Ignition is the interpreter of v8, Crankshaft is the compiler of the older generation, and Turbofan is the newer one. The bytecode in discussion corresponds to the Ignition bytecode, and the compiled intermediate code is TurboFan IR. Ignition bytecode is the focus of the article.
We can see the sequence of Ignition bytecode by using the d8 tool of v8. Just add the --print-bytecode parameter:
./d8 --print-bytecode
If there are no d8 tools on hand, a node is acceptable:
node --print-bytecode
Now, we are ready for Ignition bytecode.
Let's start with the smallest statement.
undefined;
This is small enough. Let's look at its corresponding three bytecode instructions:
0x63e081d407a @ 0 : 0e LdaUndefined
0x63e081d407b @ 1 : c4 Star0
0x63e081d407c @ 2 : a9 Return
As a statement, it has its return value. This statement is returning undefined.
Ld* belongs to the instruction set loaded into the accumulator. Star is a register operation instruction. Retur
n is the return instruction.
When we enter undefined;
again, as the bytecode has been converted, the conversion will not be done again. The previously generated bytecode will be run directly.
Similarly, there is LdaTrue
bytecode for true
.
0x63e081d42d6 @ 0 : 11 LdaTrue
0x63e081d42d7 @ 1 : c4 Star0
0x63e081d42d8 @ 2 : a9 Return
null
corresponds to LdaNull
:
0x63e081d43e6 @ 0 : 0f LdaNull
0x63e081d43e7 @ 1 : c4 Star0
0x63e081d43e8 @ 2 : a9 Return
There is a special LdaZero
instruction for 0
:
0x63e081d4772 @ 0 : 0c LdaZero
0x63e081d4773 @ 1 : c4 Star0
0x63e081d4774 @ 2 : a9 Return
If it is 1
, there will be a LoadSmi
instruction:
0x63e081d4856 @ 0 : 0d 01 LdaSmi [1]
0x63e081d4858 @ 2 : c4 Star0
0x63e081d4859 @ 3 : a9 Return
LoadSmi is a two-byte instruction. The instruction code 0d
is followed by an immediate number of bytes. Let's look at what will happen when it is -1
:
0x63e081d493a @ 0 : 0d ff LdaSmi [-1]
0x63e081d493c @ 2 : c4 Star0
0x63e081d493d @ 3 : a9 Return
If you want to load an immediate number of two bytes, LdaSmi
will become the LdaSmi.Wide
instruction:
0x2c11081d556a @ 0 : 00 0d 10 27 LdaSmi.Wide [10000]
There is also the Lda.ExtraWide
instruction for the case of 4 bytes. For example,
let n1 = 100_000_000;
The corresponding instruction is:
0x2c11081d5456 @ 0 : 01 0d 00 e1 f5 05 LdaSmi.ExtraWide [100000000]
What if it's 1.1? At this time, the instruction does not fit. The number 1.1 is stored on the heap, and the LdaConstant
instruction reads this value from the heap according to the index of the next byte.
Bytecode Age: 0
0x63e081d4a46 @ 0 : 13 00 LdaConstant [0]
0x63e081d4a48 @ 2 : c4 Star0
0x63e081d4a49 @ 3 : a9 Return
Constant pool (size = 1)
0x63e081d4a0d: [FixedArray] in OldSpace
- map: 0x063e08002209 <Map>
- length: 1
0: 0x063e081d4a19 <HeapNumber 1.1>
What if it's 1+1?:
0x63e081d4c1a @ 0 : 0d 02 LdaSmi [2]
0x63e081d4c1c @ 2 : c4 Star0
0x63e081d4c1d @ 3 : a9 Return
When generating bytecode, the interpreter has calculated the immediate number and will not waste any more instructions.
One interesting thing that is well-known is the comparison between 0.0 and -0.0. For 0.0, the interpreter treats it as 0:
0x63e081d4e0a @ 0 : 0c LdaZero
0x63e081d4e0b @ 1 : c4 Star0
0x63e081d4e0c @ 2 : a9 Return
-0.0 is treated as a floating point constant, but 0.0 is stored on the heap instead of -0.0:
0x63e081d4d26 @ 0 : 13 00 LdaConstant [0]
0x63e081d4d28 @ 2 : c4 Star0
0x63e081d4d29 @ 3 : a9 Return
Constant pool (size = 1)
0x63e081d4ced: [FixedArray] in OldSpace
- map: 0x063e08002209 <Map>
- length: 1
0: 0x063e081d4cf9 <HeapNumber 0.0>
Let's start introducing variables now. If it is a local scope, it is similar to only using immediate numbers. For example:
{let a2=1};
Convert it to bytecode:
0x63e081d500e @ 0 : 0d 01 LdaSmi [1]
0x63e081d5010 @ 2 : c3 Star1
0x63e081d5011 @ 3 : 0e LdaUndefined
0x63e081d5012 @ 4 : a9 Return
If variables are defined globally:
let a1 = 0;
A new instruction StaCurrentContextSlot
will be introduced:
0x63e081d4f06 @ 0 : 0c LdaZero
0x63e081d4f07 @ 1 : 25 02 StaCurrentContextSlot [2]
0x63e081d4f09 @ 3 : 0e LdaUndefined
0x63e081d4f0a @ 4 : a9 Return
Let's start doing some arithmetic. First, focus on the increment operator.
{let a3 = 1; a3 ++;}
The corresponding instructions are Inc
:
0x63e081d53ca @ 0 : 0d 01 LdaSmi [1]
0x63e081d53cc @ 2 : c3 Star1
0x63e081d53cd @ 3 : 75 00 ToNumeric [0]
0x63e081d53cf @ 5 : c2 Star2
0x63e081d53d0 @ 6 : 51 00 Inc [0]
0x63e081d53d2 @ 8 : c3 Star1
0x63e081d53d3 @ 9 : 19 f8 fa Mov r2, r0
0x63e081d53d6 @ 12 : 0b fa Ldar r0
0x63e081d53d8 @ 14 : a9 Return
It is worth noting that before performing arithmetic operations, the ToNumberic
instruction is used to convert the current value to numeric.
For the decrement operator:
{let a4=100; a4--};
It was only replaced by Dec
instructions:
0x63e081d54da @ 0 : 0d 64 LdaSmi [100]
0x63e081d54dc @ 2 : c3 Star1
0x63e081d54dd @ 3 : 75 00 ToNumeric [0]
0x63e081d54df @ 5 : c2 Star2
0x63e081d54e0 @ 6 : 52 00 Dec [0]
0x63e081d54e2 @ 8 : c3 Star1
0x63e081d54e3 @ 9 : 19 f8 fa Mov r2, r0
0x63e081d54e6 @ 12 : 0b fa Ldar r0
0x63e081d54e8 @ 14 : a9 Return
Let's look at an additive example first:
{let a5 = 2; a5 = a5 + 2;}
Translate it into AddSmi instruction:
0x63e081d5712 @ 0 : 0d 02 LdaSmi [2]
0x63e081d5714 @ 2 : c3 Star1
0x63e081d5715 @ 3 : 45 02 00 AddSmi [2], [0]
0x63e081d5718 @ 6 : c3 Star1
0x63e081d5719 @ 7 : c4 Star0
0x63e081d571a @ 8 : a9 Return
If it is not for the immediate value but for two variable operations:
{let a6 = 1; let a7=2; let a8 = a6 + a7;}
It will be replaced by an Add
instruction:
0x63e081d583a @ 0 : 0d 01 LdaSmi [1]
0x63e081d583c @ 2 : c3 Star1
0x63e081d583d @ 3 : 0d 02 LdaSmi [2]
0x63e081d583f @ 5 : c2 Star2
0x63e081d5840 @ 6 : 0b f8 Ldar r2
0x63e081d5842 @ 8 : 39 f9 00 Add r1, [0]
0x63e081d5845 @ 11 : c1 Star3
0x63e081d5846 @ 12 : 0e LdaUndefined
0x63e081d5847 @ 13 : a9 Return
What kind of instructions will be generated if we operate on constants?:
const a11 = 0; a11 +=1;
When saving constants, we still use StaCurrentContextSlot
instructions, but when loading, we use LdaImmutableCurrentContextSlot
. CallRuntime
will be called to call ThrowConstAssignError
exceptions at runtime for AddSmi
instructions.
0x63e081d5b7a @ 0 : 0c LdaZero
0x63e081d5b7b @ 1 : 25 02 StaCurrentContextSlot [2]
0x63e081d5b7d @ 3 : 17 02 LdaImmutableCurrentContextSlot [2]
0x63e081d5b7f @ 5 : 45 01 00 AddSmi [1], [0]
0x63e081d5b82 @ 8 : 65 66 01 fa 00 CallRuntime [ThrowConstAssignError], r0-r0
0x63e081d5b87 @ 13 : c4 Star0
0x63e081d5b88 @ 14 : a9 Return
When a var declared variable is written at the top level, it will be converted into a global variable:
var a13 = 0;
DeclareGlobals
participates at runtime. The var declared variable is finally saved as a global variable through StaGlobal
:
0x63e081d5cda @ 0 : 13 00 LdaConstant [0]
0x63e081d5cdc @ 2 : c3 Star1
0x63e081d5cdd @ 3 : 19 fe f8 Mov <closure>, r2
0x63e081d5ce0 @ 6 : 65 54 01 f9 02 CallRuntime [DeclareGlobals], r1-r2
0x63e081d5ce5 @ 11 : 0c LdaZero
0x63e081d5ce6 @ 12 : 23 01 00 StaGlobal [1], [0]
0x63e081d5ce9 @ 15 : 0e LdaUndefined
0x63e081d5cea @ 16 : a9 Return
The Ignition instruction set provides instructions created by three literals:
This is easy to understand. It is aimed at [], {}, //.
Let's look at several examples:
1. Empty Array
let a1 = [];
As a common operation, Ignition provides a special instruction for it: CreateEmptyArrayLiteral
.
0x24b2081d3386 @ 0 : 7b 00 CreateEmptyArrayLiteral [0]
0x24b2081d3388 @ 2 : 25 02 StaCurrentContextSlot [2]
0x24b2081d338a @ 4 : 0e LdaUndefined
0x24b2081d338b @ 5 : a9 Return
2. Empty Object
let a2={};
Ignition provides a special instruction for it: CreateEmptyObjectLiteral
.
0x24b2081d411e @ 0 : 7d CreateEmptyObjectLiteral
0x24b2081d411f @ 1 : 25 02 StaCurrentContextSlot [2]
0x24b2081d4121 @ 3 : 0e LdaUndefined
0x24b2081d4122 @ 4 : a9 Return
3. Regular Expression
let a3 = /a*/;
We prefer a regular expression with values. The literals are finally converted into strings on the heap.
Bytecode Age: 0
0x24b2081d42e2 @ 0 : 78 00 00 00 CreateRegExpLiteral [0], [0], #0
0x24b2081d42e6 @ 4 : 25 02 StaCurrentContextSlot [2]
0x24b2081d42e8 @ 6 : 0e LdaUndefined
0x24b2081d42e9 @ 7 : a9 Return
Constant pool (size = 1)
0x24b2081d42b5: [FixedArray] in OldSpace
- map: 0x24b208002209 <Map>
- length: 1
0: 0x24b2081d424d <String[2]: #a*>
Ignition
provides LdaNamedProperty
and StaNamedProperty
instructions to access properties by name.
Let's look at an example:
let a4 = {}; a4.a = 1;
The following are the generated instructions:
0x24b2081d4412 @ 0 : 7d CreateEmptyObjectLiteral
0x24b2081d4413 @ 1 : 25 02 StaCurrentContextSlot [2]
0x24b2081d4415 @ 3 : 16 02 LdaCurrentContextSlot [2]
0x24b2081d4417 @ 5 : c3 Star1
0x24b2081d4418 @ 6 : 0d 01 LdaSmi [1]
0x24b2081d441a @ 8 : c2 Star2
0x24b2081d441b @ 9 : 32 f9 00 00 StaNamedProperty r1, [0], [0]
0x24b2081d441f @ 13 : 19 f8 fa Mov r2, r0
0x24b2081d4422 @ 16 : 0b fa Ldar r0
0x24b2081d4424 @ 18 : a9 Return
Constant pool (size = 1)
0x24b2081d43e5: [FixedArray] in OldSpace
- map: 0x24b208002209 <Map>
- length: 1
0: 0x24b208008265 <String[1]: #a>
Note: a4 does not appear because of the Current Context Slot
.
Read it again:
a4['a'];
At this time, a4 will be passed to LdaNamedProperty
as a parameter.
Bytecode Age: 0
0x24b2081d466a @ 0 : 21 00 00 LdaGlobal [0], [0]
0x24b2081d466d @ 3 : c3 Star1
0x24b2081d466e @ 4 : 2d f9 01 02 LdaNamedProperty r1, [1], [2]
0x24b2081d4672 @ 8 : c4 Star0
0x24b2081d4673 @ 9 : a9 Return
Constant pool (size = 2)
0x24b2081d4639: [FixedArray] in OldSpace
- map: 0x24b208002209 <Map>
- length: 2
0: 0x24b2081d437d <String[2]: #a4>
1: 0x24b208008265 <String[1]: #a>
The function is created using the CreateClosure
instruction.
Let's look at an example of an arrow function:
let f1 = () => 0;
The following are the generated instructions:
Bytecode Age: 0
0x24b2081d481a @ 0 : 80 00 00 00 CreateClosure [0], [0], #0
0x24b2081d481e @ 4 : 25 02 StaCurrentContextSlot [2]
0x24b2081d4820 @ 6 : 0e LdaUndefined
0x24b2081d4821 @ 7 : a9 Return
Constant pool (size = 1)
0x24b2081d47ed: [FixedArray] in OldSpace
- map: 0x24b208002209 <Map>
- length: 1
0: 0x24b2081d47b9 <SharedFunctionInfo f1>
The common function is also CreateClosure
:
let f2 = function(x) { return x * x;}
The bytecode is the same as the preceding one:
Bytecode Age: 0
0x24b2081d4996 @ 0 : 80 00 00 00 CreateClosure [0], [0], #0
0x24b2081d499a @ 4 : 25 02 StaCurrentContextSlot [2]
0x24b2081d499c @ 6 : 0e LdaUndefined
0x24b2081d499d @ 7 : a9 Return
Constant pool (size = 1)
0x24b2081d4969: [FixedArray] in OldSpace
- map: 0x24b208002209 <Map>
- length: 1
0: 0x24b2081d4935 <SharedFunctionInfo f2>
Where is the code of the function? It can only be seen when calling. The instruction called is CallUndefinedReceiver
, and the one without parameters is CallUndefinedReceiver0
.
f1();
The bytecode of f1 will be output separately after the call.
[generated bytecode for function: (0x24b2081d4a51 <SharedFunctionInfo>)]
...
Bytecode Age: 0
0x24b2081d4ac2 @ 0 : 21 00 00 LdaGlobal [0], [0]
0x24b2081d4ac5 @ 3 : c3 Star1
0x24b2081d4ac6 @ 4 : 61 f9 02 CallUndefinedReceiver0 r1, [2]
0x24b2081d4ac9 @ 7 : c4 Star0
0x24b2081d4aca @ 8 : a9 Return
...
[generated bytecode for function: f1 (0x24b2081d47b9 <SharedFunctionInfo f1>)]
...
Bytecode Age: 0
0x24b2081d4b52 @ 0 : 0c LdaZero
0x24b2081d4b53 @ 1 : a9 Return
Look at f2:
f2(1.1);
Since f2 has a parameter, the instruction called is CallUndefinedReceiver1
:
[generated bytecode for function: (0x24b2081d4c49 <SharedFunctionInfo>)]
...
Bytecode Age: 0
0x24b2081d4cca @ 0 : 21 00 00 LdaGlobal [0], [0]
0x24b2081d4ccd @ 3 : c3 Star1
0x24b2081d4cce @ 4 : 13 01 LdaConstant [1]
0x24b2081d4cd0 @ 6 : c2 Star2
0x24b2081d4cd1 @ 7 : 62 f9 f8 02 CallUndefinedReceiver1 r1, r2, [2]
0x24b2081d4cd5 @ 11 : c4 Star0
0x24b2081d4cd6 @ 12 : a9 Return
...
[generated bytecode for function: f2 (0x24b2081d4935 <SharedFunctionInfo f2>)]
...
Bytecode Age: 0
0x24b2081d4d5e @ 0 : 0b 03 Ldar a0
0x24b2081d4d60 @ 2 : 3b 03 00 Mul a0, [0]
0x24b2081d4d63 @ 5 : a9 Return
The branch jump instruction is divided into two parts. One is to set the flag bit for the Test class, and the other is to jump according to the flag bit.
Look at an example:
let f3 = (x) => {if (x>0) return 0; else return -1;}; f3(0);
Extract this part of the function body only, which consists of TestGreaterThan
and JumpIfFalse
. JumpIfFalse
executes if it is true. If not, it jumps.
0x24b2081d561a @ 0 : 0c LdaZero
0x24b2081d561b @ 1 : 6e 03 00 TestGreaterThan a0, [0]
0x24b2081d561e @ 4 : 99 04 JumpIfFalse [4](0x24b2081d5622 @ 8)
0x24b2081d5620 @ 6 : 0c LdaZero
0x24b2081d5621 @ 7 : a9 Return
0x24b2081d5622 @ 8 : 0d ff LdaSmi [-1]
0x24b2081d5624 @ 10 : a9 Return
In addition to the if statement, operators like "?." also generate branch judgments. Let's look at the following example:
let f5 = (x) => {return x?.length;}; f5(null);
The null judgment operator corresponds to the JumpIfUndefinedOrNull
instruction.
Bytecode Age: 0
0x24b2081d6766 @ 0 : 0b 03 Ldar a0
0x24b2081d6768 @ 2 : 19 03 fa Mov a0, r0
0x24b2081d676b @ 5 : 9e 08 JumpIfUndefinedOrNull [8](0x24b2081d6773 @ 13)
0x24b2081d676d @ 7 : 2d fa 00 00 LdaNamedProperty r0, [0], [0]
0x24b2081d6771 @ 11 : 8a 03 Jump [3](0x24b2081d6774 @ 14)
0x24b2081d6773 @ 13 : 0e LdaUndefined
0x24b2081d6774 @ 14 : a9 Return
Constant pool (size = 1)
0x24b2081d6739: [FixedArray] in OldSpace
- map: 0x24b208002209 <Map>
- length: 1
0: 0x24b208004cb1 <String[6]: #length>
The judgment we wrote cannot generate this instruction:
let f6 = (x) => {return x===null || x===undefined}; f6(0);
The generated instructions are literally translated into TestNull
and TestUndefined
:
0x24b2081d695a @ 0 : 0b 03 Ldar a0
0x24b2081d695c @ 2 : 1e TestNull
0x24b2081d695d @ 3 : 98 05 JumpIfTrue [5](0x24b2081d6962 @ 8)
0x24b2081d695f @ 5 : 0b 03 Ldar a0
0x24b2081d6961 @ 7 : 1f TestUndefined
0x24b2081d6962 @ 8 : a9 Return
Similarly, the nullish
operator is also a branch statement:
let a2 = a1 ?? 100;
It is also an implementation of JumpIfUndefinedOrNull
, whose author seems to be the same:
0x2c11081d409e @ 0 : 21 00 00 LdaGlobal [0], [0]
0x2c11081d40a1 @ 3 : 9e 04 JumpIfUndefinedOrNull [4](0x2c11081d40a5 @ 7)
0x2c11081d40a3 @ 5 : 8a 04 Jump [4](0x2c11081d40a7 @ 9)
0x2c11081d40a5 @ 7 : 0d 64 LdaSmi [100]
0x2c11081d40a7 @ 9 : 25 02 StaCurrentContextSlot [2]
An exception is a branch instruction in another sense, but it involves exception context.
try{ a4.a = 1;} catch(e) {};
CreateCatchContext
provider creates an exception context and then switches the context through PushContext and PopContext.
Bytecode Age: 0
0x24b2081d60c2 @ 0 : 0e LdaUndefined
0x24b2081d60c3 @ 1 : c4 Star0
0x24b2081d60c4 @ 2 : 19 ff f9 Mov <context>, r1
0x24b2081d60c7 @ 5 : 21 00 00 LdaGlobal [0], [0]
0x24b2081d60ca @ 8 : c2 Star2
0x24b2081d60cb @ 9 : 0d 01 LdaSmi [1]
0x24b2081d60cd @ 11 : c1 Star3
0x24b2081d60ce @ 12 : 32 f8 01 02 StaNamedProperty r2, [1], [2]
0x24b2081d60d2 @ 16 : 19 f7 fa Mov r3, r0
0x24b2081d60d5 @ 19 : 0b f7 Ldar r3
0x24b2081d60d7 @ 21 : 8a 0f Jump [15](0x24b2081d60e6 @ 36)
0x24b2081d60d9 @ 23 : c2 Star2
0x24b2081d60da @ 24 : 82 f8 02 CreateCatchContext r2, [2]
0x24b2081d60dd @ 27 : c3 Star1
0x24b2081d60de @ 28 : 10 LdaTheHole
0x24b2081d60df @ 29 : a6 SetPendingMessage
0x24b2081d60e0 @ 30 : 0b f9 Ldar r1
0x24b2081d60e2 @ 32 : 1a f8 PushContext r2
0x24b2081d60e4 @ 34 : 1b f8 PopContext r2
0x24b2081d60e6 @ 36 : 0b fa Ldar r0
0x24b2081d60e8 @ 38 : a9 Return
Constant pool (size = 3)
0x24b2081d608d: [FixedArray] in OldSpace
- map: 0x24b208002209 <Map>
- length: 3
0: 0x24b2081d437d <String[2]: #a4>
1: 0x24b208008265 <String[1]: #a>
2: 0x24b2081d603d <ScopeInfo CATCH_SCOPE>
Handler Table (size = 16)
from to hdlr (prediction, data)
( 5, 19) -> 23 (prediction=1, data=1)
We also see instructions like LdaTheHole
first that involve internal mechanisms. According to a member of the v8 team, this hole is doing some inspection here. There are several possibilities. It is necessary to distinguish the specific situation. The following articles will carefully analyze the source code.
If we use the optional catch feature added in ES2019, no CreateCatchContext
will be generated. The following is the generated code:
Bytecode Age: 0
0x2c11081d5b3a @ 0 : 0e LdaUndefined
0x2c11081d5b3b @ 1 : c4 Star0
0x2c11081d5b3c @ 2 : 19 ff f9 Mov <context>, r1
0x2c11081d5b3f @ 5 : 21 00 00 LdaGlobal [0], [0]
0x2c11081d5b42 @ 8 : c2 Star2
0x2c11081d5b43 @ 9 : 0d 01 LdaSmi [1]
0x2c11081d5b45 @ 11 : c1 Star3
0x2c11081d5b46 @ 12 : 32 f8 01 02 StaNamedProperty r2, [1], [2]
0x2c11081d5b4a @ 16 : 19 f7 fa Mov r3, r0
0x2c11081d5b4d @ 19 : 0b f7 Ldar r3
0x2c11081d5b4f @ 21 : 8a 06 Jump [6](0x2c11081d5b55 @ 27)
0x2c11081d5b51 @ 23 : 10 LdaTheHole
0x2c11081d5b52 @ 24 : a6 SetPendingMessage
0x2c11081d5b53 @ 25 : 0b f9 Ldar r1
0x2c11081d5b55 @ 27 : 0b fa Ldar r0
0x2c11081d5b57 @ 29 : a9 Return
Constant pool (size = 2)
0x2c11081d5b09: [FixedArray] in OldSpace
- map: 0x2c1108002209 <Map>
- length: 2
0: 0x2c11081d413d <String[2]: #a4>
1: 0x2c1108008265 <String[1]: #a>
Handler Table (size = 16)
from to hdlr (prediction, data)
( 5, 19) -> 23 (prediction=1, data=1)
First, let's look at the traditional C-like for loop:
let f4 = (x) => {for(let i=0;i<x;i++) {console.log(i);}}; f4(10);
Its implementation is similar to the branch statement. An additional JumpLoop
instruction is added.
0x24b2081d582a @ 0 : 0c LdaZero
0x24b2081d582b @ 1 : c4 Star0
0x24b2081d582c @ 2 : 0b 03 Ldar a0
0x24b2081d582e @ 4 : 6d fa 00 TestLessThan r0, [0]
0x24b2081d5831 @ 7 : 99 18 JumpIfFalse [24](0x24b2081d5849 @ 31)
0x24b2081d5833 @ 9 : 21 00 01 LdaGlobal [0], [1]
0x24b2081d5836 @ 12 : c2 Star2
0x24b2081d5837 @ 13 : 2d f8 01 03 LdaNamedProperty r2, [1], [3]
0x24b2081d583b @ 17 : c3 Star1
0x24b2081d583c @ 18 : 5e f9 f8 fa 05 CallProperty1 r1, r2, r0, [5]
0x24b2081d5841 @ 23 : 0b fa Ldar r0
0x24b2081d5843 @ 25 : 51 07 Inc [7]
0x24b2081d5845 @ 27 : c4 Star0
0x24b2081d5846 @ 28 : 89 1a 00 JumpLoop [26], [0](0x24b2081d582c @ 2)
Next, watch what is done behind an iteration of an array:
let a21 = [1,2,3,4,5];
let sum=0;
for(let i of a21) {sum+=i;}
Ignition
provides the GetIterator
instruction for the iterator pattern, followed by the handling of various exceptions:
0x24b2081d5dc2 @ 0 : 0e LdaUndefined
0x24b2081d5dc3 @ 1 : c3 Star1
0x24b2081d5dc4 @ 2 : 21 00 00 LdaGlobal [0], [0]
0x24b2081d5dc7 @ 5 : be Star6
0x24b2081d5dc8 @ 6 : b1 f4 02 04 GetIterator r6, [2], [4]
0x24b2081d5dcc @ 10 : 9f 07 JumpIfJSReceiver [7](0x24b2081d5dd3 @ 17)
0x24b2081d5dce @ 12 : 65 c3 00 fa 00 CallRuntime [ThrowSymbolIteratorInvalid], r0-r0
0x24b2081d5dd3 @ 17 : bf Star5
0x24b2081d5dd4 @ 18 : 2d f5 01 06 LdaNamedProperty r5, [1], [6]
0x24b2081d5dd8 @ 22 : c0 Star4
0x24b2081d5dd9 @ 23 : 12 LdaFalse
0x24b2081d5dda @ 24 : be Star6
0x24b2081d5ddb @ 25 : 19 ff f1 Mov <context>, r9
0x24b2081d5dde @ 28 : 11 LdaTrue
0x24b2081d5ddf @ 29 : be Star6
0x24b2081d5de0 @ 30 : 5d f6 f5 08 CallProperty0 r4, r5, [8]
0x24b2081d5de4 @ 34 : ba Star10
0x24b2081d5de5 @ 35 : 9f 07 JumpIfJSReceiver [7](0x24b2081d5dec @ 42)
0x24b2081d5de7 @ 37 : 65 bb 00 f0 01 CallRuntime [ThrowIteratorResultNotAnObject], r10-r10
0x24b2081d5dec @ 42 : 2d f0 02 0a LdaNamedProperty r10, [2], [10]
0x24b2081d5df0 @ 46 : 96 27 JumpIfToBooleanTrue [39](0x24b2081d5e17 @ 85)
0x24b2081d5df2 @ 48 : 2d f0 03 0c LdaNamedProperty r10, [3], [12]
0x24b2081d5df6 @ 52 : ba Star10
0x24b2081d5df7 @ 53 : 12 LdaFalse
0x24b2081d5df8 @ 54 : be Star6
0x24b2081d5df9 @ 55 : 19 f0 fa Mov r10, r0
0x24b2081d5dfc @ 58 : 19 fa f7 Mov r0, r3
0x24b2081d5dff @ 61 : 21 04 0e LdaGlobal [4], [14]
0x24b2081d5e02 @ 64 : b9 Star11
0x24b2081d5e03 @ 65 : 0b fa Ldar r0
0x24b2081d5e05 @ 67 : 39 ef 10 Add r11, [16]
0x24b2081d5e08 @ 70 : b8 Star12
0x24b2081d5e09 @ 71 : 23 04 11 StaGlobal [4], [17]
0x24b2081d5e0c @ 74 : 19 ee f9 Mov r12, r1
0x24b2081d5e0f @ 77 : 19 f7 f0 Mov r3, r10
0x24b2081d5e12 @ 80 : 0b f9 Ldar r1
0x24b2081d5e14 @ 82 : 89 36 00 JumpLoop [54], [0](0x24b2081d5dde @ 28)
0x24b2081d5e17 @ 85 : 0d ff LdaSmi [-1]
0x24b2081d5e19 @ 87 : bc Star8
0x24b2081d5e1a @ 88 : bd Star7
0x24b2081d5e1b @ 89 : 8a 05 Jump [5](0x24b2081d5e20 @ 94)
0x24b2081d5e1d @ 91 : bc Star8
0x24b2081d5e1e @ 92 : 0c LdaZero
0x24b2081d5e1f @ 93 : bd Star7
0x24b2081d5e20 @ 94 : 10 LdaTheHole
0x24b2081d5e21 @ 95 : a6 SetPendingMessage
0x24b2081d5e22 @ 96 : bb Star9
0x24b2081d5e23 @ 97 : 0b f4 Ldar r6
0x24b2081d5e25 @ 99 : 96 23 JumpIfToBooleanTrue [35](0x24b2081d5e48 @ 134)
0x24b2081d5e27 @ 101 : 19 ff ef Mov <context>, r11
0x24b2081d5e2a @ 104 : 2d f5 05 13 LdaNamedProperty r5, [5], [19]
0x24b2081d5e2e @ 108 : 9e 1a JumpIfUndefinedOrNull [26](0x24b2081d5e48 @ 134)
0x24b2081d5e30 @ 110 : b8 Star12
0x24b2081d5e31 @ 111 : 5d ee f5 15 CallProperty0 r12, r5, [21]
0x24b2081d5e35 @ 115 : 9f 13 JumpIfJSReceiver [19](0x24b2081d5e48 @ 134)
0x24b2081d5e37 @ 117 : b7 Star13
0x24b2081d5e38 @ 118 : 65 bb 00 ed 01 CallRuntime [ThrowIteratorResultNotAnObject], r13-r13
0x24b2081d5e3d @ 123 : 8a 0b Jump [11](0x24b2081d5e48 @ 134)
0x24b2081d5e3f @ 125 : b9 Star11
0x24b2081d5e40 @ 126 : 0c LdaZero
0x24b2081d5e41 @ 127 : 1c f3 TestReferenceEqual r7
0x24b2081d5e43 @ 129 : 98 05 JumpIfTrue [5](0x24b2081d5e48 @ 134)
0x24b2081d5e45 @ 131 : 0b ef Ldar r11
0x24b2081d5e47 @ 133 : a8 ReThrow
0x24b2081d5e48 @ 134 : 0b f1 Ldar r9
0x24b2081d5e4a @ 136 : a6 SetPendingMessage
0x24b2081d5e4b @ 137 : 0c LdaZero
0x24b2081d5e4c @ 138 : 1c f3 TestReferenceEqual r7
0x24b2081d5e4e @ 140 : 99 05 JumpIfFalse [5](0x24b2081d5e53 @ 145)
0x24b2081d5e50 @ 142 : 0b f2 Ldar r8
0x24b2081d5e52 @ 144 : a8 ReThrow
0x24b2081d5e53 @ 145 : 0b f9 Ldar r1
0x24b2081d5e55 @ 147 : a9 Return
The details will be discussed later. The important thing now is perceptual understanding.
First, define an empty class and see what happens:
class A {};
As seen in the CreateClosure
instruction, the system still generates a constructor by default.
Bytecode Age: 0
0x24b2081d6b32 @ 0 : 81 00 CreateBlockContext [0]
0x24b2081d6b34 @ 2 : 1a f9 PushContext r1
0x24b2081d6b36 @ 4 : 10 LdaTheHole
0x24b2081d6b37 @ 5 : bf Star5
0x24b2081d6b38 @ 6 : 80 02 00 00 CreateClosure [2], [0], #0
0x24b2081d6b3c @ 10 : c2 Star2
0x24b2081d6b3d @ 11 : 13 01 LdaConstant [1]
0x24b2081d6b3f @ 13 : c1 Star3
0x24b2081d6b40 @ 14 : 19 f8 f6 Mov r2, r4
0x24b2081d6b43 @ 17 : 65 25 00 f7 03 CallRuntime [DefineClass], r3-r5
0x24b2081d6b48 @ 22 : c1 Star3
0x24b2081d6b49 @ 23 : 1b f9 PopContext r1
0x24b2081d6b4b @ 25 : 0b f6 Ldar r4
0x24b2081d6b4d @ 27 : 25 02 StaCurrentContextSlot [2]
0x24b2081d6b4f @ 29 : 0e LdaUndefined
0x24b2081d6b50 @ 30 : a9 Return
Constant pool (size = 3)
0x24b2081d6afd: [FixedArray] in OldSpace
- map: 0x24b208002209 <Map>
- length: 3
0: 0x24b2081d6a11 <ScopeInfo CLASS_SCOPE>
1: 0x24b2081d6ad9 <FixedArray[7]>
2: 0x24b2081d6a25 <SharedFunctionInfo A>
Let's look at another new
object:
let a100 = new A();
The new
operator is translated into the Construct
instruction:
Bytecode Age: 0
0x24b2081d6ee2 @ 0 : 21 00 00 LdaGlobal [0], [0]
0x24b2081d6ee5 @ 3 : c3 Star1
0x24b2081d6ee6 @ 4 : 69 f9 fa 00 02 Construct r1, r0-r0, [2]
0x24b2081d6eeb @ 9 : 25 02 StaCurrentContextSlot [2]
0x24b2081d6eed @ 11 : 0e LdaUndefined
0x24b2081d6eee @ 12 : a9 Return
Constant pool (size = 1)
0x24b2081d6eb5: [FixedArray] in OldSpace
- map: 0x24b208002209 <Map>
- length: 1
0: 0x24b2081d69a5 <String[1]: #A>
As mentioned earlier, LdaSmi
accepts an immediate number of up to 4 bytes. If there are more, it becomes a floating point number. ES2020 provides us with BigInt
.
Let's look at how the interpreter helps us implement it by adding "n" to the end:
let la = 123n;
The system handles it as a FixedArray
:
Bytecode Age: 0
0x24b2081d7ada @ 0 : 13 00 LdaConstant [0]
0x24b2081d7adc @ 2 : 25 02 StaCurrentContextSlot [2]
0x24b2081d7ade @ 4 : 0e LdaUndefined
0x24b2081d7adf @ 5 : a9 Return
Constant pool (size = 1)
0x24b2081d7a9d: [FixedArray] in OldSpace
- map: 0x24b208002209 <Map>
- length: 1
0: 0x24b2081d7aa9 <BigInt 123>
Since ES2020, we can use # to define private variables:
class A3{
#priv_data = 100
get_data(){
return this.#priv_data;
}
}
let a3 = new A3();
a3.get_data();
The solution to v8 is to hand it over to the Runtime
and call the Runtime
CreatePrivateNameSymbol
:
Bytecode Age: 0
0x27b1081d352e @ 0 : 81 00 CreateBlockContext [0]
0x27b1081d3530 @ 2 : 1a f9 PushContext r1
0x27b1081d3532 @ 4 : 13 02 LdaConstant [2]
0x27b1081d3534 @ 6 : c1 Star3
0x27b1081d3535 @ 7 : 13 02 LdaConstant [2]
0x27b1081d3537 @ 9 : c1 Star3
0x27b1081d3538 @ 10 : 65 78 01 f7 01 CallRuntime [CreatePrivateNameSymbol], r3-r3
0x27b1081d353d @ 15 : 25 02 StaCurrentContextSlot [2]
0x27b1081d353f @ 17 : 10 LdaTheHole
0x27b1081d3540 @ 18 : bf Star5
0x27b1081d3541 @ 19 : 80 03 00 00 CreateClosure [3], [0], #0
0x27b1081d3545 @ 23 : c2 Star2
0x27b1081d3546 @ 24 : 13 01 LdaConstant [1]
0x27b1081d3548 @ 26 : c1 Star3
0x27b1081d3549 @ 27 : 80 04 01 00 CreateClosure [4], [1], #0
0x27b1081d354d @ 31 : be Star6
0x27b1081d354e @ 32 : 19 f8 f6 Mov r2, r4
0x27b1081d3551 @ 35 : 65 25 00 f7 04 CallRuntime [DefineClass], r3-r6
0x27b1081d3556 @ 40 : c1 Star3
0x27b1081d3557 @ 41 : 80 05 02 00 CreateClosure [5], [2], #0
0x27b1081d355b @ 45 : c0 Star4
0x27b1081d355c @ 46 : 32 f8 06 00 StaNamedProperty r2, [6], [0]
0x27b1081d3560 @ 50 : 1b f9 PopContext r1
0x27b1081d3562 @ 52 : 0b f8 Ldar r2
0x27b1081d3564 @ 54 : 25 02 StaCurrentContextSlot [2]
0x27b1081d3566 @ 56 : 16 02 LdaCurrentContextSlot [2]
0x27b1081d3568 @ 58 : c3 Star1
0x27b1081d3569 @ 59 : 69 f9 fa 00 02 Construct r1, r0-r0, [2]
0x27b1081d356e @ 64 : 25 03 StaCurrentContextSlot [3]
0x27b1081d3570 @ 66 : 16 03 LdaCurrentContextSlot [3]
0x27b1081d3572 @ 68 : c2 Star2
0x27b1081d3573 @ 69 : 2d f8 07 04 LdaNamedProperty r2, [7], [4]
0x27b1081d3577 @ 73 : c3 Star1
0x27b1081d3578 @ 74 : 5d f9 f8 06 CallProperty0 r1, r2, [6]
0x27b1081d357c @ 78 : c4 Star0
0x27b1081d357d @ 79 : a9 Return
Constant pool (size = 8)
0x27b1081d34e5: [FixedArray] in OldSpace
- map: 0x27b108002209 <Map>
- length: 8
0: 0x27b1081d3365 <ScopeInfo CLASS_SCOPE>
1: 0x27b1081d34c1 <FixedArray[7]>
2: 0x27b1081d3291 <String[10]: ##priv_data>
3: 0x27b1081d33a9 <SharedFunctionInfo A3>
4: 0x27b1081d33dd <SharedFunctionInfo get_data>
5: 0x27b1081d3411 <SharedFunctionInfo <instance_members_initializer>>
6: 0x27b108005895 <Symbol: (class_fields_symbol)>
7: 0x27b1081d32a9 <String[8]: #get_data>
The bytecode of Ignition
is not the only intermediate code in v8, but also the IR of TurboFan
.
Due to space constraints, we will post a picture of TurboFan IR:
Let's take arithmetic IR JSAdd as an example. Look at the structure of TurboFan IR:
This article introduces basic knowledge about Ignition
. It can be seen that compared with JVM bytecode:
Ignition
instruction set is rich, such as empty judgment instructions. Even GetIterator
has instructions.CallRuntime
. With the upgrade of ES, runtime functions continue to expand. For example, ES2019 adds Dynamic Import, while v8 adds a DynamicImportCall
Runtime function without modifying the instruction set.LdaTheHole
) to serve the internal state.66 posts | 3 followers
FollowAlibaba F(x) Team - June 20, 2022
Alibaba Clouder - December 14, 2020
Alex - August 16, 2018
Alibaba F(x) Team - August 29, 2022
Alibaba Cloud Native Community - January 19, 2023
hyj1991 - June 20, 2019
66 posts | 3 followers
FollowA low-code development platform to make work easier
Learn MoreHelp enterprises build high-quality, stable mobile apps
Learn MoreAlibaba Cloud (in partnership with Whale Cloud) helps telcos build an all-in-one telecommunication and digital lifestyle platform based on DingTalk.
Learn MoreMore Posts by Alibaba F(x) Team