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

  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 {
    // If the try returns, the finally must execute, but we propagate the return
    Logger.debug(
      "[STryStatement] Propagating ReturnException from try block.",
    );
    rethrow;
  } on BreakException {
    // Propagate for outer loops/switch
    Logger.debug("[STryStatement] Propagating BreakException from try block");
    rethrow;
  } on ContinueException {
    // Propagate for outer loops
    Logger.debug(
      "[STryStatement] Propagating ContinueException from try block",
    );
    rethrow;
  } 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);
    caughtStackTrace = 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;

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

    for (final clause in 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!;
        if (typeNode is SNamedType) {
          targetCatchTypeName = typeNode.name!.name;
          Logger.debug(
            "[STryStatement] Checking catch clause for type: $targetCatchTypeName",
          );

          // Use originalThrownValue for type checking
          switch (targetCatchTypeName) {
            case 'int':
              typeMatch = originalThrownValue is int;
              break;
            case 'double':
              typeMatch = originalThrownValue is double;
              break;
            case 'num':
              typeMatch = originalThrownValue is num;
              break;
            case 'String':
              typeMatch = originalThrownValue is String;
              break;
            case 'bool':
              typeMatch = originalThrownValue is bool;
              break;
            case 'List':
              typeMatch = originalThrownValue is List;
              break;
            case 'Null':
              // This is tricky. 'on Null' might not be common.
              // Check if the original value is null.
              typeMatch = originalThrownValue == null;
              break;
            case 'Object':
              // Everything non-null is an Object?
              // Dart's 'on Object' catches non-null exceptions.
              typeMatch = originalThrownValue != null;
              break;
            case 'dynamic': // 'on dynamic' catches everything, like no 'on' clause
              typeMatch = true;
              break;
            case 'void': // Cannot catch on void
              typeMatch = false;
              break;
            case 'Exception':
              // Match any native Exception subtype
              typeMatch = originalThrownValue is Exception;
              break;
            case 'Error':
              // Match any native Error subtype
              typeMatch = originalThrownValue is Error;
              break;
            case 'FormatException':
              typeMatch = originalThrownValue is FormatException;
              break;
            case 'StateError':
              typeMatch = originalThrownValue is StateError;
              break;
            case 'ArgumentError':
              typeMatch = originalThrownValue is ArgumentError;
              break;
            case 'RangeError':
              typeMatch = originalThrownValue is RangeError;
              break;
            case 'TypeError':
              typeMatch = originalThrownValue is TypeError;
              break;
            case 'UnsupportedError':
              typeMatch = originalThrownValue is UnsupportedError;
              break;
            default:
              // User-defined type
              try {
                final targetType = environment.get(targetCatchTypeName);
                if (targetType is InterpretedClass) {
                  // Check if the ORIGINAL thrown value is an instance of the target type
                  if (originalThrownValue is InterpretedInstance) {
                    typeMatch = originalThrownValue.klass.isSubtypeOf(
                      targetType,
                    );
                    Logger.debug(
                      "[STryStatement]   Checking instance '${originalThrownValue.klass.name}' against class '$targetCatchTypeName'. Result: $typeMatch",
                    );
                  } else {
                    // Native value cannot be subtype of user-defined class
                    typeMatch = false;
                    Logger.debug(
                      "[STryStatement]   Thrown value is native (${originalThrownValue?.runtimeType}), cannot match user class '$targetCatchTypeName'.",
                    );
                  }
                } else if (targetType is BridgedClass) {
                  // G-DCLI-08/12 FIX: Handle native exceptions matched against bridged types
                  // When a native exception (e.g., RunException, CopyException) is thrown
                  // and caught with 'on RunException catch (e)', we need to match the
                  // native exception's type against the BridgedClass.
                  if (originalThrownValue != null) {
                    // Handle BridgedInstance (exceptions created by interpreter via bridged constructors)
                    if (originalThrownValue is BridgedInstance) {
                      typeMatch =
                          originalThrownValue.bridgedClass.nativeType ==
                              targetType.nativeType ||
                          originalThrownValue.bridgedClass.name ==
                              targetType.name;
                      Logger.debug(
                        "[STryStatement]   Checking BridgedInstance '${originalThrownValue.bridgedClass.name}' against bridged class '$targetCatchTypeName'. Result: $typeMatch",
                      );
                    } else {
                      try {
                        final thrownBridge = globalEnvironment.toBridgedClass(
                          originalThrownValue.runtimeType,
                        );
                        // Check if the thrown value's bridge matches the catch type
                        typeMatch =
                            thrownBridge.nativeType ==
                                targetType.nativeType ||
                            thrownBridge.name == targetType.name;
                        Logger.debug(
                          "[STryStatement]   Checking native thrown '${thrownBridge.name}' against bridged class '$targetCatchTypeName'. Result: $typeMatch",
                        );
                      } catch (_) {
                        // Thrown value has no bridge - try runtime type name match
                        final thrownTypeName = originalThrownValue.runtimeType
                            .toString();
                        typeMatch =
                            thrownTypeName == targetType.name ||
                            thrownTypeName.startsWith('${targetType.name}<');
                        Logger.debug(
                          "[STryStatement]   No bridge for thrown type '$thrownTypeName'. Name match against '$targetCatchTypeName': $typeMatch",
                        );
                      }
                    }
                  } else {
                    typeMatch = false;
                  }
                } else {
                  // Target type name resolved, but it's not an InterpretedClass or BridgedClass
                  typeMatch = false;
                  Logger.warn(
                    "[STryStatement] Catch clause type '$targetCatchTypeName' not found or not a class/mixin.",
                  );
                }
              } catch (e) {
                // Error resolving targetCatchTypeName
                Logger.warn(
                  "[STryStatement] Error resolving catch clause type '$targetCatchTypeName': $e",
                );
                typeMatch = false;
              }
          }
        } else {
          // Handle other type nodes like FunctionType if necessary
          Logger.warn(
            "[STryStatement] Unsupported catch clause type node: ${clause.exceptionType.runtimeType}",
          );
          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 {
          // The catch made a return, we propagate it immediately but the finally must execute
          Logger.debug(
            "[STryStatement] Caught ReturnException in CATCH block",
          );
          // IMPORTANT: Clean the rethrow state BEFORE rethrowing
          _isInCatchBlock = false;
          _originalCaughtInternalExceptionForRethrow = null;
          rethrow; // IMPORTANT: Ensure the return ends the function
        } 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)

  // 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 {
      node.finallyBlock!.accept<Object?>(this);
      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
  }

  // 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;
}