visitTryStatement method
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;
}