From fcf6c8658bfbc23f1e7ed8c4641bcb1840ff082f Mon Sep 17 00:00:00 2001 From: Faktorial Agent Date: Tue, 2 Jun 2026 00:12:12 +0200 Subject: [PATCH] Admit expression keys for computed member calls --- .../UnifiedBytecodeCompiler.cs | 35 ++++++++++++++++--- .../UnifiedBytecodeProductionEligibility.cs | 25 ++++++++++--- ...ifiedBytecodeProductionEligibilityTests.cs | 2 +- ...nifiedBytecodeProductionInvocationTests.cs | 26 ++++++++++++++ 4 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs index 6f770c760..e37aa585b 100644 --- a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs +++ b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs @@ -5403,25 +5403,27 @@ private static bool TryAppendComputedMemberCallTargetPreparation( } } - var keyIndex = callTargetIndexInProgram - 1; + var keyStartIndex = FindComputedCallKeyStart(expressionProgram, callTargetIndexInProgram); if (!TryAppendNamedReceiverOperations( expressionProgram, activationSlots, unified, stringConstants, - keyIndex, + keyStartIndex, allowDeepChain: false, out reason)) { return false; } - if (!TryAppendComputedPropertyKeyLoad( - expressionProgram.GetOperation(keyIndex), + if (!TryAppendComputedPropertyKeySpan( expressionProgram, activationSlots, unified, literalConstants, + stringConstants, + startInclusive: keyStartIndex, + endExclusive: callTargetIndexInProgram, out reason)) { return false; @@ -5445,6 +5447,31 @@ private static bool TryAppendComputedMemberCallTargetPreparation( out reason); } + private static int FindComputedCallKeyStart( + ExpressionProgram expressionProgram, + int callTargetIndexInProgram) + { + var stringConstants = expressionProgram.StringConstants.AsSpan(); + var keyStartIndex = 1; + while (keyStartIndex < callTargetIndexInProgram && + IsPlainNamedPropertyRead(expressionProgram.GetOperation(keyStartIndex), stringConstants)) + { + keyStartIndex++; + } + + return keyStartIndex; + } + + private static bool IsPlainNamedPropertyRead( + PackedExpressionOp operation, + ReadOnlySpan stringConstants) + { + return operation.Kind == ExpressionOpKind.GetNamedProperty && + !operation.IsOptional && + !operation.ShortCircuitOnNullishTarget && + !operation.GetString(stringConstants).IsPrivateName(); + } + private static bool TryAppendNamedSuperCallTargetPreparation( ExpressionProgram expressionProgram, UnifiedBytecodeSlotLayout slotLayout, diff --git a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs index 053f33421..78d195c4e 100644 --- a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs +++ b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs @@ -2730,7 +2730,7 @@ private static bool TryIsFirstBoundaryCallTargetPreparationCandidate( if (computedCallTargetIndex >= 2) { var computedCallTarget = program.GetOperation(computedCallTargetIndex); - var keyIndex = computedCallTargetIndex - 1; + var keyStartIndex = FindComputedCallKeyStart(program, computedCallTargetIndex, stringConstants); return !computedCallTarget.IsOptional && !computedCallTarget.ShortCircuitOnNullishTarget && IsSupportedNamedReceiverChain( @@ -2738,10 +2738,12 @@ private static bool TryIsFirstBoundaryCallTargetPreparationCandidate( identifierConstants, stringConstants, activationSlots, - keyIndex, + keyStartIndex, allowDeepChain: false) && - IsSimpleComputedPropertyKey( - program.GetOperation(keyIndex), + IsSupportedComputedPropertyKeySpan( + program, + startInclusive: keyStartIndex, + endExclusive: computedCallTargetIndex, identifierConstants, activationSlots) && HasSimpleCallArguments( @@ -2755,6 +2757,21 @@ private static bool TryIsFirstBoundaryCallTargetPreparationCandidate( return false; } + private static int FindComputedCallKeyStart( + ExpressionProgram program, + int computedCallTargetIndex, + ReadOnlySpan stringConstants) + { + var keyStartIndex = 1; + while (keyStartIndex < computedCallTargetIndex && + IsPlainNamedPropertyRead(program.GetOperation(keyStartIndex), stringConstants)) + { + keyStartIndex++; + } + + return keyStartIndex; + } + private static bool IsFirstBoundaryDirectEvalCallCandidate( ExpressionProgram program, ReadOnlySpan identifierConstants, diff --git a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs index f5bbf7715..7bb501989 100644 --- a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs +++ b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs @@ -2446,7 +2446,7 @@ function invokeComputedExpressionKey(box, left, right) { } """, "invokeComputedExpressionKey", - (int)UnifiedBytecodeProductionDeclineCode.CallDependency)] + (int)UnifiedBytecodeProductionDeclineCode.None)] [InlineData( """ function invokeDeepComputedCallee(root, key, value) { diff --git a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs index 48aedec5e..6652619f9 100644 --- a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs +++ b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs @@ -1994,6 +1994,32 @@ static record => record.Message.Contains( StringComparison.Ordinal)); } + [Fact(Timeout = 5000)] + public async Task ComputedMemberCallWithExpressionKey_UsesUnifiedBytecodeProductionFastPathAndPreservesThis() + { + await using var engine = CreateEngine(); + var result = await engine.Evaluate(""" + var box = { + offset: 1, + read(value) { + return value + this.offset; + } + }; + + function invoke(box, left, right, value) { + return box[left + right](value); + } + + invoke(box, "re", "ad", 41); + """); + + Assert.Equal(42d, result); + Assert.Contains(CurrentLogger!.Collector.Snapshot(), + static record => record.Message.Contains( + "unified-bytecode-production-fast-path func=invoke argc=4", + StringComparison.Ordinal)); + } + [Fact(Timeout = 5000)] public async Task ComputedMemberCall_NullishReceiverThrowsBeforeKeyCoercion() {