visitBinaryExpression method

  1. @override
Object? visitBinaryExpression(
  1. BinaryExpression node
)
override

Implementation

@override
Object? visitBinaryExpression(BinaryExpression node) {
  final operator = node.operator.type;

  // Handle logical OR (||) with short-circuiting FIRST - don't evaluate right operand yet
  if (operator == TokenType.BAR_BAR) {
    final leftValue = node.leftOperand.accept<Object?>(this);
    if (leftValue is AsyncSuspensionRequest) return leftValue;
    if (leftValue is! bool) {
      throw RuntimeD4rtException(
        "Left operand of '||' must be bool, got ${leftValue?.runtimeType}.",
      );
    }
    // If left is true, return true without evaluating right
    if (leftValue) return true;
    // Left is false, now evaluate right operand
    final rightValue = node.rightOperand.accept<Object?>(this);
    if (rightValue is AsyncSuspensionRequest) return rightValue;
    if (rightValue is! bool) {
      throw RuntimeD4rtException(
        "Right operand of '||' must be bool, got ${rightValue?.runtimeType}.",
      );
    }
    return rightValue;
  }

  // Handle logical AND (&&) with short-circuiting SECOND - don't evaluate right operand yet
  if (operator == TokenType.AMPERSAND_AMPERSAND) {
    final leftValue = node.leftOperand.accept<Object?>(this);
    if (leftValue is AsyncSuspensionRequest) return leftValue;
    if (leftValue is! bool) {
      throw RuntimeD4rtException(
        "Left operand of '&&' must be bool, got ${leftValue?.runtimeType}.",
      );
    }
    // If left is false, return false without evaluating right
    if (!leftValue) return false;
    // Left is true, now evaluate right operand
    final rightValue = node.rightOperand.accept<Object?>(this);
    if (rightValue is AsyncSuspensionRequest) return rightValue;
    if (rightValue is! bool) {
      throw RuntimeD4rtException(
        "Right operand of '&&' must be bool, got ${rightValue?.runtimeType}.",
      );
    }
    return rightValue;
  }

  // Handle null-coalescing (??) with short-circuiting - don't evaluate right operand yet
  if (operator == TokenType.QUESTION_QUESTION) {
    final leftValue = node.leftOperand.accept<Object?>(this);
    if (leftValue is AsyncSuspensionRequest) return leftValue;
    // If left is not null, return it without evaluating right
    if (leftValue != null) return leftValue;
    // Left is null, evaluate right operand
    final rightValue = node.rightOperand.accept<Object?>(this);
    if (rightValue is AsyncSuspensionRequest) return rightValue;
    return rightValue;
  }

  // For all other operators, evaluate both operands
  final leftOperandValue = node.leftOperand.accept<Object?>(this);
  final rightOperandValue = node.rightOperand.accept<Object?>(this);

  if (Logger.isDebug) {
    Logger.debug("[BinaryExpression DEBUG] Operator: ${operator.lexeme}");
    Logger.debug("  Left operand type: ${leftOperandValue?.runtimeType}");
    Logger.debug("  Left operand value: $leftOperandValue");
    Logger.debug("  Right operand type: ${rightOperandValue?.runtimeType}");
    Logger.debug("  Right operand value: $rightOperandValue");
  }

  if (leftOperandValue is AsyncSuspensionRequest) {
    return leftOperandValue;
  }
  if (rightOperandValue is AsyncSuspensionRequest) {
    return rightOperandValue;
  }

  // Perf (particle_field GC freeze): `num`/`String`/`bool` all have
  // direct-type stdlib bridges, so `toBridgedInstance` would mint a fresh
  // BridgedInstance for every arithmetic / comparison operand — millions of
  // throwaway wrappers per second in a physics+paint loop — only for the
  // value to be immediately unwrapped via `.nativeObject` (which equals the
  // primitive itself). Skip the wrap for these natively-handled value types:
  // numeric arithmetic is done inline below, and primitives never dispatch
  // through a bridged-operator adapter. `(null, false)` is identical in
  // effect to the wrapped-then-unwrapped result.
  final leftBridgedInstance =
      (leftOperandValue is num ||
          leftOperandValue is String ||
          leftOperandValue is bool)
      ? (null, false)
      : toBridgedInstance(leftOperandValue);
  // GEN-095 (D8f): an InterpretedFunction is a *function value*, not a
  // thunk. Auto-invoking it here corrupts equality / null-check semantics
  // for callbacks (e.g. `onHorizontalDrag == null` would invoke the
  // closure with zero args and fail when its parameter list isn't empty).
  // Only auto-call zero-arg functions so the existing arithmetic-on-thunk
  // leniency (e.g. a getter that yielded a `() => 1` thunk) still works.
  final left = leftBridgedInstance.$2
      ? leftBridgedInstance.$1!.nativeObject
      : (leftOperandValue is InterpretedFunction &&
            leftOperandValue.canCallWithoutArgs)
      ? leftOperandValue.call(this, [])
      : leftOperandValue;
  final rightBridgedInstance =
      (rightOperandValue is num ||
          rightOperandValue is String ||
          rightOperandValue is bool)
      ? (null, false)
      : toBridgedInstance(rightOperandValue);
  final right = rightBridgedInstance.$2
      ? rightBridgedInstance.$1!.nativeObject
      : (rightOperandValue is InterpretedFunction &&
            rightOperandValue.canCallWithoutArgs)
      ? rightOperandValue.call(this, [])
      : rightOperandValue;

  // Plan G: null-propagation for arithmetic and bitwise operators.
  //
  // In strict Dart, `null * x`, `x - null`, etc. throw `NoSuchMethodError`
  // (or a type-cast error inside a bridged adapter). The D4rt interpreter
  // is intentionally lenient for dynamic UI scripts that may sample
  // transient nulls — for example, an animated controller value read
  // before the bridged getter is ready, or a layout parameter that hasn't
  // been pumped yet — and returns `null` so the surrounding expression can
  // null-cascade rather than aborting the render pipeline mid-frame.
  //
  // Only arithmetic and bitwise operators participate; equality (`==`,
  // `!=`), comparison (`<`, `<=`, `>`, `>=`), and the short-circuit /
  // null-coalescing operators (`&&`, `||`, `??`) keep their normal
  // null-aware semantics. `+` is excluded so String concatenation keeps
  // its existing `'$left$right'` stringify fallback (`'foo' + null`
  // continues to produce `'foonull'`).
  const nullPropagatingOps = <String>{
    '*',
    '/',
    '~/',
    '%',
    '-',
    '&',
    '|',
    '^',
    '<<',
    '>>',
    '>>>',
  };
  if (nullPropagatingOps.contains(operator.lexeme) &&
      (left == null || right == null)) {
    return null;
  }

  if (left is num && right is num) {
    switch (operator) {
      case TokenType.PLUS:
        return left + right;
      case TokenType.MINUS:
        return left - right;
      case TokenType.STAR:
        return left * right;
      case TokenType.SLASH:
        // `/` always produces a double, so a zero divisor is Infinity or
        // NaN rather than an error. Both operands are widened because
        // `int / int` is a double division too.
        return left.toDouble() / right.toDouble();
      case TokenType.GT:
        return left > right;
      case TokenType.LT:
        return left < right;
      case TokenType.GT_EQ:
        return left >= right;
      case TokenType.LT_EQ:
        return left <= right;
      // SCC30: deliberately NO zero-divisor guard on either operator.
      // `left` and `right` are native `num`s here, so these dispatch to the
      // SDK operators, which already raise
      // `IntegerDivisionByZeroException` — an `UnsupportedError` a script
      // can catch either by name or by supertype. A guard here can only
      // disagree with that: the previous one threw a `RuntimeD4rtException`
      // no `on` clause could name, and because `0.0 == 0` it also fired on
      // doubles, where `1.0 % 0.0` is NaN and `1.0 ~/ 0.0` is a different
      // SDK error entirely.
      case TokenType.PERCENT:
        return left % right;
      case TokenType.TILDE_SLASH:
        return left ~/ right;
      default:
        break;
    }
  }

  // Moved this block BEFORE standard ==, !=, <, <=, >, >= checks
  final operatorName = operator.lexeme;

  // Check for class operator methods FIRST (before extensions and built-in operators)
  if (leftOperandValue is InterpretedInstance) {
    final operatorMethod = leftOperandValue.findOperator(operatorName);
    if (operatorMethod != null) {
      Logger.debug(
        "[BinaryExpression] Found class operator '$operatorName' on ${leftOperandValue.klass.name}. Calling...",
      );
      try {
        return operatorMethod.bind(leftOperandValue).call(this, [
          rightOperandValue,
        ], {});
      } on ReturnException catch (e) {
        return e.value;
      } catch (e) {
        throw RuntimeD4rtException(
          "Error executing class operator '$operatorName': $e",
        );
      }
    }
  }

  // DFUB9: dispatch a binary operator declared on an extension type before
  // the native comparison/arithmetic switch (operator methods are stored on
  // InterpretedExtensionType.methods keyed by the operator lexeme). This must
  // run early — comparison operators like `>` would otherwise reach
  // `left as dynamic > right` and throw NoSuchMethodError on the instance.
  if (leftOperandValue is InterpretedExtensionTypeInstance) {
    final operatorMethod =
        leftOperandValue.extensionType.methods[operatorName];
    if (operatorMethod != null) {
      Logger.debug(
        "[BinaryExpression] Found extension-type operator '$operatorName' on ${leftOperandValue.extensionType.name}. Calling...",
      );
      try {
        return operatorMethod.bind(leftOperandValue).call(this, [
          rightOperandValue,
        ], {});
      } on ReturnException catch (e) {
        return e.value;
      } catch (e) {
        throw RuntimeD4rtException(
          "Error executing extension-type operator '$operatorName': $e",
        );
      }
    }
  }

  // Fix J: Intercept enum equality BEFORE toBridgedInstance wraps them incorrectly
  if ((operatorName == '==' || operatorName == '!=') &&
      (leftOperandValue is Enum ||
          leftOperandValue is BridgedEnumValue ||
          rightOperandValue is Enum ||
          rightOperandValue is BridgedEnumValue)) {
    // Unwrap BridgedEnumValue to native enum for comparison
    final leftNative = leftOperandValue is BridgedEnumValue
        ? leftOperandValue.nativeValue
        : leftOperandValue;
    final rightNative = rightOperandValue is BridgedEnumValue
        ? rightOperandValue.nativeValue
        : rightOperandValue;
    if (operatorName == '==') {
      return leftNative == rightNative;
    } else {
      return leftNative != rightNative;
    }
  }

  // Check for bridged operator methods (e.g., +, -, *, /, etc. on BridgedInstance)
  // Reuse the wrappers already resolved above (lines ~1438/1451): each
  // `toBridgedInstance` call mints a fresh BridgedInstance and re-runs the
  // type-resolution walk, so re-calling here would triple the per-operator
  // bridged-wrapper churn that drives the particle_field GC freeze.
  if (leftBridgedInstance.$2) {
    final bridgedInstance = leftBridgedInstance.$1!;
    final bridgedClass = bridgedInstance.bridgedClass;

    final methodAdapter = bridgedClass.findInstanceMethodAdapter(
      operatorName,
    );

    if (methodAdapter != null) {
      Logger.debug(
        "[BinaryExpression] Found bridged operator '$operatorName' for ${bridgedClass.name}. Calling adapter...",
      );
      try {
        // Unwrap right operand if it's a BridgedInstance
        final rightArg = rightBridgedInstance.$2
            ? rightBridgedInstance.$1!.nativeObject
            : rightOperandValue;
        return methodAdapter(
          this,
          bridgedInstance.nativeObject,
          [rightArg],
          {},
          null,
        );
      } catch (e, s) {
        Logger.error(
          "[BinaryExpression] Native exception during bridged operator '$operatorName' on ${bridgedClass.name}: $e\\n$s",
        );
        throw RuntimeD4rtException(
          "Native error during bridged operator '$operatorName' on ${bridgedClass.name}: $e",
          originalException: e,
          originalStackTrace: s,
        );
      }
    }
  }

  // Bridged enum operator methods (e.g. the `&`, `|`, `^` overloads Flutter's
  // `WidgetState` gains from the `WidgetStateOperators` mixin). `WidgetState`
  // is bridged as an enum whose definition carries operator adapters, so a
  // `WidgetState.hovered & ~WidgetState.disabled` expression has a
  // BridgedEnumValue left operand. Dispatch to the bridged operator adapter
  // before the built-in switch (which only knows int/BigInt) so these don't
  // fall through to "Unsupported binary operator".
  if (leftOperandValue is BridgedEnumValue &&
      leftOperandValue.hasMethod(operatorName)) {
    // Unwrap a BridgedEnumValue right operand to its native value so the
    // native operator receives the type it expects (e.g. a
    // WidgetStatesConstraint rather than the interpreter's wrapper).
    final rightArg = rightOperandValue is BridgedEnumValue
        ? rightOperandValue.nativeValue
        : rightOperandValue;
    return leftOperandValue.invoke(this, operatorName, [rightArg], {});
  }

  // Only try extension immediately for operators where standard checks might bypass it
  // (e.g., ==, !=, <, >, <=, >= which have generic fallbacks)
  bool checkExtensionEarly = [
    '==',
    '!=',
    '<',
    '<=',
    '>',
    '>=',
    '|', // Also check early for missing operators like |
    '&', // and &
    '^', // and ^ if BigInt support was incomplete
    // Add other operators here if needed
  ].contains(operatorName);

  if (checkExtensionEarly) {
    // Bucket #14: only the lookup belongs in the outer try. Wrapping the
    // call invocation here too made the broad `on RuntimeD4rtException`
    // handler swallow re-thrown call-site errors and fall through to
    // "Unsupported binary operator", masking the real cause.
    Callable? extensionOperator;
    try {
      extensionOperator = environment.findExtensionMember(
        leftOperandValue,
        operatorName,
      );
    } on RuntimeD4rtException catch (findError) {
      // findExtensionMember throws if no member is found at all.
      Logger.debug(
        "[BinaryExpression] No extension member '$operatorName' found (early check) for type ${leftOperandValue?.runtimeType}. Error: ${findError.message}",
      );
      // Continue to standard checks even if lookup failed early
    }
    if (extensionOperator is ExtensionMemberCallable &&
        extensionOperator.isOperator) {
      Logger.debug(
        "[BinaryExpression] Found extension operator '$operatorName' (early check) for type ${leftOperandValue?.runtimeType}. Calling...",
      );
      final extensionPositionalArgs = [leftOperandValue, rightOperandValue];
      try {
        return extensionOperator.call(this, extensionPositionalArgs, {});
      } on ReturnException catch (e) {
        return e.value;
      } on RuntimeD4rtException {
        // Already a runtime exception with context — propagate as-is so
        // the user sees the underlying cause rather than swallowing it.
        rethrow;
      } catch (e) {
        throw RuntimeD4rtException(
          "Error executing extension operator '$operatorName': $e",
        );
      }
    }
    // If no suitable extension operator found early, continue to standard checks
    Logger.debug(
      "[BinaryExpression] No suitable extension operator '$operatorName' found (early check) for type ${leftOperandValue?.runtimeType}. Continuing...",
    );
  }

  switch (operator.lexeme) {
    case '+':
      if (left is String && right is String) return left + right;
      if (left is BigInt && right is BigInt) return left + right;
      if (left is Duration && right is Duration) return left + right;
      if (left is List && right is List) {
        // `List.+` demands `List<E>` for the *receiver's* element type, and
        // d4rt evaluates a list literal to `List<Object?>`. So on a
        // typed-data receiver `left + right` fails a container cast inside
        // the SDK even when every element is the right type — the failure is
        // about the list's type argument, not its contents. Concatenating
        // element-wise builds the result in the receiver's own element type
        // and still rejects an element that genuinely does not fit.
        final combined = left.toList();
        for (final element in right) {
          combined.add(element);
        }
        return combined;
      }
    case '-':
      if (left is BigInt && right is BigInt) return left - right;
      if (left is Duration && right is Duration) return left - right;
    case '*':
      if (left is String && right is int) return left * right;
      if (left is BigInt && right is BigInt) return left * right;
      if (left is Duration && right is num) return left * right;
    case '/':
      if (left is BigInt && right is BigInt) return left / right;
    case '~/':
      if (left is BigInt && right is BigInt) return left ~/ right;
      if (left is Duration && right is int) return left ~/ right;
    case '%':
      if (left is BigInt && right is BigInt) return left % right;
    case '==':
      // Special handling for BridgedEnumValue comparison
      if (leftOperandValue is BridgedEnumValue &&
          rightOperandValue is BridgedEnumValue) {
        final result = leftOperandValue == rightOperandValue;
        return result;
      }

      // Special handling for mixed native enum and BridgedEnumValue comparison
      if ((leftOperandValue is BridgedEnumValue &&
              rightOperandValue is Enum) ||
          (leftOperandValue is Enum &&
              rightOperandValue is BridgedEnumValue)) {
        // Convert both to their native enum values for comparison
        final leftNative = leftOperandValue is BridgedEnumValue
            ? leftOperandValue.nativeValue
            : leftOperandValue;
        final rightNative = rightOperandValue is BridgedEnumValue
            ? rightOperandValue.nativeValue
            : rightOperandValue;

        final result = leftNative == rightNative;
        return result;
      }

      // Special handling for Type vs BridgedClass comparison
      // (e.g., value.runtimeType == int)
      if (left is Type && right is BridgedClass) {
        return left == right.nativeType;
      }
      if (left is BridgedClass && right is Type) {
        return left.nativeType == right;
      }

      return left == right;
    case '!=':
      // Special handling for BridgedEnumValue comparison
      if (leftOperandValue is BridgedEnumValue &&
          rightOperandValue is BridgedEnumValue) {
        return leftOperandValue != rightOperandValue;
      }

      // Special handling for mixed native enum and BridgedEnumValue comparison
      if ((leftOperandValue is BridgedEnumValue &&
              rightOperandValue is Enum) ||
          (leftOperandValue is Enum &&
              rightOperandValue is BridgedEnumValue)) {
        final leftNative = leftOperandValue is BridgedEnumValue
            ? leftOperandValue.nativeValue
            : leftOperandValue;
        final rightNative = rightOperandValue is BridgedEnumValue
            ? rightOperandValue.nativeValue
            : rightOperandValue;
        return leftNative != rightNative;
      }

      // Special handling for Type vs BridgedClass comparison
      // (e.g., value.runtimeType != int)
      if (left is Type && right is BridgedClass) {
        return left != right.nativeType;
      }
      if (left is BridgedClass && right is Type) {
        return left.nativeType != right;
      }

      return left != right;
    case '<':
      return left as dynamic < right;
    case '<=':
      return left as dynamic <= right;
    case '>':
      return left as dynamic > right;
    case '>=':
      return left as dynamic >= right;
    // `bool` declares `& | ^` as well as `int` does. They are the
    // non-short-circuiting siblings of `&& ||` — both operands are already
    // evaluated by the time we get here, which is exactly the semantics Dart
    // requires and the one case `&&` cannot express. Only bool-bool is
    // accepted: `true & 1` is a type error in Dart and must stay one here.
    case '^':
      if (left is int && right is int) return left ^ right;
      if (left is bool && right is bool) return left ^ right;
      if (left is BigInt && right is BigInt) return left ^ right;
      throw RuntimeD4rtException('Unsupported binary operator "$operator"');
    case '&':
      if (left is int && right is int) return left & right;
      if (left is bool && right is bool) return left & right;
      if (left is BigInt && right is BigInt) return left & right;
      throw RuntimeD4rtException('Unsupported binary operator "$operator"');
    case '|':
      if (left is int && right is int) return left | right;
      if (left is bool && right is bool) return left | right;
      if (left is BigInt && right is BigInt) return left | right;
      throw RuntimeD4rtException('Unsupported binary operator "$operator"');
    case '>>':
      if (left is int && right is int) return left >> right;
      if (left is BigInt && right is int) return left >> right;
      throw RuntimeD4rtException('Unsupported binary operator "$operator"');
    case '<<':
      if (left is int && right is int) return left << right;
      if (left is BigInt && right is int) return left << right;
      throw RuntimeD4rtException('Unsupported binary operator "$operator"');
    case '>>>':
      if (left is int && right is int) return left >>> right;
      // Note: BigInt doesn't support >>> operator in Dart
      throw RuntimeD4rtException('Unsupported binary operator "$operator"');
    default:
      break;
  }

  if (operator == TokenType.PLUS && (left is String || right is String)) {
    return '${stringify(left)}${stringify(right)}'; // stringify already handles BridgedInstance indirectly via toString
  }

  if (!checkExtensionEarly) {
    // Only run this if we didn't already check (and potentially succeed/fail) earlier
    try {
      final extensionOperator = environment.findExtensionMember(
        leftOperandValue,
        operatorName,
      );

      if (extensionOperator is ExtensionMemberCallable &&
          extensionOperator.isOperator) {
        Logger.debug(
          "[BinaryExpression] Found extension operator '$operatorName' (late check) for type ${leftOperandValue?.runtimeType}. Calling...",
        );
        final extensionPositionalArgs = [leftOperandValue, rightOperandValue];
        try {
          return extensionOperator.call(this, extensionPositionalArgs, {});
        } on ReturnException catch (e) {
          return e.value; // Should not happen for operators, but handle
        } catch (e) {
          throw RuntimeD4rtException(
            "Error executing extension operator '$operatorName': $e",
          );
        }
      }
      Logger.debug(
        "[BinaryExpression] No suitable extension operator '$operatorName' found (late check) for type ${leftOperandValue?.runtimeType}.",
      );
    } on RuntimeD4rtException catch (findError) {
      Logger.debug(
        "[BinaryExpression] No extension member '$operatorName' found (late check) for type ${leftOperandValue?.runtimeType}. Error: ${findError.message}",
      );
      // Fall through to the final standard error below.
    }
  }

  throw RuntimeD4rtException(
    'Unsupported operator ($operator) for types ${leftOperandValue?.runtimeType} and ${rightOperandValue?.runtimeType}',
  );
}