diff --git a/docs/unified-bytecode-expansion-contract.md b/docs/unified-bytecode-expansion-contract.md index d9de6baa6..dc68c9724 100644 --- a/docs/unified-bytecode-expansion-contract.md +++ b/docs/unified-bytecode-expansion-contract.md @@ -336,7 +336,7 @@ must still obey the no-mixed-execution rule. | `DeleteDependency` | `delete` expressions outside the admitted ordinary named/computed property delete lane and the with-backed dynamic-name delete lane | Existing sync IR delete route | Delete semantics lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_PropertyReadAdjacentFamilies_DeclineWithExplicitCodes"` | | `SuperPropertyDependency` | Out-of-boundary super call targets; super property reads/writes/updates are admitted by dedicated VM opcodes | Existing class / constructor route for remaining call-target shapes | Super semantics lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_SuperPropertyAccess_AcceptsOwnedOpcodes"` | | `OptionalChainDependency` | Optional chains outside the admitted optional property-read, optional-call, and exact optional named/computed delete boundaries | Existing sync IR optional-chain route | Optional-chain widening lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_OptionalChainNamedPrefixPlainCallExpressionPlan_AcceptsExecutableInvocationBoundary"` | -| `ObjectLiteralOrSpreadDependency` | Non-simple object spread sources, unsupported array spread, spread construct arguments, and object methods/accessors only when they appear inside restricted simple literal spans | Existing sync IR literal/spread route for remaining spans | Literal/spread lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_NonSimpleSourceArraySpread_DeclinesWithExplicitCode"` | +| `ObjectLiteralOrSpreadDependency` | Non-simple object spread sources, unsupported array spread, spread super construct arguments, and object methods/accessors only when they appear inside restricted simple literal spans | Existing sync IR literal/spread route for remaining spans | Literal/spread lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_NonSimpleSourceArraySpread_DeclinesWithExplicitCode"` | | `PrivateFieldDependency` | Private-name operations outside the admitted routes; `#name in obj`, direct private named reads/writes/updates, direct private named compound/logical writes, and direct private named method calls are VM-owned when the surrounding class method is otherwise production-eligible. Private member deletes are parser early errors before production eligibility. | Existing private-name route for remaining private member access | Private-name lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_PrivateFieldIn_AcceptsAndVmChecksPrivateBrand"` | | `ForInDriverStateDependency` | Unsupported for-in driver state such as awaited object source | Existing for-in IR driver route | Driver-state lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~IsSupportedForInInit_AwaitedSource_Declines"` | | `DestructuringDependency` | Binding declarations with defaults, computed binding names, assignment targets, awaited binding values, unsupported destructuring driver shapes, and targets outside the admitted driver or descriptor-backed lanes | Existing destructuring IR route for remaining shapes | Destructuring driver lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_UnsupportedDestructuringDriverShapes_DeclineWithExplicitReason"` | @@ -570,15 +570,16 @@ the final post-compile production subset check before VM entry. - Synchronous construct calls are admitted (gh2690 plus the construct-boundary widening): `new F(...)`, `new F(...args)`, `new box.Ctor(...)`, and `new box[key](...)` when the constructor/key/argument subexpressions are - already production-safe. The constructor value and each logical argument are - pushed left-to-right by their own loads (no receiver/`this`), then a single - `ConstructInvocationBoundary` opcode invokes `[[Construct]]` with the - constructor itself as `new.target`, mirroring the spec-conformant construct - reference helper. Its operand uses the same low-16-bit argument count plus - high-bit spread-mask reference encoding as `CallInvocationBoundary`; the VM - flattens spread iterables at the construct boundary before calling - `ReflectHelper.Construct`. A non-constructor target throws `TypeError` at the - boundary. + already production-safe. Construct results may feed trailing plain named + property reads such as `new F(...args).x`. The constructor value and each + logical argument are pushed left-to-right by their own loads (no + receiver/`this`), then a single `ConstructInvocationBoundary` opcode invokes + `[[Construct]]` with the constructor itself as `new.target`, mirroring the + spec-conformant construct reference helper. Its operand uses the same + low-16-bit argument count plus high-bit spread-mask reference encoding as + `CallInvocationBoundary`; the VM flattens spread iterables at the construct + boundary before calling `ReflectHelper.Construct`. A non-constructor target + throws `TypeError` at the boundary. - Derived-constructor non-spread `super(...)` calls are admitted through `SuperConstructInvocationBoundary`. The VM resolves the dynamic super constructor from the current super binding, invokes `[[Construct]]` with the @@ -795,10 +796,11 @@ support today. spread calls are now admitted (gh2676). Optional calls are now admitted (gh2689, ADR 0289): `box?.read(args)`, `box.read?.(args)`, and `box[key]?.(args)`. Synchronous construct calls (`new F(...)`, spread - arguments, and member/computed constructor targets) are now admitted (gh2690, - ADR 0286 plus construct-boundary widening). The first bounded super - invocation shapes are now admitted too (PR #2862, ADR 0307): non-spread - derived-constructor `super(...)` plus named/computed super-member calls. + arguments, member/computed constructor targets, and trailing plain named + result reads such as `new F(...args).x`) are now admitted (gh2690, ADR 0286 + plus construct-boundary widening). The first bounded super invocation shapes + are now admitted too (PR #2862, ADR 0307): non-spread derived-constructor + `super(...)` plus named/computed super-member calls. Simple array and object literal arguments (`fn([a, b])`, `fn({x: a})`) are now admitted (gh2705, ADR 0290). Optional-start computed member spread calls (`a?.box[key](...args)`) are also admitted through the shared spread-mask diff --git a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs index 5801761e3..996900cbf 100644 --- a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs +++ b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs @@ -1544,14 +1544,19 @@ private static bool TryIsConstructInvocationCandidate( ReadOnlySpan identifierConstants, ActivationSlotShape activationSlots) { - if (program.OperationCount < 2 || - program.GetOperation(program.OperationCount - 1).Kind != ExpressionOpKind.Construct) + if (program.OperationCount < 2) + { + return false; + } + + var constructIndex = FindConstructInvocationBoundaryIndex(program); + if (constructIndex < 1) { return false; } var stringConstants = program.StringConstants.AsSpan(); - for (var operationIndex = 0; operationIndex < program.OperationCount - 1; operationIndex++) + for (var operationIndex = 0; operationIndex < constructIndex; operationIndex++) { var operation = program.GetOperation(operationIndex); if (IsPrivateNamedPropertyOperation(operation, stringConstants)) @@ -1583,6 +1588,26 @@ private static bool TryIsConstructInvocationCandidate( return true; } + private static int FindConstructInvocationBoundaryIndex(ExpressionProgram program) + { + var constructIndex = program.OperationCount - 1; + if (program.GetOperation(constructIndex).Kind == ExpressionOpKind.Construct) + { + return constructIndex; + } + + var stringConstants = program.StringConstants.AsSpan(); + while (constructIndex > 0 && + IsPlainNamedPropertyRead(program.GetOperation(constructIndex), stringConstants)) + { + constructIndex--; + } + + return program.GetOperation(constructIndex).Kind == ExpressionOpKind.Construct + ? constructIndex + : -1; + } + private static bool TryIsConstructComputedPropertyRead( ExpressionProgram program, int getComputedIndex, diff --git a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs index 0f02c4f8f..8b522bf4c 100644 --- a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs +++ b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs @@ -947,6 +947,29 @@ function invoke(ctor, args) { Assert.NotEmpty(result.Program.CallSpreadMasks); } + [Fact] + public void Evaluate_SpreadConstructResultNamedRead_AcceptsConstructInvocationBoundary() + { + var plan = GetFunctionPlan(""" + function invoke(ctor, args) { + return new ctor(...args).x; + } + """, + "invoke"); + + var result = UnifiedBytecodeProductionEligibility.Evaluate( + plan, + new UnifiedBytecodeProductionActivationDescriptor()); + + Assert.True(result.IsEligible, result.Reason); + Assert.Equal(UnifiedBytecodeProductionDeclineCode.None, result.Code); + Assert.Contains(result.Program.Instructions, instruction => + instruction.OpCode == UnifiedBytecodeOpCode.ConstructInvocationBoundary); + Assert.Contains(result.Program.Instructions, instruction => + instruction.OpCode == UnifiedBytecodeOpCode.GetNamedProperty); + Assert.NotEmpty(result.Program.CallSpreadMasks); + } + [Fact] public void Evaluate_IdentifierConstructExpressionPlan_AcceptsConstructInvocationBoundary() { diff --git a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionSpreadCallTests.cs b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionSpreadCallTests.cs index a3a380e38..2f265d9b4 100644 --- a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionSpreadCallTests.cs +++ b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionSpreadCallTests.cs @@ -202,7 +202,7 @@ static record => record.Message.Contains( } [Fact(Timeout = 5000)] - public async Task SpreadConstruct_DeclinesProductionFastPath() + public async Task SpreadConstructResultNamedRead_UsesProductionFastPath() { await using var engine = CreateEngine(); var result = await engine.Evaluate(""" @@ -219,7 +219,7 @@ function invoke(Point, args) { """); Assert.Equal(41d, result); - Assert.DoesNotContain(CurrentLogger!.Collector.Snapshot(), + Assert.Contains(CurrentLogger!.Collector.Snapshot(), static record => record.Message.Contains( "unified-bytecode-production-fast-path func=invoke", StringComparison.Ordinal));