From ab8f88b64e533626f86e034676de493cc033f8e0 Mon Sep 17 00:00:00 2001 From: Faktorial Agent Date: Mon, 1 Jun 2026 23:03:37 +0200 Subject: [PATCH] Admit binary template substitutions in call args --- docs/unified-bytecode-expansion-contract.md | 2 +- .../UnifiedBytecodeCompiler.cs | 56 ++++++++++++++++++- .../UnifiedBytecodeProductionEligibility.cs | 22 +++++++- ...ifiedBytecodeProductionEligibilityTests.cs | 12 +++- ...nifiedBytecodeProductionInvocationTests.cs | 19 +++++++ 5 files changed, 105 insertions(+), 6 deletions(-) diff --git a/docs/unified-bytecode-expansion-contract.md b/docs/unified-bytecode-expansion-contract.md index 4cda25711..6d134e198 100644 --- a/docs/unified-bytecode-expansion-contract.md +++ b/docs/unified-bytecode-expansion-contract.md @@ -328,7 +328,7 @@ must still obey the no-mixed-execution rule. | `ArgumentsObjectDependency` | Activation descriptor and expression scan for real `arguments` object access or call targets; parameter/lexical `arguments` reads, `typeof`, updates, and call targets 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 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~UnifiedBytecodeProductionConstructCallTests&FullyQualifiedName~Constructor"` | -| `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"` | +| `CallDependency` | Direct eval outside the one-argument non-spread eval-identifier boundary, out-of-boundary call-target preparation, complex call arguments excluding admitted simple/binary template-literal substitutions, 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_CallWithComplexComputedKeyObjectArg_DeclinesCallDependency"` | | `DynamicLookupDependency` | Unresolved identifier loads/stores/typeof/update outside the with-backed dynamic-name path | Existing sync IR / environment lookup route | Dynamic-name lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_DynamicIdentifierLookup_DeclinesWithDynamicLookupDependency"` | | `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"` | | `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"` | diff --git a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs index f7ce574f1..635782691 100644 --- a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs +++ b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs @@ -8819,7 +8819,44 @@ private static bool TryAppendSimpleTemplateLiteralSpan( continue; } - // Substitution part: simple-operand, ToString, Binary(Add) + // Substitution part: simple binary expression, ToString, Binary(Add) + if (i + 4 < expressionProgram.OperationCount) + { + var rightOperand = expressionProgram.GetOperation(i + 1); + var binary = expressionProgram.GetOperation(i + 2); + var toString = expressionProgram.GetOperation(i + 3); + var add = expressionProgram.GetOperation(i + 4); + if (binary.Kind == ExpressionOpKind.Binary && + IsSupportedBinaryOperator(binary.Operator) && + toString.Kind == ExpressionOpKind.ToString && + add.Kind == ExpressionOpKind.Binary && + add.Operator == BinaryOperator.Add && + CanAppendSimpleOperandLoad(op, expressionProgram, activationSlots) && + CanAppendSimpleOperandLoad(rightOperand, expressionProgram, activationSlots)) + { + TryAppendSimpleOperandLoad( + op, + expressionProgram, + activationSlots, + unified, + literalConstants, + out _); + TryAppendSimpleOperandLoad( + rightOperand, + expressionProgram, + activationSlots, + unified, + literalConstants, + out _); + unified.Add(new UnifiedBytecodeInstruction(UnifiedBytecodeOpCode.Binary, (int)binary.Operator)); + unified.Add(new UnifiedBytecodeInstruction(UnifiedBytecodeOpCode.ToString)); + unified.Add(new UnifiedBytecodeInstruction(UnifiedBytecodeOpCode.Binary, (int)BinaryOperator.Add)); + i += 5; + continue; + } + } + + // Substitution part: simple operand, ToString, Binary(Add) if (i + 2 < expressionProgram.OperationCount) { var toString = expressionProgram.GetOperation(i + 1); @@ -8845,6 +8882,23 @@ private static bool TryAppendSimpleTemplateLiteralSpan( return true; } + private static bool CanAppendSimpleOperandLoad( + PackedExpressionOp operation, + ExpressionProgram expressionProgram, + ActivationSlotShape activationSlots) + { + return operation.Kind switch + { + ExpressionOpKind.LoadIdentifier => !operation.IsArguments && + TryResolveActivationSlot( + operation.GetIdentifier(expressionProgram.IdentifierConstants.AsSpan()), + activationSlots, + out _), + ExpressionOpKind.LoadLiteral or ExpressionOpKind.LoadThis or ExpressionOpKind.LoadNewTarget => true, + _ => false + }; + } + private static bool TryAppendSimpleOperandLoad( PackedExpressionOp operation, ExpressionProgram expressionProgram, diff --git a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs index c27188d77..0455915af 100644 --- a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs +++ b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs @@ -3654,7 +3654,27 @@ private static bool TryMeasureSimpleTemplateLiteralSpan( break; } - // Substitution part: simple-operand, ToString, Binary(Add) + // Substitution part: simple binary expression, ToString, Binary(Add) + if (i + 4 < program.OperationCount && + IsSimpleOperand(op, identifierConstants, activationSlots)) + { + var rightOperand = program.GetOperation(i + 1); + var binary = program.GetOperation(i + 2); + var toString = program.GetOperation(i + 3); + var add = program.GetOperation(i + 4); + if (IsSimpleOperand(rightOperand, identifierConstants, activationSlots) && + binary.Kind == ExpressionOpKind.Binary && + IsProductionBinaryOperator(binary.Operator) && + toString.Kind == ExpressionOpKind.ToString && + add.Kind == ExpressionOpKind.Binary && + add.Operator == BinaryOperator.Add) + { + i += 5; + continue; + } + } + + // Substitution part: simple operand, ToString, Binary(Add) if (IsSimpleOperand(op, identifierConstants, activationSlots)) { if (i + 2 < program.OperationCount) diff --git a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs index 8cbe6cba9..130d2d882 100644 --- a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs +++ b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs @@ -5223,7 +5223,7 @@ function logFull(receiver, first, last) { } [Fact] - public void Evaluate_CallWithComplexTemplateLiteralSubstitution_DeclinesCallDependency() + public void Evaluate_CallWithBinaryTemplateLiteralSubstitution_Accepts() { var plan = GetFunctionPlan(""" function logExpr(receiver, a, b) { @@ -5236,8 +5236,14 @@ function logExpr(receiver, a, b) { plan, new UnifiedBytecodeProductionActivationDescriptor()); - Assert.False(result.IsEligible); - Assert.Equal(UnifiedBytecodeProductionDeclineCode.CallDependency, result.Code); + Assert.True(result.IsEligible, result.Reason); + Assert.Equal(UnifiedBytecodeProductionDeclineCode.None, result.Code); + Assert.Contains(result.Program.Instructions, instruction => + instruction.OpCode == UnifiedBytecodeOpCode.Binary && instruction.Operand == (int)BinaryOperator.Add); + Assert.Contains(result.Program.Instructions, instruction => + instruction.OpCode == UnifiedBytecodeOpCode.ToString); + Assert.Contains(result.Program.Instructions, instruction => + instruction.OpCode == UnifiedBytecodeOpCode.CallInvocationBoundary); } [Fact] diff --git a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs index 7096e2307..da8d7dfc9 100644 --- a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs +++ b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs @@ -6594,6 +6594,25 @@ static record => record.Message.Contains( StringComparison.Ordinal)); } + [Fact(Timeout = 5000)] + public async Task CallWithBinaryTemplateLiteralSubstitution_UsesUnifiedBytecodeProductionFastPath() + { + await using var engine = CreateEngine(); + var result = await engine.Evaluate(""" + function logExpr(receiver, a, b) { + return receiver(`result: ${a + b}`); + } + + logExpr(function(value) { return value; }, 40, 2); + """); + + Assert.Equal("result: 42", result?.ToString()); + Assert.Contains(CurrentLogger!.Collector.Snapshot(), + static record => record.Message.Contains( + "unified-bytecode-production-fast-path func=logExpr argc=3", + StringComparison.Ordinal)); + } + [Fact(Timeout = 5000)] public async Task ObjectSpreadGetterThrow_StopsBeforeLaterSpread_OnFastPath() {