HQL is a Lisp-shaped language that compiles to JavaScript and TypeScript. It is designed to keep Lisp's small syntax while staying close to the JS ecosystem.
Create hello.hql:
(fn greet [name: string = "World"]
(str "Hello, " name "!"))
(print (greet name: "Ada"))
Run it:
hql hello.hql
# Hello, Ada!
For a one-shot interactive expression:
hql '(+ 1 2)'
run is available as an explicit spelling, but is optional for files and
expressions. A JavaScript or TypeScript entry may also import .hql modules and
run with the same command: hql main.ts.
For a persistent editor session, install an integration from
editors/.
One rule explains most syntax: parentheses are code; brackets and braces are data; separators choose the data lane.
| Data shape | HQL persistent lane | JS-native lane |
|---|---|---|
| Vector / Array | [1 2 3], [1], (vector) | [1, 2, 3], [1,], [], (js/array) |
| Map / Object | {}, {:name "Ada"}, {"name" "Ada"} | {name: "Ada"}, {"name": "Ada"}, (js/object) |
| Set | #[], #[1 2 3] | #[1, 2, 3], (js/set) |
| Code list | (...) uses spaces and rejects commas | n/a |
(+ 1 2 3) ;; code
[1 2 3] ;; persistent vector
[1, 2, 3] ;; JS array
{:name "Ada"} ;; persistent map
{name: "Ada"} ;; JS object
Commas are rejected in code forms. In data forms, spaces choose the persistent
HQL lane; commas or JS property colons choose the JS-native lane. Mixed
separators such as [1 2, 3] and #[1, 2 3] are rejected.
(let nums [1 2 3]) ;; immutable binding
(var count 0) ;; reassignable binding
(let answer 42) ;; module-level definition — same form
(let [head ...tail] [1 2 3]) ;; destructuring
(let {name: n, age: a} {name: "Ada", age: 37})
Use let for normal values. Use var only when the binding must be reassigned.
(fn add [a b]
(+ a b))
(fn connect [host: string = "localhost" port: number = 8080] -> string
(str host ":" port))
(connect)
(connect host: "api.com" port: 443)
Named arguments work for known lexical HQL function declarations. Unknown, imported, host, rest-parameter, and pattern-parameter calls stay positional.
Anonymous functions:
(let double (fn [x] (* x 2)))
(let triple (=> [x] (* x 3)))
defn is the Clojure-style named function declaration.
(let x 0)
(let ready? true)
(let value 42)
(if (> x 0) "positive" "other")
(cond
((< x 0) "negative")
((zero? x) "zero")
(:else "positive"))
(when ready?
(print "go"))
(match value
(case 0 "zero")
(case 42 "the answer")
(case _ "other"))
HQL is expression-oriented, so control-flow forms can return values.
(for [i 3]
(print i))
(for [i from: 0 to: 10 by: 2]
(print i))
(for-of [n [1 2 3]]
(print (* n 2)))
(loop [i 0 total 0]
(if (< i 5)
(recur (+ i 1) (+ total i))
total))
Use loop and recur when the loop state should stay explicit and
tail-recursive.
(import [parse stringify] from "@hql/json")
(parse "{\"ok\":true}")
(stringify {ok: true})
HQL imports JavaScript and TypeScript modules directly. Raw .hql source should
go through the HQL toolchain; plain JS runtimes consume compiled ESM output.
See Packages.
(new js/Date)
(js/call js/JSON "parse" "{\"ok\":true}")
(let items [1, 2])
(.push items 3)
items.length
Use comma-separated arrays and JS-object colons when a JavaScript API expects native JS data. Use persistent collections for HQL-first data.
See JavaScript Interop.
(take 3 (range 10))
(map inc [1 2 3])
(assoc {:name "Ada"} :role "engineer")
(str-join ", " ["a" "b" "c"])
The standard library is generated from source and includes runtime-verified examples.
hql publish ./my-lib --dry-run
hql new ./site --kind web
hql dev ./site --dry-run
hql new ./app --kind mobile
hql run ios ./app --dry-run
Package inference, hql.json, dependencies, and publishing are defined in
Packages. Application workflows are defined in
Browser, Web, And Mobile; use the CLI as the
command index.
(+ 1, 2) is invalid. Use spaces.[1] to be a JS array. It is a persistent vector; use [1,].{name: "Ada"} when you wanted a persistent map. Use {:name "Ada"}..hql files in a plain JS runtime without compiling first.