Skip to content

Conversation

@0xe
Copy link
Contributor

@0xe 0xe commented Nov 27, 2025

Fixes a few things in generators for spec compliance:

  1. Default parameters for generators should be evaluated after the activation is created but before any other declarations are added.

In FunctionDeclarationInstatiation, the ordering is

  • Create arguments (22)
  • Init args and substitute by evaluating default expressions (24-26, can refer to arguments)
  • Create function declarations (36)

This is called from EvaluateGeneratorBody.

  1. Prototype for generator functions should be generatorProto.

From InstantiateGeneratorFunctionExpression

  1. Let prototype be OrdinaryObjectCreate(%GeneratorPrototype%).

*/
public static void evaluateGeneratorDefaultParams(
JSFunction funObj, Scriptable activationScope) {
Context cx = Context.getCurrentContext();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not providing the context as param? Looks like you have the context already in hand when calling this

}
}

public static Scriptable createNativeGenerator(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here regarding the context, i think this is a more or less internal function, we cahn change the params without having to take care of backward compatibility. Again we can provide the context from the outside.

String[] generatorDefaultParams =
jsFunc.getDescriptor().getGeneratorDefaultParams();
if (generatorDefaultParams != null && generatorDefaultParams.length > 0) {
for (int i = 0; i < generatorDefaultParams.length; i += 2) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be a copy of wha we are doing in the opt runtime in this case. Maybe we have to have it only once

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, will refactor it out to ScriptRuntime and just call it from the interpreter. It's the exact same code. I intended to do that and forgot. Thanks for calling it out!

@0xe 0xe requested a review from rbri November 27, 2025 13:23
public static Scriptable createNativeGenerator(
JSFunction funObj, Scriptable scope, Scriptable thisObj, int maxLocals, int maxStack) {
Context cx = Context.getCurrentContext();
JSFunction funObj,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again see the comments on the other pr

@0xe 0xe requested a review from rbri November 28, 2025 06:18
Copy link
Contributor

@andreabergia andreabergia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand this correctly, for:

function *f(a = 1) { yield 2; }

what you are doing is storing in the JSDescriptor for f an array of string which contains ["a", "1"] and then you evaluate the string with 1 at runtime.
This feels really horrible for performances.

Why aren't we generating some code that does:

function *f(a) {
  {
  var a = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'dEFUALT';
  }
  yield 2;
}

like babel or swc do? I get that this is pretty complex for generators, given that they have their state, but dynamically eval-ing strings at every single call doesn't seem good to me.

@0xe
Copy link
Contributor Author

0xe commented Dec 1, 2025

I agree that evaluating at runtime is pretty terrible for perf. However, I think, we cannot use Babel's approach of putting the initialization inside the generator body.

According to spec, EvaluateGeneratorBody, generator evaluation must:

  • Step 1: Perform FunctionDeclarationInstantiation (which evaluates default parameters)
  • Step 2: OrdinaryCreateFromConstructor (calls GetPrototypeFromConstructor to read functionObject.prototype to determine the generator instance's [[Prototype]])

Default parameter evaluation happens in FunctionDeclarationInstantiation,

  • Step 22: Create the arguments object
  • Step 28: evaluates default parameters with access to arguments (perform IteratorBindingInitialization of formals)
  • Step 37: Instantiate function declarations

There's a corresponding 262 case.

For example:

function* g(a = (g.prototype = null)) {}
var oldProto = g.prototype;
var it = g();
Object.getPrototypeOf(it) === oldProto; // Should be FALSE per spec

The default param mutates g.prototype during FunctionDeclarationInstantiation (step 1), then the generator instance is created in step 2 using the mutated value. If we evaluated the default after returning the generator, this test would fail.

While jsc, engine262 and rhino evaluate this correctly, v8 fails this.

It appears that v8 fixed this reasonably recently (Apr 2025) and their rationale seems to be coherent with the above.

Given that we need to evaluate the arguments at runtime, to improve the performance perhaps I can store the AstNodes instead in JSDescriptor to avoid re-parsing. Thoughts? I think with the different modes we have it gets pretty complicated fast.

@0xe 0xe force-pushed the scratch/satish/fix-default-parameter-generators branch from 8534f77 to 137bd8a Compare December 1, 2025 09:32
@0xe 0xe requested a review from andreabergia December 1, 2025 09:33
@0xe
Copy link
Contributor Author

0xe commented Dec 2, 2025

I've changed the implementation to just generate the bytecode directly instead of storing the source for expressions evaluation for defaults, so for the 262 test case above:

function* g(a = (g.prototype = null)) {}
var oldProto = g.prototype;
var it = g();
Object.getPrototypeOf(it) === oldProto; // => false
 [0] REG_STR_C0 "a"
 [1] NAME
 [2] UNDEF
 [3] SHEQ
 [4] IFNE 20 // jump as no default substitution needed
 [7] LINE : 1
 [10] REG_STR_C0 "a"
 [11] BINDNAME
 [12] REG_STR_C1 "g"
 [13] NAME
 [14] NULL
 [15] REG_STR_C2 "prototype"
 [16] SETPROP
 [17] REG_STR_C0 "a"
 [18] SETNAME
 [19] POP
 [20] GENERATOR : 1 // create generator
 [23] GENERATOR_END : 1

and for compiled code:

      21: invokedynamic #209,  0            // InvokeDynamic #0:"NAME:GET:a":(Lorg/mozilla/javascript/Scriptable;Lorg/mozilla/javascript/Context;)Ljava/lang/Object;
      26: getstatic     #48                 // Field org/mozilla/javascript/Undefined.instance:Ljava/lang/Object;
      29: invokedynamic #143,  0            // InvokeDynamic #0:"MATH:SHALLOWEQ":(Ljava/lang/Object;Ljava/lang/Object;)Z
      34: ifne          40
      37: goto          70 // jump as no default substitution needed
      40: aload_3
      41: aload_0
      42: invokedynamic #212,  0            // InvokeDynamic #0:"NAME:BIND:a":(Lorg/mozilla/javascript/Scriptable;Lorg/mozilla/javascript/Context;)Lorg/mozilla/javascript/Scriptable;
      47: aload_3
      48: aload_0
      49: invokedynamic #63,  0             // InvokeDynamic #0:"NAME:GET:g":(Lorg/mozilla/javascript/Scriptable;Lorg/mozilla/javascript/Context;)Ljava/lang/Object;
      54: aconst_null
      55: aload_0
      56: aload_3
      57: invokedynamic #216,  0            // InvokeDynamic #0:"PROP:SET:prototype":(Ljava/lang/Object;Ljava/lang/Object;Lorg/mozilla/javascript/Context;Lorg/mozilla/javascript/Scriptable;)Ljava/lang/Object;
      62: aload_0
      63: aload_3
      64: invokedynamic #219,  0            // InvokeDynamic #0:"NAME:SET:a":(Lorg/mozilla/javascript/Scriptable;Ljava/lang/Object;Lorg/mozilla/javascript/Context;Lorg/mozilla/javascript/Scriptable;)Ljava/lang/Object;
      69: pop
      70: aload_0
      71: aload_3
      72: aload         4
      74: aload_1
      75: iconst_0
      76: iconst_0
      77: invokestatic  #223                // Method org/mozilla/javascript/optimizer/OptRuntime.createNativeGenerator:(Lorg/mozilla/javascript/Context;Lorg/mozilla/javascript/Scriptable;Lorg/mozilla/javascript/Scriptable;Lorg/mozilla/javascript/JSFunction;II)Lorg/mozilla/javascript/Scriptable;

@0xe
Copy link
Contributor Author

0xe commented Dec 2, 2025

This follows what jsc is doing:

bb#1
Predecessors: [ ]
[   0] enter
[   1] to_this            srcDst:this, ecmaMode:NotStrictMode, valueProfile:1
[   6] create_lexical_environment dst:loc7, scope:loc4, symbolTable:Cell: 0x101044cc8 (0x300004870:[0x4870/18544, SymbolTable, (0/0, 0/0){}, NonArray, Unknown, Leaf]), StructureID: 18544(const1), initialValue:<JSValue()>(const2)
[  11] mov                dst:loc4, src:loc7
[  14] get_argument       dst:loc8, index:1, valueProfile:2
[  18] stricteq           dst:loc9, lhs:loc8, rhs:Undefined(const3)
[  22] jfalse             condition:loc9, targetLabel:28(->50)  // JUMP to generator creation if default value needn't be evaluated
Successors: [ #3 #2 ]

bb#2
Predecessors: [ #1 ]
[  25] resolve_scope      dst:loc10, scope:loc4, var:0, resolveType:GlobalProperty, localScopeDepth:1
[  32] get_from_scope     dst:loc11, scope:loc10, var:0, getPutInfo:2048<ThrowIfNotFound|GlobalProperty|NotInitialization|NotStrictMode>, localScopeDepth:1, offset:0, valueProfile:3
[  41] mov                dst:loc8, src:Null(const4)
[  44] put_by_id          base:loc11, property:1, value:loc8, flags:
Successors: [ #3 ]

bb#3
Predecessors: [ #1 #2 ]
[  50] put_to_scope       scope:loc7, var:2, value:loc8, getPutInfo:1048580<DoNotThrowIfNotFound|ResolvedClosureVar|Initialization|NotStrictMode>, symbolTableOrScopeDepth:1, offset:0
[  58] create_lexical_environment dst:loc5, scope:loc4, symbolTable:Cell: 0x101044bd8 (0x300004870:[0x4870/18544, SymbolTable, (0/0, 0/0){}, NonArray, Unknown, Leaf]), StructureID: 18544(const0), initialValue:Undefined(const3)
[  63] mov                dst:loc4, src:loc5
[  66] create_generator   dst:loc6, callee:callee
[  70] new_func_exp       dst:loc8, scope:loc4, functionDecl:0
[  74] put_internal_field base:loc6, index:2, value:loc8
[  78] put_internal_field base:loc6, index:3, value:this
[  82] ret                value:loc6
Successors: [ ]


Identifiers:
  id0 = g
  id1 = prototype
  id2 = a

@andreabergia
Copy link
Contributor

I've changed the implementation to just generate the bytecode directly instead of storing the source for expressions evaluation for defaults

This looks way better! Thanks for doing it!
I guess we can, relatively easily, extend this to all functions and not just generators, right? But that's worth a separate PR.

@0xe
Copy link
Contributor Author

0xe commented Dec 5, 2025

Closing this as #2173 which was stacked on this is already merged.

@0xe 0xe closed this Dec 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants