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
12 changes: 7 additions & 5 deletions docs/unified-bytecode-expansion-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ must still obey the no-mixed-execution rule.
| `ClassConstructorActivation` | Activation descriptor gate for class constructor activation outside the admitted simple base constructor route, explicit derived-constructor `super(...)` route with post-super `this` body reads/writes, and default-derived constructor `super(...args)` forwarding route | Constructor route | Constructor boundary lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionConstructCallTests&FullyQualifiedName~Constructor"` |
| `CallDependency` | Direct eval outside the one-argument non-spread eval-identifier boundary, out-of-boundary call-target preparation, and complex call arguments excluding admitted simple/binary template-literal substitutions, simple/binary computed object keys, and zero-argument activation-resolved identifier-call computed object keys | Existing sync IR call route | Wider call invocation lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests"` |
| `DynamicLookupDependency` | Unresolved identifier loads/stores/update outside the admitted ordinary, with-backed, and direct-eval-backed dynamic-name paths; plans that need direct eval plus post-eval dynamic identifier reads now route when captured activation, with-chain, and arguments-object dependencies are absent | Existing sync IR / environment lookup route | Dynamic-name lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_DirectEvalDeclaredVarRead_AcceptsOrdinaryDynamicNameProgram"` |
| `PropertyReadBoundaryOutOfScope` | Named/computed property reads outside the admitted activation-resolved boundaries | Existing sync IR property route | Property read widening lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_ComputedPropertyReadOutsideFirstBoundary_DeclinesWithBoundaryCode"` |
| `PropertyReadBoundaryOutOfScope` | Named/computed property reads outside the admitted activation-resolved and read-only dynamic-identifier-base boundaries | Existing sync IR property route | Property read widening lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_ComputedPropertyReadOutsideFirstBoundary_DeclinesWithBoundaryCode"` |
| `PropertyWriteDependency` | Property writes and compound/logical property writes outside the admitted direct property-write shapes, supported computed expression-key mutation shapes, simple nested named receiver assignment shape, nested named compound-write shape, and nested named logical-write shape | Existing sync IR property-write route | Property write widening lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_LogicalAndAssignment_UnsupportedShapes_DeclineWithExplicitCodes"` |
| `PropertyUpdateDependency` | Property and identifier update expressions outside the admitted direct update, computed expression-key update, and simple nested named receiver update boundary | Existing sync IR update route | Property update lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_NestedNamedPropertyUpdate_AcceptsOwnedPropertyOpcodes"` |
| `DeleteDependency` | `delete` expressions outside the admitted ordinary named/computed property delete lane, ordinary dynamic-key computed delete lane, and 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_NestedComputedPropertyDeleteDynamicKey_AcceptsOrdinaryDynamicNameOpcode"` |
Expand All @@ -354,7 +354,7 @@ not always have a `UnifiedBytecodeProductionDeclineCode`.
| Pre-gate key | Owning source / current example | Current fallback route | Planned batch / lane | Proof command |
|---|---|---|---|---|
| `pre-gate:IsClassConstructor` | Class constructor invokers outside the admitted simple base constructor route and explicit derived-constructor `super(...)` route with post-super `this` body reads/writes | Constructor route | Constructor boundary lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionInvocationTests"` |
| `pre-gate:IsArrowFunction` | Arrow functions outside the admitted simple-return route whose lowered body proves no super, closure-variable, or dynamic-identifier dependency. Captured lexical `this` / `new.target` values are VM-owned; lexical-this environments remain a separate pre-gate. | Existing arrow invocation route | Arrow route lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionInvocationTests"` |
| `pre-gate:IsArrowFunction` | Arrow functions outside the admitted simple-return route whose lowered body proves no super, call-target, assignment, update, delete, nested function/class literal, or other unowned dependency. Captured lexical `this` / `new.target`, statically resolved flat-slot reads, read-only ordinary environment identifier reads, and named property reads from those dynamic identifier bases are VM-owned; lexical-this environments remain a separate pre-gate. | Existing arrow invocation route | Arrow route lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionInvocationTests"` |
| `pre-gate:IsAsyncLike` | Async and formerly-async ordinary invokers | Async route | Resumable async/generator lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_AsyncLikeActivation_DeclinesBeforePlanInspection"` |
| `pre-gate:IsGenerator` | Generator functions before ordinary sync production routing | Generator route | Resumable async/generator lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_GeneratorActivation_DeclinesBeforePlanInspection"` |
| `pre-gate:hasParameterExpressions` | Default/rest/destructured parameter expressions | Existing parameter environment route | Parameter semantics lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionInvocationTests"` |
Expand Down Expand Up @@ -461,9 +461,11 @@ the final post-compile production subset check before VM entry.
- Sloppy-mode `this` coercion is handled before VM entry: `boundThis` is
computed via `CoerceThisValueForNonStrict` in `TryInvokeProductionUnifiedBytecode`,
so the VM's `LoadThis` opcode always receives the correctly coerced value.
- Simple-return arrow functions whose lowered body has no lexical `this`,
`new.target`, super, closure-variable, dynamic-identifier, or nested
function/class literal dependency may route. Dependency-bearing arrows still
- Simple-return arrow functions whose lowered body has no super, call-target,
assignment, update, delete, nested function/class literal, or other unowned
dependency may route. Captured lexical `this` / `new.target`, flat-slot reads,
read-only ordinary environment identifier reads, and named property reads from
those dynamic identifier bases are VM-owned. Dependency-bearing arrows still
decline via the `IsArrowFunction`, captured-activation, or
`_lexicalThisEnvironment is not null` pre-gates.
- There is no retained `this` decline in the production activation descriptor;
Expand Down
26 changes: 23 additions & 3 deletions src/Asynkron.JsEngine/Ast/TypedAstEvaluator.SyncFunctionInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3398,7 +3398,12 @@ private bool CanUseProductionUnifiedBytecodeFastPath(ExecutionPlan plan, JsValue
return false;
}

return CanUseProductionUnifiedBytecodePlanShape(plan, canUseDynamicNamePath) ||
return CanUseProductionUnifiedBytecodePlanShape(
plan,
canUseDynamicNamePath ||
canUseOrdinaryDynamicNamePath ||
canUseArrowFunctionPath ||
canUseImplicitArgumentsObjectReadPath) ||
(canUseDerivedClassConstructorPath || canUseBaseClassConstructorPath) &&
plan.ActivationSlots is not null;
}
Expand Down Expand Up @@ -3442,7 +3447,9 @@ private UnifiedBytecodeProductionActivationDescriptor CreateProductionUnifiedByt
IsClassConstructor && !canUseDerivedClassConstructorPath && !canUseBaseClassConstructorPath,
HasDynamicLookupDependency: hasUnprovenDynamicActivation,
AllowsOrdinaryDynamicIdentifierEnvironmentOperations:
canUseOrdinaryDynamicNamePath || canUseImplicitArgumentsObjectReadPath);
canUseOrdinaryDynamicNamePath ||
canUseImplicitArgumentsObjectReadPath ||
canUseArrowFunctionPath);
}

[MethodImpl(JsEngineConstants.Inlining)]
Expand Down Expand Up @@ -3719,7 +3726,8 @@ private static bool CanUseProductionUnifiedBytecodeArrowActivationDependencyPath
}

if (TryGetIdentifierDependency(operation, identifierConstants, out var identifier) &&
!ResolvesToOwnActivationSlot(identifier, activationSlots))
!ResolvesToOwnActivationOrFlatSlot(identifier, activationSlots) &&
!CanUseArrowDynamicIdentifierRead(operation, identifier))
{
return false;
}
Expand Down Expand Up @@ -3765,6 +3773,18 @@ private static bool ResolvesToOwnActivationSlot(
activationSlots.SlotMap.ContainsKey(identifier.Name);
}

private static bool ResolvesToOwnActivationOrFlatSlot(
IdentifierOperand identifier,
ActivationSlotShape activationSlots) =>
ResolvesToOwnActivationSlot(identifier, activationSlots) ||
identifier.FlatSlotId >= 0;

private static bool CanUseArrowDynamicIdentifierRead(
PackedExpressionOp operation,
IdentifierOperand identifier) =>
identifier.FlatSlotId < 0 &&
operation.Kind is ExpressionOpKind.LoadIdentifier or ExpressionOpKind.TypeOfIdentifier;

private bool CanUseProductionUnifiedBytecodeBaseClassConstructorActivation(
ExecutionPlan plan,
JsValue newTarget)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3783,7 +3783,9 @@ private static bool TryAppendExpressionProgramOps(
expressionProgram,
activationSlots,
unified,
literalConstants,
stringConstants,
allowsDynamicIdentifiers,
out reason))
{
return true;
Expand Down Expand Up @@ -8485,7 +8487,9 @@ private static bool TryAppendFirstBoundaryNamedPropertyReadChain(
ExpressionProgram expressionProgram,
ActivationSlotShape activationSlots,
ImmutableArray<UnifiedBytecodeInstruction>.Builder unified,
ImmutableArray<JsValue>.Builder literalConstants,
ImmutableArray<string>.Builder stringConstants,
bool allowsDynamicIdentifiers,
out string reason)
{
if (expressionProgram.OperationCount < 2)
Expand All @@ -8511,7 +8515,15 @@ private static bool TryAppendFirstBoundaryNamedPropertyReadChain(
}
}

if (!TryAppendActivationValueLoad(baseLoad, expressionProgram, activationSlots, unified, out reason))
if (!TryAppendSimpleOperandLoadWithDynamic(
baseLoad,
expressionProgram,
activationSlots,
allowsDynamicIdentifiers,
unified,
literalConstants,
stringConstants,
out reason))
{
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1113,7 +1113,11 @@ private static bool TryFindExpressionDecline(
break;
}

if (TryIsFirstBoundaryNamedPropertyReadCandidate(program, identifierConstants, activationSlots))
if (TryIsFirstBoundaryNamedPropertyReadCandidate(
program,
identifierConstants,
activationSlots,
allowsDynamicIdentifiers))
{
break;
}
Expand Down Expand Up @@ -1247,7 +1251,7 @@ private static bool TryFindExpressionDecline(

declineCode = UnifiedBytecodeProductionDeclineCode.PropertyReadBoundaryOutOfScope;
declineReason =
"Named property reads are outside the first production property-read boundary unless they are direct activation-resolved base reads or exact two-hop named chains.";
"Named property reads are outside the first production property-read boundary unless they are activation-resolved or admitted dynamic-identifier base reads.";
return true;

case ExpressionOpKind.GetComputedProperty:
Expand Down Expand Up @@ -1902,14 +1906,17 @@ private static bool TryGetActivationOrImplicitArgumentsObjectReadValue(
private static bool TryIsFirstBoundaryNamedPropertyReadCandidate(
ExpressionProgram program,
ReadOnlySpan<IdentifierOperand> identifierConstants,
ActivationSlotShape activationSlots)
ActivationSlotShape activationSlots,
bool allowsDynamicIdentifiers)
{
if (program.OperationCount < 2)
{
return false;
}

if (!TryGetActivationResolvedValue(program.GetOperation(0), identifierConstants, activationSlots))
if (!TryGetActivationResolvedValue(program.GetOperation(0), identifierConstants, activationSlots) &&
!(allowsDynamicIdentifiers &&
TryGetPlainDynamicIdentifierReadValue(program.GetOperation(0), identifierConstants, activationSlots)))
{
return false;
}
Expand All @@ -1928,6 +1935,21 @@ private static bool TryIsFirstBoundaryNamedPropertyReadCandidate(
return true;
}

private static bool TryGetPlainDynamicIdentifierReadValue(
PackedExpressionOp operation,
ReadOnlySpan<IdentifierOperand> identifierConstants,
ActivationSlotShape activationSlots)
{
if (operation.Kind != ExpressionOpKind.LoadIdentifier || operation.IsArguments)
{
return false;
}

var identifier = operation.GetIdentifier(identifierConstants);
return identifier.FlatSlotId < 0 &&
!TryResolveActivationSlot(identifier, activationSlots);
}

private static bool TryIsFirstBoundaryObjectLiteralNamedPropertyReadCandidate(
ExpressionProgram program,
ReadOnlySpan<IdentifierOperand> identifierConstants,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ private sealed record ProductionUnifiedBytecodeProofPackShape(
"Evaluate_LinearSlotLiteralReturnPlan_Accepts",
"Execute_LinearSlotLiteralReturnPlan_ReturnsSlotValueInProductionVm",
"LinearSlotReturnFunction_UsesUnifiedBytecodeProductionFastPath",
"ArrowFunction_CapturedThis_DeclinesUnifiedBytecodeProductionFastPath",
"BinaryReturnFunction_KeepsExistingSpecializedFastPath",
"SourceGate_ProductionUnifiedBytecodeAcceptedPath_DoesNotDelegateToAstOrExecutionPlanRunner")
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,30 @@ function read(box) {
Assert.Equal("value", Assert.Single(result.Program.StringConstants));
}

[Fact]
public void Evaluate_NamedPropertyReadWithDynamicIdentifierBase_AcceptsWhenDynamicReadsAreAdmitted()
{
var plan = GetFunctionPlan("""
function read() {
return outer.value;
}
""",
"read");

var result = UnifiedBytecodeProductionEligibility.Evaluate(
plan,
new UnifiedBytecodeProductionActivationDescriptor(
AllowsOrdinaryDynamicIdentifierEnvironmentOperations: true));

Assert.True(result.IsEligible, result.Reason);
Assert.Equal(UnifiedBytecodeProductionDeclineCode.None, result.Code);
Assert.Contains(result.Program.Instructions, instruction =>
instruction.OpCode == UnifiedBytecodeOpCode.LoadDynamicIdentifier);
Assert.Contains(result.Program.Instructions, instruction =>
instruction.OpCode == UnifiedBytecodeOpCode.GetNamedProperty);
Assert.Equal(new[] { "outer", "value" }, result.Program.StringConstants);
}

[Fact]
public void Evaluate_TwoHopNamedPropertyReadCandidate_AcceptsOwnedPropertyOpcodes()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6657,22 +6657,22 @@ record => record.Message.Contains(
}

[Fact(Timeout = 5000)]
public async Task ArrowFunction_CapturedThis_DeclinesUnifiedBytecodeProductionFastPath()
public async Task ArrowFunction_CapturedOuterEnvironmentRead_UsesUnifiedBytecodeProductionFastPath()
{
await using var engine = CreateEngine();
var result = await engine.Evaluate("""
function makeGetter(obj) {
var get = () => obj.value;
return get();
return () => obj.value;
}

makeGetter({ value: 42 });
var get = makeGetter({ value: 42 });
get();
""");

Assert.Equal(42d, result);
Assert.DoesNotContain(CurrentLogger!.Collector.Snapshot(),
Assert.Contains(CurrentLogger!.Collector.Snapshot(),
record => record.Message.Contains(
"unified-bytecode-production-fast-path func=get",
"unified-bytecode-production-fast-path func=<anonymous>",
StringComparison.Ordinal));
}

Expand Down