Introduction
Calcit is a scripting language that combines the power of Clojure-like functional programming with modern tooling and hot code swapping.
An interpreter for Calcit snapshots with hot code swapping support.
Calcit is built with Rust and can also emit JavaScript in ES Modules syntax. It's inspired primarily by ClojureScript and designed for interactive development.
Key Features
- Immutable persistent data structures by default
- Structural editing with the Calcit Editor
- Hot code swapping for rapid development
- JavaScript interop with ES Modules support
- Indentation-based syntax alternative to parentheses
- MCP (Model Context Protocol) server for tool integration
Quick Start
You can try Calcit WASM build online for simple snippets, or see the Quick Reference for common commands and syntax.
Design Philosophy
Calcit experiments with several interesting ideas:
- Code is stored in data snapshot files, enabling structural editing
- Pattern matching of tagged unions (experimental)
- File-as-key/value model for MCP server integration, use Markdown docs
Most other features are inherited from ClojureScript. Calcit-js is commonly used for web development with Respo, a virtual DOM library migrated from ClojureScript.
For more details, see Overview and From Clojure.
Overview
- Immutable Data
Values and states are represented in different data structures, which is the semantics from functional programming. Internally it's im in Rust and a custom finger tree in JavaScript.
- Lisp(Code is Data)
Calcit-js was designed based on experiences from ClojureScript, with a bunch of builtin macros. It offers similar experiences to ClojureScript. So Calcit offers much power via macros, while keeping its core simple.
- Indentations
With bundle_calcit command, Calcit code can be written as an indentation-based language. So you don't have to match parentheses like in Clojure. It also means now you need to handle indentations very carefully.
- Hot code swapping
Calcit was built with hot swapping in mind. Combined with calcit-editor, it watches code changes by default, and re-runs program on updates. For calcit-js, it works with Vite and Webpack to reload, learning from Elm, ClojureScript and React.
- ES Modules Syntax
To leverage the power of modern browsers with help of Vite, we need another ClojureScript that emits import/export for Vite. Calcit-js does this! And this page is built with Calcit-js as well, open Console to find out more.
Features from Clojure
Calcit is mostly a ClojureScript dialect. So it should also be considered a Clojure dialect.
There are some significant features Calcit is learning from Clojure,
- Runtime persistent data by default, you can only simulate states with
Refs. - Namespaces
- Hygienic macros(although less powerful)
- Higher order functions
- Keywords, although Calcit changed the name to "tag" since
0.7 - Compiles to JavaScript, interops
- Hot code swapping while code modified, and trigger an
on-reloadfunction - HUD for JavaScript errors
Also there are some differences:
| Feature | Calcit | Clojure |
|---|---|---|
| Host Language | Rust, and use dylibs for extending | Java/Clojure, import Mavan packages |
| Syntax | Indentations / Syntax Tree Editor | Parentheses |
| Persistent data | unbalanced 2-3 Tree, with tricks from FingerTree | HAMT / RRB-tree |
| Package manager | git clone to a folder | Clojars |
| bundle js modules | ES Modules, with ESBuild/Vite | Google Closure Compiler / Webpack |
| operand order | at first | at last |
| Polymorphism | at runtime, slow .map ([] 1 2 3) f | at compile time, also supports multi-arities |
| REPL | only at command line: cr eval "+ 1 2" | a real REPL |
[] syntax | [] is a built-in function | builtin syntax |
{} syntax | {} (:a b) is macro, expands to &{} :a :b | builtin syntax |
also Calcit is a one-person language, it has too few features compared to Clojure.
Calcit shares many paradiams I learnt while using ClojureScript. But meanwhile it's designed to be more friendly with ES Modules ecosystem.
Indentation Syntax in the MCP Server
When using the MCP (Model Context Protocol) server, each documentation or code file is exposed as a key (the filename) with its content as the value. This means you can programmatically fetch, update, or analyze any file as a single value, making it easy for tools and agents to process Calcit code and documentation. Indentation-based syntax is preserved in the file content, so structure and meaning are maintained when accessed through the MCP server.
Indentation-based Syntax
Calcit was designed based on tools from Cirru Project, which means, it's suggested to be programming with Calcit Editor. It will emit a file compact.cirru containing data of the code. And the data is still written in Cirru EDN, Clojure EDN but based on Cirru Syntax.
For Cirru Syntax, read http://text.cirru.org/, and you may find a live demo at http://repo.cirru.org/parser.coffee/. A normal snippet looks like: this
defn fibo (x)
if (< x 2) 1
+ (fibo $ - x 1) (fibo $ - x 2)
But also, you can write in files and bundle compact.cirru with a command line bundle_calcit.
To run compact.cirru, internally it's doing steps:
- parse Cirru Syntax into vectors,
- turn Cirru vectors into Cirru EDN, which is a piece of data,
- build program data with quoted Calcit data(very similar to EDN, but got more data types),
- interpret program data.
Since Cirru itself is very generic lispy syntax, it may represent various semantics, both for code and for data.
Inside compact.cirru, code is like quoted data inside (quote ...) blocks:
{} (:package |app)
:configs $ {} (:init-fn |app.main/main!) (:reload-fn |app.main/reload!)
:entries $ {}
:prime $ {} (:init-fn |app.main/try-prime) (:reload-fn |app.main/try-prime)
:modules $ []
:files $ {}
|app.main $ {}
:ns $ %{} :CodeEntry (:doc |)
:code $ quote
ns app.main $ :require
:defs $ {}
|fibo $ %{} :CodeEntry (:doc |)
:code $ quote
defn fibo (x)
if (< x 2) (, 1)
+ (fibo $ - x 1) (fibo $ - x 2)
Notice that in Cirru |s prepresents a string "s", it's always trying to use prefixed syntax. "\"s" also means |s, and double quote marks existed for providing context of "character escaping".
MCP Tool
The tool parse_cirru_to_json can be used to parse Cirru syntax into JSON format, which is useful for understanding how Cirru syntax is structured.
You can generate Cirru from JSON using format_json_to_cirru vice versa.
More about Cirru
A review of Cirru in Chinese:
Cirru Syntax Essentials
1. Indentation = Nesting
Cirru uses 2-space indentation to represent nested structures:
defn add (a b)
&+ a b
Equivalent JSON:
["defn", "add", ["a", "b"], ["&+", "a", "b"]]
2. The $ Operator (Single-Child Expand)
$ creates a single nested expression on the same line:
; Without $: explicit nesting
let
x 1
println x
; With $: inline nesting
let (x 1)
println x
; Multiple $ chain right-to-left
println $ str $ &+ 1 2
; Equivalent to: (println (str (&+ 1 2)))
Rule: a $ b c → ["a", ["b", "c"]]
3. The | Prefix (String Literals)
| marks a string literal:
println |hello
println |hello-world
println "|hello world with spaces"
|hello→"hello"(string, not symbol)- Without
|:hellois a symbol/identifier - For strings with spaces:
"|hello world"
4. The , Operator (Expression Terminator)
, forces the end of current expression, starting a new sibling:
; Without comma - ambiguous
if true 1 2
; With comma - clear structure
if true
, 1
, 2
Useful in cond, case, let bindings:
cond
&< x 0
, |negative ; comma separates condition from result
(&= x 0) |zero
true |positive
5. Quasiquote, Unquote, Unquote-Splicing
For macros:
quasiquoteor backtick: template~(unquote): insert evaluated value~@(unquote-splicing): splice list contents
defmacro when-not (cond & body)
quasiquote $ if (not ~cond)
do ~@body
JSON equivalent:
[
"defmacro",
"when-not",
["cond", "&", "body"],
["quasiquote", ["if", ["not", "~cond"], ["do", "~@body"]]]
]
6. Common Patterns
Function Definition
defn function-name (arg1 arg2)
body-expression
Let Binding
let
x 1
y $ &+ x 2
&* x y
Conditional
if condition
then-branch
else-branch
Multi-branch Cond
cond
(test1) result1
(test2) result2
true default-result
JSON Format Rules
When using -j or --json-input:
- Everything is arrays or strings:
["defn", "name", ["args"], ["body"]] - Numbers as strings:
["&+", "1", "2"]not["&+", 1, 2] - Preserve prefixes:
"|string","~var","~@list" - No objects: JSON
{}cannot be converted to Cirru
Common Mistakes
| ❌ Wrong | ✅ Correct | Reason |
|---|---|---|
println hello | println \|hello | Missing \| for string |
$ a b c at line start | a b c | A line is an expression, no need of $ for extra nesting |
a$b | a $ b | Missing space around $ |
["&+", 1, 2] | ["&+", "1", "2"] | Numbers in syntax tree must be strings in JSON |
| Tabs for indent | 2 spaces | Cirru requires spaces |
Quick Reference
This page provides a quick overview of key Calcit concepts and commands for rapid lookup.
Installation & Setup
# Install Rust first
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install Calcit
cargo install calcit
# Test installation
cr eval "echo |done"
Core Commands
cr- Run Calcit program (default:compact.cirru)cr eval "code"- Evaluate code snippetcr js- Generate JavaScriptcr ir- Generate IR representationbundle_calcit- Bundle indentation syntax tocompact.cirrucaps- Download dependenciescr-mcp- Start MCP server for tool integration
Data Types
- Numbers:
1,3.14 - Strings:
|text,"|with spaces","\"escaped" - Tags:
:keyword(immutable strings, like Clojure keywords) - Lists:
[] 1 2 3 - HashMaps:
{} (:a 1) (:b 2) - HashSets:
#{} :a :b :c - Record:
%{} :Name (:key1 val1) (:key2 val2), similar to structs
Basic Syntax
; Function definition
defn add (a b)
+ a b
; Conditional
if (> x 0) |positive |negative
; Let binding
let
a 1
b 2
+ a b
; Thread macro
-> data
filter some-fn
map transform-fn
File Structure
calcit.cirru- Editor snapshot (source for structural editing)compact.cirru- Runtime format (compiled,crcommand actually uses this)deps.cirru- Dependencies.compact-inc.cirru- Hot reload trigger, including incremental changes
For detailed information, see the specific documentation files in the table of contents.
cargo install calcit
Installation
To install Calcit, you first need to install Rust. Then, you can install Calcit using Rust's package manager:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
After installing Rust, install Calcit with:
cargo install calcit
Once installed, Calcit is available as a command-line tool. You can test it with:
cr eval "echo |done"
Binaries
Several binaries are included:
cr: the main command-line tool for running Calcit programsbundle_calcit: bundles Calcit code into acompact.cirrufilecaps: downloads Calcit packagescr-mcp: provides a Model Context Protocol (MCP) server for Calcit compact filescr-sync: syncs changes fromcompact.cirruback tocalcit.cirru
Another important command is ct, which is the "Calcit Editor" and is available in a separate repository.
Modules directory
Packages are managed with caps command, which wraps git clone and git pull to manage modules.
Configurations inside calcit.cirru and compact.cirru:
:configs $ {}
:modules $ [] |memof/compact.cirru |lilac/
Paths defined in :modules field are just loaded as files from ~/.config/calcit/modules/, i.e. ~/.config/calcit/modules/memof/compact.cirru.
Modules that ends with /s are automatically suffixed compact.cirru since it's the default filename.
To load modules in CI environments, make use of caps --ci.
Rust bindings
API status: unstable.
Rust supports extending with dynamic libraries. A demo project can be found at https://github.com/calcit-lang/dylib-workflow
Currently two APIs are supported, based on Cirru EDN data.
First one is a synchronous Edn API with type signature:
#![allow(unused)] fn main() { #[no_mangle] pub fn demo(args: Vec<Edn>) -> Result<Edn, String> { } }
The other one is an asynchorous API, it can be called multiple times, which relies on Arc type(not sure if we can find a better solution yet),
#![allow(unused)] fn main() { #[no_mangle] pub fn demo( args: Vec<Edn>, handler: Arc<dyn Fn(Vec<Edn>) -> Result<Edn, String> + Send + Sync + 'static>, finish: Box<dyn FnOnce() + Send + Sync + 'static>, ) -> Result<Edn, String> { } }
in this snippet, the function handler is used as the callback, which could be called multiple times.
The function finish is used for indicating that the task has finished. It can be called once, or not being called.
Internally Calcit tracks with a counter to see if all asynchorous tasks are finished.
Process need to keep running when there are tasks running.
Asynchronous tasks are based on threads, which is currently decoupled from core features of Calcit. We may need techniques like tokio for better performance in the future, but current solution is quite naive yet.
Also to declare the ABI version, we need another function with specific name so that Calcit could check before actually calling it,
#![allow(unused)] fn main() { #[no_mangle] pub fn abi_version() -> String { String::from("0.0.9") } }
Call in Calcit
Rust code is compiled into dylibs, and then Calcit could call with:
&call-dylib-edn (get-dylib-path "\"/dylibs/libcalcit_std") "\"read_file" name
first argument is the file path to that dylib. And multiple arguments are supported:
&call-dylib-edn (get-dylib-path "\"/dylibs/libcalcit_std") "\"add_duration" (nth date 1) n k
calling a function is special, we need another function, with last argument being the callback function:
&call-dylib-edn-fn (get-dylib-path "\"/dylibs/libcalcit_std") "\"set_timeout" t cb
Notice that both functions call dylibs and then library instances are cached, for better consistency and performance, with some cost in memory occupation. Linux and MacOS has different strategies loading dylibs while loaded repeatedly, so Calcit just cached them and only load once.
Extensions
Currently there are some early extensions:
- Std - some collections of util functions
- WebSocket server binding
- Regex
- HTTP client binding
- HTTP server binding
- Wasmtime binding
- fswatch
GitHub Actions
To load Calcit 0.9.18 in a Ubuntu container:
- uses: calcit-lang/setup-cr@0.0.8
with:
version: "0.9.18"
Latest release could be found on https://github.com/calcit-lang/setup-cr/releases/ .
Then to load packages defined in deps.cirru with caps:
caps --ci
The JavaScript dependency lives in package.json:
"@calcit/procs": "^0.9.18"
Up to date example can be found on https://github.com/calcit-lang/respo-calcit-workflow/blob/main/.github/workflows/upload.yaml#L11 .
Running Calcit
Calcit can be run in several different modes.
Running a Program
To run a local compact.cirru file, simply use:
cr
This is equivalent to:
cr compact.cirru
By default, Calcit launches a watcher. If you want to run without the watcher, use:
cr -1
Eval Mode
To quickly evaluate a snippet of code:
cr eval 'println "|Hello world"'
Generating JavaScript
To generate JavaScript code:
cr js
To generate JavaScript only once (without the watcher):
cr js -1
Generating IR
To generate IR (Intermediate Representation):
cr ir
Run in Eval mode
use --eval or -e to eval code from CLI:
$ cr eval 'echo |demo'
1
took 0.07ms: nil
$ cr eval 'echo "|spaced string demo"'
spaced string demo
took 0.074ms: nil
You may also run multiple snippets:
=>> cr eval '
-> (range 10)
map $ fn (x)
* x x
'
calcit version: 0.5.25
took 0.199ms: ([] 0 1 4 9 16 25 36 49 64 81)
CLI Options
Usage: cr [<input>] [-1] [--disable-stack] [--skip-arity-check] [--emit-path <emit-path>] [--init-fn <init-fn>] [--reload-fn <reload-fn>] [--entry <entry>] [--reload-libs] [--watch-dir <watch-dir>] [<command>] [<args>]
Top-level command.
Positional Arguments:
input input source file, defaults to "compact.cirru"
Options:
-1, --once skip watching mode, just run once
--disable-stack disable stack trace for errors
--skip-arity-check
skip arity check in js codegen
--emit-path entry file path, defaults to "js-out/"
--init-fn specify `init_fn` which is main function
--reload-fn specify `reload_fn` which is called after hot reload
--entry specify with config entry
--reload-libs force reloading libs data during code reload
--watch-dir specify a path to watch assets changes
--help display usage information
Commands:
js emit JavaScript rather than interpreting
ir emit Cirru EDN representation of program to program-ir.cirru
eval run program
Load Dependencies
caps command is used for downloading dependencies declared in deps.cirru. The name "caps" stands for "Calcit Dependencies".
deps.cirru declares dependencies, which correspond to repositories on GitHub. Specify a branch or a tag:
{}
:calcit-version |0.9.18
:dependencies $ {}
|calcit-lang/memof |0.0.11
|calcit-lang/lilac |main
Run caps to download. Sources are downloaded into ~/.config/calcit/modules/. If a module contains build.sh, it will be executed mostly for compiling Rust dylibs.
To load modules, use :modules configuration in calcit.cirru and compact.cirru:
:configs $ {}
:modules $ [] |memof/compact.cirru |lilac/
Paths defined in :modules field are just loaded as files from ~/.config/calcit/modules/, i.e. ~/.config/calcit/modules/memof/compact.cirru.
Modules that ends with /s are automatically suffixed compact.cirru since it's the default filename.
Outdated
To check outdated modules, run:
caps outdated
CLI Options
caps --help
Usage: caps [<input>] [-v] [--pull-branch] [--ci] [--local-debug] [<command>] [<args>]
Top-level command.
Positional Arguments:
input input file
Options:
-v, --verbose verbose mode
--pull-branch pull branch in the repo
--ci CI mode loads shallow repo via HTTPS
--local-debug debug mode, clone to test-modules/
--help, help display usage information
Commands:
outdated show outdated versions
download download named packages with org/repo@branch
- "pull branch" to fetch update if only branch name is specified like
main. - "ci" does not support
git@protocol, onlyhttps://protocol.
Hot Swapping
Since there are two platforms for running Calcit, soutions for hot swapping are implemented differently.
Rust runtime
Hot swapping is built inside Rust runtime. When you specity :reload-fn in compact.cirru:
{}
:configs $ {}
:init-fn |app.main/main!
:reload-fn |app.main/reload!
the interpreter learns that the function reload! is to be re-run after hot swapping.
It relies on change event on .compact-inc.cirru for detecting code changes. .compact-inc.cirru contains informations about which namespace / which definition has changed, and interpreter will patch into internal state of the program. Program caches of current namespace will be replaced, in case that dependants also need changes. Data inside atoms are retained. Calcit encourages usages of mostly pure functions with a few atoms, programs can be safely replaced in many cases.
But also notice that if you have effects like events listening, you have to dispose and re-attach listeners in reload!.
JavaScript runtime
While Calcit-js is compiled to JavaScript beforing running, we need tools from JavaScript side for hot swapping, or HMR(hot module replacement). The tool I use most frequestly is Vite, with extra entry file of code:
import { main_$x_ } from "./js-out/app.main.mjs";
main_$x_();
if (import.meta.hot) {
import.meta.hot.accept("./js-out/app.main.mjs", (main) => {
main.reload_$x_();
});
}
There's also a js-out/calcit.build-errors.mjs file for hot swapping when compilation errors are detected. With this file, you can hook up you own HUD error alert with some extra code, hud! is the function for showing the alert:
ns app.main
:require
"\"./calcit.build-errors" :default build-errors
"\"bottom-tip" :default hud!
defn reload! () $ if (nil? build-errors)
do (remove-watch *reel :changes) (clear-cache!)
add-watch *reel :changes $ fn (reel prev) (render-app!)
reset! *reel $ refresh-reel @*reel schema/store updater
hud! "\"ok~" "\"Ok"
hud! "\"error" build-errors
One tricky thing to hot swap is macros. But you don't need to worry about that in newer versions.
Vite is for browsers. When you want to HMR in Node.js , Webpack provides some mechanism for that, you can refer to the boilerplate. However I'm not using this since Calcit-js switched to .mjs files. Node.js can run .mjs files without a bundler, it's huge gain in debugging. Plus I want to try more in Calcit-rs when possible since packages from Rust also got good qualitiy, and it's better to have hot swapping in Calcit Rust runtime.
Bundle Mode
Calcit programs are primarily designed to be written using the calcit-editor, a structural editor.
You can also try short code snippets in eval mode:
cr eval "+ 1 2"
# => 3
If you prefer to write Calcit code without the calcit-editor, that's possible too. See the example in minimal-calcit.
With the bundle_calcit command, Calcit code can be written using indentation-based syntax. This means you don't need to match parentheses as in Clojure, but you must pay close attention to indentation.
First, bundle your files into a compact.cirru file. Then, use the cr command to run it. A .compact-inc.cirru file will also be generated to enable hot code swapping. Simply launch these two watchers in parallel.
Entries
By default Calcit reads :init-fn and :reload-fn inside compact.cirru configs. You may also specify functions,
cr compact.cirru --init-fn='app.main/main!' --reload-fn='app.main/reload!'
and even configure :entries in compact.cirru:
cr compact.cirru --entry server
Here's an example, first lines of a compact.cirru file may look like:
{} (:package |app)
:configs $ {} (:init-fn |app.client/main!) (:reload-fn |app.client/reload!) (:version |0.0.1)
:modules $ [] |respo.calcit/ |lilac/ |recollect/ |memof/ |respo-ui.calcit/ |ws-edn.calcit/ |cumulo-util.calcit/ |respo-message.calcit/ |cumulo-reel.calcit/
:entries $ {}
:server $ {} (:init-fn |app.server/main!) (:port 6001) (:reload-fn |app.server/reload!) (:storage-key |calcit.cirru)
:modules $ [] |lilac/ |recollect/ |memof/ |ws-edn.calcit/ |cumulo-util.calcit/ |cumulo-reel.calcit/ |calcit-wss/ |calcit.std/
:files $ {}
There is base configs attached with :configs, with :init-fn :reload-fn defined, which is the inital entry of the program.
Then there is :entries with :server entry defined, which is another entry of the program. It has its own :init-fn :reload-fn and :modules options. And to invoke it, you may use --entry server option.
Data Types
Calcit provides several core data types, all immutable by default for functional programming:
Primitive Types
- Bool:
true,false - Number:
f64in Rust, Number in JavaScript (1,3.14,-42) - Tag: Immutable strings starting with
:(:keyword,:demo) - similar to Clojure keywords - String: Text data with special prefix syntax (
|text,"|with spaces")
Collection Types
- Vector: Ordered collection serving both List and Vector roles (
[] 1 2 3) - HashMap: Key-value pairs (
{} (:a 1) (:b 2)) - HashSet: Unordered unique elements (
#{} :a :b :c)
Function Types
- Function: User-defined functions and built-in procedures
- Proc: Internal procedure type for built-in functions
Implementation Details
- Rust runtime: Uses rpds for HashMap/HashSet and ternary-tree for vectors
- JavaScript runtime: Uses ternary-tree.ts for all collections
All data structures are persistent and immutable, following functional programming principles. For detailed information about specific types, see:
- String - String syntax and Tags
- Persistent Data - Implementation details
- EDN - Data notation format
String
The way strings are represented in Calcit is a bit unique. Strings are distinguished by a prefix. For example, |A represents the string A. If the string contains spaces, you need to enclose it in double quotes, such as "|A B", where | is the string prefix. Due to the history of the structural editor, " is also a string prefix, but it is special: when used inside a string, it must be escaped as "\"A". This is equivalent to |A and also to "|A". The outermost double quotes can be omitted when there is no ambiguity.
This somewhat unusual design exists because the structural editor naturally wraps strings in double quotes. When writing with indentation-based syntax, the outermost double quotes can be omitted for convenience.
Tag
The most commonly used string type in Calcit is the Tag, which starts with a :, such as :demo. Its type is Tag in Rust and string in JavaScript. Unlike regular strings, Tags are immutable, meaning their value cannot be changed once created. This allows them to be used as keys in key-value pairs and in other scenarios where immutable values are needed. In practice, Tags are generally used to represent property keys, similar to keywords in the Clojure language.
Persistent Data
Calcit uses rpds for HashMap and HashSet, and use Ternary Tree in Rust.
For Calcit-js, it's all based on ternary-tree.ts, which is my own library. This library is quite naive and you should not count on it for good performance.
Optimizations for vector in Rust
Although named "ternary tree", it's actually unbalanced 2-3 tree, with tricks learnt from finger tree for better performance on .push_right() and .pop_left().
- ternary-tree initial idea(old)
- Intro about optimization learnt from FingerTree(Chinese)
- internal tree layout from size 1~59
For example, this is the internal structure of vector (range 14):
when a element 14 is pushed at right, it's simply adding element at right, creating new path at a shallow branch, which means littler memory costs(compared to deeper branches):
and when another new element 15 is pushed at right, the new element is still placed at a shallow branch. Meanwhile the previous branch was pushed deeper into the middle branches of the tree:
so in this way, we made it cheaper in pushing new elements at right side. These steps could be repeated agained and again, new elements are always being handled at shallow branches.
This was the trick learnt from finger tree. The library Calcit using is not optimal, but should be fast enough for many cases of scripting.
Cirru Extensible Data Notation
Data notation based on Cirru. Learnt from Clojure EDN.
EDN data is designed to be transferred across networks are strings. 2 functions involved:
parse-cirru-ednformat-cirru-edn
although items of a HashSet nad fields of a HashMap has no guarantees, they are being formatted with an given order in order that its returns are reasonably stable.
Liternals
For literals, if written in text syntax, we need to add do to make sure it's a line:
do nil
for a number:
do 1
for a symbol:
do 's
there's also "keyword", which is called "tag" since Calcit 0.7:
do :k
String escaping
for a string:
do |demo
or wrap with double quotes to support special characters like spaces:
do "|demo string"
or use a single double quote for mark strings:
do "\"demo string"
\n \t \" \\ are supported.
Data structures:
for a list:
[] 1 2 3
or nested list inside list:
[] 1 2
[] 3 4
HashSet for unordered elements:
#{} :a :b :c
HashMap:
{}
:a 1
:b 2
also can be nested:
{}
:a 1
:c $ {}
:d 3
Also a record:
%{} :A
:a 1
Quotes
For quoted data, there's a special semantics for representing them, since that was neccessary for compact.cirru usage, where code lives inside a piece of data, marked as:
quote $ def a 1
at runtime, it's represented with tuples:
:: 'quote $ [] |def |a |1
which means you can eval:
$ cr eval "println $ format-cirru-edn $ :: 'quote $ [] |def |a |1"
quote $ def a 1
took 0.027ms: nil
and also:
$ cr eval 'parse-cirru-edn "|quote $ def a 1"'
took 0.011ms: (:: 'quote ([] |def |a |1))
This is not a generic solution, but tuple is a special data structure in Calcit and can be used for marking up different types of data.
Buffers
there's a special syntax for representing buffers in EDN using pairs of Hex digits as u8:
buf 03 55 77 ff 00
which corresponds to:
&buffer 0x03 0x55 0x77 0xff 0x00
Comments
Comment expressions are started with ;. They are evaluated into nothing, but not available anywhere, at least not available at head or inside a pair.
Some usages:
[] 1 2 3 (; comment) 4 (; comment)
{}
; comment
:a 1
Also notice that comments should also obey Cirru syntax. It's comments inside the syntax tree, rather than in parser.
Features
Calcit inherits most features from Clojure/ClojureScript while adding its own innovations:
Core Features
- Immutable persistent data structures - All data is immutable by default
- Functional programming - First-class functions, higher-order functions
- Lisp syntax - Code as data, powerful macro system
- Hot code swapping - Live code updates during development
- JavaScript interop - Seamless integration with JS ecosystem
Unique to Calcit
- Indentation-based syntax - Alternative to parentheses using
bundle_calcit - Structural editing - Visual tree-based code editing with Calcit Editor
- ES Modules output - Modern JavaScript module format
- MCP integration - Model Context Protocol server for tool integration
- Ternary tree collections - Custom persistent data structures
Language Features
For detailed information about specific features:
- List - Persistent vectors and operations
- HashMap - Key-value data structures
- Macros - Code generation and syntax extension
- JavaScript Interop - Calling JS from Calcit
- Imports - Module system and dependencies
- Polymorphism - Object-oriented programming simulation
Development Features
- Pattern matching - Tagged unions (experimental)
- Type checking - Runtime type validation
- Error handling - Stack traces and debugging tools
- Package management - Git-based dependency system with
caps
Calcit is designed to be familiar to Clojure developers while providing modern tooling and development experience.
List
Calcit List is persistent vector that wraps on ternary-Tree in Rust, which is 2-3 tree with optimization trick from fingertrees.
In JavaScript, it's ternary-tree in older version, but also with a extra CalcitSliceList for optimizing. CalcitSliceList is fast and cheap in append-only cases, but might be bad for GC in complicated cases.
But overall, it's slower since it's always immutable at API level.
Usage
Build a list:
[] 1 2 3
consume a list:
let
xs $ [] 1 2 3 4
xs2 $ append xs 5
xs3 $ conj xs 5 6
xs4 $ prepend xs 0
xs5 $ slice xs 1 2
xs6 $ take xs 3
println $ count xs
println $ nth xs 0
println $ get xs 0
println $ map xs $ fn (x) $ + x 1
&doseq (x xs) (println a)
thread macros are often used in transforming lists:
-> (range 10)
filter $ fn (x) $ > x 5
map $ fn (x) $ pow x 2
Why not just Vector from rpds?
Vector is fast operated at tail. In Clojure there are List and Vector serving 2 different usages. Calcit wants to use a unified structure to reduce brain overhead.
It is possible to extend foreign data types via FFI, but not made yet.
HashMap
In Rust implementation of Calcit it's using rpds::HashTrieMap. And in JavaScript, it's built on top of ternary-tree with some tricks for very small dicts.
Usage
{} is a macro, you can quickly write in pairs:
{}
:a 1
:b 2
internally it's turned into a native function calling arguments:
&{} :a 1 :b 2
let
dict $ {}
:a 1
:b 2
println $ to-pairs dict
println $ map-kv dict $ fn (k v)
[] k (inc v)
Macros
Like Clojure, Calcit uses macros to support new syntax. And macros ared evaluated during building to expand syntax tree. A defmacro block returns list and symbols, as well as literals:
defmacro noted (x0 & xs)
if (empty? xs) x0
last xs
A normal way to use macro is to use quasiquote paired with ~x and ~@xs to insert one or a span of items. Also notice that ~x is internally expanded to (~ x), so you can also use (~ x) and (~@ xs) as well:
defmacro if-not (condition true-branch ? false-branch)
quasiquote $ if ~condition ~false-branch ~true-branch
To create new variables inside macro definitions, use (gensym) or (gensym |name):
defmacro case (item default & patterns)
&let
v (gensym |v)
quasiquote
&let (~v ~item)
&case ~v ~default ~@patterns
Calcit was not designed to be identical to Clojure, so there are many details here and there.
Debug Macros
use macroexpand-all for debugging:
$ cr eval 'println $ format-to-cirru $ macroexpand-all $ quote $ let ((a 1) (b 2)) (+ a b)'
&let (a 1)
&let (b 2)
+ a b
format-to-cirru and format-to-lisp are 2 custom code formatters:
$ cr eval 'println $ format-to-lisp $ macroexpand-all $ quote $ let ((a 1) (b 2)) (+ a b)'
(&let (a 1) (&let (b 2) (+ a b)))
The syntax macroexpand only expand syntax tree once:
$ cr eval 'println $ format-to-cirru $ macroexpand $ quote $ let ((a 1) (b 2)) (+ a b)'
&let (a 1)
let
b 2
+ a b
JavaScript Interop
To access JavaScript global value:
do js/window.innerWidth
To access property of an object:
.-name obj
To call a method of an object, slightly different from Clojure:
.!setItem js/localStorage |key |value
To be noticed:
(.m a p1 p2)is calling an internal implementation of polymorphism in Calcit.
To construct an array:
let
a $ js-array 1 2
.!push a 3 4
, a
To construct an object:
js-object
:a 1
:b 2
To create new instance from a constructor:
new js/Date
Imports
Calcit loads namespaces from compact.cirru and modules from ~/.config/calcit/modules/. It's using 2 rules:
ns app.demo
:require
app.lib :as lib
app.lib :refer $ f1 f2
By using :as, it's loading a namespace as lib, then access a definition like lib/f1. By using :refer, it's importing the definition.
JavaScript imports
Imports for JavaScript is similar,
ns app.demo
:require
app.lib :as lib
app.lib :refer $ f1 f2
after it compiles, the namespace is eliminated, and ES Modules import syntax is generated:
import * as $calcit from "./calcit.core";
import * as $app_DOT_lib from "app.lib"; // also it will generate `$app_DOT_lib.f1` for `lib/f1`
import { f1, f2 } from "app.lib";
There's an extra :default rule for loading Module.default.
ns app.demo
:require
app.lib :as lib
app.lib :refer $ f1 f2
|chalk :default chalk
which generates:
// ...
import chalk from "chalk";
Polymorphism
Calcit uses tuples to simulate objects. Inherence not supported.
Core idea is inspired by JavaScript and also borrowed from a trick of Haskell since Haskell is simulating OOP with immutable data structures.
Terms
- "Tuple", the data structure of 2 or more items, written like
(:: a b). It's more "tagged union" in the case of Calcit. - "class", it's a concept between "JavaScript class" and "JavaScript prototype", it's using a record containing functions to represent the prototype of objects.
- "object", Calcit has no "OOP Objects", it's only tuples that simulating objects to support polymorphism. It's based on immutable data.
which makes "tuple" a really special data type Calcit.
Tuple has a structure of three parts:
%:: %class :tag p1 p2 p3
%classdefines the class, which is a hidden property, not counted in index:tagis a tag to identify the tuple by convention, index is0.- parameters, can be 0 or many arguments, index starts with
1. for example(:: :none)is an example of a tuple with 0 arguments, index0gets:none.
There was another shorthand for defining tuples, which internall uses an empty class:
:: :tag p1 p2 p3
Usage
Define a class:
defrecord! MyNum
:inc $ fn (self)
update self 1 inc
:show $ fn (self)
str $ &tuple:nth self 1
notice that self in this context is (%:: MyNum :my-num 1) rather than a bare liternal.
get an obejct and call method:
let
a $ %:: MyNum :my-num 1
println $ .show a
Not to be confused with JavaScript native method function which uses
.!method.
Use it with chaining:
-> (%:: MyNum :my-num 1)
.update
.show
println
In the runtime, a method call will try to check first element in the passed tuple and use it as the prototype, looking up the method name, and then really call it. It's roughly same behavoirs running in JavaScript except that JavaScript need to polyfill this with partial functions.
Built-in classes
Many of core data types inside Calcit are treated like "tagged unions" inside the runtime, with some class being initialized at program start:
&core-number-class
&core-string-class
&core-set-class
&core-list-class
&core-map-class
&core-record-class
&core-nil-class
&core-fn-class
that's why you can call (.fract 1.1) to run (&number:fract 1.1) since 1 is treated like (:: &core-number-class 1) when passing to method syntax.
The cost of this syntax is the code related are always initialized when Calcit run, even all of the method syntaxes not actually called.
Some old materials
- Dev log(中文) https://github.com/calcit-lang/calcit/discussions/44
- Dev log in video(中文) https://www.bilibili.com/video/BV1Ky4y137cv
Structural Editor
As demonstrated in Cirru Project, it's for higher goals of auto-layout code editor. Calcit Editor was incubated in Cirru.
Structural editing makes Calcit a lot different from existing languages, even unique among Lisps.
Calcit Editor uses a calcit.cirru as snapshot file, which contains much informations. And it is compiled into compact.cirru for evaluating.
Example of a compact.cirru file is more readable:
{} (:package |app)
:configs $ {} (:init-fn |app.main/main!) (:reload-fn |app.main/reload!)
:modules $ []
:files $ {}
|app.main $ %{} :FileEntry
:defs $ {}
|main! $ quote
defn main! () (+ 1 2)
|reload! $ quote
defn reload! ()
:ns $ quote
ns app.main $ :require

Also Hovenia Editor is another experiment rendering S-Expressions into Canvas.

Ecosystem
Libraries:
Useful libraries are maintained at https://libs.calcit-lang.org/base.cirru.
Frameworks:
- Respo: virtual DOM library
- Phlox: virtual DOM like wrapper on top of PIXI
- Quaterfoil: thin virtual DOM wrapper over three.js
- Triadica: toy project rendering interactive 3D shapes with math and shader
- tiny tool for drawing 3D shapes with WebGPU
- Cumulo: template for tiny realtime apps
- Quamolit: what if we make animations in React's way?