From ddd05d91378d67b0ac4d816a777c4d06b8daed6b Mon Sep 17 00:00:00 2001 From: Faktorial Agent Date: Tue, 2 Jun 2026 04:41:30 +0200 Subject: [PATCH] Admit dynamic optional identifier calls to bytecode --- docs/unified-bytecode-expansion-contract.md | 35 +++--- .../TypedAstEvaluator.SyncFunctionInvoker.cs | 1 + .../UnifiedBytecodeCompiler.cs | 42 ++++++- .../UnifiedBytecodeProductionEligibility.cs | 13 +- .../UnifiedBytecode/UnifiedBytecodeProgram.cs | 1 + .../UnifiedBytecodeVirtualMachine.cs | 35 ++++++ ...ifiedBytecodeProductionEligibilityTests.cs | 42 +++++++ ...nifiedBytecodeProductionInvocationTests.cs | 119 ++++++++++++++++++ 8 files changed, 266 insertions(+), 22 deletions(-) diff --git a/docs/unified-bytecode-expansion-contract.md b/docs/unified-bytecode-expansion-contract.md index 66ba54063..d0f7fc6de 100644 --- a/docs/unified-bytecode-expansion-contract.md +++ b/docs/unified-bytecode-expansion-contract.md @@ -269,6 +269,7 @@ statement interpretation. - `PrepareIdentifierCallTarget` - `PrepareIdentifierOptionalCallTarget` - `PrepareDynamicIdentifierCallTarget` +- `PrepareDynamicIdentifierOptionalCallTarget` - `PrepareNamedCallTarget` - `PrepareComputedCallTarget` - `PrepareNamedOptionalCallTarget` @@ -334,7 +335,7 @@ must still obey the no-mixed-execution rule. | `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"` | -| `CallDependency` | Direct eval outside the one-argument non-spread eval-identifier boundary, out-of-boundary call-target preparation, and complex call arguments excluding admitted simple/binary template-literal substitutions, simple/binary computed object keys, zero-argument activation-resolved identifier-call computed object keys, and simple-return captured identifier calls | Existing sync IR call route | Wider call invocation lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests"` | +| `CallDependency` | Direct eval outside the one-argument non-spread eval-identifier boundary, out-of-boundary call-target preparation, and complex call arguments excluding admitted simple/binary template-literal substitutions, simple/binary computed object keys, zero-argument activation-resolved identifier-call computed object keys, simple-return captured identifier calls, and simple-return captured optional identifier calls | Existing sync IR call route | Wider call invocation lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests"` | | `DynamicLookupDependency` | Unresolved identifier loads/stores/update outside the admitted ordinary, with-backed, direct-eval-backed, and simple-return captured-closure dynamic-name paths; plans that need direct eval plus post-eval dynamic identifier reads now route when captured activation, with-chain, and arguments-object dependencies are absent | Existing sync IR / environment lookup route | Dynamic-name lane | `rtk dotnet test tests/Asynkron.JsEngine.Tests --filter "FullyQualifiedName~UnifiedBytecodeProductionEligibilityTests&FullyQualifiedName~Evaluate_DirectEvalDeclaredVarRead_AcceptsOrdinaryDynamicNameProgram"` | | `PropertyReadBoundaryOutOfScope` | Named/computed property reads outside the admitted activation-resolved and read-only dynamic-identifier-base boundaries, including captured closure dynamic bases | 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-return dynamic-base named/computed property writes and compound/logical writes, 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"` | @@ -604,14 +605,15 @@ the final post-compile production subset check before VM entry. invokes `[[Construct]]` with the caller's `new.target`, initializes `this`, and preserves the existing double-super-call `ReferenceError`. - Accepted identifier-call programs use `PrepareIdentifierCallTarget`, - `PrepareIdentifierOptionalCallTarget`, or - `PrepareDynamicIdentifierCallTarget` followed by `CallInvocationBoundary`; - the VM resolves the callable from unified bytecode-owned slot state or the - active dynamic environment and invokes it through existing callable invocation - helpers with the active `EvaluationContext` and caller `JsEnvironment` when - the callee needs environment-aware or debug-aware invocation state. The - optional variant packs a nullish short-circuit jump target and jumps past - argument lowering and the call boundary when the callee slot is nullish. + `PrepareIdentifierOptionalCallTarget`, `PrepareDynamicIdentifierCallTarget`, + or `PrepareDynamicIdentifierOptionalCallTarget` followed by + `CallInvocationBoundary`; the VM resolves the callable from unified + bytecode-owned slot state or the active dynamic environment and invokes it + through existing callable invocation helpers with the active + `EvaluationContext` and caller `JsEnvironment` when the callee needs + environment-aware or debug-aware invocation state. The optional variants pack + a nullish short-circuit jump target and jump past argument lowering and the + call boundary when the callee is nullish. - Accepted named member-call programs use `PrepareNamedCallTarget` followed by `CallInvocationBoundary`; the VM keeps the final receiver on the stack, loads the named callee from that receiver, and invokes through the existing @@ -637,17 +639,20 @@ the final post-compile production subset check before VM entry. - Accepted optional call programs either use a jump-owned optional-start prefix followed by an ordinary call-target preparation opcode (`a?.b.c(args)`, `a.x?.b.c(args)`, `a?.b[k](args)`) or use - `PrepareIdentifierOptionalCallTarget`, `PrepareNamedOptionalCallTarget`, or - `PrepareComputedOptionalCallTarget` followed by `CallInvocationBoundary`. + `PrepareIdentifierOptionalCallTarget`, + `PrepareDynamicIdentifierOptionalCallTarget`, + `PrepareNamedOptionalCallTarget`, or `PrepareComputedOptionalCallTarget` + followed by `CallInvocationBoundary`. The optional preparation opcodes pack a call-target constant index (low 16 bits) and a nullish short-circuit jump target (high 16 bits) into a single operand. The VM checks the receiver or callee for nullish before argument evaluation proceeds; if nullish, the stack top becomes `Undefined` and execution jumps past the call boundary. Admitted callee/receiver-optional - patterns include callee-optional identifier calls (`fn?.(args)`), - receiver-optional named calls (`box?.read(args)`), callee-optional named - calls (`box.read?.(args)`), and callee-optional computed calls - (`box[key]?.(args)`). The `IsOptionalReceiverCheck` flag in + patterns include activation-resolved and captured/dynamic callee-optional + identifier calls (`fn?.(args)`), receiver-optional named calls + (`box?.read(args)`), callee-optional named calls (`box.read?.(args)`), and + callee-optional computed calls (`box[key]?.(args)`). The + `IsOptionalReceiverCheck` flag in `UnifiedBytecodeCallTarget` distinguishes the two named variants. (Optional member calls admitted as of gh2689; ADR 0289.) - Direct eval programs use `PrepareIdentifierCallTarget` followed by diff --git a/src/Asynkron.JsEngine/Ast/TypedAstEvaluator.SyncFunctionInvoker.cs b/src/Asynkron.JsEngine/Ast/TypedAstEvaluator.SyncFunctionInvoker.cs index d3e9b6181..a7b25eedf 100644 --- a/src/Asynkron.JsEngine/Ast/TypedAstEvaluator.SyncFunctionInvoker.cs +++ b/src/Asynkron.JsEngine/Ast/TypedAstEvaluator.SyncFunctionInvoker.cs @@ -3280,6 +3280,7 @@ UnifiedBytecodeOpCode.UpdateDynamicIdentifier or UnifiedBytecodeOpCode.TypeOfDynamicIdentifier or UnifiedBytecodeOpCode.DeleteDynamicIdentifier or UnifiedBytecodeOpCode.PrepareDynamicIdentifierCallTarget or + UnifiedBytecodeOpCode.PrepareDynamicIdentifierOptionalCallTarget or UnifiedBytecodeOpCode.ApplyBindingTarget or UnifiedBytecodeOpCode.ApplyDeclarationBindingTarget or UnifiedBytecodeOpCode.DeclareClass or diff --git a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs index 942f7e678..c1a18f286 100644 --- a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs +++ b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeCompiler.cs @@ -4815,6 +4815,7 @@ private static bool TryAppendFirstBoundaryCallTargetPreparation( callTargetConstants, call, callIndex, + allowsDynamicIdentifiers, out reason)) { return true; @@ -4985,6 +4986,7 @@ private static bool TryAppendCalleeOptionalIdentifierCallTarget( ImmutableArray.Builder callTargetConstants, PackedExpressionOp call, int callIndex, + bool allowsDynamicIdentifiers, out string reason) { if (callIndex < 2) @@ -5010,10 +5012,42 @@ private static bool TryAppendCalleeOptionalIdentifierCallTarget( var identifier = callTarget.GetIdentifier(expressionProgram.IdentifierConstants.AsSpan()); if (!TryResolveActivationCallTargetSlot(identifier, slotLayout, out var slotIndex)) { - reason = callTarget.IsArguments - ? "arguments call targets are outside the optional identifier call-target preparation boundary." - : "Optional identifier call targets require an activation-resolved identifier slot."; - return false; + if (callTarget.IsArguments || + !allowsDynamicIdentifiers || + identifier.FlatSlotId >= 0) + { + reason = callTarget.IsArguments + ? "arguments call targets are outside the optional identifier call-target preparation boundary." + : "Optional identifier call targets require an activation-resolved identifier slot or admitted dynamic identifier operations."; + return false; + } + + var dynamicNameIndex = stringConstants.Count; + stringConstants.Add(identifier.Name.Name ?? string.Empty); + var dynamicPrepareIndex = unified.Count; + unified.Add(new UnifiedBytecodeInstruction( + UnifiedBytecodeOpCode.PrepareDynamicIdentifierOptionalCallTarget, + dynamicNameIndex)); + + if (!TryAppendCallArguments( + expressionProgram, + slotLayout, + unified, + literalConstants, + stringConstants, + callTargetConstants, + argsStartIndex: 2, + call, + callIndex, + out reason)) + { + return false; + } + + unified[dynamicPrepareIndex] = new UnifiedBytecodeInstruction( + UnifiedBytecodeOpCode.PrepareDynamicIdentifierOptionalCallTarget, + dynamicNameIndex | (unified.Count << 16)); + return true; } var nameIndex = stringConstants.Count; diff --git a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs index a43012cec..238f843aa 100644 --- a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs +++ b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProductionEligibility.cs @@ -2926,7 +2926,11 @@ private static bool TryIsFirstBoundaryCallTargetPreparationCandidate( // Jump/SwapTopTwo/Pop structure that the non-optional branches below would // reject (or never reach, because they end in Pop rather than Call). Detect // them first so the dedicated optional candidates own these shapes. - if (TryIsFirstBoundaryCalleeOptionalIdentifierCallCandidate(program, identifierConstants, activationSlots) || + if (TryIsFirstBoundaryCalleeOptionalIdentifierCallCandidate( + program, + identifierConstants, + activationSlots, + allowsDynamicIdentifiers) || TryIsFirstBoundaryReceiverOptionalNamedCallCandidate(program, identifierConstants, stringConstants, activationSlots) || TryIsFirstBoundaryCalleeOptionalNamedCallCandidate(program, identifierConstants, stringConstants, activationSlots) || TryIsFirstBoundaryCalleeOptionalComputedCallCandidate(program, identifierConstants, activationSlots) || @@ -3096,7 +3100,8 @@ private static bool IsDirectEvalSingleArgumentCandidate(PackedExpressionOp opera private static bool TryIsFirstBoundaryCalleeOptionalIdentifierCallCandidate( ExpressionProgram program, ReadOnlySpan identifierConstants, - ActivationSlotShape activationSlots) + ActivationSlotShape activationSlots, + bool allowsDynamicIdentifiers) { if (program.OperationCount < 6) { @@ -3127,7 +3132,8 @@ private static bool TryIsFirstBoundaryCalleeOptionalIdentifierCallCandidate( } var identifier = callTarget.GetIdentifier(identifierConstants); - return TryResolveActivationSlot(identifier, activationSlots) && + return (TryResolveActivationSlot(identifier, activationSlots) || + allowsDynamicIdentifiers && identifier.FlatSlotId < 0) && HasSimpleCallArguments( program, identifierConstants, @@ -5280,6 +5286,7 @@ private static bool TryFindPrototypeOnlyOpcode( case UnifiedBytecodeOpCode.PrepareIdentifierCallTarget: case UnifiedBytecodeOpCode.PrepareIdentifierOptionalCallTarget: case UnifiedBytecodeOpCode.PrepareDynamicIdentifierCallTarget: + case UnifiedBytecodeOpCode.PrepareDynamicIdentifierOptionalCallTarget: case UnifiedBytecodeOpCode.PrepareNamedCallTarget: case UnifiedBytecodeOpCode.PrepareComputedCallTarget: case UnifiedBytecodeOpCode.PrepareNamedOptionalCallTarget: diff --git a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProgram.cs b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProgram.cs index aa8365490..41ade5b8e 100644 --- a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProgram.cs +++ b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeProgram.cs @@ -113,6 +113,7 @@ internal enum UnifiedBytecodeOpCode : byte PrepareIdentifierCallTarget, PrepareIdentifierOptionalCallTarget, PrepareDynamicIdentifierCallTarget, + PrepareDynamicIdentifierOptionalCallTarget, PrepareNamedCallTarget, PrepareComputedCallTarget, PrepareNamedOptionalCallTarget, diff --git a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeVirtualMachine.cs b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeVirtualMachine.cs index d066b769f..4bb56ab5a 100644 --- a/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeVirtualMachine.cs +++ b/src/Asynkron.JsEngine/Execution/UnifiedBytecode/UnifiedBytecodeVirtualMachine.cs @@ -424,6 +424,41 @@ bool TryHandleCurrentContextThrow(Span currentSlots) programCounter++; break; + case UnifiedBytecodeOpCode.PrepareDynamicIdentifierOptionalCallTarget: + { + var dynamicOptionalCallEnvironment = RequireDynamicEnvironment(currentCallingEnvironment); + var dynamicOptionalNameIndex = instruction.Operand & 0xFFFF; + var dynamicOptionalJumpTarget = instruction.Operand >> 16; + PrepareDynamicIdentifierCallTarget( + program.StringConstants[dynamicOptionalNameIndex], + dynamicOptionalCallEnvironment, + stack, + ref stackPointer, + context); + ClearTopTwoShortCircuitFlags(); + if (context.ShouldStopEvaluation) + { + if (TryHandleCurrentContextThrow(slots)) + { + break; + } + + return JsValue.Undefined; + } + + var dynamicOptionalCallable = stack[stackPointer - 1]; + if (dynamicOptionalCallable.IsNullOrUndefined) + { + stackPointer -= 2; + PushValue(JsValue.Undefined); + programCounter = dynamicOptionalJumpTarget; + break; + } + + programCounter++; + break; + } + case UnifiedBytecodeOpCode.PrepareNamedCallTarget: var namedCallTarget = program.CallTargetConstants[instruction.Operand]; if (namedCallTarget.Kind != UnifiedBytecodeCallTargetKind.NamedMember || diff --git a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs index 8e9573c02..591418a69 100644 --- a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs +++ b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionEligibilityTests.cs @@ -1130,6 +1130,48 @@ function invoke(fn, args) { Assert.NotEmpty(result.Program.CallSpreadMasks); } + [Fact] + public void Evaluate_DynamicOptionalIdentifierCallTarget_AcceptsEnvironmentOpcode() + { + var plan = GetFunctionPlan(""" + function invoke(value) { + return externalFn?.(value); + } + """, + "invoke"); + + var result = UnifiedBytecodeProductionEligibility.Evaluate( + plan, + new UnifiedBytecodeProductionActivationDescriptor( + AllowsOrdinaryDynamicIdentifierEnvironmentOperations: true)); + + Assert.True(result.IsEligible, result.Reason); + Assert.Equal(UnifiedBytecodeProductionDeclineCode.None, result.Code); + Assert.Contains(result.Program.Instructions, instruction => + instruction.OpCode == UnifiedBytecodeOpCode.PrepareDynamicIdentifierOptionalCallTarget); + Assert.Contains(result.Program.Instructions, instruction => + instruction.OpCode == UnifiedBytecodeOpCode.CallInvocationBoundary); + } + + [Fact] + public void Evaluate_DynamicOptionalIdentifierCallTarget_WithComplexArgument_DeclinesOptionalChainDependency() + { + var plan = GetFunctionPlan(""" + function invoke(value) { + return externalFn?.(value + 1); + } + """, + "invoke"); + + var result = UnifiedBytecodeProductionEligibility.Evaluate( + plan, + new UnifiedBytecodeProductionActivationDescriptor( + AllowsOrdinaryDynamicIdentifierEnvironmentOperations: true)); + + Assert.False(result.IsEligible); + Assert.Equal(UnifiedBytecodeProductionDeclineCode.OptionalChainDependency, result.Code); + } + [Fact] public void Evaluate_NamedPropertyReadCandidate_AcceptsOwnedPropertyOpcode() { diff --git a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs index ccf06d5b3..0ee865911 100644 --- a/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs +++ b/tests/Asynkron.JsEngine.Tests/UnifiedBytecodeProductionInvocationTests.cs @@ -6792,6 +6792,125 @@ record => record.Message.Contains( StringComparison.Ordinal)); } + [Fact(Timeout = 5000)] + public async Task ArrowFunction_CapturedOptionalIdentifierCallExpression_UsesUnifiedBytecodeProductionFastPath() + { + await using var engine = CreateEngine(); + var result = await engine.Evaluate(""" + function makeInvoker(fn) { + return () => fn?.(42); + } + + function probe(value) { + "use strict"; + return this === undefined ? value : 0; + } + + var invoke = makeInvoker(probe); + invoke(); + """); + + Assert.Equal(42d, result); + Assert.Contains(CurrentLogger!.Collector.Snapshot(), + record => record.Message.Contains( + "unified-bytecode-production-fast-path func=", + StringComparison.Ordinal)); + } + + [Fact(Timeout = 5000)] + public async Task FunctionExpression_CapturedOptionalIdentifierCallExpression_UsesUnifiedBytecodeProductionFastPath() + { + await using var engine = CreateEngine(); + var result = await engine.Evaluate(""" + function makeInvoker(fn) { + return function invoke() { + return fn?.(42); + }; + } + + function probe(value) { + "use strict"; + return this === undefined ? value : 0; + } + + var invoke = makeInvoker(probe); + invoke(); + """); + + Assert.Equal(42d, result); + Assert.Contains(CurrentLogger!.Collector.Snapshot(), + record => record.Message.Contains( + "unified-bytecode-production-fast-path func=invoke", + StringComparison.Ordinal)); + } + + [Fact(Timeout = 5000)] + public async Task ArrowFunction_CapturedOptionalIdentifierCallExpression_ReturnsUndefinedOnNullishFastPath() + { + await using var engine = CreateEngine(); + var result = await engine.Evaluate(""" + function makeInvoker(fn) { + return () => fn?.(42); + } + + var invoke = makeInvoker(null); + invoke() === undefined; + """); + + Assert.Equal(true, result); + Assert.Contains(CurrentLogger!.Collector.Snapshot(), + record => record.Message.Contains( + "unified-bytecode-production-fast-path func=", + StringComparison.Ordinal)); + } + + [Fact(Timeout = 5000)] + public async Task FunctionExpression_CapturedOptionalIdentifierCallExpression_ReturnsUndefinedOnNullishFastPath() + { + await using var engine = CreateEngine(); + var result = await engine.Evaluate(""" + function makeInvoker(fn) { + return function invoke() { + return fn?.(42); + }; + } + + var invoke = makeInvoker(null); + invoke() === undefined; + """); + + Assert.Equal(true, result); + Assert.Contains(CurrentLogger!.Collector.Snapshot(), + record => record.Message.Contains( + "unified-bytecode-production-fast-path func=invoke", + StringComparison.Ordinal)); + } + + [Fact(Timeout = 5000)] + public async Task ArrowFunction_CapturedOptionalIdentifierCallExpression_WithComplexArgument_DoesNotUseUnifiedBytecodeProductionFastPath() + { + await using var engine = CreateEngine(); + var result = await engine.Evaluate(""" + function makeInvoker(fn, value) { + return () => fn?.(value + 1); + } + + function probe(value) { + "use strict"; + return this === undefined ? value : 0; + } + + var invoke = makeInvoker(probe, 41); + invoke(); + """); + + Assert.Equal(42d, result); + Assert.DoesNotContain(CurrentLogger!.Collector.Snapshot(), + record => record.Message.Contains( + "unified-bytecode-production-fast-path func=", + StringComparison.Ordinal)); + } + [Fact(Timeout = 5000)] public async Task ArrowFunction_CapturedIdentifierAssignmentExpression_UsesUnifiedBytecodeProductionFastPath() {