Skip to content

関数テンプレートとインスタンスオブジェクト

テンプレートとインスタンスの違い

ファンクション・テンプレートは、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 the mkPrinter property of the global object.
  • The inner function inside mkPrinter is represented by a function template stored as part of the mkPrinter function inner function table.
  • The p1 and p2 function objects are separate Function objects created by a CLOSURE instruction occurring in the bytecode of mkPrinter. 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 for p1 and p2 is the declarative environment created when mkPrinter was entered, and contains the str binding needed to print separate texts when p1 and p2 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 a constructor property pointing back to the function
  • caller: 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.