visitReturnStatement method

  1. @override
Object? visitReturnStatement(
  1. 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);
}