From bd2e9a797dc025d6b38ec541c098a16dd4b3714d Mon Sep 17 00:00:00 2001 From: Faktorial Agent Date: Mon, 1 Jun 2026 12:32:36 +0200 Subject: [PATCH] feat: route shadowed arguments updates --- docs/rules/unified-bytecode-prototypes.md | 4 +- docs/unified-bytecode-expansion-contract.md | 4 +- .../UnifiedBytecodeCompiler.cs | 12 ++-- .../UnifiedBytecodeProductionEligibility.cs | 17 +++--- ...ifiedBytecodeProductionEligibilityTests.cs | 37 +++++++++++ ...nifiedBytecodeProductionInvocationTests.cs | 61 +++++++++++++++++++ 6 files changed, 115 insertions(+), 20 deletions(-) diff --git a/docs/rules/unified-bytecode-prototypes.md b/docs/rules/unified-bytecode-prototypes.md index 09cbd5ce9..6cf1684d3 100644 --- a/docs/rules/unified-bytecode-prototypes.md +++ b/docs/rules/unified-bytecode-prototypes.md @@ -119,8 +119,8 @@ all-or-nothing until a separate routing issue proves production readiness. 10b. Treat `arguments` as an arguments-object dependency only after binding resolution proves it is not an ordinary activation slot. A parameter named `arguments` or a lexical body binding named `arguments` is regular slot - traffic for production unified bytecode reads and `typeof`; the real implicit - arguments object, arguments call targets, and writes/updates/deletes of the + traffic for production unified bytecode reads, `typeof`, and updates; the real implicit + arguments object, arguments call targets, and writes/deletes of the implicit arguments binding still decline before VM execution. Keep the invocation descriptor, expression selector, and compiler in agreement so a name-only `operation.IsArguments` check cannot block shadowed bindings or diff --git a/docs/unified-bytecode-expansion-contract.md b/docs/unified-bytecode-expansion-contract.md index dcd1e07ac..78c797ab1 100644 --- a/docs/unified-bytecode-expansion-contract.md +++ b/docs/unified-bytecode-expansion-contract.md @@ -267,7 +267,7 @@ must still obey the no-mixed-execution rule. | `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"` | -| `ArgumentsObjectDependency` | Activation descriptor and expression scan for real `arguments` object access or arguments call targets; parameter/lexical `arguments` reads and `typeof` now 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_ArgumentsAccess_DeclinesWithArgumentsDependency"` | +| `ArgumentsObjectDependency` | Activation descriptor and expression scan for real `arguments` object access or arguments call targets; parameter/lexical `arguments` reads, `typeof`, and updates now 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_ArgumentsAccess_DeclinesWithArgumentsDependency"` | | `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 explicit derived-constructor `super(...)` route without `this` body reads/writes | Constructor route | Constructor boundary lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionConstructCallTests&FullyQualifiedName~SuperConstruct"` | | `CallDependency` | Direct eval outside the one-argument non-spread eval-identifier boundary, out-of-boundary call-target preparation, complex call arguments, and descriptor-level non-parameter callee calls | Existing sync IR call route | Wider call invocation lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_CallWithComplexTemplateLiteralSubstitution_DeclinesCallDependency"` | @@ -302,7 +302,7 @@ not always have a `UnifiedBytecodeProductionDeclineCode`. | `pre-gate:IsDefaultDerivedConstructor` | Default derived class constructor setup | Constructor route | Constructor boundary lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionInvocationTests"` | | `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"` | | `pre-gate:hasOnlySimpleIdentifierParameters` | Non-simple parameter lists | Existing parameter environment route | Parameter semantics lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionInvocationTests"` | -| `pre-gate:usesArguments` | Functions that read the real implicit `arguments` object; shadowing parameter/lexical `arguments` reads and `typeof` are activation-slot traffic | Existing arguments-object route | Arguments object lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_ArgumentsAccess_DeclinesWithArgumentsDependency"` | +| `pre-gate:usesArguments` | Functions that read the real implicit `arguments` object; shadowing parameter/lexical `arguments` reads, `typeof`, and updates are activation-slot traffic | Existing arguments-object route | Arguments object lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_ArgumentsAccess_DeclinesWithArgumentsDependency"` | | `pre-gate:needsArgumentsBinding` | Sloppy mapped arguments binding when an implicit arguments object is needed and not admitted by with-backed dynamic-name routing | Existing arguments-object route | Arguments object lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionInvocationTests"` | | `pre-gate:allowIdentifierCache` | Identifier cache disabled outside the with-backed dynamic-name lane | Existing environment lookup route | Dynamic-name lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_DynamicIdentifierLookup_DeclinesWithDynamicLookupDependency"` | | `pre-gate:lexicalThisEnvironment` | Lexical `this` environment captured by arrow / class-derived shapes | Existing lexical-this route | This-binding lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionInvocationTests"` | diff --git a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs index 3226db23c..0187e8801 100644 --- a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs +++ b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs @@ -4128,12 +4128,6 @@ private static bool TryAppendExpressionProgramOps( break; case ExpressionOpKind.UpdateIdentifier: - if (operation.IsArguments) - { - reason = "arguments update is not supported."; - return false; - } - var updateIdentifier = operation.GetIdentifier(expressionProgram.IdentifierConstants.AsSpan()); if (TryResolveActivationSlot(updateIdentifier, slotLayout, out var updateSlot)) { @@ -4143,6 +4137,12 @@ private static bool TryAppendExpressionProgramOps( break; } + if (operation.IsArguments) + { + reason = "arguments update is not supported."; + return false; + } + if (!allowsDynamicIdentifiers) { reason = diff --git a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs index 8934592dc..af1443b15 100644 --- a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs +++ b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs @@ -1234,18 +1234,15 @@ private static bool TryFindExpressionDecline( return true; case ExpressionOpKind.UpdateIdentifier: - if (!operation.IsArguments) + var updateIdentifier = operation.GetIdentifier(identifierConstants); + if (TryResolveActivationSlot(updateIdentifier, activationSlots)) { - var updateIdentifier = operation.GetIdentifier(identifierConstants); - if (TryResolveActivationSlot(updateIdentifier, activationSlots)) - { - break; - } + break; + } - if (allowsDynamicIdentifiers) - { - break; - } + if (!operation.IsArguments && allowsDynamicIdentifiers) + { + break; } declineCode = UnifiedBytecodeProductionDeclineCode.PropertyUpdateDependency; diff --git a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs index f4b292f5d..85124d97e 100644 --- a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs +++ b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs @@ -2694,6 +2694,43 @@ function readArguments() { Assert.Equal(UnifiedBytecodeProductionDeclineCode.ArgumentsObjectDependency, result.Code); } + [Fact] + public void Evaluate_UpdateParameterNamedArguments_AcceptsAsActivationSlot() + { + var plan = GetFunctionPlan(""" + function bump(arguments) { + arguments++; + return arguments; + } + """, + "bump"); + + var result = UnifiedBytecodeProductionEligibility.Evaluate( + plan, + new UnifiedBytecodeProductionActivationDescriptor()); + + Assert.True(result.IsEligible); + } + + [Fact] + public void Evaluate_UpdateImplicitArgumentsObject_DeclinesWithDynamicLookupDependency() + { + var plan = GetFunctionPlan(""" + function bump() { + arguments++; + return 1; + } + """, + "bump"); + + var result = UnifiedBytecodeProductionEligibility.Evaluate( + plan, + new UnifiedBytecodeProductionActivationDescriptor()); + + Assert.False(result.IsEligible); + Assert.Equal(UnifiedBytecodeProductionDeclineCode.DynamicLookupDependency, result.Code); + } + [Fact] public void Evaluate_DynamicIdentifierLookup_DeclinesWithDynamicLookupDependency() { diff --git a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs index 00d5446b8..18a9b54f3 100644 --- a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs +++ b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs @@ -5029,6 +5029,67 @@ record => record.Message.Contains( StringComparison.Ordinal)); } + [Fact(Timeout = 5000)] + public async Task UpdateParameterNamedArguments_UsesUnifiedBytecodeProductionFastPath() + { + await using var engine = CreateEngine(); + var result = await engine.Evaluate(""" + function bump(arguments) { + arguments++; + return arguments; + } + + bump(41); + """); + + Assert.Equal(42d, result); + Assert.Contains(CurrentLogger!.Collector.Snapshot(), + record => record.Message.Contains( + "unified-bytecode-production-fast-path func=bump argc=1", + StringComparison.Ordinal)); + } + + [Fact(Timeout = 5000)] + public async Task UpdateLexicalArgumentsBinding_UsesUnifiedBytecodeProductionFastPath() + { + await using var engine = CreateEngine(); + var result = await engine.Evaluate(""" + function bump() { + let arguments = 41; + ++arguments; + return arguments; + } + + bump(); + """); + + Assert.Equal(42d, result); + Assert.Contains(CurrentLogger!.Collector.Snapshot(), + record => record.Message.Contains( + "unified-bytecode-production-fast-path func=bump argc=0", + StringComparison.Ordinal)); + } + + [Fact(Timeout = 5000)] + public async Task UpdateImplicitArgumentsObject_DeclinesAndFallsBack() + { + await using var engine = CreateEngine(); + var result = await engine.Evaluate(""" + function bump() { + arguments++; + return 1; + } + + bump(41); + """); + + Assert.Equal(1d, result); + Assert.DoesNotContain(CurrentLogger!.Collector.Snapshot(), + record => record.Message.Contains( + "unified-bytecode-production-fast-path func=bump", + StringComparison.Ordinal)); + } + [Theory(Timeout = 5000)] [InlineData( """