visitReturnStatement method
Visit a SReturnStatement.
Implementation
@override
Object? visitReturnStatement(SReturnStatement node) {
Object? returnValue;
if (node.expression != null) {
returnValue = node.expression!.accept<Object?>(this);
if (returnValue is AsyncSuspensionRequest) {
return returnValue;
}
} else {
returnValue = null;
}
// Use currentFunction (set by InterpretedFunction.call) to get the
// declared return type. SAstNode has no parent references, so we cannot
// walk up the AST to find the enclosing function declaration.
final currentCallable = currentFunction;
if (currentCallable != null) {
bool isNullable = currentCallable.isNullable;
// Extract function name from the callable's toString, or use a default.
// InterpretedFunction.toString returns '<fn name>'.
final fnStr = currentCallable.toString();
String functionName = fnStr;
if (fnStr.startsWith('<fn ') && fnStr.endsWith('>')) {
functionName = fnStr.substring(4, fnStr.length - 1);
}
RuntimeType? declaredType = currentCallable.declaredReturnType;
RuntimeType? valueRuntimeType;
try {
// 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);
// Guard: skip building these interpolated diagnostics on every return
// when debug logging is off.
if (Logger.isDebug) {
Logger.debug("[visitReturnStatement] Function: '$functionName'");
Logger.debug(
"[visitReturnStatement] Declared Type: ${declaredType?.name ?? 'N/A'}",
);
Logger.debug(
"[visitReturnStatement] Value Runtime Type: ${valueRuntimeType?.name ?? 'N/A'}",
);
Logger.debug(
"[visitReturnStatement] Return Value: $returnValue (Type: ${returnValue?.runtimeType})",
);
Logger.debug(
"[visitReturnStatement] Is Declared Type Nullable: $isNullable",
);
}
// Check null return value against non-nullable declared type
if (returnValue == null &&
declaredType != null &&
!isNullable &&
declaredType.name != 'void' &&
declaredType.name != 'dynamic') {
final declaredTypeName = declaredType.name;
throw RuntimeD4rtException(
"A value of type 'Null' can't be returned from the function '$functionName' because it has a return type of '$declaredTypeName'.",
);
}
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
if (currentCallable.isAsync && returnValue is Future) {
showError = false;
}
// Bug-93 FIX: Dart implicitly promotes int to double
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'.",
);
}
}
}
}
} catch (e) {
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);
}