visitBinaryExpression method

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

Implementation

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

  // Handle logical OR (||) with short-circuiting FIRST - don't evaluate right operand yet
  if (operator == '||') {
    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 == '&&') {
    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 == '??') {
    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("[SBinaryExpression DEBUG] Operator: $operator");
    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 because String concatenation
  // and other typed-`+` adapters expect to short-circuit on the dedicated
  // String/Bridged paths below.
  const nullPropagatingOps = <String>{
    '*', '/', '~/', '%', '-',
    '&', '|', '^', '<<', '>>', '>>>',
  };
  if (nullPropagatingOps.contains(operator) &&
      (left == null || right == null)) {
    return null;
  }

  if (left is num && right is num) {
    switch (operator) {
      case '+':
        return left + right;
      case '-':
        return left - right;
      case '*':
        return left * right;
      case '/':
        // For double division, Dart returns infinity for division by zero
        if (left is double || right is double) {
          return left.toDouble() / right.toDouble();
        }
        // For integer division that will produce double, also return infinity
        return left.toDouble() / right.toDouble();
      case '>':
        return left > right;
      case '<':
        return left < right;
      case '>=':
        return left >= right;
      case '<=':
        return left <= right;
      case '%':
        if (right == 0) throw RuntimeD4rtException("Modulo by zero.");
        return left % right;
      case '~/':
        if (right == 0) {
          throw RuntimeD4rtException("Integer division by zero.");
        }
        return left ~/ right;
      default:
        break;
    }
  }

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

  // 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(
        "[SBinaryExpression] 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",
        );
      }
    }
  }

  // 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 (leftBridgedInstance /
  // rightBridgedInstance): 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(
        "[SBinaryExpression] 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(
          "[SBinaryExpression] 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,
        );
      }
    }
  }

  // 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) {
    Callable? extensionOperator;
    try {
      extensionOperator = environment.findExtensionMember(
        leftOperandValue,
        operatorName,
      );
    } on RuntimeD4rtException catch (findError) {
      // findExtensionMember throws if no member is found at all.
      Logger.debug(
        "[SBinaryExpression] 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(
        "[SBinaryExpression] 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 — let it 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 (built-in numeric handling, etc.).
    Logger.debug(
      "[SBinaryExpression] No suitable extension operator '$operatorName' found (early check) for type ${leftOperandValue?.runtimeType}. Continuing...",
    );
  }

  switch (operator) {
    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) return left + right;
    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;
    case '^':
      if (left is int && right is int) 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 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 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 == '+' && (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(
          "[SBinaryExpression] 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(
        "[SBinaryExpression] No suitable extension operator '$operatorName' found (late check) for type ${leftOperandValue?.runtimeType}.",
      );
    } on RuntimeD4rtException catch (findError) {
      Logger.debug(
        "[SBinaryExpression] 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}',
  );
}