import ts from "typescript"; import type { Emitter } from "../types.ts"; import { createBindingPlan } from "../bindings.ts"; import { createComputedCallForExpression, filterRelevantDataFlows, } from "../helpers.ts"; import { createUnlessCall, createWhenCall } from "../../builtins/ifelse.ts"; import { registerSyntheticCallType, selectDataFlowsReferencedIn, } from "../../../ast/mod.ts"; import { isSimpleOpaqueRefAccess } from "../opaque-ref.ts"; /** * Check if an expression is JSX (element, fragment, or self-closing). * Also handles parenthesized JSX like `(
...
)`. */ function isJsxExpression(expr: ts.Expression): boolean { // Unwrap parentheses while (ts.isParenthesizedExpression(expr)) { expr = expr.expression; } return ts.isJsxElement(expr) || ts.isJsxFragment(expr) || ts.isJsxSelfClosingElement(expr); } export const emitBinaryExpression: Emitter = ({ expression, dataFlows, analysis, context, rewriteChildren, }) => { if (!ts.isBinaryExpression(expression)) return undefined; if (dataFlows.all.length === 0) return undefined; // Optimize && operator: convert to when instead of wrapping entire expression in derive // Example: showPanel && // Becomes: when(showPanel, ) or when(derive(condition), ) // // The when/unless optimization is beneficial when the right side (value) is expensive // to construct, like JSX. This allows short-circuit evaluation to skip constructing // the value when the condition is falsy. // // If the right side is simple (not JSX, no reactive deps), using when/unless is just // overhead - better to wrap the whole expression in derive. if (expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) { const leftDataFlows = selectDataFlowsReferencedIn( dataFlows, expression.left, ); const rightDataFlows = selectDataFlowsReferencedIn( dataFlows, expression.right, ); // Check if right side is "expensive" - JSX or has reactive dependencies that need derive const rightIsJsx = isJsxExpression(expression.right); const rightNeedsDerive = rightDataFlows.length > 0 && !isSimpleOpaqueRefAccess(expression.right, context.checker); const rightIsExpensive = rightIsJsx || rightNeedsDerive; // Only use when optimization if right side is expensive if (rightIsExpensive) { // Process left side - derive if it has reactive deps, otherwise pass as-is let condition: ts.Expression = expression.left; if (leftDataFlows.length > 0) { if (!isSimpleOpaqueRefAccess(expression.left, context.checker)) { const plan = createBindingPlan(leftDataFlows); const computedCondition = createComputedCallForExpression( expression.left, plan, context, ); if (computedCondition) { condition = computedCondition; } } // If it's a simple opaque ref, pass it directly (no derive needed) } // Process right side - rewrite children to handle nested opaque refs const value = rewriteChildren(expression.right) || expression.right; // Create when(condition, value) // This is equivalent to: ifElse(condition, value, condition) // Preserves && semantics where falsy values are returned as-is const whenCall = createWhenCall({ condition, value, factory: context.factory, ctHelpers: context.ctHelpers, }); // Register the result type for schema injection // The result type is the union of condition and value types (from the original && expression) if (context.options.typeRegistry) { const resultType = context.checker.getTypeAtLocation(expression); registerSyntheticCallType( whenCall, resultType, context.options.typeRegistry, ); } return whenCall; } } // Optimize || operator: convert to unless instead of wrapping entire expression in derive // Example: value || // Becomes: unless(value, ) or unless(derive(condition), ) // // Same rationale as &&: only beneficial when right side is expensive. if (expression.operatorToken.kind === ts.SyntaxKind.BarBarToken) { const leftDataFlows = selectDataFlowsReferencedIn( dataFlows, expression.left, ); const rightDataFlows = selectDataFlowsReferencedIn( dataFlows, expression.right, ); // Check if right side is "expensive" - JSX or has reactive dependencies that need derive const rightIsJsx = isJsxExpression(expression.right); const rightNeedsDerive = rightDataFlows.length > 0 && !isSimpleOpaqueRefAccess(expression.right, context.checker); const rightIsExpensive = rightIsJsx || rightNeedsDerive; // Only use unless optimization if right side is expensive if (rightIsExpensive) { // Process left side - derive if it has reactive deps, otherwise pass as-is let condition: ts.Expression = expression.left; if (leftDataFlows.length > 0) { if (!isSimpleOpaqueRefAccess(expression.left, context.checker)) { const plan = createBindingPlan(leftDataFlows); const computedCondition = createComputedCallForExpression( expression.left, plan, context, ); if (computedCondition) { condition = computedCondition; } } // If it's a simple opaque ref, pass it directly (no derive needed) } // Process right side - rewrite children to handle nested opaque refs const value = rewriteChildren(expression.right) || expression.right; // Create unless(condition, value) // This is equivalent to: ifElse(condition, condition, value) // Preserves || semantics where truthy values are returned as-is const unlessCall = createUnlessCall({ condition, value, factory: context.factory, ctHelpers: context.ctHelpers, }); // Register the result type for schema injection // The result type is the union of condition and fallback types (from the original || expression) if (context.options.typeRegistry) { const resultType = context.checker.getTypeAtLocation(expression); registerSyntheticCallType( unlessCall, resultType, context.options.typeRegistry, ); } return unlessCall; } } // Fallback: wrap entire expression in derive (original behavior) const relevantDataFlows = filterRelevantDataFlows( dataFlows.all, analysis, context, ); if (relevantDataFlows.length === 0) return undefined; const plan = createBindingPlan(relevantDataFlows); return createComputedCallForExpression(expression, plan, context); };