Skip to content

Commit 8e834b1

Browse files
authored
Admit captured closure reads to production bytecode (#3039)
1 parent 94f8a1a commit 8e834b1

4 files changed

Lines changed: 94 additions & 5 deletions

File tree

docs/unified-bytecode-expansion-contract.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ must still obey the no-mixed-execution rule.
324324
| `None` | `UnifiedBytecodeProductionEligibility.Accept` for accepted programs, for example `function passThrough(x) { var y = x; return y; }` | Production unified-bytecode VM | Baseline accepted route | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_LinearSlotLiteralReturnPlan_Accepts"` |
325325
| `AsyncLikeFunction` | `Evaluate` activation gate and awaited with-object plan decline | Existing async / awaited IR route | Resumable async/generator lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_AsyncLikeActivation_DeclinesBeforePlanInspection"` |
326326
| `GeneratorFunction` | `Evaluate` activation gate for ordinary sync production routing | Existing generator IR route | Resumable async/generator lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_GeneratorActivation_DeclinesBeforePlanInspection"` |
327-
| `CapturedOrDynamicActivation` | Activation descriptor `HasCapturedOrDynamicActivation`, including captured function scope or unresolved dynamic activation outside the with-backed lane | Existing sync IR / environment route | Dynamic activation lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_ActivationDependencies_DeclineBeforeCompile"` |
327+
| `CapturedOrDynamicActivation` | Activation descriptor `HasCapturedOrDynamicActivation`, including captured function scope outside the admitted read-only captured-closure route or unresolved dynamic activation outside the with-backed lane | Existing sync IR / environment route | Dynamic activation lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_ActivationDependencies_DeclineBeforeCompile"` |
328328
| `ArgumentsObjectDependency` | Activation descriptor gate for unbounded real `arguments` object dependency; implicit `arguments` reads/assignments/updates/calls materialize the existing arguments object and use the bounded identifier route, while parameter/lexical `arguments` reads, `typeof`, assignments, updates, and call targets route as activation slots | Existing sync IR / arguments-object route | Arguments object lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_CallImplicitArgumentsObject_AcceptsDynamicIdentifierCallTarget"` |
329329
| `ArrowLexicalThisDependency` | Activation descriptor gate for arrow lexical `this` / `new.target` ownership before ordinary sync routing | Existing arrow invocation route | Arrow route lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_OrdinarySyncActivationDescriptorBlockers_DeclineBeforeCompile"` |
330330
| `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"` |
@@ -468,6 +468,11 @@ the final post-compile production subset check before VM entry.
468468
those dynamic identifier bases are VM-owned. Dependency-bearing arrows still
469469
decline via the `IsArrowFunction`, captured-activation, or
470470
`_lexicalThisEnvironment is not null` pre-gates.
471+
- Simple-return ordinary function expressions that capture an outer activation
472+
may route when the body is read-only and uses the same ordinary environment
473+
identifier / named property read subset. Captured writes, updates, deletes,
474+
calls, `arguments`, eval, with-backed closures, super/private/home-object
475+
shapes, and nested function/class literals remain outside this route.
471476
- There is no retained `this` decline in the production activation descriptor;
472477
future shapes that cannot thread `this` through the VM should introduce a
473478
concrete gate with a current failing example instead of carrying a placeholder.

src/Asynkron.JsEngine/Ast/TypedAstEvaluator.SyncFunctionInvoker.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3323,6 +3323,7 @@ private bool TryGetProductionUnifiedBytecodeProgram(
33233323
CanUseProductionUnifiedBytecodeDynamicNameFastPath(),
33243324
CanUseProductionUnifiedBytecodeOrdinaryDynamicNameFastPath(plan),
33253325
CanUseProductionUnifiedBytecodeArrowFunctionActivation(plan, newTarget),
3326+
CanUseProductionUnifiedBytecodeCapturedClosureActivation(plan, newTarget),
33263327
CanUseProductionUnifiedBytecodeDerivedClassConstructorActivation(plan, newTarget),
33273328
CanUseProductionUnifiedBytecodeBaseClassConstructorActivation(plan, newTarget),
33283329
CanUseProductionUnifiedBytecodeImplicitArgumentsObjectReadPath(plan)));
@@ -3369,6 +3370,8 @@ private bool CanUseProductionUnifiedBytecodeFastPath(ExecutionPlan plan, JsValue
33693370
var canUseDynamicNamePath = CanUseProductionUnifiedBytecodeDynamicNameFastPath();
33703371
var canUseOrdinaryDynamicNamePath = CanUseProductionUnifiedBytecodeOrdinaryDynamicNameFastPath(plan);
33713372
var canUseArrowFunctionPath = CanUseProductionUnifiedBytecodeArrowFunctionActivation(plan, newTarget);
3373+
var canUseCapturedClosurePath =
3374+
CanUseProductionUnifiedBytecodeCapturedClosureActivation(plan, newTarget);
33723375
var canUseDerivedClassConstructorPath =
33733376
CanUseProductionUnifiedBytecodeDerivedClassConstructorActivation(plan, newTarget);
33743377
var canUseBaseClassConstructorPath =
@@ -3382,6 +3385,7 @@ private bool CanUseProductionUnifiedBytecodeFastPath(ExecutionPlan plan, JsValue
33823385
canUseDynamicNamePath,
33833386
canUseOrdinaryDynamicNamePath,
33843387
canUseArrowFunctionPath,
3388+
canUseCapturedClosurePath,
33853389
canUseDerivedClassConstructorPath,
33863390
canUseBaseClassConstructorPath,
33873391
canUseImplicitArgumentsObjectReadPath);
@@ -3403,6 +3407,7 @@ private bool CanUseProductionUnifiedBytecodeFastPath(ExecutionPlan plan, JsValue
34033407
canUseDynamicNamePath ||
34043408
canUseOrdinaryDynamicNamePath ||
34053409
canUseArrowFunctionPath ||
3410+
canUseCapturedClosurePath ||
34063411
canUseImplicitArgumentsObjectReadPath) ||
34073412
(canUseDerivedClassConstructorPath || canUseBaseClassConstructorPath) &&
34083413
plan.ActivationSlots is not null;
@@ -3421,6 +3426,7 @@ private UnifiedBytecodeProductionActivationDescriptor CreateProductionUnifiedByt
34213426
bool canUseDynamicNamePath,
34223427
bool canUseOrdinaryDynamicNamePath,
34233428
bool canUseArrowFunctionPath = false,
3429+
bool canUseCapturedClosurePath = false,
34243430
bool canUseDerivedClassConstructorPath = false,
34253431
bool canUseBaseClassConstructorPath = false,
34263432
bool canUseImplicitArgumentsObjectReadPath = false)
@@ -3433,7 +3439,9 @@ private UnifiedBytecodeProductionActivationDescriptor CreateProductionUnifiedByt
34333439
IsAsyncLike: IsAsyncLike,
34343440
IsGenerator: _function.IsGenerator,
34353441
HasCapturedOrDynamicActivation:
3436-
_hasCapturedActivationInClosure && !canUseArrowFunctionPath ||
3442+
_hasCapturedActivationInClosure &&
3443+
!canUseArrowFunctionPath &&
3444+
!canUseCapturedClosurePath ||
34373445
_hasClosureWithObject && !canUseDynamicNamePath ||
34383446
hasUnprovenDynamicActivation,
34393447
HasArgumentsObjectDependency:
@@ -3449,7 +3457,8 @@ private UnifiedBytecodeProductionActivationDescriptor CreateProductionUnifiedByt
34493457
AllowsOrdinaryDynamicIdentifierEnvironmentOperations:
34503458
canUseOrdinaryDynamicNamePath ||
34513459
canUseImplicitArgumentsObjectReadPath ||
3452-
canUseArrowFunctionPath);
3460+
canUseArrowFunctionPath ||
3461+
canUseCapturedClosurePath);
34533462
}
34543463

34553464
[MethodImpl(JsEngineConstants.Inlining)]
@@ -3702,6 +3711,35 @@ _superPrototype is null &&
37023711
CanUseSimpleIrActivationPlanShape(plan);
37033712
}
37043713

3714+
private bool CanUseProductionUnifiedBytecodeCapturedClosureActivation(
3715+
ExecutionPlan plan,
3716+
JsValue newTarget)
3717+
{
3718+
return !IsArrowFunction &&
3719+
newTarget.IsUndefined &&
3720+
!IsClassConstructor &&
3721+
!IsAsyncLike &&
3722+
!_function.IsGenerator &&
3723+
!_function.IsDefaultDerivedConstructor &&
3724+
!_hasParameterExpressions &&
3725+
_hasOnlySimpleIdentifierParameters &&
3726+
!_usesArguments &&
3727+
!_needsArgumentsBinding &&
3728+
_allowIdentifierCache &&
3729+
_hasCapturedActivationInClosure &&
3730+
!_hasClosureWithObject &&
3731+
_lexicalThisEnvironment is null &&
3732+
_homeObject is null &&
3733+
PrivateNameScope is null &&
3734+
_capturedPrivateNameScopes.IsDefaultOrEmpty &&
3735+
_superConstructor is null &&
3736+
_superPrototype is null &&
3737+
_instanceFields.IsDefaultOrEmpty &&
3738+
CanUseProductionUnifiedBytecodeArrowProgramShape(plan) &&
3739+
CanUseProductionUnifiedBytecodeArrowActivationDependencyPath(plan) &&
3740+
CanUseSimpleIrActivationPlanShape(plan);
3741+
}
3742+
37053743
private static bool CanUseProductionUnifiedBytecodeArrowProgramShape(ExecutionPlan plan)
37063744
{
37073745
return plan.SimpleReturnProgram is { } returnProgram &&

tests/Asynkron.JsEngine.Tests/ActivationSemanticsProofPackTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public sealed class ActivationSemanticsProofPackTests(ITestOutputHelper output)
1717
private const string SimpleIrParameterBinaryFastPathLog = "simple-ir-parameter-binary-fast-path";
1818
private const string SimpleIrParameterBinaryChainFastPathLog = "simple-ir-parameter-binary-chain-fast-path";
1919
private const string SimpleIrReturnFastPathLog = "simple-ir-return-fast-path";
20+
private const string UnifiedBytecodeProductionFastPathLog = "unified-bytecode-production-fast-path";
2021

2122
[Fact(Timeout = 5000)]
2223
public async Task SimpleSyncFunction_DoesNotUseCallerBinaryOrSimpleReturnFastPaths()
@@ -667,7 +668,7 @@ function makeCounter(start) {
667668
}
668669

669670
[Fact(Timeout = 5000)]
670-
public async Task ClosureCaptureRead_UsesIrActivationFastPath()
671+
public async Task ClosureCaptureRead_UsesProductionUnifiedBytecodeFastPath()
671672
{
672673
await using var engine = CreateEngine();
673674
var result = await engine.Evaluate("""
@@ -683,7 +684,7 @@ function makeReader(a) {
683684

684685
Assert.Equal(42d, result);
685686
Assert.Contains(CurrentLogger!.Collector.Snapshot(),
686-
static record => record.Message.Contains(SimpleIrActivationFastPathLog, StringComparison.Ordinal));
687+
static record => record.Message.Contains(UnifiedBytecodeProductionFastPathLog, StringComparison.Ordinal));
687688
}
688689

689690
[Fact(Timeout = 5000)]

tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6676,6 +6676,51 @@ record => record.Message.Contains(
66766676
StringComparison.Ordinal));
66776677
}
66786678

6679+
[Fact(Timeout = 5000)]
6680+
public async Task FunctionExpression_CapturedOuterEnvironmentRead_UsesUnifiedBytecodeProductionFastPath()
6681+
{
6682+
await using var engine = CreateEngine();
6683+
var result = await engine.Evaluate("""
6684+
function makeGetter(obj) {
6685+
return function get() {
6686+
return obj.value;
6687+
};
6688+
}
6689+
6690+
var get = makeGetter({ value: 42 });
6691+
get();
6692+
""");
6693+
6694+
Assert.Equal(42d, result);
6695+
Assert.Contains(CurrentLogger!.Collector.Snapshot(),
6696+
record => record.Message.Contains(
6697+
"unified-bytecode-production-fast-path func=get",
6698+
StringComparison.Ordinal));
6699+
}
6700+
6701+
[Fact(Timeout = 5000)]
6702+
public async Task FunctionExpression_CapturedOuterEnvironmentWrite_DoesNotUseUnifiedBytecodeProductionFastPath()
6703+
{
6704+
await using var engine = CreateEngine();
6705+
var result = await engine.Evaluate("""
6706+
function makeSetter(obj) {
6707+
return function set() {
6708+
obj.value = 43;
6709+
return obj.value;
6710+
};
6711+
}
6712+
6713+
var set = makeSetter({ value: 42 });
6714+
set();
6715+
""");
6716+
6717+
Assert.Equal(43d, result);
6718+
Assert.DoesNotContain(CurrentLogger!.Collector.Snapshot(),
6719+
record => record.Message.Contains(
6720+
"unified-bytecode-production-fast-path func=set",
6721+
StringComparison.Ordinal));
6722+
}
6723+
66796724
public static TheoryData<string, string, double, string> UnsupportedControlFlowFunctions =>
66806725
new()
66816726
{

0 commit comments

Comments
 (0)