visitReturnStatement method
Object?
visitReturnStatement(
- ReturnStatement node
)
override
Implementation
@override
Object? visitReturnStatement(ReturnStatement node) {
// Walk up the AST to find the enclosing function
// Bug-73, Bug-74 FIX: A FunctionDeclaration contains a FunctionExpression as its child,
// so we need to find the FunctionDeclaration (which has the name) rather than stopping
// at the inner FunctionExpression.
AstNode? eDecl = node;
while (eDecl != null) {
if (eDecl is FunctionDeclaration) {
// Named function - prefer this over FunctionExpression
break;
}
if (eDecl is FunctionExpression) {
// Check if parent is a FunctionDeclaration - if so, use that instead
if (eDecl.parent is FunctionDeclaration) {
eDecl = eDecl.parent;
break;
}
// Otherwise it's a true anonymous function (lambda, callback)
break;
}
eDecl = eDecl.parent;
}
Object? returnValue;
if (node.expression != null) {
returnValue = node.expression!.accept<Object?>(this);
if (returnValue is AsyncSuspensionRequest) {
return returnValue;
}
} else {
returnValue = null;
}
if (eDecl != null &&
(eDecl is FunctionDeclaration || eDecl is FunctionExpression)) {
bool isNullable = false;
String functionName = '<anonymous>';
RuntimeType? declaredType;
RuntimeType? valueRuntimeType;
try {
InterpretedFunction? currentCallable;
if (eDecl is FunctionDeclaration) {
functionName = eDecl.name.lexeme;
// Use the visitor's currentFunction directly (set by
// InterpretedFunction.call) rather than looking the name up in the
// environment on every return. A function declaration is a final
// binding, so `environment.get(name)` always resolves to the same
// InterpretedFunction that is currently executing — the per-return
// lookup was redundant. This also mirrors tom_d4rt_ast's visitor and
// sidesteps the old Cluster C29 shadowing hazard (a same-named local
// of a different type can no longer be mistaken for the function).
currentCallable = currentFunction;
} else if (eDecl is FunctionExpression) {
// For anonymous functions (closures), use currentFunction from visitor
currentCallable = currentFunction;
}
if (currentCallable is InterpretedFunction) {
declaredType = currentCallable.declaredReturnType;
isNullable = currentCallable.isNullable;
// Special handling for async* generators: return without value should be allowed
if (currentCallable.isAsyncGenerator && returnValue == null) {
throw ReturnException(returnValue); // Exit generator cleanly
}
}
valueRuntimeType = environment.getRuntimeType(returnValue);
// Debug diagnostics: building these details (type-name lookups,
// hashCodes, and an extra `isSubtypeOf` evaluation) is pure logging
// work that otherwise runs on every return. Guard it so nothing is
// interpolated or computed when debug logging is off.
if (Logger.isDebug) {
String declaredTypeDetails = "N/A";
if (declaredType != null) {
declaredTypeDetails =
"Name: ${declaredType.name}, Hash: ${declaredType.hashCode}";
if (declaredType is BridgedClass) {
declaredTypeDetails +=
", NativeType: ${declaredType.nativeType}, NativeHash: ${declaredType.nativeType.hashCode}";
}
}
String valueRuntimeTypeDetails = "N/A";
if (valueRuntimeType != null) {
valueRuntimeTypeDetails =
"Name: ${valueRuntimeType.name}, Hash: ${valueRuntimeType.hashCode}";
if (valueRuntimeType is BridgedClass) {
valueRuntimeTypeDetails +=
", NativeType: ${valueRuntimeType.nativeType}, NativeHash: ${valueRuntimeType.nativeType.hashCode}";
}
}
Logger.debug("[visitReturnStatement] Function: '$functionName'");
Logger.debug(
"[visitReturnStatement] Declared Type: $declaredTypeDetails",
);
Logger.debug(
"[visitReturnStatement] Value Runtime Type: $valueRuntimeTypeDetails",
);
Logger.debug(
"[visitReturnStatement] Return Value: $returnValue (Type: ${returnValue?.runtimeType})",
);
Logger.debug(
"[visitReturnStatement] Is Declared Type Nullable: $isNullable",
);
if (declaredType != null && valueRuntimeType != null) {
Logger.debug(
"[visitReturnStatement] valueRuntimeType.isSubtypeOf(declaredType) = ${valueRuntimeType.isSubtypeOf(declaredType)}",
);
}
}
// Cluster RETURNTYPE / I-MISC-212: Returning `null` from a function
// with a non-nullable declared return type must throw, even when
// the legacy `Null.isSubtypeOf(T)` rule would let the value pass.
// Mirrors the equivalent check in
// tom_d4rt_ast/lib/src/runtime/interpreter_visitor.dart.
if (returnValue == null &&
declaredType != null &&
!isNullable &&
declaredType.name != 'void' &&
declaredType.name != 'dynamic') {
throw RuntimeD4rtException(
"A value of type 'Null' can't be returned from the function '$functionName' because it has a return type of '${declaredType.name}'.",
);
}
if (valueRuntimeType != null) {
if (declaredType != null) {
if (declaredType.name != "dynamic" &&
!valueRuntimeType.isSubtypeOf(
declaredType,
value: returnValue,
)) {
bool showError = true;
if (isNullable && returnValue == null) {
showError = false;
}
if (declaredType.name == "void" && returnValue == null) {
showError = false;
}
if (declaredType.name == "Object" && returnValue != null) {
showError = false;
}
// Bug-73 FIX: In async functions, returning a Future<T> when declared type is T is allowed
// Dart automatically awaits the inner Future. Check if:
// 1. The function is async (currentCallable.isAsync)
// 2. The return value is a Future
// 3. The declared type matches the Future's inner type (or is void)
if (currentCallable != null &&
currentCallable.isAsync &&
returnValue is Future) {
// Allow returning Future from async function - Dart awaits it
showError = false;
}
// Bug-93 FIX: Dart implicitly promotes int to double when the
// declared return type is double and the value is an int.
if (declaredType.name == 'double' && returnValue is int) {
showError = false;
returnValue = returnValue.toDouble();
}
if (showError) {
final declaredTypeName = isNullable
? '${declaredType.name}?'
: declaredType.name;
throw RuntimeD4rtException(
"A value of type '${valueRuntimeType.name}' can't be returned from the function '$functionName' because it has a return type of '$declaredTypeName'.",
);
}
}
}
}
// DFUB6: applied generic return validation. The base subtype check
// above treats `Box<String>` / `List<String>` as their raw base type,
// so a generic/collection value with mismatched type arguments slips
// through. Enforce the arguments element-wise here.
//
// Skip async/generator functions: their declared return type is a
// `Future<T>` / `Stream<T>` / `Iterable<T>` wrapper, but the value in a
// `return` statement is the *inner* `T` (Dart wraps it automatically).
// Comparing the inner value's applied type against the wrapper's
// arguments would spuriously reject e.g. `Future<List<String>> f()
// async => [1,2,3]`, so leave the wrapper case to the base check.
final skipAppliedGenericReturn =
currentCallable is InterpretedFunction &&
(currentCallable.isAsync ||
currentCallable.isGenerator ||
currentCallable.isAsyncGenerator);
if (!skipAppliedGenericReturn) {
_checkAppliedGenericReturn(
returnValue,
eDecl,
functionName,
isNullable,
);
}
} catch (e) {
// Log before rethrow for more context in case of unexpected error here
Logger.error(
"[visitReturnStatement] Error during type check for function '$functionName': $e",
);
if (e is Error) {
Logger.error("Stack trace: ${e.stackTrace}");
}
rethrow;
}
}
// For non-suspended results, throw the exception to unwind the stack.
throw ReturnException(returnValue);
}