Implementation: src/hql/transpiler/syntax/conditional.ts (if, switch, case, ?, do, return, throw), src/hql/lib/macro/core.hql (cond, when, unless, if-let, when-let)
HQL provides conditional expressions for control flow:
if - Binary conditional (true/false branches) [transpiler]cond - Multi-way conditional (nested if chains) [macro]switch - JavaScript-style switch statement with case/default/fallthrough [transpiler]case - Clojure-style expression switch (flat val/result pairs) [transpiler]? - Ternary operator (always expression, never statement) [transpiler]when - Execute body when condition is true, else nil [macro]unless - Execute body when condition is false, else nil [macro]if-let - Conditional binding with else branch [macro]when-let - Conditional binding, single branch [macro]Supporting forms also in conditional.ts:
do - Sequence expression (comma operator or IIFE)return - Return statement (non-local return via throw inside IIFE)throw - Throw statementAll conditionals are expressions that return values.
(if condition then-expr else-expr)
(if condition then-expr) ;; else defaults to nil
Compilation: condition ? then : else (ConditionalExpression) or IfStatement when branches contain control flow (return, throw, break, continue, loops).
Validation: Requires 2 or 3 arguments (condition + then + optional else).
;; Grouped syntax (each clause is a 2-element list)
(cond
((< x 5) "small")
((< x 15) "medium")
(else "large"))
;; Flat syntax (alternating test/result pairs)
(cond
(< x 5) "small"
(< x 15) "medium"
else "large")
;; true as fallback (equivalent to else)
(cond
((< 5 3) "won't match")
(true "default"))
Compilation: Macro-expands to nested if expressions. The else keyword is recognized as an unconditional match.
Detection: If the first clause is a list with exactly 2 elements, grouped syntax is used. Otherwise flat syntax.
(switch expr
(case val1 body...)
(case val2 :fallthrough body...)
(default body...))
Compilation: Optimized to chained ternaries when all cases are simple (no fallthrough, single return per case). Falls back to IIFE-wrapped JS switch for complex cases (fallthrough or multiple statements).
If no default is provided, an implicit default returning null is added.
(case expr
val1 result1
val2 result2
default-result) ;; optional (odd number of args = last is default)
Compilation: Always optimized to chained ternaries: expr === val1 ? result1 : expr === val2 ? result2 : default. Uses === for comparison.
If no default is provided (even number of args after expr), unmatched cases return null.
(? condition then-expr else-expr)
Compilation: Always condition ? then : else (ConditionalExpression). Unlike if, never generates IfStatement. Requires exactly 3 arguments.
(when test body...)
Compilation: Macro-expands to (if test (do body...) nil).
(unless test body...)
Compilation: Macro-expands to (if test nil (do body...)).
(if-let [name expr] then-expr else-expr)
(if-let (name expr) then-expr else-expr)
Compilation: Macro-expands to bind name to expr, then execute then-expr if truthy, otherwise else-expr. Both bracket [] and paren () binding syntax are supported.
(when-let [name expr] body...)
(when-let (name expr) body...)
Compilation: Macro-expands to bind name to expr, then execute body if truthy. Both bracket [] and paren () binding syntax are supported.
transformIf decides between ConditionalExpression and IfStatement:
recur in branches: IfStatement with return wrappingtransformDo uses two strategies:
transformSwitch generates:
transformReturn checks if inside an IIFE context. If so, it generates a throw with a special early-return object (caught by IIFE wrapper) instead of a normal return.
if-let macro expands to an immediately-invoked function that binds the value and checks truthiness:
(if-let [x (expr)] then else)
;; expands to:
((fn [x] (if x then else)) (expr))
when-let macro similarly uses an immediately-invoked function:
(when-let [x (expr)] body...)
;; expands to:
((fn [x] (when x body...)) (expr))
ifLet is an alias for if-letwhenLet is an alias for when-let