HQL Guide

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.

Run Your First Program

Create hello.hql:

(fn greet [name: string = "World"]
  (str "Hello, " name "!"))

(print (greet name: "Ada"))

Run it:

hql run hello.hql
# Hello, Ada!

For an interactive session, start HLVM and type HQL expressions:

hlvm

The Reader Rule

One rule explains most syntax: parentheses are code; brackets and braces are data; separators choose the data lane.

FormMeaning
(...)Code form or quoted code-data list
[1 2 3]HQL persistent vector
[1, 2, 3]JavaScript array
{:name "Ada"}HQL persistent map
{name: "Ada"}JavaScript object
#[1 2 3]HQL persistent set
#[1, 2, 3]JavaScript Set
(+ 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. Spaces vs commas matter only in bracketed data forms.

Data And Bindings

(let nums [1 2 3])              ; immutable binding
(var count 0)                   ; reassignable binding
(def answer 42)                 ; module-level definition

(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.

Functions And Types

(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 an alias for named fn.

Control Flow

(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")
  (default "other"))

HQL is expression-oriented, so control-flow forms can return values.

Loops

(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.

Modules

(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.

JavaScript Interop

(new js/Date)
(js-call 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.

Stdlib

(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.

Packages, Browser, Web, And Mobile

hql init ./my-lib --name @example/my-lib --version 0.1.0
hql check ./my-lib
hql pack ./my-lib --target all
hql new ./site --web --yes
hql dev ./site --dry-run
hql new ./app --mobile --yes
hql run ios ./app --dry-run

See CLI, Packages, and Browser, Web, And Mobile.

Common Mistakes

  • Writing commas in code forms: (+ 1, 2) is invalid. Use spaces.
  • Expecting [1] to be a JS array. It is a persistent vector; use [1,].
  • Using {name: "Ada"} when you wanted a persistent map. Use {:name "Ada"}.
  • Mixing positional and named arguments in one call.
  • Passing named labels to imported or host functions. Keep those positional.
  • Loading raw .hql files in a plain JS runtime without compiling first.

Next