From 6fa6098329643e1675939b9d0f165d3682126e4b Mon Sep 17 00:00:00 2001 From: Faktorial Agent Date: Tue, 2 Jun 2026 03:09:57 +0200 Subject: [PATCH] Admit captured closure reads to production bytecode --- docs/unified-bytecode-expansion-contract.md | 7 ++- .../TypedAstEvaluator.SyncFunctionInvoker.cs | 42 ++++++++++++++++- .../ActivationSemanticsProofPackTests.cs | 5 ++- ...nifiedBytecodeProductionInvocationTests.cs | 45 +++++++++++++++++++ 4 files changed, 94 insertions(+), 5 deletions(-) diff --git a/docs/unified-bytecode-expansion-contract.md b/docs/unified-bytecode-expansion-contract.md index ab4b3f2a2..8c2bd043e 100644 --- a/docs/unified-bytecode-expansion-contract.md +++ b/docs/unified-bytecode-expansion-contract.md @@ -324,7 +324,7 @@ must still obey the no-mixed-execution rule. | `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"` | | `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"` | | `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"` | -| `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"` | +| `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"` | | `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"` | | `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"` | | `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. those dynamic identifier bases are VM-owned. Dependency-bearing arrows still decline via the `IsArrowFunction`, captured-activation, or `_lexicalThisEnvironment is not null` pre-gates. +- Simple-return ordinary function expressions that capture an outer activation + may route when the body is read-only and uses the same ordinary environment + identifier / named property read subset. Captured writes, updates, deletes, + calls, `arguments`, eval, with-backed closures, super/private/home-object + shapes, and nested function/class literals remain outside this route. - There is no retained `this` decline in the production activation descriptor; future shapes that cannot thread `this` through the VM should introduce a concrete gate with a current failing example instead of carrying a placeholder. diff --git a/src/Asynkron.JsEngine/Ast/TypedAstEvaluator.SyncFunctionInvoker.cs b/src/Asynkron.JsEngine/Ast/TypedAstEvaluator.SyncFunctionInvoker.cs index 48e269bfa..6adde874d 100644 --- a/src/Asynkron.JsEngine/Ast/TypedAstEvaluator.SyncFunctionInvoker.cs +++ b/src/Asynkron.JsEngine/Ast/TypedAstEvaluator.SyncFunctionInvoker.cs @@ -3323,6 +3323,7 @@ private bool TryGetProductionUnifiedBytecodeProgram( CanUseProductionUnifiedBytecodeDynamicNameFastPath(), CanUseProductionUnifiedBytecodeOrdinaryDynamicNameFastPath(plan), CanUseProductionUnifiedBytecodeArrowFunctionActivation(plan, newTarget), + CanUseProductionUnifiedBytecodeCapturedClosureActivation(plan, newTarget), CanUseProductionUnifiedBytecodeDerivedClassConstructorActivation(plan, newTarget), CanUseProductionUnifiedBytecodeBaseClassConstructorActivation(plan, newTarget), CanUseProductionUnifiedBytecodeImplicitArgumentsObjectReadPath(plan))); @@ -3369,6 +3370,8 @@ private bool CanUseProductionUnifiedBytecodeFastPath(ExecutionPlan plan, JsValue var canUseDynamicNamePath = CanUseProductionUnifiedBytecodeDynamicNameFastPath(); var canUseOrdinaryDynamicNamePath = CanUseProductionUnifiedBytecodeOrdinaryDynamicNameFastPath(plan); var canUseArrowFunctionPath = CanUseProductionUnifiedBytecodeArrowFunctionActivation(plan, newTarget); + var canUseCapturedClosurePath = + CanUseProductionUnifiedBytecodeCapturedClosureActivation(plan, newTarget); var canUseDerivedClassConstructorPath = CanUseProductionUnifiedBytecodeDerivedClassConstructorActivation(plan, newTarget); var canUseBaseClassConstructorPath = @@ -3382,6 +3385,7 @@ private bool CanUseProductionUnifiedBytecodeFastPath(ExecutionPlan plan, JsValue canUseDynamicNamePath, canUseOrdinaryDynamicNamePath, canUseArrowFunctionPath, + canUseCapturedClosurePath, canUseDerivedClassConstructorPath, canUseBaseClassConstructorPath, canUseImplicitArgumentsObjectReadPath); @@ -3403,6 +3407,7 @@ private bool CanUseProductionUnifiedBytecodeFastPath(ExecutionPlan plan, JsValue canUseDynamicNamePath || canUseOrdinaryDynamicNamePath || canUseArrowFunctionPath || + canUseCapturedClosurePath || canUseImplicitArgumentsObjectReadPath) || (canUseDerivedClassConstructorPath || canUseBaseClassConstructorPath) && plan.ActivationSlots is not null; @@ -3421,6 +3426,7 @@ private UnifiedBytecodeProductionActivationDescriptor CreateProductionUnifiedByt bool canUseDynamicNamePath, bool canUseOrdinaryDynamicNamePath, bool canUseArrowFunctionPath = false, + bool canUseCapturedClosurePath = false, bool canUseDerivedClassConstructorPath = false, bool canUseBaseClassConstructorPath = false, bool canUseImplicitArgumentsObjectReadPath = false) @@ -3433,7 +3439,9 @@ private UnifiedBytecodeProductionActivationDescriptor CreateProductionUnifiedByt IsAsyncLike: IsAsyncLike, IsGenerator: _function.IsGenerator, HasCapturedOrDynamicActivation: - _hasCapturedActivationInClosure && !canUseArrowFunctionPath || + _hasCapturedActivationInClosure && + !canUseArrowFunctionPath && + !canUseCapturedClosurePath || _hasClosureWithObject && !canUseDynamicNamePath || hasUnprovenDynamicActivation, HasArgumentsObjectDependency: @@ -3449,7 +3457,8 @@ private UnifiedBytecodeProductionActivationDescriptor CreateProductionUnifiedByt AllowsOrdinaryDynamicIdentifierEnvironmentOperations: canUseOrdinaryDynamicNamePath || canUseImplicitArgumentsObjectReadPath || - canUseArrowFunctionPath); + canUseArrowFunctionPath || + canUseCapturedClosurePath); } [MethodImpl(JsEngineConstants.Inlining)] @@ -3702,6 +3711,35 @@ _superPrototype is null && CanUseSimpleIrActivationPlanShape(plan); } + private bool CanUseProductionUnifiedBytecodeCapturedClosureActivation( + ExecutionPlan plan, + JsValue newTarget) + { + return !IsArrowFunction && + newTarget.IsUndefined && + !IsClassConstructor && + !IsAsyncLike && + !_function.IsGenerator && + !_function.IsDefaultDerivedConstructor && + !_hasParameterExpressions && + _hasOnlySimpleIdentifierParameters && + !_usesArguments && + !_needsArgumentsBinding && + _allowIdentifierCache && + _hasCapturedActivationInClosure && + !_hasClosureWithObject && + _lexicalThisEnvironment is null && + _homeObject is null && + PrivateNameScope is null && + _capturedPrivateNameScopes.IsDefaultOrEmpty && + _superConstructor is null && + _superPrototype is null && + _instanceFields.IsDefaultOrEmpty && + CanUseProductionUnifiedBytecodeArrowProgramShape(plan) && + CanUseProductionUnifiedBytecodeArrowActivationDependencyPath(plan) && + CanUseSimpleIrActivationPlanShape(plan); + } + private static bool CanUseProductionUnifiedBytecodeArrowProgramShape(ExecutionPlan plan) { return plan.SimpleReturnProgram is { } returnProgram && diff --git a/tests/Asynkron.JsEngine.Tests/ActivationSemanticsProofPackTests.cs b/tests/Asynkron.JsEngine.Tests/ActivationSemanticsProofPackTests.cs index 760c9a39b..194356da2 100644 --- a/tests/Asynkron.JsEngine.Tests/ActivationSemanticsProofPackTests.cs +++ b/tests/Asynkron.JsEngine.Tests/ActivationSemanticsProofPackTests.cs @@ -17,6 +17,7 @@ public sealed class ActivationSemanticsProofPackTests(ITestOutputHelper output) private const string SimpleIrParameterBinaryFastPathLog = "simple-ir-parameter-binary-fast-path"; private const string SimpleIrParameterBinaryChainFastPathLog = "simple-ir-parameter-binary-chain-fast-path"; private const string SimpleIrReturnFastPathLog = "simple-ir-return-fast-path"; + private const string UnifiedBytecodeProductionFastPathLog = "unified-bytecode-production-fast-path"; [Fact(Timeout = 5000)] public async Task SimpleSyncFunction_DoesNotUseCallerBinaryOrSimpleReturnFastPaths() @@ -667,7 +668,7 @@ function makeCounter(start) { } [Fact(Timeout = 5000)] - public async Task ClosureCaptureRead_UsesIrActivationFastPath() + public async Task ClosureCaptureRead_UsesProductionUnifiedBytecodeFastPath() { await using var engine = CreateEngine(); var result = await engine.Evaluate(""" @@ -683,7 +684,7 @@ function makeReader(a) { Assert.Equal(42d, result); Assert.Contains(CurrentLogger!.Collector.Snapshot(), - static record => record.Message.Contains(SimpleIrActivationFastPathLog, StringComparison.Ordinal)); + static record => record.Message.Contains(UnifiedBytecodeProductionFastPathLog, StringComparison.Ordinal)); } [Fact(Timeout = 5000)] diff --git a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs index 684d26d47..51b0d3478 100644 --- a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs +++ b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs @@ -6676,6 +6676,51 @@ record => record.Message.Contains( StringComparison.Ordinal)); } + [Fact(Timeout = 5000)] + public async Task FunctionExpression_CapturedOuterEnvironmentRead_UsesUnifiedBytecodeProductionFastPath() + { + await using var engine = CreateEngine(); + var result = await engine.Evaluate(""" + function makeGetter(obj) { + return function get() { + return obj.value; + }; + } + + var get = makeGetter({ value: 42 }); + get(); + """); + + Assert.Equal(42d, result); + Assert.Contains(CurrentLogger!.Collector.Snapshot(), + record => record.Message.Contains( + "unified-bytecode-production-fast-path func=get", + StringComparison.Ordinal)); + } + + [Fact(Timeout = 5000)] + public async Task FunctionExpression_CapturedOuterEnvironmentWrite_DoesNotUseUnifiedBytecodeProductionFastPath() + { + await using var engine = CreateEngine(); + var result = await engine.Evaluate(""" + function makeSetter(obj) { + return function set() { + obj.value = 43; + return obj.value; + }; + } + + var set = makeSetter({ value: 42 }); + set(); + """); + + Assert.Equal(43d, result); + Assert.DoesNotContain(CurrentLogger!.Collector.Snapshot(), + record => record.Message.Contains( + "unified-bytecode-production-fast-path func=set", + StringComparison.Ordinal)); + } + public static TheoryData UnsupportedControlFlowFunctions => new() {