HiLisp is a tree-walking Lisp interpreter built entirely in hica, a language that compiles to Koka. It serves as a proof of concept that hica is expressive enough to implement a real language, including closures, recursion, and higher-order functions.
HiLisp is also designed to be embedded. Host applications import
hica-lisp/src/lisp and register a native callback via register_host_dispatch to
expose their own operations under the host/ prefix. This is how
hedit uses HiLisp as its plugin and configuration
language — user scripts call (host/set "tabsize" 4) or define key bindings, and
hedit handles the side effects.
Inspiration: Carp (special form vocabulary) and Jank (loop/recur tail-recursive iteration).
# Run the REPL
./hilisp
# Run a .hl file
./hilisp examples/recursion.hl
# Build from source (requires hica ≥ 0.49.2 and Koka 3.2.3)
hica build -o hilispNew to Lisp? Start with docs/lisp-primer.md.
HiLisp is dynamically typed. Values are numbers, booleans, strings, lists, functions, or nil. All code is an s-expression.
| Form | Description |
|---|---|
(def name val) |
Bind a value |
(defn name (p1 p2) body) |
Define a named function (supports recursion) |
(fn (p1 p2) body) |
Anonymous lambda |
(if cond then else) |
Conditional — all three branches required |
(cond p1 e1 p2 e2 …) |
Multi-branch; use true as final predicate for default |
(let (x 1 y 2) body) |
Local bindings |
(do e1 e2 …) |
Sequence; returns last value |
(quote x) / 'x |
Quote — return unevaluated |
(loop [x init …] body) |
Tail-recursive loop with named bindings |
(recur v1 v2 …) |
Restart the enclosing loop with new values — no stack growth |
+ - * / = < > <= >= not and or null? empty?
car cdr cons list length
println str
hash-map hash-get hash-set hash-del hash-has? hash-keys hash-vals hash?
symbol? symbol-name
Config-friendly key→value maps with string keys. Two constructors:
(def a (hash-map "tabsize" 4 "theme" "gruvbox"))
(def b {"tabsize" 4 "theme" "gruvbox"}) ; reader literal, desugars to (hash-map …)
(hash-get a "theme") ; → "gruvbox"
(hash-get a "missing" 99) ; → 99 (default when key absent)
(hash-set a "theme" "nord") ; → new map, original unchanged
(hash-keys a) ; → ("tabsize" "theme")
(= a b)
8000
; → true (order-independent)Maps nest freely:
(def config {"editor" {"tabsize" 4} "ui" {"theme" "gruvbox"}})
(hash-get (hash-get config "editor") "tabsize") ; → 4See examples/hashmap.hl for the full API.
String literals support the usual escapes: \n, \t, \r, \\, \".
Values round-trip through lval_show, so (str …) and (println (str …))
produce readable output regardless of embedded newlines or quotes.
'name evaluates to a symbol value — a distinct type from string. Use
symbol? to test and symbol-name to extract the name; equality is
structural on the name.
(symbol? 'save) ; → true
(symbol? "save") ; → false
(symbol-name 'save) ; → "save"
(= 'save 'save) ; → true
(= 'save "save") ; → falseThis makes command-dispatch tables idiomatic — a config file writes
(bind "Ctrl-s" 'save) and the host builtin can pattern-match on the symbol.
Runtime errors from builtins carry the source span of the calling form:
error[type/wrong-type]: hash-get expects (hash key) or (hash key default)
--> init.hl:3:2
|
3 | (hash-get)
| ^
HiLisp is designed to be embedded. Host applications (like
hedit) can register a single native
Hica callback that handles any builtin name starting with host/:
// In your embedder's Hica code
import "hica-lisp/src/lisp"
// A callback that dispatches on the operation name and can mutate
// application state via a shared handle
fun my_host_dispatch(name: string, args: list<LVal>, env: Env) : (LVal, Env) =>
match (name, args) {
("host/set", [LStr(k), v]) => (LNil, apply_my_set(k, v, env)),
("host/get", [LStr(k)]) => (lookup_my_get(k), env),
_ => (lerror("host/unknown", "unknown op: " + name), env)
}
let env = make_env()
let env2 = register_host_dispatch(env, my_host_dispatch)
// eval scripts against env2; (host/set …) / (host/get …) now route to your callback
In HiLisp source you can then write (host/set "tabsize" 4) directly, or
alias the ugly host/ prefix in a preamble the host injects:
(def set host/set)
(def get host/get)
(set "tabsize" 4)
(set "auto-indent" true)If nothing is registered, host/foo calls produce a source-located
error[host/not-registered] — never silent failure. See
src/builtins.hc for register_host_dispatch and the five regression
tests covering dispatch behaviour.
(defn factorial (n)
(if (<= n 1) 1 (* n (factorial (- n 1)))))
(println (factorial 10)) ; 3628800(defn make_adder (n) (fn (x) (+ x n)))
(def add10 (make_adder 10))
(println (add10 32)) ; 42(defn mod (a b) (- a (* b (/ a b))))
(defn fizzbuzz (n)
(cond
(= (mod n 15) 0) "fizzbuzz"
(= (mod n 3) 0) "fizz"
(= (mod n 5) 0) "buzz"
true n))
(loop [i 1]
(if (<= i 20)
(do (println (fizzbuzz i)) (recur (+ i 1)))
nil)); Sum 1..100 without recursion or stack growth
(def total
(loop [i 1 acc 0]
(if (> i 100)
acc
(recur (+ i 1) (+ acc i)))))
(println total) ; 5050The examples/ directory contains runnable .hl files:
| File | What it shows |
|---|---|
arithmetic.hl |
Basic operators, def, expressions |
functions.hl |
Higher-order functions, closures |
recursion.hl |
Factorial, Fibonacci, power |
lists.hl |
car/cdr/cons, map, filter |
closures.hl |
Closure factories, compose |
fizzbuzz.hl |
cond, loops, custom mod |
sorting.hl |
Merge sort and insertion sort |
statistics.hl |
Mean, median, variance, standard deviation |
higher_order.hl |
scan, windows, flat_map, partition, enumerate |
loop.hl |
loop/recur — countdown, accumulator sum, O(n) Fibonacci |
hashmap.hl |
Hash-map constructor, {k v …} reader literal, nested maps |
symbols.hl |
Quoted symbols vs strings, symbol?, symbol-name, dispatch tables |
src/
ast.hc - LVal and Env types
types.hc - env_get / env_set
tokeniser.hc - source text → token list
parser.hc - token list → LVal AST
display.hc - lval_show
builtins.hc - built-in functions + make_env()
eval.hc - tree-walking evaluator
lisp.hc - barrel re-export
main.hc - CLI, REPL, file runner
Load lib/prelude.hl as the first argument to get a full set of list, math, and
string utilities:
./hilisp lib/prelude.hl examples/sorting.hlSee docs/hilisp-hica-guide.md for the full function reference.
printlnuseslval_show, so strings display with quotes.- Recursive
defnfunctions withoutloop/recurwill stack-overflow on deep input — useloop/recurfor iteration.