visitBinaryExpression method
Visit a SBinaryExpression.
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}',
);
}