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