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 with case/default/fallthrough [transpiler]case - Clojure-style expression switch (flat val/result pairs) [transpiler]? - Ternary operator (always expression) [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]All conditionals are expressions that return values (not statements).
// Basic if
(if condition then-expr else-expr)
// Example
(if true 1 2) // => 1
(if false 1 2) // => 2
// If with comparison
(if (> 5 3) "yes" "no") // => "yes"
// If without else (defaults to nil)
(if true 1) // => 1
(if false 1) // => nil
// If with multiple statements (use do)
(if condition
(do
(var x 10)
(+ x 5))
(do
(var y 20)
(- y 5)))
// Nested if
(if outer-condition
(if inner-condition result1 result2)
result3)
// If as expression
(let result (if (< 3 5) "less" "greater"))
// If as return value
(fn check [n]
(if (> n 0) "positive" "non-positive"))
// 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"))
// Example with expressions
(let x 10)
(cond
((< x 5) "small")
((< x 15) "medium")
(true "large")) // => "medium"
Detection: if the first clause is a list with exactly 2 elements, grouped syntax is used. Otherwise flat syntax.
// Basic switch
(switch value
(case 1 "one")
(case 2 "two")
(default "other"))
// Switch with string cases
(let status "active")
(switch status
(case "active" "Running")
(case "pending" "Waiting")
(case "error" "Failed")
(default "Unknown"))
// Switch with fallthrough
(switch grade
(case "A" :fallthrough)
(case "B" "Good")
(default "Other"))
// Switch as expression
(let result
(switch code
(case 200 "OK")
(case 404 "Not Found")
(default "Error")))
// Switch with no default (implicit null for unmatched)
(switch x
(case 1 "one")
(case 2 "two"))
Optimized to chained ternaries when all cases are simple. Falls back to IIFE-wrapped JS switch when fallthrough or multiple statements exist.
// Flat val/result pairs
(case day
"monday" "Start of week"
"friday" "Almost weekend"
"Other day") // odd arg count = last is default
// Case as expression
(let message (case status
"ok" "Success"
"error" "Failed"
"Unknown"))
// Case with numbers
(case code
200 "OK"
404 "Not Found"
500 "Server Error"
"Unknown")
// Case in expression position
(+ (case x 1 10 2 20 0)
(case y 1 100 2 200 0))
// No default (even arg count = null for unmatched)
(case day
"monday" "Start of week"
"friday" "Almost weekend") // unmatched => null
Uses === for comparison. Always compiled to chained ternaries.
// Always an expression, never a statement
(? true "yes" "no") // => "yes"
(? false "yes" "no") // => "no"
// With expressions
(? (> 5 3) "greater" "lesser")
// Nested
(? (< x 0) "negative"
(? (== x 0) "zero"
(? (< x 10) "small" "large")))
// In expression position
(+ 10 (? true 5 3)) // => 15
// In let binding
(let result (? (> x 5) "big" "small"))
Requires exactly 3 arguments. Unlike if, always compiles to ConditionalExpression (never IfStatement).
// Execute when true
(when (> x 10)
(print "x is large")
(do-something))
// Returns nil if condition is false
(when false
(print "never prints")) // => nil
Macro-expands to (if test (do body...) nil).
// Execute when false
(unless (isEmpty list)
(print "list has items")
(process list))
// Returns nil if condition is true
(unless true
(print "never prints")) // => nil
Macro-expands to (if test nil (do body...)).
// Conditional binding - execute then-branch if binding is truthy
(if-let [user (find-user id)]
(greet user) // user is bound and truthy
(print "User not found")) // else branch
// Paren syntax also works
(if-let (result (compute))
(use result)
(handle-error))
Macro-expands to bind the variable and check truthiness. Both bracket [] and paren () binding syntax are supported. Alias: ifLet.
// Conditional binding (single branch)
(when-let [data (fetch-data)]
(process data)
(save data)) // Only if data is truthy
// Chained
(when-let [user (get-user)]
(when-let [email user.email]
(send-notification email)))
Macro-expands to bind the variable and check truthiness. Both bracket [] and paren () binding syntax are supported. Alias: whenLet.
Compilation:
(if condition then else)
// Compiles to (when branches are pure expressions):
condition ? then : else
// Compiles to (when branches contain control flow):
if (condition) { then } else { else }
Characteristics:
Compilation:
(cond
(test1 result1)
(test2 result2)
(else default))
// Macro-expands to nested ifs:
(if test1 result1 (if test2 result2 default))
Characteristics:
else is conventional (equivalent to (true ...))Compilation (simple cases):
(switch x (case 1 "one") (case 2 "two") (default "other"))
// Compiles to chained ternaries:
x === 1 ? "one" : x === 2 ? "two" : "other"
Compilation (with fallthrough):
(switch x
(case 1 :fallthrough)
(case 2 "one or two")
(default "other"))
// Compiles to IIFE-wrapped switch:
(() => { switch(x) { case 1: case 2: return "one or two"; default: return "other"; } })()
Compilation:
(case x 1 "one" 2 "two" "other")
// Always compiles to chained ternaries:
x === 1 ? "one" : x === 2 ? "two" : "other"
HQL Source
|
S-expression Parser
|
Macro Expansion (cond, when, unless, if-let, when-let)
|
Conditional Transformers (if, switch, case, ?, do, return, throw)
|
IR Nodes (ConditionalExpression, IfStatement, SwitchStatement)
|
ESTree AST
|
JavaScript
The do, return, and throw forms are also implemented in conditional.ts:
do - Sequence expression. Uses comma operator (SequenceExpression) when all children are pure expressions. Falls back to IIFE when statements or control flow are present. Handles async (await), generator (yield*), and early return (throw for non-local return inside IIFE).return - Return statement. Inside an IIFE context, generates a throw with a special early-return object (caught by IIFE wrapper). Otherwise, generates a normal ReturnStatement.throw - Throw statement. Requires exactly one argument.