HQL is a Lisp-shaped language that compiles to JavaScript and TypeScript. This reference describes the current pre-release syntax. The language authority is SPEC.md; this file expands that specification with examples.
This document is the expanded syntax reference; SPEC.md is the normative language SSOT. For cross-feature expression and return semantics, see expression-foundation.md. For the opt-in pure/IO discipline layer, see PURE-MODE.md.
HQL uses a Clojure-default reader with an explicit JS interop lane:
(...) is S-expression code and code-data. It uses spaces, never commas.[], {}, and #[] are data forms. Spaces choose HQL persistent data;
commas or JS property colons choose JS-native data.[] is the empty JS array. Use (vector) for an empty persistent vector.{} is the empty persistent map. Use (js/object) for an empty native JS
object.#[] is the empty persistent set. Use (js/set) for an empty native JS Set.(js/array ...), (js/object ...),
(js/set ...), and (js/map ...). HQL reader
|
+-----------+-----------+
| | |
(...) [...] {...} / #[...]
code data data
spaces separator chooses persistent vs JS-native data
|
+-- quote changes evaluation, not punctuation
'(a b c) is still parenthesized S-expression syntax
| Data shape | HQL persistent lane | JS-native lane | Empty form |
|---|---|---|---|
| Vector / Array | [1 2], [1], (vector 1 2) | [1, 2], [1,], (js/array 1 2) | (vector) vs [] |
| Map / Object | {}, {:name "Ada"}, {"name" "Ada"} | {name: "Ada"}, {"name": "Ada"}, (js/object "name" "Ada") | {} vs (js/object) |
| Set | #[], #[1 2], (hash-set 1 2) | #[1, 2], (js/set 1 2) | #[] vs (js/set) |
| Form | Rule |
|---|---|
(...) | Code form or quoted code-data list. Items are space-separated; commas are rejected. |
[1 2, 3], [1, 2 3] | Rejected: vector/array separators cannot mix. |
{:a 1, :b 2} | Rejected: persistent map literals do not use commas. |
#[1 2, 3], #[1, 2 3] | Rejected: set separators cannot mix. |
Use js/ forms when the value must be native JavaScript data.
(+ 1 2 3)
[1 2 3]
[1, 2, 3] ;; JS array
[] ;; JS array
[1] ;; persistent vector
(vector)
(js/array)
(js/array 1 2 3)
{:name "Ada"}
{"name" "Ada" "age" 37}
{"name": "Ada", "age": 37}
(js/object "name" "Ada" "age" 37)
#[1 2 3]
#[1, 2, 3] ;; JS Set
(js/set 1 2 3)
'(1 2 3)
(type Handler (value:number, opts:{mode:string}) => string)
Invalid source forms:
(+ 1, 2)
[1 2, 3]
{:name "Ada", :age 37}
#{1 2 3}
'(1, 2)
{name: n} remains valid in binding and match patterns, where it destructures
object properties. The same JS property-colon spelling is also the public JS
object value literal lane when used as an expression.
42
"hello"
true
false
nil
[1 2 3]
[1, 2, 3]
{}
{:name "Ada"}
{"name" "Ada"}
(js/array 1 2 3)
(js/object "name" "Ada" "age" 37)
(js/set 1 2 3)
(js/map "name" "Ada")
#[1 2 3]
#[1, 2, 3]
Objects are data only. They are not named parameters:
(fn inspect [value] value)
(inspect {:name "Ada"})
(inspect (js/object "name" "Ada"))
pr-str and read-string use the same delimiter rule for code-data strings:
(pr-str (quote (a b c))) ;; "(a b c)"
(pr-str (quote [a b c])) ;; "[a b c]"
(pr-str (quote {})) ;; "{}"
(pr-str (quote {:kind "x"})) ;; "{:kind \"x\"}"
(pr-str (quote #[1 2])) ;; "#[1 2]"
(read-string "(a b c)") ;; parenthesized code list
(read-string "[a b c]") ;; vector code data
(read-string "{}") ;; persistent map code data
(read-string "{:kind \"x\"}") ;; map code data
(let x 10)
(var count 0)
(let pi 3.14159)
(let [head ...tail] [1 2 3])
(let {name: n, age: a} (js/object "name" "Ada" "age" 37))
The named declaration shape is:
(fn name [param: Type = default other = value]
body)
defn is the Clojure-style named function declaration.
Examples:
(fn add [a b]
(+ a b))
(defn add2 [a b]
(+ a b))
(fn connect [host: string = "localhost" port: number = 8080]
(+ host ":" (.toString port)))
A function expression omits the name. => is the anonymous shorthand. Both use
the same [] parameter list:
(let double (fn [x] (* x 2)))
(let triple (=> [x] (* x 3)))
Calls can be positional:
(fn connect [host: string = "localhost" port: number = 8080]
(+ host ":" (.toString port)))
(connect "api.com" 443)
Or named, for known lexical HQL fn declarations:
(fn connect [host: string = "localhost" port: number = 8080]
(+ host ":" (.toString port)))
(connect port: 443 host: "api.com")
Named labels are rejected for unknown calls, imported/host calls, constructors, method calls, rest-parameter functions, and pattern-parameter functions. Positional and named arguments do not mix in one call.
Parameter items are structured list items:
name
name: Type
name = default
name: Type = default
...rest
...rest: Type
(class Counter
(var value:number)
(constructor [initial: number = 0]
(set! this.value initial))
(fn inc [by: number = 1]
(set! this.value (+ this.value by))
this.value))
(let c (new Counter 10))
(c.inc 5)
(let x 0)
(let state "open")
(let value 42)
(if (> x 0) "positive" "other")
(cond
((< x 0) "negative")
((=== x 0) "zero")
(:else "positive"))
(switch state
(case "open" 1)
(default 0))
(match value
(case 0 "zero")
(case 42 "the answer")
(case _ "other"))
do groups expressions and evaluates to its last expression:
(do
(print "step")
42)
Errors use try/catch with throw:
(try
(throw (js/Error "boom"))
(catch e "caught"))
Loop and iteration binding vectors use spaces:
(loop [i 0 total 0]
(if (< i 5)
(recur (+ i 1) (+ total i))
total))
(for [i 3]
(print i))
(for [i from: 0 to: 10 by: 2]
(print i))
(for-of [item [1 2 3]]
(print item))
For async iterables, use the same binding shape with for-await-of:
(for-await-of [event events]
(print event))
(type Id number)
(type Status "open" | "done")
(type Pair [number, string])
(type Payload {id:number, name:string})
(type Handler (value:number, opts:{mode:string}, ...flags:boolean[]) => string[])
(fn parse [input: string] -> number
(js/Number input))
(protocol Store<T> [
(prop value T)
(fn save [item:T label?:string] -> T)
(call [item:T] -> boolean)
])
(import [parse stringify] from "@hql/json")
(parse "{\"ok\":true}")
(stringify {ok: true})
(export [parse stringify])
Package metadata, dependency management, executable builds, and registry publishing are tooling workflows rather than HQL language syntax. Their one canonical user-facing contract is Dependencies And Publishing; command syntax is indexed in HQL CLI. Published libraries are ordinary ESM, so consumers do not need HQL.
(new js/Date)
(js/console.log "hello")
(let items [1, 2])
(.push items 3)
items.length
Interop calls are positional. Named labels are only for known HQL fn
declarations.
(macro when [condition ...body]
`(if ~condition
(do ~@body)
nil))
Macro templates operate on S-expression code-data, so quoted lists use spaces:
'(1 2 3)
Macro templates use quasiquote, unquote, and splice forms:
`(+ ~x 1)
Public macro parameters use the same [] parameter-list syntax as functions.
The macro expander may receive bracket forms as normalized (vector ...) lists
internally; that is an implementation detail, not public syntax.
Spaces separate top-level items in structured delimiters. Some items contain spaces or punctuation internally:
| Context | One Item | Example |
|---|---|---|
| Parameter | name: Type = default | [host: string = "localhost" port: number] |
| Rest parameter | ...name: Type | [label: string ...nums: Array<number>] |
| Import alias | name as alias | [identity as id pair as pair-of] |
| Export alias | name as alias | [read as load write as save] |
| Loop binding | from: start | [i from: 0 to: 10 by: 2] |
| Interface member | (fn name [...] -> T) | [(prop id string) (fn get [id:string] -> User)] |
HQL is pre-release, so legacy spellings are removed instead of kept as aliases.
{name: "Ada" age: 37} ;; use {name: "Ada", age: 37}
#{1 2 3} ;; use #[1 2 3]
#[1 2, 3] ;; do not mix persistent and JS set separators
(fn add (a b) ...) ;; use (fn add [a b] ...)
(fn greet {name: "world"} ...) ;; use [name: string = "world"]
(connect {host: "api.com"}) ;; one object argument; use (connect host: "api.com") for named call
(add a: 1, b: 2) ;; use (add a: 1 b: 2)
(fn sum [first & rest] ...) ;; use (fn sum [first ...rest] ...)
(constructor (x) ...) ;; use (constructor [x] ...)