Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions docs/unified-bytecode-expansion-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"` |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1544,14 +1544,19 @@ private static bool TryIsConstructInvocationCandidate(
ReadOnlySpan<IdentifierOperand> 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))
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("""
Expand All @@ -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));
Expand Down
Loading