visitTryStatement method

  1. @override
Object? visitTryStatement(
  1. STryStatement node
)
override

Visit a STryStatement.

Implementation

@override
Object? visitTryStatement(STryStatement node) {
  // Store the internal exception if caught
  InternalInterpreterD4rtException? caughtInternalException;
  StackTrace? caughtStackTrace;

  Object? tryResult;
  Object? returnValue; // Store either the try result or the catch result

  // SCC12: a `return` / `break` / `continue` out of the try or a catch block is
  // held here rather than rethrown on the spot, so that the finally block still
  // runs before it leaves the statement. It used to rethrow immediately — with
  // a comment saying "the finally must execute" next to the line that ensured
  // it did not.
  Object? pendingControlFlow;

  final originalEnv = environment; // Save to restore after catch/finally

  try {
    // 1. Execute the try block
    Logger.debug("[STryStatement] Entering try block");
    tryResult = node.body!.accept<Object?>(this);
    returnValue = tryResult; // Default value if no exception
    Logger.debug("[STryStatement] Try block completed normally");
  } on ReturnException catch (e) {
    Logger.debug(
      "[STryStatement] Holding ReturnException from try block until the "
      "finally block has run.",
    );
    pendingControlFlow = e;
  } on BreakException catch (e) {
    Logger.debug("[STryStatement] Holding BreakException from try block");
    pendingControlFlow = e;
  } on ContinueException catch (e) {
    Logger.debug("[STryStatement] Holding ContinueException from try block");
    pendingControlFlow = e;
  } on InternalInterpreterD4rtException catch (e, s) {
    // Catch ONLY the exceptions already encapsulated (coming from a 'throw')
    Logger.debug(
      "[STryStatement] Caught internal exception in try block: ${e.originalThrownValue}",
    );
    caughtInternalException = e; // Store the internal exception
    caughtStackTrace = s;
    returnValue = null; // No normal try result
  } catch (userException, userStack) {
    // Catch any other exception (potentially native)
    Logger.debug(
      "[STryStatement] Caught unexpected non-InternalInterpreterException in TRY: $userException",
    );
    // OPEN B.5: a bridged adapter that threw a native/user exception wraps it
    // in a RuntimeError carrying the original object. Recover the original so
    // `on <NativeType>` / bare `catch` dispatch matches the real exception
    // type rather than the RuntimeError wrapper.
    final thrownValue =
        (userException is RuntimeD4rtException &&
            userException.originalException != null)
        ? userException.originalException
        : userException;
    // Encapsulate the user/native exception in our internal type
    caughtInternalException = InternalInterpreterD4rtException(thrownValue);
    // SCC11: recovering the value without the trace left `catch (e, st)`
    // reporting the *wrap site* inside the interpreter. That reads as an
    // interpreter bug in any script that prints a trace, and it makes
    // `Error.throwWithStackTrace` — whose entire purpose is carrying an
    // earlier trace — indistinguishable from a plain `throw`.
    caughtStackTrace =
        (userException is RuntimeD4rtException
            ? userException.originalStackTrace
            : null) ??
        userStack;
    returnValue = null;
  }

  // 2. Execute the catch blocks (if an internal exception was raised AND stored)
  if (caughtInternalException != null) {
    // Use the ORIGINAL value from the internal exception for checks
    final originalThrownValue = caughtInternalException.originalThrownValue;

    // Diagnostics only. A bridged error constructed in script code
    // (`throw StateError('x')`) arrives as a `BridgedInstance`, and the
    // native type behind the wrapper is what makes a log line readable.
    // MATCHING no longer uses this view: SCC20 hands the wrapper itself to
    // [_valueHasType], whose bridged path consults the wrapper's own
    // `bridgedClass` before falling back to the native object — strictly more
    // precise than unwrapping up front, which is what SC5 had to do when the
    // matching code here was a hand-written switch.
    final thrownValueNativeView = originalThrownValue is BridgedInstance
        ? originalThrownValue.nativeObject
        : originalThrownValue;

    Logger.debug(
      "[STryStatement] Looking for catch clauses for thrown value: ${stringify(originalThrownValue)} (type: ${thrownValueNativeView?.runtimeType})",
    );

    // SCC31: an undefined name is a defect in the program text, not a runtime
    // condition, so no clause may claim it — not `on Object`, not a bare
    // `catch (e)`. Real Dart rejects such a program at compile time, where no
    // handler exists to run; the nearest honest equivalent for an interpreter
    // that has already started executing is an error that unwinds past every
    // handler to the host. Skipping the loop rather than short-circuiting the
    // whole block is deliberate: `caughtInternalException` stays non-null, so
    // the finally block below still runs and the error still rethrows.
    final isUnhandleable = originalThrownValue is UndefinedNameD4rtException;

    for (final clause
        in isUnhandleable ? const <SCatchClause>[] : node.catchClauses) {
      bool typeMatch = false;
      String? targetCatchTypeName;

      // Type check (on Type)
      if (clause.exceptionType == null) {
        // No 'on Type' clause, matches anything
        typeMatch = true;
        Logger.debug("[STryStatement] Catch clause matches any type.");
      } else {
        final typeNode = clause.exceptionType!;
        targetCatchTypeName = typeNode is SNamedType
            ? typeNode.name!.name
            : '$typeNode';

        // SCC20: `on T` asks exactly the question `x is T` asks, so it asks
        // it through the same predicate. This used to be a flat switch over
        // sixteen hardcoded type names plus a bridge-identity probe — a
        // SMALLER predicate than [_valueHasType], not a copy of it — and the
        // difference was measurable from a script: `on Exception` missed a
        // script class that implements `Exception`, `on List<int>` caught a
        // `List<String>` because the type arguments were discarded,
        // `on Box<int>` caught a `Box<String>` for the same reason,
        // `on int Function(int)` was rejected as an "unsupported type node",
        // and a prefixed `on c.HashSet` never resolved. The two trees had
        // also drifted apart on the prefixed case, because SCB7's fix had to
        // be placed differently in each; one predicate re-converges them.
        //
        // The WRAPPER is passed, not the unwrapped native view: the shared
        // predicate does its own unwrapping where the host `is` operator
        // needs it, and its bridged path prefers the wrapper's own bridge.
        try {
          typeMatch = _valueHasType(typeNode, originalThrownValue);
        } on InternalInterpreterD4rtException catch (e) {
          // The one thing a catch clause needs that `is` does not: an
          // unresolvable `on T` must MISS, not throw. [_valueHasType] reports
          // a failed type lookup by throwing, and letting that escape here
          // would replace the exception being dispatched with a lookup
          // failure and lose the original — so a resolution failure is read
          // as "this clause does not match", which is what the old
          // warn-and-continue path did.
          Logger.warn(
            "[STryStatement] Could not resolve catch clause type '$targetCatchTypeName': ${e.originalThrownValue}",
          );
          typeMatch = false;
        } on UnimplementedD4rtException catch (e) {
          Logger.warn(
            "[STryStatement] Unsupported catch clause type node ${typeNode.runtimeType}: ${e.message}",
          );
          typeMatch = false;
        }
      }

      if (typeMatch) {
        Logger.debug(
          "[STryStatement] Found matching catch clause${targetCatchTypeName != null ? ' for type $targetCatchTypeName' : ''}.",
        );
        final exceptionParameterName = clause.exceptionParameter?.name;
        final stackTraceParameterName = clause.stackTraceParameter?.name;

        // Create an environment for the catch block
        environment =
            originalEnv; // Restore the environment before creating the catch environment
        final catchEnv = Environment(enclosing: environment);
        if (exceptionParameterName != null) {
          // Define with the ORIGINAL thrown value
          catchEnv.define(exceptionParameterName, originalThrownValue);
          Logger.debug(
            "[STryStatement] Defined exception var '$exceptionParameterName' with original value: ${stringify(originalThrownValue)}",
          );
        }
        if (stackTraceParameterName != null) {
          // Store the textual representation of the stack trace
          // Ensure caughtStackTrace is not null before calling toString()
          final stackTraceString =
              caughtStackTrace?.toString() ?? "Stack trace unavailable";
          catchEnv.define(stackTraceParameterName, stackTraceString);
          Logger.debug(
            "[STryStatement] Defined stacktrace var '$stackTraceParameterName'.",
          ); // Don't print full trace here
        }

        // Execute the catch block in its environment
        environment = catchEnv;
        _isInCatchBlock = true;
        _originalCaughtInternalExceptionForRethrow =
            caughtInternalException; // Store the internal exception for potential rethrow
        //
        try {
          Logger.debug("[STryStatement] Entering catch block body");
          returnValue = clause.body!.accept<Object?>(this);
          Logger.debug("[STryStatement] Catch block completed normally");
          // The exception is handled, clear caughtInternalException to not rethrow it after finally
          caughtInternalException = null;
        } on ReturnException catch (e) {
          // SCC12: held, not rethrown. The finally block has to run on the way
          // out of a `return` in a catch clause, exactly as it does on the way
          // out of one in the try body.
          Logger.debug(
            "[STryStatement] Holding ReturnException from CATCH block until "
            "the finally block has run.",
          );
          _isInCatchBlock = false;
          _originalCaughtInternalExceptionForRethrow = null;
          caughtInternalException = null; // The catch handled the exception.
          pendingControlFlow = e;
        } on BreakException catch (e) {
          _isInCatchBlock = false;
          _originalCaughtInternalExceptionForRethrow = null;
          caughtInternalException = null;
          pendingControlFlow = e;
        } on ContinueException catch (e) {
          _isInCatchBlock = false;
          _originalCaughtInternalExceptionForRethrow = null;
          caughtInternalException = null;
          pendingControlFlow = e;
        } on InternalInterpreterD4rtException catch (
          catchInternalError,
          catchStack
        ) {
          if (identical(
            catchInternalError,
            _originalCaughtInternalExceptionForRethrow,
          )) {
            // This is the exception rethrown by 'rethrow'. It must be allowed to propagate.
            Logger.debug(
              "[STryStatement] Identified rethrown exception. Propagating.",
            );
            // IMPORTANT: Clean the rethrow state BEFORE rethrowing
            _isInCatchBlock = false;
            _originalCaughtInternalExceptionForRethrow = null;
            rethrow; // Relaunch to let the outer mechanism handle it
          } else {
            // This is a NEW internal exception coming from the catch body.
            Logger.debug(
              "[STryStatement] Caught NEW internal exception in CATCH block: ${catchInternalError.originalThrownValue}",
            );
            caughtInternalException =
                catchInternalError; // The new internal exception replaces the old one
            caughtStackTrace = catchStack; // Update stack trace too
            // The new exception is NOT handled by this try/catch
            returnValue = null;
          }
        } catch (nativeError, nativeStack) {
          // Catch other unexpected errors from catch block
          Logger.debug(
            "[STryStatement] Caught unexpected non-InternalInterpreterException in CATCH: $nativeError",
          );
          // Wrap it as InternalInterpreterException to propagate
          caughtInternalException = InternalInterpreterD4rtException(
            nativeError,
          );
          caughtStackTrace = nativeStack;
          returnValue = null;
        } finally {
          // IMPORTANT: Clean the rethrow state if we exit the catch
          _isInCatchBlock = false;
          _originalCaughtInternalExceptionForRethrow = null;
          environment =
              originalEnv; // Restore the environment after the catch
        }
        // Exit the for loop of catch clauses, because we found a match
        break;
      } else {
        Logger.debug(
          "[STryStatement] Skipping catch clause (type mismatch: needed $targetCatchTypeName, got ${originalThrownValue?.runtimeType})",
        );
      }
    } // fin boucle for catchClauses
  } // fin if (caughtInternalException != null)

  // SCC12: the protected region suspended on an `await`, so it has NOT
  // finished — the async driver will replay this whole statement once the
  // future completes. Running the finally block now would run it twice, and a
  // teardown clause that releases a resource twice is a behavioural difference
  // a script can see. Native Dart runs a finally exactly once.
  if (returnValue is AsyncSuspensionRequest) {
    Logger.debug(
      "[STryStatement] Protected region suspended; deferring the finally "
      "block to the resumed pass.",
    );
    environment = originalEnv;
    return returnValue;
  }

  // 3. Execute the finally block (always)
  // Store potential exception from finally block (must be internal type now)
  InternalInterpreterD4rtException? finallyInternalException;

  if (node.finallyBlock != null) {
    environment = originalEnv; // Ensure we are in the correct environment
    Logger.debug("[STryStatement] Entering finally block");
    try {
      final finallyResult = node.finallyBlock!.accept<Object?>(this);
      // SCC12: the finally block's value was discarded, and with it any
      // `AsyncSuspensionRequest` an `await` inside it raised. The interpreter
      // drives `await` by returning that sentinel up through every statement
      // visitor to the async driver, so swallowing it does not lose a value —
      // it loses the *future*, and the program then never completes at all.
      // `try { … } finally { await release(); }` hung forever.
      if (finallyResult is AsyncSuspensionRequest) {
        Logger.debug("[STryStatement] Finally block suspended. Propagating.");
        environment = originalEnv;
        return finallyResult;
      }
      Logger.debug("[STryStatement] Finally block completed normally");
    } on ReturnException {
      // If finally returns, it overrides everything
      Logger.debug("[STryStatement] Caught ReturnException in FINALLY block");
      rethrow; // The return of the finally is the final value
    } on InternalInterpreterD4rtException catch (e) {
      // Catch internal exceptions coming from finally (throw/rethrow in finally)
      Logger.debug(
        "[STryStatement] Caught internal exception in FINALLY block: ${e.originalThrownValue}",
      );
      // The internal exception of the finally prevails
      finallyInternalException = e; // Store internal exception
      // We might want to store the stack trace too if needed later
    } catch (e) {
      // Catch other unexpected errors from finally block
      Logger.debug(
        "[STryStatement] Caught unexpected non-InternalInterpreterException in FINALLY: $e",
      );
      // Wrap it as InternalInterpreterException
      finallyInternalException = InternalInterpreterD4rtException(e);
    }
  }

  // 4. Déterminer le résultat final
  if (finallyInternalException != null) {
    Logger.debug(
      "[STryStatement] Rethrowing internal exception from FINALLY: ${finallyInternalException.originalThrownValue}",
    );
    throw finallyInternalException; // The internal exception of the Finally always prevails
  }

  // SCC12: the held `return` / `break` / `continue` resumes here, after the
  // finally block has run. An exception raised by the finally outranks it —
  // checked above — which is what native Dart does too.
  if (pendingControlFlow != null) {
    Logger.debug(
      "[STryStatement] Resuming held control flow: "
      "${pendingControlFlow.runtimeType}",
    );
    throw pendingControlFlow;
  }

  // If there is an unhandled internal exception (either original, or from a catch) and no exception from the finally
  if (caughtInternalException != null /* && !exceptionHandled */ ) {
    // Note: If it was handled, caughtInternalException was set to null inside the matching catch block.
    // So, if caughtInternalException is still non-null here, it means it wasn't handled.
    Logger.debug(
      "[STryStatement] Rethrowing unhandled internal exception from TRY/CATCH: ${caughtInternalException.originalThrownValue}",
    );
    throw caughtInternalException;
  }

  // Otherwise, return the value (either from the try, or from the catch that handled the exception)
  // Note: if a catch made a return, it was already propagated by the 'rethrow' above.
  Logger.debug("[STryStatement] Exiting normally, returning: $returnValue");
  return returnValue;
}