Skip to content

Commit cdd6ce0

Browse files
Copilotrogeralsing
andauthored
Split ExecutePlan/ExecuteInstructionLoop into dedicated partial file (#377)
* Initial plan * Split ExecutePlan and ExecuteInstructionLoop into TypedAstEvaluator.ExecutionPlanRunner.Loop.cs Co-authored-by: rogeralsing <647031+rogeralsing@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rogeralsing <647031+rogeralsing@users.noreply.github.com> Co-authored-by: Roger Johansson <roger@asynkron.se>
1 parent a19af98 commit cdd6ce0

File tree

2 files changed

+315
-292
lines changed

2 files changed

+315
-292
lines changed
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
#region
2+
3+
using System.Collections.Immutable;
4+
using System.Runtime.CompilerServices;
5+
using System.Runtime.InteropServices;
6+
using Asynkron.JsEngine.Execution;
7+
using Asynkron.JsEngine.Execution.Instructions;
8+
using Asynkron.JsEngine.JsTypes;
9+
using Asynkron.JsEngine.Runtime;
10+
using Asynkron.JsEngine.StdLib;
11+
using Microsoft.Extensions.Logging;
12+
13+
#endregion
14+
15+
namespace Asynkron.JsEngine.Ast;
16+
17+
public static partial class TypedAstEvaluator
18+
{
19+
private sealed partial class ExecutionPlanRunner
20+
{
21+
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
22+
private JsValue ExecutePlan(ResumeMode mode, JsValue resumeValue)
23+
{
24+
if (_plan is null)
25+
{
26+
throw new InvalidOperationException("No generator plan available.");
27+
}
28+
29+
JsEnvironment environment;
30+
EvaluationContext context;
31+
32+
// Fast path for non-generator, non-async functions - skip all generator/async machinery
33+
if (!_isGenerator && !_isAsync)
34+
{
35+
environment = EnsureExecutionEnvironment();
36+
context = EnsureEvaluationContext();
37+
}
38+
else
39+
{
40+
// Full generator/async path with state machine support
41+
if (_state == GeneratorState.Executing)
42+
{
43+
_state = GeneratorState.Completed;
44+
_done = true;
45+
_programCounter = -1;
46+
TryCatchStateRef.TryStack.Clear();
47+
YieldStateRef.ResumeContext.Clear();
48+
var throwContext = _context ??= _realmState.CreateContext(
49+
ScopeKind.Function,
50+
DetermineGeneratorScopeMode());
51+
throw StandardLibrary.ThrowTypeError("Generator is already executing", throwContext, _realmState);
52+
}
53+
54+
var wasStart = _state == GeneratorState.Start;
55+
if (_done || _state == GeneratorState.Completed)
56+
{
57+
_done = true;
58+
return FinishExternalCompletion(mode, resumeValue);
59+
}
60+
61+
if (mode is ResumeMode.Throw or ResumeMode.Return && wasStart)
62+
{
63+
_state = GeneratorState.Completed;
64+
_done = true;
65+
return FinishExternalCompletion(mode, resumeValue);
66+
}
67+
68+
_state = GeneratorState.Executing;
69+
PreparePendingResumeValue(mode, resumeValue, wasStart);
70+
71+
environment = EnsureExecutionEnvironment();
72+
73+
// Track the environment we resumed with (if resuming from suspend).
74+
// This prevents returning it to the pool while we're still using it.
75+
IteratorStateRef.ResumedWithEnvironment = wasStart ? null : environment;
76+
context = EnsureEvaluationContext();
77+
78+
// If we're resuming from a yield that happened during AST evaluation
79+
// (via StatementInstruction), handle based on the resume mode.
80+
_realmState.Logger?.LogInformation(
81+
"ExecutePlan resume check: wasStart={WasStart} mode={Mode} YieldStateRef.LastYieldSourceStart={Start}",
82+
wasStart, mode, YieldStateRef.LastYieldSourceStart);
83+
84+
if (!wasStart && YieldStateRef.LastYieldSourceStart >= 0)
85+
{
86+
switch (mode)
87+
{
88+
case ResumeMode.Next:
89+
// For next(), set up resume state so the yield expression returns the resume value
90+
SetYieldResumeValue(environment, resumeValue, YieldStateRef.LastYieldSourceStart,
91+
YieldStateRef.LastYieldSourceEnd);
92+
break;
93+
case ResumeMode.Return:
94+
// For return(), close any active iterators and complete the generator.
95+
// Don't re-evaluate the statement - just close and return.
96+
_realmState.Logger?.LogInformation("ExecutePlan: early CompleteReturn for Return mode");
97+
YieldStateRef.LastYieldSourceStart = -1;
98+
YieldStateRef.LastYieldSourceEnd = -1;
99+
return CompleteReturn(resumeValue);
100+
}
101+
// For Throw mode, we'll let the normal flow handle it via AsyncStateRef.PendingResumeKind
102+
103+
YieldStateRef.LastYieldSourceStart = -1;
104+
YieldStateRef.LastYieldSourceEnd = -1;
105+
}
106+
107+
// Restore active with-scopes when resuming
108+
// The _activeWithScopes stack contains the slots in reverse order (bottom to top)
109+
// We need to restore environments from bottom to top
110+
if (WithStateRef.ActiveWithScopes.Count > 0)
111+
{
112+
var scopesToRestore = WithStateRef.ActiveWithScopes.ToArray();
113+
// The array is in stack order (top first), so reverse to get bottom-to-top order
114+
for (var i = scopesToRestore.Length - 1; i >= 0; i--)
115+
{
116+
var slot = scopesToRestore[i];
117+
if (TryGetSymbolValueJsValue(environment, slot, out var storedEnvValue) &&
118+
storedEnvValue.TryGetObject<JsEnvironment>(out var storedWithEnv))
119+
{
120+
environment = storedWithEnv;
121+
}
122+
}
123+
}
124+
125+
// If we are resuming after a pending await, thread the resolved
126+
// value into the per-site await state so subsequent evaluations
127+
// of the AwaitExpression see the fulfilled value instead of the
128+
// original promise object.
129+
if (_isAsync && AsyncStateRef.PendingAwaitKey is { } awaitKey)
130+
{
131+
var (kind, value) = ConsumeResumeValue();
132+
var isThrow = kind == ResumePayloadKind.Throw;
133+
134+
// Store the resolved value (or thrown error) in AwaitState so
135+
// EvaluateAwaitInGenerator can retrieve it when re-evaluated.
136+
if (kind == ResumePayloadKind.Value || isThrow)
137+
{
138+
if (environment.TryGetObject<AwaitState>(awaitKey, out var state))
139+
{
140+
state.HasResult = true;
141+
state.IsThrow = isThrow;
142+
state.Result = value;
143+
environment.AssignJsValue(awaitKey, JsValue.FromObjectUnsafe(state));
144+
}
145+
else
146+
{
147+
var newState = new AwaitState { HasResult = true, IsThrow = isThrow, Result = value };
148+
if (environment.HasBinding(awaitKey))
149+
{
150+
environment.AssignJsValue(awaitKey, JsValue.FromObjectUnsafe(newState));
151+
}
152+
else
153+
{
154+
environment.DefineJsValue(awaitKey, JsValue.FromObjectUnsafe(newState));
155+
}
156+
}
157+
}
158+
159+
AsyncStateRef.PendingAwaitKey = null;
160+
}
161+
}
162+
163+
return ExecuteInstructionLoop(ref environment, context);
164+
}
165+
166+
#if NO_INLINING
167+
[MethodImpl(MethodImplOptions.NoInlining)]
168+
#else
169+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
170+
#endif
171+
private JsValue ExecuteInstructionLoop(ref JsEnvironment environment, EvaluationContext context)
172+
{
173+
// Cache debug mode check outside the hot loop - avoid virtual property access per iteration
174+
var debugMode = _realmState.Options.DebugMode;
175+
var instructions = _plan!.Instructions;
176+
var instructionsLength = instructions.Length;
177+
178+
// Allocate flat slots array for O(1) variable access if this plan uses flat slots.
179+
// Each JsVariable will be populated when its scope is entered via PushEnvironment.
180+
var flatSlotCount = _plan.FlatSlotCount;
181+
if (flatSlotCount > 0 && _flatSlots is null)
182+
{
183+
_flatSlots = new JsVariable[flatSlotCount];
184+
}
185+
186+
// Get underlying array from ImmutableArray and reference to start - enables bounds-check-free access
187+
var instructionsArray = ImmutableCollectionsMarshal.AsArray(instructions)!;
188+
ref var instructionsRef = ref MemoryMarshal.GetArrayDataReference(instructionsArray);
189+
190+
// Cache try-catch state check - avoid repeated null checks in hot loop
191+
var hasTryCatchState = _tryCatchState is not null;
192+
193+
bool continueAfterCatch;
194+
do
195+
{
196+
continueAfterCatch = false;
197+
try
198+
{
199+
while ((uint)_programCounter < (uint)instructionsLength)
200+
{
201+
// Check if HandleAbruptCompletion restored the environment (e.g., jumping to catch handler)
202+
// This ensures block-scoped bindings from inside the try are no longer visible.
203+
// Only check when TryCatchState has been allocated.
204+
if (hasTryCatchState && _tryCatchState!.RestoredEnvironmentFromTry is { } restored)
205+
{
206+
environment = restored;
207+
_tryCatchState.RestoredEnvironmentFromTry = null;
208+
}
209+
210+
_currentInstructionIndex = _programCounter;
211+
// Use profiling wrapper to measure instruction fetch cost
212+
var instruction = ProfileFetchInstruction(ref instructionsRef, _programCounter);
213+
var instructionKind = instruction.Kind;
214+
215+
// Trace instruction execution when debug logging is enabled
216+
if (debugMode)
217+
{
218+
_realmState.Logger?.LogTrace(
219+
"[IR:{PC,3}] {Instruction}",
220+
_programCounter,
221+
ExecutionPlanPrinter.FormatInstruction(instruction));
222+
}
223+
224+
// Detailed IR execution trace with environment depth
225+
#pragma warning disable CS0162 // Unreachable code detected (TraceIrExecution is compile-time constant)
226+
if (JsEngineConstants.TraceIrExecution && _realmState.Logger is not null)
227+
{
228+
ExecutionPlanPrinter.TraceInstruction(
229+
_realmState.Logger,
230+
_programCounter,
231+
instruction,
232+
environment.Depth,
233+
environment.ScopeId,
234+
environment.GetHashCode()
235+
);
236+
}
237+
#pragma warning restore CS0162
238+
239+
// ═══════════════════════════════════════════════════════════════════════════
240+
// FAST PATH: Handle the hottest instructions before switch dispatch
241+
// For a 1M iteration loop, this saves millions of switch table lookups
242+
// ═══════════════════════════════════════════════════════════════════════════
243+
244+
// Jump is the simplest - just update program counter
245+
if (instructionKind == InstructionKind.Jump)
246+
{
247+
_programCounter = ProfileHandleJump(Unsafe.As<JumpInstruction>(instruction));
248+
continue;
249+
}
250+
251+
// Branch is hot - handle before switch dispatch
252+
if (instructionKind == InstructionKind.Branch)
253+
{
254+
var result = HandleBranchFastPath(Unsafe.As<BranchInstruction>(instruction), environment, context, out var returnValue);
255+
if (result == InstructionResult.Return) return returnValue;
256+
continue;
257+
}
258+
259+
var loopResult = InstructionHandlers[(int)instructionKind](this, instruction, ref environment, context, out var loopReturnValue);
260+
if (loopResult == InstructionResult.Return) return loopReturnValue;
261+
}
262+
}
263+
catch (ThrowSignal signal)
264+
{
265+
// A ThrowSignal was thrown from code evaluation (e.g., from EvaluateAwaitInGenerator
266+
// when resuming after a rejected promise). Route it through HandleAbruptCompletion
267+
// to check if there's a JS catch block that can handle it.
268+
269+
// Clear any stale throw state from context before handling - this ensures
270+
// finally blocks don't see the stale throw state
271+
if (context.IsThrow)
272+
{
273+
context.Clear();
274+
}
275+
276+
if (HandleAbruptCompletion(AbruptKind.Throw, signal.ThrownValue, environment))
277+
{
278+
// A catch block will handle this - continue execution from the catch handler
279+
if (_programCounter == _currentInstructionIndex)
280+
{
281+
// When already inside a finally block, ensure forward progress
282+
// instead of re-executing the same instruction repeatedly.
283+
_programCounter = _currentInstructionIndex + 1;
284+
}
285+
286+
continueAfterCatch = true;
287+
continue;
288+
}
289+
290+
// No catch block - mark as completed and re-throw
291+
_state = GeneratorState.Completed;
292+
_done = true;
293+
_programCounter = -1;
294+
TryCatchStateRef.TryStack.Clear();
295+
YieldStateRef.ResumeContext.Clear();
296+
throw;
297+
}
298+
catch
299+
{
300+
_state = GeneratorState.Completed;
301+
_done = true;
302+
_programCounter = -1;
303+
TryCatchStateRef.TryStack.Clear();
304+
YieldStateRef.ResumeContext.Clear();
305+
throw;
306+
}
307+
} while (continueAfterCatch);
308+
309+
_state = GeneratorState.Completed;
310+
_done = true;
311+
TryCatchStateRef.TryStack.Clear();
312+
return CreateIteratorResult(JsValue.Undefined, true);
313+
}
314+
}
315+
}

0 commit comments

Comments
 (0)