関数テンプレートとインスタンスオブジェクト
テンプレートとインスタンスの違い
ファンクション・テンプレートは、Duktape内部のECMAScript Functionオブジェクトです。関数テンプレートはユーザーコードに公開されず、具体的な周辺環境なしにコンパイルされた関数を表します。関数テンプレートは、すべての関数と内部関数に対してコンパイラによって作成されます。テンプレートは周囲の字句環境を持たないため、関数として呼び出すことはできません。
関数テンプレートは、新しいFunctionオブジェクトを作成し、テンプレートフィールドのほとんど(すべてではない)をFunctionオブジェクトにコピーし、外側の辞書環境のようなインスタンス固有のフィールドを適切に初期化することによって、具体的な関数インスタンス(closureとも呼ばれる)にインスタンス化されます。関数インスタンスは、関数のコンパイル結果や、内部関数が後で(CLOSURE命令で)インスタンス化されるときに作成されます。
この分離が必要なのは、ある関数テンプレートが、毎回異なる外部環境で複数回インスタンス化される可能性があるからです。次のようなことを考えてみましょう:
function mkPrinter(str) {
// inner function
return function() { print(str); }
}
var p1 = mkPrinter("Hello world");
var p2 = mkPrinter("still here");
p1();
p2();
print(p1 === p2); // => false
In this example:
- The
mkPrinter
function is first compiled into a function template and then immediately converted to a function instance. The function instance has the global environment as its outer environment. The instance is then associated with themkPrinter
property of the global object. - The inner function inside
mkPrinter
is represented by a function template stored as part of themkPrinter
function inner function table. - The
p1
andp2
function objects are separate Function objects created by a CLOSURE instruction occurring in the bytecode ofmkPrinter
. They have their own properties and separate outer lexical environments, but shared the same bytecode, pc-to-line conversion data, etc. The outer lexical environment forp1
andp2
is the declarative environment created whenmkPrinter
was entered, and contains thestr
binding needed to print separate texts whenp1
andp2
are called.
A function instance does not reference a function template from a garbage collection point of view. The function template can be collected while the function instance remains reachable.
Properties of a function template
The E5 specification does not recognize a "function template", so there are no standard properties for function templates. The properties can also change from release to release because they are not exposed to user code. The following properties are used:
Property Description
_Varmap
Maps register-bound variable names to their register numbers. Example: { arg1: 0, arg2: 1, myvar: 2 }
.
_Formals
An array of formal argument names. formals.length
provides the number of formal arguments. Note that the number of formal arguments does not need to match function nargs
: the function might access all args through the arguments object and have nargs
set to zero. This property is used to initialize the arguments object (in non-strict code); the compiler should omit this whenever possible. Example: [ "arg1", "arg2" ]
.
name
Function name, set for function declarations and named function expressions. If DUK_HOBJECT_FLAG_NAMEBINDING is set, the value of this property is bound in the function's environment (used for named function expressions). Example: "func"
.
fileName
Source filename (or equivalent). Used to add source file information to error objects and tracebacks.
_Source
Function source code. E5 specifies that the source code of a function must be valid syntax.
_Pc2line
Debug information: maps bytecode index to a source line number. Space-optimized binary format.
The compiler should omit whatever internal properties are not needed to save space. For instance:
_Varmap
is not needed if the function can never perform a slow path identifier reference._Formals
is not needed unless a non-strict arguments object is potentially constructed. (However,_Formals
is also used for deriving the "length" of the instance. If _Formals is omitted, something else needs to be set in the template to allow instance "length" to be initialized.)
When debugging, it may be necessary to store more function properties than needed by plain execution. For instance, source code should be available even for dynamically generated code.
Properties of a function instance
The creation of function instances is described in E5 Section 13.2. Each function instance (each closure created from a function expression or declaration) has the following standard properties:
length
: set to number of formal parameters (length of_Formals
).prototype
: points to a fresh object which has aconstructor
property pointing back to the functioncaller
: thrower (strict functions only)arguments
: thrower (strict functions only)
There is considerable variance in practical implementations:
smjs:
// the "name" property is non-standard; "arguments" and "caller" are // present for a non-strict function js> f = function foo() {} (function () {}) js> Object.getOwnPropertyNames(f) ["prototype", "length", "name", "arguments", "caller"] // for strict mode, the same properties are present. js> f = function foo() { "use strict"; } (function foo() {"use strict";}) js> Object.getOwnPropertyNames(f); ["prototype", "length", "name", "arguments", "caller"] // the "name" property contains the function expression name js> f.name "foo" // "name" is non-writable, non-configurable (and non-enumerable) // -> works as a reliable "internal" property too js> Object.getOwnPropertyDescriptor(f, 'name') ({configurable:false, enumerable:false, value:"foo", writable:false})
nodejs (v8):
// "name" is non-standard; "arguments" and "caller" are present // for even a non-strict function > f = function foo() {} [Function: foo] > Object.getOwnPropertyNames(f) [ 'length', 'caller', 'arguments', 'name', 'prototype' ] > f.name 'foo' // strict mode is the same > f = function foo() { "use strict"; } [Function: foo] > Object.getOwnPropertyNames(f) [ 'name', 'length', 'arguments', 'prototype', 'caller' ] // 'name' is writable but not configurable/enumerable > f.name 'foo' > Object.getOwnPropertyDescriptor(f, 'name') { value: 'foo', writable: true, enumerable: false, configurable: false }
rhino:
// "name" is non-standard, "arity" is non-standard, "arguments" // is present (but "caller" is not) js> f = function foo() {} [...] js> Object.getOwnPropertyNames(f) arguments,prototype,name,arity,length // name is non-writable, non-enumerable, non-configurable js> pd = Object.getOwnPropertyDescriptor(f, 'name') [object Object] js> pd.writable false js> pd.enumerable false js> pd.configurable false // strict mode functions are similar
Notes:
- "caller" and "arguments" would be nice as virtual properties to minimize object property count. They can't be inherited in the ordinary way without breaking compliance (the standard requires they be own properties).
- "prototype" would be nice as a virtual property: it's quite expensive to have for every function instance.
The properties for function instances are (these are also documented in user documentation for the exposed parts):
Property Description
length
Set to the number of formal parameters. For normal functions parsed from ECMAScript source code, this is set to _Formals.length
. Built-in functions may be special.
prototype
Points to a fresh object which has a constructor
property pointing back to the function instance.
caller
For strict functions, set to the [[ThrowTypeError]]
function object defined in E5 Section 13.2.3.
arguments
Like caller
.
name
See function templates.
fileName
See function templates.
_Varmap
See function templates.
_Formals
See function templates.
_Source
See function templates.
_Pc2line
See function templates.
Built-in functions
The properties of built-in functions are a special case, because they are not created with the algorithm in E5 Section 13.2; instead, their properties are described explicitly in E5 Section 15.
There is considerable variance between implementations on what properties built-in functions get.
Duktape/C functions
Duktape/C functions are also represented by an ECMAScript Function object. The properties of such functions are extremely minimal; for instance, they are missing the length
property. This is done to keep the object size as small as possible. This means, however, that the Function objects are non-standard.
Duktape/C functions also don't have any need for control variables such as _Lexenv
, _Pc2line
, etc.
pc2line format
_Pc2line
property allows a program counter (bytecode index) to be converted to an approximate line number of the expression which generated the bytecode instruction in question. Logically it can be considered an array (in fact, Lua implements a similar structure as a simple array):
PC Line
0 1
1 1
2 3
3 4
4 7
If the line number is represented as a 4-byte integer, the structure would take as much memory as the related bytecode, doubling memory usage. Clearly a more space efficient format is desirable, as long as performance is not impacted too much when throwing and catching errors.
Although the line number generally stays the same or increases when PC increases, this is not always the case (e.g. in loop structures). This rules out search structures relying on monotonicity properties. It's nice if an arbitrary mapping can be expressed if necessary.
Error line number is needed when:
- Accessing the non-standard
lineNumber
property. This property can be implemented as a getter in the Error prototype, which will get the PC from the traceback data (if any), and do the PC-to-line conversion only when actually needed. - Creating a string-formatted traceback. PC-to-line conversions are needed for most traceback lines.
The current format is based on the observation that when PC increases by one, the typical delta for the line number is very small (and is usually zero or positive). Deltas can be expressed efficiently with variable bit length encoding. To provide a reasonably fast random access, explicit starting point values are recorded for every nth bytecode instruction (currently, every 64th; SKIP=64 below). During a lookup one can first skip close to the desired mapping entry and then scan the bit-packed format forwards.
The format consists of a header structure followed by bit packed diff streams (each bit packed stream begins at a byte boundary):
Offset Type Description
0 u32 PC limit (maximum PC, exclusive)
4 u32 Line number for PC 0*SKIP
8 u32 Byte offset of diff bitstream for PC 0*SKIP
12 u32 Line number for PC 1*SKIP
16 u32 Byte offset of diff bitstream for PC 1*SKIP
... A total of ceil(bytecode_length/SKIP) line/offset entries
... Diff bitstreams
The diff bitstream consists of SKIP-1 diff entries for a certain starting point. Each diff entry simply encodes the line number difference when PC increases by one; the difference may be negative, zero, or positive. The diff is encoded as one of the following entry types:
Bits Description
0 Difference is +0
1 0 <2 bits> Difference is: +1, +2, +3, or +4 (encoded as 2 bits)
1 1 0 <8 bits> Difference is a signed 8-bit value, encoded with bias +0x80 (as unsigned 0x00 ... 0xff)
1 1 1 <32 bits> Fallback, linenumber encoded as absolute 32-bit value
These cases are not optimized, but rather best guesses combined with some experimentation:
- Usually multiple bytecode instructions are generated from a single line of source code, hence the case +0 is important to encode efficiently.
- When line changes, there are either no lines without code, or there are a few such lines (empty lines for readability, perhaps a few comment lines). The cases +1...+4 are encoded compactly for these cases.
- The signed 8-bit offset covers large comment blocks, and the occasional negative steps (e.g. in loop structures).
- As a fallback, an absolute 32-bit line number can be encoded. This covers any remaining cases and provides completeness.
As an example, the bitstream for the diffs [+0, +2, +9, -3, +0] would be:
0 1001 11000001001 11011111101 0
=> 01001110 00001001 11011111 10100000 (padded with 0)
=> 0x4e 0x09 0xdf 0xa0
Typically the pc2line data is about 10-15% of the size of the corresponding bytecode, a very modest addition to footprint compared to the 100% addition of a straight table approach.