|
| 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