Implementation: Macros (loop.hql) + Transpiler primitives (loop-recur.ts)
Coverage: Tested
HQL provides loop constructs for iteration and recursion:
loop/recur - Tail-call optimization (functional recursion) — transpiler primitivewhile - Condition-based loop — macro expanding to loop/recurrepeat - Execute n times — macro expanding to loop/recurfor - Range and collection iteration — macro expanding to for-offor-of - Collection iteration — transpiler primitivefor-await-of - Async iteration over async iterables — transpiler primitivebreak/continue - Loop control statements (optional label) — transpiler primitiveslabel - Named loops for multi-level control — transpiler primitiverange - Lazy number sequence generator — stdlib functionloop/recur is a transpiler primitive. It uses [] for bindings (Clojure-style).
// Basic loop/recur
(loop [binding init-value ...]
body
(recur new-value ...))
// Example: Sum 0 to 4
(loop [i 0 sum 0]
(if (< i 5)
(recur (+ i 1) (+ sum i))
sum))
// => 10 (0+1+2+3+4)
// Factorial
(loop [n 5 acc 1]
(if (<= n 1)
acc
(recur (- n 1) (* acc n))))
// => 120
// Fibonacci
(loop [n 7 a 0 b 1]
(if (=== n 0)
a
(recur (- n 1) b (+ a b))))
// => 13 (7th fibonacci)
// Zero bindings
(var counter 0)
(loop []
(if (< counter 3)
(do
(= counter (+ counter 1))
(recur))
counter))
// => 3
while is a macro that expands to loop/recur.
// Basic while
(while condition
body...)
// Example: Count to 5
(var count 0)
(var sum 0)
(while (< count 5)
(= sum (+ sum count))
(= count (+ count 1)))
sum
// => 10
// Early termination via condition
(var i 0)
(var found false)
(while (and (< i nums.length) (not found))
(if (isMatch (get nums i))
(= found true)
nil)
(= i (+ i 1)))
Macro expansion:
(while condition body...)
// expands to:
(loop []
(if condition
(do body... (recur))
nil))
repeat is a macro that expands to loop/recur.
// Basic repeat
(repeat count
body...)
// Example: 3 times
(var result [])
(repeat 3
(.push result "hello"))
result
// => ["hello", "hello", "hello"]
// With counter (manual)
(var sum 0)
(var counter 0)
(repeat 5
(= sum (+ sum counter))
(= counter (+ counter 1)))
sum
// => 10 (0+1+2+3+4)
Macro expansion:
(repeat count body...)
// expands to:
(loop [i 0]
(if (< i count)
(do body... (recur (+ i 1)))
nil))
The internal counter variable is NOT exposed to the body. If you need an iteration counter, manage one yourself.
for is a macro that expands to for-of with runtime helpers (__hql_range, __hql_toIterable).
// Single arg: 0 to n-1
(for [i 3]
(print i))
// Prints: 0, 1, 2
// Two args: start to end-1
(for [i 5 8]
(print i))
// Prints: 5, 6, 7
// Three args: start to end-1 by step
(for [i 0 10 2]
(print i))
// Prints: 0, 2, 4, 6, 8
// Named syntax: to:
(for [i to: 3]
(print i))
// Prints: 0, 1, 2
// Named syntax: from: to:
(for [i from: 5 to: 8]
(print i))
// Prints: 5, 6, 7
// Named syntax: from: to: by:
(for [i from: 0 to: 10 by: 2]
(print i))
// Prints: 0, 2, 4, 6, 8
// Named syntax: to: by: (without from:)
(for [i to: 10 by: 3]
(print i))
// Prints: 0, 3, 6, 9
// Collection iteration
(for [x [1, 2, 3]]
(print (* x 2)))
// Prints: 2, 4, 6
for-of is a transpiler primitive. It returns null (expression-everywhere semantics, like Clojure's doseq).
(for-of [x [1 2 3]]
(print x))
// Prints: 1, 2, 3
// Returns: null
// Can be used in expression position
(let result (for-of [x [1 2 3]] (print x)))
// result => null
for-await-of is a transpiler primitive for iterating over async iterables.
// Iterate over async iterables
(for-await-of [item asyncIterable]
body...)
// With async generator
(async fn* fetch-pages []
(yield (await (fetch "/page1")))
(yield (await (fetch "/page2"))))
(async fn process []
(for-await-of [page (fetch-pages)]
(print page.data)))
break is a transpiler primitive. It exits the innermost loop. Supports optional label.
// Exit loop early
(for [i 10]
(when (=== i 5)
(break))
(print i))
// Prints: 0, 1, 2, 3, 4
// Break in while loop
(var i 0)
(while true
(if (>= i 10)
(break)
(do
(print i)
(= i (+ i 1)))))
// Break with label (multi-level)
(label outer
(for [i 3]
(for [j 3]
(when (and (=== i 1) (=== j 1))
(break outer))
(print i j))))
// Prints: 0 0, 0 1, 0 2, 1 0
continue is a transpiler primitive. It skips to the next iteration. Supports optional label.
// Skip iteration
(for [i 10]
(when (=== (% i 2) 0)
(continue))
(print i))
// Prints: 1, 3, 5, 7, 9
// Continue with label
(label outer
(for-of [x [1 2 3 4 5]]
(if (=== x 3)
(continue outer)
(results.push x))))
// results => [1, 2, 4, 5]
label is a transpiler primitive. It names a loop so break/continue can target it.
// Label syntax
(label name
body...)
// Nested loops with labels
(label outer
(for-of [x [1 2 3]]
(for-of [y ["a" "b" "c"]]
(if (and (=== x 2) (=== y "b"))
(break outer)
(results.push (str x y))))))
// results => ["1a", "1b", "1c", "2a"]
// Labeled for-of inside do block
(label outer
(do
(results.push "start")
(for-of [x [1 2 3]]
(if (=== x 2) (break outer))
(results.push x))))
// results => ["start", 1]
range is a stdlib function that generates lazy sequences of numbers.
// Basic range (0 to n-1)
(range 5) // => lazy seq of 0, 1, 2, 3, 4
// Range with start and end
(range 1 6) // => lazy seq of 1, 2, 3, 4, 5
// Range with step
(range 0 10 2) // => lazy seq of 0, 2, 4, 6, 8
// Negative step (countdown)
(range 10 0 -1) // => lazy seq of 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
// Use with for loop
(for [i (range 5)]
(print i)) // Prints: 0, 1, 2, 3, 4
// Use with doall to materialize
(let nums (doall (range 5))) // => [0, 1, 2, 3, 4]
Simple loops (single if body with recur in one branch) are optimized to native while loops. Complex loops use a recursive function with an IIFE wrapper.
Simple loop optimization:
(loop [i 0 sum 0]
(if (< i 100)
(recur (+ i 1) (+ sum i))
sum))
// Compiles to (simplified):
((__init_0, __init_1) => {
let i = __init_0;
let sum = __init_1;
while (i < 100) {
const __hql_temp_sum = sum + i;
i++;
sum = __hql_temp_sum;
}
return sum;
})(0, 0)
Complex loop (recursive function):
(loop [i 0]
(when (< i 3)
(print i)
(recur (+ i 1))))
// Compiles to (simplified):
((__init_0) => {
function loop_0(i) {
if (i < 3) {
console.log(i);
return loop_0(i + 1);
}
}
return loop_0(__init_0);
})(0)
Arithmetic updates are optimized: (+ i 1) becomes i++, (- i 1) becomes i--, (+ i n) becomes i += n (when safe).
Macro that expands to loop [] (if condition (do body (recur)) nil). The loop/recur then gets optimized to a native while loop.
Macro that expands to loop [i 0] (if (< i count) (do body (recur (+ i 1))) nil). The internal counter variable is not exposed to the body.
Macro that dispatches based on binding spec:
(for [i 3]): expands to (for-of [i (__hql_toIterable 3)]) — numbers become range(0, n)(for [i 5 8]): expands to (for-of [i (__hql_range 5 8)])(for [i 0 10 2]): expands to (for-of [i (__hql_range 0 10 2)])(for [i from: 0 to: 10 by: 2]): expands to (for-of [i (__hql_range 0 10 2)])(for [x coll]): same as 1-arg, __hql_toIterable passes iterables throughTranspiler primitive. Generates for (const x of collection) { ... } wrapped in an IIFE that returns null (expression-everywhere semantics).
When the body contains return statements, the IIFE wrapper is omitted so return escapes to the outer function. When used with labeled break/continue, the label's transform handles the IIFE wrapping.
Same as for-of but generates for await (const x of collection) { ... } with an async IIFE wrapper.
Transpiler primitives. Generate break; / continue; or break label; / continue label;.
break and continue require being inside a loop context (enforced at compile time).
Transpiler primitive. Generates label: statement. When a for-of inside the label targets it with break/continue, the label wraps everything in an IIFE so the label works correctly with the for-of IIFE.
// Loop/recur (tail-call optimized, constant stack space)
(loop [i 0 sum 0]
(if (< i 10000)
(recur (+ i 1) (+ sum i))
sum))
(for [i from: 0 to: 10]
(print i))
(while (and (not done) (< attempts maxAttempts))
(processItem))
(repeat 5
(print "Hello"))
HQL's loop constructs: