Skip to content

Commit 44b36fe

Browse files
authored
Admit spread construct result reads (#3008)
1 parent a625fd4 commit 44b36fe

4 files changed

Lines changed: 69 additions & 19 deletions

File tree

docs/unified-bytecode-expansion-contract.md

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ must still obey the no-mixed-execution rule.
336336
| `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"` |
337337
| `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"` |
338338
| `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"` |
339-
| `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"` |
339+
| `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"` |
340340
| `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"` |
341341
| `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"` |
342342
| `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.
570570
- Synchronous construct calls are admitted (gh2690 plus the construct-boundary
571571
widening): `new F(...)`, `new F(...args)`, `new box.Ctor(...)`, and
572572
`new box[key](...)` when the constructor/key/argument subexpressions are
573-
already production-safe. The constructor value and each logical argument are
574-
pushed left-to-right by their own loads (no receiver/`this`), then a single
575-
`ConstructInvocationBoundary` opcode invokes `[[Construct]]` with the
576-
constructor itself as `new.target`, mirroring the spec-conformant construct
577-
reference helper. Its operand uses the same low-16-bit argument count plus
578-
high-bit spread-mask reference encoding as `CallInvocationBoundary`; the VM
579-
flattens spread iterables at the construct boundary before calling
580-
`ReflectHelper.Construct`. A non-constructor target throws `TypeError` at the
581-
boundary.
573+
already production-safe. Construct results may feed trailing plain named
574+
property reads such as `new F(...args).x`. The constructor value and each
575+
logical argument are pushed left-to-right by their own loads (no
576+
receiver/`this`), then a single `ConstructInvocationBoundary` opcode invokes
577+
`[[Construct]]` with the constructor itself as `new.target`, mirroring the
578+
spec-conformant construct reference helper. Its operand uses the same
579+
low-16-bit argument count plus high-bit spread-mask reference encoding as
580+
`CallInvocationBoundary`; the VM flattens spread iterables at the construct
581+
boundary before calling `ReflectHelper.Construct`. A non-constructor target
582+
throws `TypeError` at the boundary.
582583
- Derived-constructor non-spread `super(...)` calls are admitted through
583584
`SuperConstructInvocationBoundary`. The VM resolves the dynamic super
584585
constructor from the current super binding, invokes `[[Construct]]` with the
@@ -795,10 +796,11 @@ support today.
795796
spread calls are now admitted (gh2676). Optional calls are now admitted
796797
(gh2689, ADR 0289): `box?.read(args)`, `box.read?.(args)`, and
797798
`box[key]?.(args)`. Synchronous construct calls (`new F(...)`, spread
798-
arguments, and member/computed constructor targets) are now admitted (gh2690,
799-
ADR 0286 plus construct-boundary widening). The first bounded super
800-
invocation shapes are now admitted too (PR #2862, ADR 0307): non-spread
801-
derived-constructor `super(...)` plus named/computed super-member calls.
799+
arguments, member/computed constructor targets, and trailing plain named
800+
result reads such as `new F(...args).x`) are now admitted (gh2690, ADR 0286
801+
plus construct-boundary widening). The first bounded super invocation shapes
802+
are now admitted too (PR #2862, ADR 0307): non-spread derived-constructor
803+
`super(...)` plus named/computed super-member calls.
802804
Simple array and object literal arguments (`fn([a, b])`, `fn({x: a})`) are
803805
now admitted (gh2705, ADR 0290). Optional-start computed member spread calls
804806
(`a?.box[key](...args)`) are also admitted through the shared spread-mask

src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1544,14 +1544,19 @@ private static bool TryIsConstructInvocationCandidate(
15441544
ReadOnlySpan<IdentifierOperand> identifierConstants,
15451545
ActivationSlotShape activationSlots)
15461546
{
1547-
if (program.OperationCount < 2 ||
1548-
program.GetOperation(program.OperationCount - 1).Kind != ExpressionOpKind.Construct)
1547+
if (program.OperationCount < 2)
1548+
{
1549+
return false;
1550+
}
1551+
1552+
var constructIndex = FindConstructInvocationBoundaryIndex(program);
1553+
if (constructIndex < 1)
15491554
{
15501555
return false;
15511556
}
15521557

15531558
var stringConstants = program.StringConstants.AsSpan();
1554-
for (var operationIndex = 0; operationIndex < program.OperationCount - 1; operationIndex++)
1559+
for (var operationIndex = 0; operationIndex < constructIndex; operationIndex++)
15551560
{
15561561
var operation = program.GetOperation(operationIndex);
15571562
if (IsPrivateNamedPropertyOperation(operation, stringConstants))
@@ -1583,6 +1588,26 @@ private static bool TryIsConstructInvocationCandidate(
15831588
return true;
15841589
}
15851590

1591+
private static int FindConstructInvocationBoundaryIndex(ExpressionProgram program)
1592+
{
1593+
var constructIndex = program.OperationCount - 1;
1594+
if (program.GetOperation(constructIndex).Kind == ExpressionOpKind.Construct)
1595+
{
1596+
return constructIndex;
1597+
}
1598+
1599+
var stringConstants = program.StringConstants.AsSpan();
1600+
while (constructIndex > 0 &&
1601+
IsPlainNamedPropertyRead(program.GetOperation(constructIndex), stringConstants))
1602+
{
1603+
constructIndex--;
1604+
}
1605+
1606+
return program.GetOperation(constructIndex).Kind == ExpressionOpKind.Construct
1607+
? constructIndex
1608+
: -1;
1609+
}
1610+
15861611
private static bool TryIsConstructComputedPropertyRead(
15871612
ExpressionProgram program,
15881613
int getComputedIndex,

tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,29 @@ function invoke(ctor, args) {
947947
Assert.NotEmpty(result.Program.CallSpreadMasks);
948948
}
949949

950+
[Fact]
951+
public void Evaluate_SpreadConstructResultNamedRead_AcceptsConstructInvocationBoundary()
952+
{
953+
var plan = GetFunctionPlan("""
954+
function invoke(ctor, args) {
955+
return new ctor(...args).x;
956+
}
957+
""",
958+
"invoke");
959+
960+
var result = UnifiedBytecodeProductionEligibility.Evaluate(
961+
plan,
962+
new UnifiedBytecodeProductionActivationDescriptor());
963+
964+
Assert.True(result.IsEligible, result.Reason);
965+
Assert.Equal(UnifiedBytecodeProductionDeclineCode.None, result.Code);
966+
Assert.Contains(result.Program.Instructions, instruction =>
967+
instruction.OpCode == UnifiedBytecodeOpCode.ConstructInvocationBoundary);
968+
Assert.Contains(result.Program.Instructions, instruction =>
969+
instruction.OpCode == UnifiedBytecodeOpCode.GetNamedProperty);
970+
Assert.NotEmpty(result.Program.CallSpreadMasks);
971+
}
972+
950973
[Fact]
951974
public void Evaluate_IdentifierConstructExpressionPlan_AcceptsConstructInvocationBoundary()
952975
{

tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionSpreadCallTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ static record => record.Message.Contains(
202202
}
203203

204204
[Fact(Timeout = 5000)]
205-
public async Task SpreadConstruct_DeclinesProductionFastPath()
205+
public async Task SpreadConstructResultNamedRead_UsesProductionFastPath()
206206
{
207207
await using var engine = CreateEngine();
208208
var result = await engine.Evaluate("""
@@ -219,7 +219,7 @@ function invoke(Point, args) {
219219
""");
220220

221221
Assert.Equal(41d, result);
222-
Assert.DoesNotContain(CurrentLogger!.Collector.Snapshot(),
222+
Assert.Contains(CurrentLogger!.Collector.Snapshot(),
223223
static record => record.Message.Contains(
224224
"unified-bytecode-production-fast-path func=invoke",
225225
StringComparison.Ordinal));

0 commit comments

Comments
 (0)