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 '/':
// `/` 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 '>':
return left > right;
case '<':
return left < right;
case '>=':
return left >= right;
case '<=':
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 '%':
return left % right;
case '~/':
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",
);
}
}
}
// 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(
"[SBinaryExpression] 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 (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,
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) {
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) {
// `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 == '+' && (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}',
);
}