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, built with Rust.
Calcit is primarily inspired by ClojureScript and designed for interactive development. It can run natively via the Rust interpreter or compile to JavaScript in ES Modules syntax for web development.
Key Features
- Immutable persistent data structures - All data is immutable by default using ternary tree implementations
- Structural editing - Visual tree-based code editing with Calcit Editor
- Hot code swapping - Live code updates during development without losing state
- JavaScript interop - Seamless integration with JS ecosystem and ES Modules
- Indentation-based syntax - Alternative to parentheses for cleaner code
- Static type analysis - Compile-time type checking and error detection
- MCP (Model Context Protocol) server - Tool integration for AI assistants
- Fast compilation - Rust-based interpreter with excellent performance
Quick Start
You can try Calcit WASM build online for simple snippets, or see the Quick Reference for common commands and syntax.
Install Calcit via Cargo:
cargo install calcit
cargo install calcit-bundler # For indentation syntax
cargo install caps-cli # For package management
Design Philosophy
Calcit experiments with several interesting ideas:
- Code as data - Code is stored in EDN snapshot files (
.cirru), enabling structural editing and powerful metaprogramming - Pattern matching - Tagged unions and enum types with compile-time validation
- Type inference - Static analysis without requiring extensive type annotations
- Incremental compilation - Hot reload with
.compact-inc.cirrufor fast iteration - Ternary tree collections - Custom persistent data structures optimized for performance
- File-as-key/value model - MCP server integration uses Markdown docs as knowledge base
Most other features are inherited from ClojureScript. Calcit-js is commonly used for web development with Respo, a virtual DOM library migrated from ClojureScript.
Use Cases
- Web development - Compile to JS and use with Respo or other frameworks
- Scripting - Fast native execution for CLI tools and automation
- Interactive development - REPL-driven development with hot code swapping
- Teaching - Clean syntax and structural editor for learning functional programming
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"]]]
]
LLM Guidance & Optimization
To ensure high-quality code generation for Calcit, follow these rules:
1. Mandatory | Prefix for Strings
LLMs often forget the | prefix. Always use | for string literals, even short ones.
- ❌
println "hello" - ✅
println |hello - ✅
println "|hello with spaces"
2. Functional let Binding
let bindings must be a list of pairs ((name value)). Single brackets (name value) are invalid.
- ❌
let (x 1) x - ✅
let ((x 1)) x - ✅ Preferred: Use multi-line for clarity:
let x 1 y 2 + x y
3. Arity Awareness
Calcit uses strict arity checking. Many core functions like +, -, *, / have native counterparts &+, &-, &*, &/ which are binaries (2 arguments). The standard versions are often variadic macros.
- Use
&+,&-, etc. in tight loops or when 2 args are guaranteed.
4. No Inline Types in Parameters
Calcit does not support Clojure-style (defn name [^Type arg] ...).
- ❌
defn add (a :number) ... - ✅ Use
assert-typeinside the body for parameters. - ✅ Return types can be specified with
hint-fnor a trailing label after parameters:
; Parameter check inside body
defn square (n)
assert-type n :number
&* n n
; Return type as trailing label
defn get-pi () :number
3.14159
; Mixed style
defn add (a b) :number
assert-type a :number
assert-type b :number
+ a b
5. $ and , Usage
- Use
$to avoid parentheses on the same line. - Use
,to separate multiline pairs incondorcaseif indentation alone feels ambiguous.
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
CLI Options
--once/-1- Run once without watching--disable-stack- Disable stack trace for errors--skip-arity-check- Skip arity check in JS codegen--emit-path <path>- Specify output path for JS (default:js-out/)--init-fn <fn>- Specify main function--reload-fn <fn>- Specify reload function for hot reloading--entry <entry>- Use config entry--reload-libs- Force reload libs data during hot reload--watch-dir <path>- Watch assets changes
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 - Tuples:
:: :tag 1 2- tagged unions with class support - Records:
%{} RecordName (:key1 val1) (:key2 val2), similar to structs - Structs:
defstruct Point (:x :number) (:y :number)- record type definitions - Enums:
defenum Result (:ok ..) (:err :string)- sum types - Refs/Atoms:
atom 0- mutable references - Buffers:
&buffer 0x01 0x02- binary data
Basic Syntax
; "Function definition (in file context)"
defn add (a b)
+ a b
; Conditional
if (> x 0) |positive |negative
; Let binding
let
a 1
b 2
+ a b
; Thread macro
-> (range 10)
filter $ fn (x) (> x 5)
map inc
Type Annotations
; Function with type annotations
defn add (a b)
hint-fn $ return-type :number
assert-type a :number
assert-type b :number
+ a b
; Optional type (nilable)
defn maybe-get (m k)
hint-fn $ return-type $ :: :optional :any
assert-type m :map
&map:get m k
; Variadic type
defn sum (& xs)
hint-fn $ return-type :number
assert-type xs $ :: :& :number
apply + xs
; Record definition
defrecord User :name :age :email
; Type assertion (compile-time check)
assert-type x :number
Built-in Types
:number,:string,:bool,:nil,:any:list,:map,:set,:record,:fn,:tuple:dynamic- wildcard type (default when no annotation)- Generic types (Cirru style):
:: :list :number
:: :map :string
:: :fn
[] :number
:string
Static Checks (Compile-time)
- Arity checking: Function call argument count validation
- Record field checking: Validates field names in record access
- Tuple index bounds: Ensures tuple indices are valid
- Enum tag matching: Validates tags in
&caseand&extract-case - Method validation: Checks method names and class types
- Recur arity: Validates recur argument count matches function params
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
Common Functions
Math
+,-,*,/- arithmetic (variadic)&+,&-,&*,&/- binary arithmeticinc,dec- increment/decrementpow,sqrt,round,floor,ceilsin,cos- trigonometric functions&max,&min- binary min/max&number:fract- fractional part&number:rem- remainder&number:format- format numberbit-shl,bit-shr,bit-and,bit-or,bit-xor,bit-not
List Operations
[]- create listappend,prepend- add elementsconcat- concatenate listsnth,first,rest,last- access elementscount,empty?- list propertiesslice- extract sublistreverse- reverse listsort,sort-by- sortingmap,filter,reduce- functional operationsfoldl,foldl-shortcut,foldr-shortcut- foldingrange- generate number rangetake,drop- slice operationsdistinct- remove duplicates&list:contains?,&list:includes?- membership tests
Map Operations
{}or&{}- create map&map:get- get value by key&map:assoc,&map:dissoc- add/remove entries&map:merge- merge maps&map:contains?,&map:includes?- key membershipkeys,vals- extract keys/valuesto-pairs,pairs-map- convert to/from pairs&map:filter,&map:filter-kv- filter entries&map:common-keys,&map:diff-keys- key operations
Set Operations
#{}- create setinclude,exclude- add/remove elementsunion,difference,intersection- set operations&set:includes?- membership test&set:to-list- convert to list
String Operations
str- concatenate to stringstr-spaced- join with spaces&str:concat- binary concatenationtrim,split,split-lines- string manipulationstarts-with?,ends-with?- prefix/suffix tests&str:slice- extract substring&str:replace- replace substring&str:find-index- find position&str:contains?,&str:includes?- substring tests&str:pad-left,&str:pad-right- paddingparse-float- parse number from stringget-char-code,char-from-code- character operations&str:escape- escape string
Tuple Operations
::- create tuple (shorthand)%::- create tuple with class&tuple:nth- access element by index&tuple:assoc- update element&tuple:count- get element count&tuple:class- get class&tuple:params- get parameters&tuple:enum- get enum tag&tuple:with-class- change class
Record Operations
new-record- create record instancedefrecord!- define record type with methods&%{}- low-level record constructor&record:get- get field value&record:assoc- set field value&record:with- update fields&record:class- get record class&record:matches?- type check&record:from-map- convert from map&record:to-map- convert to maprecord?- predicate
Struct & Enum Operations
defstruct- define struct typedefenum- define enum type&struct::new,&enum::new- create instancesstruct?,enum?- predicates&tuple:enum-has-variant?- check variant&tuple:enum-variant-arity- get variant aritytag-match- pattern matching on enums
Traits & Methods
deftrait- define a trait (method set + type signatures)defimpl- define an impl record for a trait:defimpl ImplName Trait ...impl-traits- attach impl records to a struct/enum definition (user impls: later impls override earlier ones for same method name).method- normal method dispatch&trait-call- explicit trait method call:&trait-call Trait :method receiver & args&methods-of- list runtime-available methods (strings including leading dot)&inspect-methods- print impl/method resolution to stderr, returns the value unchangedassert-traits- runtime check that a value implements a trait, returns the value unchanged
Ref/Atom Operations
atom- create atom&atom:dereforderef- read valuereset!- set valueswap!- update with functionadd-watch,remove-watch- observe changesref?- predicate
Type Predicates
nil?,some?- nil checksnumber?,string?,tag?,symbol?list?,map?,set?,tuple?record?,struct?,enum?,ref?fn?,macro?
Control Flow
if- conditionalwhen,when-not- single-branch conditionalscond- multi-way conditionalcase- pattern matching on values&case- internal case macrotag-match- enum/tuple pattern matchingrecord-match- record pattern matchinglist-match- list destructuring matchfield-match- map field matching
Threading Macros
->- thread first->>- thread last->%- thread with%placeholder%<-- reverse thread
Other Macros
let- local bindingsdefn- define functiondefmacro- define macrofn- anonymous functionquote,quasiquote- code as datamacroexpand,macroexpand-all- debug macrosassert,assert=- assertions&doseq- side-effect iterationfor- list comprehension
Meta Operations
type-of- get type tagturn-string,turn-symbol,turn-tag- type conversionidentical?- reference equalityrecur- tail recursiongenerate-id!- unique ID generationcpu-time- timing&get-os,&get-calcit-backend- environment info
EDN/Data Operations
parse-cirru-edn,format-cirru-edn- EDN serializationparse-cirru,format-cirru- Cirru syntax&data-to-code- convert data to codepr-str- print to string
Effects/IO
echo,println- outputread-file,write-file- file operationsget-env- environment variablesraise- throw errorquit!- exit program
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 command to evaluate code snippets from CLI:
$ cr eval 'echo |demo'
1
took 0.07ms: nil
$ cr eval 'echo "|spaced string demo"'
spaced string demo
took 0.074ms: nil
Multi-line Code
You can run multiple expressions:
cr eval '
-> (range 10)
map $ fn (x)
* x x
'
# Output: calcit version: 0.5.25
# took 0.199ms: ([] 0 1 4 9 16 25 36 49 64 81)
Working with Context Files
Eval can access definitions from a loaded program:
# Load from specific file and eval with its context
cr demos/compact.cirru eval 'range 3'
# Output: ([] 0 1 2)
# Use let bindings
cr demos/compact.cirru eval 'let ((x 1)) (+ x 2)'
# Output: 3
Type Checking in Eval
Type annotations and static checks work in eval mode:
# Type mismatch will cause error
cr demos/compact.cirru eval 'let ((x 1)) (assert-type x :string) x'
# Error: Type mismatch...
# Correct type passes
cr demos/compact.cirru eval 'let ((x 1)) (assert-type x :number) x'
# Output: 1
Common Patterns
Quick Calculations
cr eval '+ 1 2 3 4'
# Output: 10
cr eval 'apply * $ range 1 6'
# Output: 120 ; factorial of 5
Testing Expressions
cr eval '&list:nth ([] :a :b :c) 1'
# Output: :b
cr eval '&map:get ({} (:x 1) (:y 2)) :x'
# Output: 1
Exploring Functions
# Check function signature
cr eval 'type-of range'
# Output: :fn
# Test with sample data
cr eval '-> (range 5) (map inc) (filter (fn (x) (> x 2)))'
# Output: ([] 3 4 5)
Important Notes
Syntax Considerations
-
No extra brackets: Cirru syntax doesn't need outer parentheses at top level
- ✅
cr eval 'range 3' - ❌
cr eval '(range 3)'(adds extra nesting)
- ✅
-
Let bindings: Use paired list format
((name value))- ✅
let ((x 1)) x - ❌
let (x 1) x(triggers "expects pairs in list for let" error)
- ✅
Error Diagnostics
- Type warnings cause eval to fail (intentional safety feature)
- Check
.calcit-error.cirrufor complete stack traces - Use
cr cirru parse-onelinerto debug parse issues
Query Examples
Use cr query examples to see usage examples:
cr demos/compact.cirru query examples calcit.core/let
cr demos/compact.cirru query examples calcit.core/defn
CLI Options
Usage: cr [<input>] [-1] [--disable-stack] [--skip-arity-check] [--warn-dyn-method] [--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
--warn-dyn-method
warn on dynamic method dispatch and trait-attachment diagnostics
--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
Detailed Option Descriptions
Input File
# Run default compact.cirru
cr
# Run specific file
cr demos/compact.cirru
Run Once (--once / -1)
By default, cr watches for file changes and hot-reloads. Use --once to run once and exit:
cr --once
cr -1 # shorthand
Error Stack Trace (--disable-stack)
Disables detailed stack traces in error messages, useful for cleaner output:
cr --disable-stack
JS Codegen Options
--skip-arity-check: When generating JavaScript, skip arity checking (use cautiously):
cr js --skip-arity-check
--emit-path: Specify output directory for generated JavaScript:
cr js --emit-path dist/
Dynamic Method Warnings (--warn-dyn-method)
Warn when dynamic method dispatch cannot be specialized at preprocess time, and surface related trait-attachment diagnostics:
cr --warn-dyn-method
Hot Reloading Configuration
--init-fn: Override the main entry function:
cr --init-fn app.main/start!
--reload-fn: Specify function called after code reload:
cr --reload-fn app.main/on-reload!
--reload-libs: Force reload library data during hot reload (normally cached):
cr --reload-libs
Config Entry (--entry)
Use specific config entry from compact.cirru:
cr --entry test
cr --entry production
Asset Watching (--watch-dir)
Watch additional directories for changes (e.g., assets, styles):
cr --watch-dir assets/
cr --watch-dir styles/ --watch-dir images/
Common Usage Patterns
# Development with hot reload
cr --reload-fn app.main/reload!
# Production build
cr js --once --emit-path dist/
# Testing without file watching
cr --once --init-fn app.test/run-tests!
# Debug mode with full stack traces
cr --reload-libs
# CI/CD environment
cr --once --disable-stack
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 (in Calcit code, not EDN data):
; "Then create an instance in EDN"
%{} A
:a 1
Also define a record type with defrecord in Calcit code and use it in deserialization:
defrecord! A :a
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
Buffers can be created using the &buffer function with hex values:
&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 using ternary tree implementations
- Functional programming - First-class functions, higher-order functions, closures
- Lisp syntax - Code as data, powerful macro system with hygienic macros
- Hot code swapping - Live code updates during development without state loss
- JavaScript interop - Seamless integration with JS ecosystem via ES Modules
- Static type analysis - Compile-time type checking and error detection
Unique to Calcit
- Indentation-based syntax - Alternative to parentheses using
bundle_calcit, similar to Python/Haskell - Structural editing - Visual tree-based code editing with Calcit Editor (Electron app)
- ES Modules output - Modern JavaScript module format, tree-shakeable
- MCP integration - Model Context Protocol server for AI assistant tool integration
- Ternary tree collections - Custom persistent data structures optimized for Rust
- Incremental compilation - Fast hot reload with
.compact-inc.cirruformat - Pattern matching - Tagged unions with compile-time validation
- Record types - Lightweight structs with field access validation
- Traits & method dispatch - Attach capability-based methods to values, with explicit disambiguation when needed
Language Features
For detailed information about specific features:
- List - Persistent vectors and list operations
- HashMap - Key-value data structures and operations
- Macros - Code generation and syntax extension
- JavaScript Interop - Calling JS from Calcit and vice versa
- Imports - Module system and dependency management
- Polymorphism - Object-oriented programming patterns
- Traits - Capability-based method dispatch and explicit trait calls
- Static Analysis - Type checking and compile-time validation
Development Features
- Type inference - Automatic type inference from literals and expressions
- Compile-time checks - Arity checking, field validation, bounds checking
- Error handling - Rich stack traces and error messages with source locations
- Package management - Git-based dependency system with
capsCLI tool - Hot module replacement - Fast iteration with live code updates
- REPL integration - Interactive development with
cr evalmode - Bundle mode - Single-file deployment with
cr bundle
Type System
Calcit's static analysis provides:
- Function arity checking - Validates argument counts at compile time
- Record field validation - Checks field names exist in record types
- Tuple bounds checking - Validates tuple index access
- Enum variant validation - Ensures correct enum construction
- Method existence checking - Verifies methods exist for types
- Recur arity validation - Checks recursive calls have correct arguments
- Return type validation - Matches function return types with declarations
Performance
- Native execution - Rust interpreter for fast CLI tools and scripting
- Zero-cost abstractions - Persistent data structures with minimal overhead
- Lazy sequences - Efficient processing of large datasets
- Optimized compilation - JavaScript output with tree-shaking support
Calcit is designed to be familiar to Clojure developers while providing modern tooling, type safety, and excellent 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)
Sets
Calcit provides HashSet data structure for storing unordered unique elements. In Rust implementation, it uses rpds::HashTrieSet, while in JavaScript it uses a custom implementation based on ternary-tree.
Creating Sets
Use #{} to create a set:
#{} :a :b :c
#{} 1 2 3 4 5
Create an empty set:
#{}
Basic Operations
Adding and Removing Elements
; Add element
include (#{} :a :b) :c
; => #{:a :b :c}
; Remove element
exclude (#{} :a :b :c) :b
; => #{:a :c}
Checking Membership
&set:includes? (#{} :a :b :c) :a
; => true
&set:includes? (#{} :a :b :c) :x
; => false
Set Operations
; Union - elements in either set
union (#{} :a :b) (#{} :b :c)
; => #{:a :b :c}
; Difference - elements in first but not second
difference (#{} :a :b :c) (#{} :b :c :d})
; => #{:a}
; Intersection - elements in both sets
intersection (#{} :a :b :c) (#{} :b :c :d})
; => #{:b :c}
Converting Between Types
; Convert set to list
&set:to-list (#{} :a :b :c)
; => ([] :a :b :c) ; order may vary
; Convert list to set
&list:to-set ([] :a :b :b :c)
; => #{:a :b :c}
Set Properties
; Get element count
&set:count (#{} :a :b :c)
; => 3
; Check if empty
&set:empty? (#{})
; => true
Filtering
&set:filter (#{} 1 2 3 4 5)
fn (x) (> x 2)
; => #{3 4 5}
Pattern Matching with Sets
Use &set:destruct to destructure sets:
&set:destruct (#{} :a :b :c)
; Returns a list of elements
Common Use Cases
Removing Duplicates from a List
-> ([] :a :b :a :c :b)
&list:to-set
&set:to-list
; => ([] :a :b :c) ; order may vary
Checking for Unique Elements
= (&set:count (#{} :a :b :c))
count ([] :a :b :c)
; => true if all elements are unique
Set Membership in Algorithms
let
visited $ #{} :page1 :page2
if (&set:includes? visited :page3)
println "|Already visited"
println "|New page found"
Type Annotations
defn process-tags (tags)
hint-fn $ return-type :set
assert-type tags :set
&set:filter tags $ fn (t) (not= t :draft)
Performance Notes
- Set operations (union, intersection, difference) are efficient due to persistent data structure sharing
- Membership tests (
&set:includes?) are O(1) average case - Sets are immutable - all operations return new sets
Tuples
Tuples in Calcit are tagged unions that can hold multiple values with a tag. They are used for representing structured data and are the foundation for records and enums.
Creating Tuples
Shorthand Syntax
Use :: to create a tuple with a tag:
:: :point 10 20
:: :ok result
:: :err message
With Class Syntax
Use %:: to create a tuple with a class:
defrecord! Point :x :y
%:: Point :point 10 20
Tuple Structure
A tuple consists of:
- Tag: A keyword identifying the tuple type (index 0)
- Class: Optional class metadata (hidden)
- Parameters: Zero or more values (indices 1+)
; Simple tuple
(:: :point 10 20)
; Index 0: :point
; Index 1: 10
; Index 2: 20
Accessing Tuple Elements
let
t $ :: :point 10 20
&tuple:nth t 0
; => :point
&tuple:nth t 1
; => 10
&tuple:nth t 2
; => 20
Tuple Properties
; Get element count
&tuple:count (:: :a 1 2 3)
; => 4 (includes tag)
; Get class
&tuple:class t
; => returns class if set
; Get parameters (without tag)
&tuple:params t
; => ([] 10 20)
; Get enum tag
&tuple:enum t
; => enum value or nil
&tuple:enum is the source-prototype API for tuples:
- If tuple is created from enum (
%::), it returns that enum value. - If tuple is created as plain tuple (
::), it returnsnil.
let
plain $ :: :point 10 20
nil? $ &tuple:enum plain
; => true
let
ApiResult $ defenum ApiResult (:ok :number) (:err :string)
ok $ %:: ApiResult :ok 1
type-of $ &tuple:enum ok
; => :enum
assert= ApiResult $ &tuple:enum ok
Accurate Origin Check (Enum Eq)
let
ApiResult $ defenum ApiResult (:ok :number) (:err :string)
x $ %:: ApiResult :ok 1
assert= (&tuple:enum x) ApiResult
Complex Branching Example (Safe + Validation)
do
defenum Result
:ok :number
:err :string
let
xs $ []
%:: Result :ok 1
%:: Result :err |bad
:: :plain 42
if (nil? (&tuple:enum (&list:nth xs 2)))
if (= (&tuple:enum (&list:nth xs 0)) Result)
, |result-and-plain
, |result-missing
, |unexpected
Updating Tuples
; Update element at index
&tuple:assoc (:: :point 10 20) 1 100
; => (:: :point 100 20)
Changing Class
let
t $ :: :point 10 20
t2 $ &tuple:with-class t PointClass
; t2 now has PointClass as its class
Pattern Matching with Tuples
tag-match
Pattern match on enum/tuple tags:
defenum Result
:ok
:err :string
let
result $ %:: Result :ok 42
tag-match result
(:ok v) (println $ str |Success: v)
(:err msg) (println $ str |Error: msg)
_ (println |Unknown)
list-match
For simple list-like destructuring:
list-match (:: :point 10 20)
() (println |Empty)
(tag x y) (println tag x y)
Enums as Tuples
Enums are specialized tuples with predefined variants:
; Define enum
defenum Option
:some :dynamic
:none
; Create enum instances
%:: Option :some 42
%:: Option :none
; Check variant
&tuple:enum-has-variant? Option :some
; => true
; Get variant arity
&tuple:enum-variant-arity Option :some
; => 1
Common Use Cases
Result Types
defenum Result
:ok
:err :string
defn divide (a b)
if (= b 0)
%:: Result :err |Division by zero
%:: Result :ok (/ a b)
let
result $ divide 10 2
tag-match result
(:ok value) (println value)
(:err msg) (println msg)
Optional Values
defenum Option
:some :dynamic
:none
defn find-item (items target)
...
if found
%:: Option :some item
%:: Option :none
Tagged Data
; Represent different message types
:: :greeting |Hello
:: :number 42
:: :list ([] 1 2 3)
Type Annotations
defn process-result (r)
hint-fn $ return-type :string
assert-type r $ :: :tuple Result
tag-match r
(:ok v) (str v)
(:err msg) msg
Tuple vs Record
| Feature | Tuple | Record |
|---|---|---|
| Access | By index | By field name |
| Structure | Tag + params | Named fields |
| Methods | Via class | Via defrecord! |
| Use case | Tagged unions | Structured objects |
Performance Notes
- Tuples are immutable
- Element access is O(1)
&tuple:assoccreates a new tuple- Use records for complex objects with named fields
Records
Calcit provides Records as a way to define structured data types with named fields, similar to structs in other languages. Records are implemented using tuples internally and support polymorphism through methods.
Creating Records
Using new-record
Create a record type using the new-record function:
new-record :Point :x :y
Using %{} Macro
Create a record instance with the %{} macro:
let
Point $ new-record :Point :x :y
p $ %{} Point
:x 1
:y 2
, p
Record Operations
Accessing Fields
let
Point $ new-record :Point :x :y
p $ %{} Point
:x 1
:y 2
&record:get p :x
; => 1
Updating Fields
let
Point $ new-record :Point :x :y
p $ %{} Point
:x 1
:y 2
p2 $ record-with p
:x 10
; p2 is a new record with :x = 10, :y unchanged
Setting Single Field
&record:assoc p :x 100
; => new record with updated :x
Record with Methods
Records can have methods that operate on the record data:
defrecord! Rectangle
:width 0
:height 0
:area $ fn (self)
* (&record:get self :width)
(&record:get self :height)
:scale $ fn (self factor)
record-with self
:width $ * (&record:get self :width) factor
:height $ * (&record:get self :height) factor
let
rect $ %{} Rectangle
:width 10
:height 5
println $ .area rect
; => 50
Type Checking
; Check if value is a record
record? p
; => true
; Check record class
&record:class p
; => returns the record class
; Check if record matches a class
&record:matches? p Point
; => true
Converting Records
To Map
&record:to-map p
; => {} (:x 1) (:y 2)
From Map
&record:from-map Point $ {} (:x 1) (:y 2)
; => record with x=1, y=2
Pattern Matching
Use record-match to pattern match on record types:
let
Circle $ new-record :Circle :radius
Square $ new-record :Square :side
shape $ %{} Circle
:radius 5
record-match shape
Circle c $ * 3.14 (* (&record:get c :radius) (&record:get c :radius))
Square s $ * (&record:get s :side) (&record:get s :side)
_ _ nil
Record Class Operations
Getting Record Name
&record:get-name Point
; => :Point
Getting Source Struct (Optional)
Use &record:struct to inspect the struct prototype behind a record value.
let
Person $ defstruct Person
:name :string
p $ %{} Person
:name |Alice
&record:struct p
; => struct Person (or nil if metadata is unavailable)
Recommended guard:
let
Person $ defstruct Person
:name :string
p $ %{} Person (:name |Alice)
s $ &record:struct p
if (nil? s)
println |No struct metadata
println $ str |Struct: s
Accurate Origin Check (Struct Eq)
When you need to verify that a record was created from a specific struct, compare structs directly:
let
Cat $ defstruct Cat
:name :string
:color :tag
kitty $ %{} Cat
:name |Kitty
:color :red
assert= (&record:struct kitty) Cat
This is stronger than only comparing record names, because struct equality also checks field shape.
Complex Branching Example (Safe + Fallback)
let
Cat $ defstruct Cat
:name :string
:color :tag
Dog $ defstruct Dog
:name :string
v1 $ %{} Cat (:name |Mimi) (:color :white)
src $ &record:struct v1
if (nil? src)
println |Unknown record origin
if (= src Cat)
println |Handle Cat branch
if (= src Dog)
println |Handle Dog branch
println |Known record, but different struct
Extending Records
&record:extend-as p :Point3D
:z 0
Changing Class
&record:with-class p NewClass
Record vs Struct
- Records: Runtime data structures with fields and optional methods
- Structs: Type definitions used for compile-time type checking
let
Person $ defstruct Person
:name :string
:age :number
%{} Person
:name |Alice
:age 30
Common Use Cases
Configuration Objects
new-record :Config
:host
:port
:debug
:log-level
let
config $ %{} Config
:port 3000
println $ &record:get config :port
; => 3000
Domain Models
new-record :Product
:id
:name
:price
:discount
let
product $ %{} Product
:id |P001
:name |Widget
:price 100
:discount 0.9
println $ * (&record:get product :price) (&record:get product :discount)
; => 90
Type Annotations
let
User $ new-record :User
:name
:age
:email
defn get-user-name (user)
hint-fn $ return-type :string
assert-type user $ :: :record User
&record:get user :name
get-user-name $ %{} User
:name |John
:age 30
:email |john@example.com
Performance Notes
- Records are immutable - updates create new records
- Field access is O(1)
- Records share structure when possible
- Use
record-withfor multiple field updates to minimize copying
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
For macros that need multiple fresh symbols, use with-gensyms from calcit.core:
defmacro swap! (a b)
with-gensyms (tmp)
quasiquote
let ((~tmp ~a))
reset! ~a ~b
reset! ~b ~tmp
Calcit was not designed to be identical to Clojure, so there are many details here and there.
Macros and Static Analysis
Macros expand before type checking, so generated code is validated:
defmacro assert-positive (x)
quasiquote
if (< ~x 0)
raise "|Value must be positive"
~x
; After expansion, type checking applies to generated code
defn process (n)
assert-type n :number
assert-positive n ; Macro expands, then type-checked
Important: Macro-generated functions (like loop's f%) are automatically excluded from certain static checks (e.g., recur arity) to avoid false positives. Functions with %, $, or __ prefix are treated as compiler-generated.
Best Practices
- Use gensym for local variables: Prevents name collision
- Keep macros simple: Complex logic belongs in functions
- Document macro behavior: Include usage examples
- Test macro expansion: Use
macroexpand-allto verify output - Avoid side effects: Macros should only transform syntax
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)))
macroexpand, macroexpand-1, and macroexpand-all also print the expansion chain on stderr when nested macros are involved (for example m1 -> m2 -> m3). This is useful when a call site expands through helper macros before reaching final syntax.
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 models polymorphism with traits. Traits define method capabilities and can be attached to struct/enum definitions with impl-traits.
For capability-based dispatch via struct/enum-attached impls (used by records/tuples created from them), see Traits.
Historically, the idea was inspired by JavaScript, and also borrowed from a trick of Haskell (simulating OOP with immutable data structures). The current model is trait-based.
Key terms
- Trait: A named capability with method signatures (defined by
deftrait). - Trait impl: An impl record providing method implementations for a trait.
- impl-traits: Attaches one or more trait impl records to a struct/enum definition.
- assert-traits: Adds a compile-time hint and performs a runtime check that a value satisfies a trait.
Define a trait
deftrait Show
:show (:: :fn ('T) ('T) :string)
deftrait Eq
:eq? (:: :fn ('T) ('T 'T) :bool)
Traits are values and can be referenced like normal symbols.
Implement a trait for a struct/enum definition
deftrait MyFoo
:foo (:: :fn ('T) ('T) :string)
defimpl MyFooImpl MyFoo
:foo $ fn (p) (str "|foo " (:name p))
let
Person0 $ defstruct Person (:name :string)
Person $ impl-traits Person0 MyFooImpl
p $ %{} Person (:name |Alice)
println $ .foo p
impl-traits returns a new struct/enum definition with trait implementations attached. You can also attach multiple traits at once:
let
Person0 $ defstruct Person (:name :string)
Person $ impl-traits Person0 ShowImpl EqImpl MyFooImpl
p $ %{} Person (:name |Alice)
println $ .show p
println $ .foo p
Trait checks and type hints
assert-traits marks a local as having a trait and validates it at runtime:
let
p $ %{} Person (:name |Alice)
assert-traits p MyFoo
.foo p
If the trait is missing or required methods are not implemented, assert-traits raises an error.
Built-in traits
Core types provide built-in trait implementations (e.g. Show, Eq, Compare, Add, Len, Mappable). These are registered by the runtime, so values like numbers, strings, lists, maps, and records already satisfy common traits.
Notes
- There is no inheritance. Behavior sharing is done via traits and
impl-traits. - Method calls resolve through attached trait impls first, then built-in implementations.
- Use
assert-traitswhen a function relies on trait methods and you want early, clear failures.
Further reading
- Dev log(中文) https://github.com/calcit-lang/calcit/discussions/44
- Dev log in video(中文) https://www.bilibili.com/video/BV1Ky4y137cv
Traits
Calcit provides a lightweight trait system for attaching method implementations to struct/enum definitions (and using them from constructed instances and built-in types).
It complements the “class-like” polymorphism described in Polymorphism:
- Struct/enum classes are about “this concrete type has these methods”.
- Traits are about “this value supports this capability (set of methods)”.
Define a trait
Use deftrait to define a trait and its method signatures (including type annotations).
deftrait MyFoo
:foo (:: :fn ('T) ('T) :string)
Implement a trait
Use defimpl to create an impl record for a trait.
defimpl MyFooImpl MyFoo
:foo $ fn (p)
str-spaced |foo (:name p)
Impl-related syntax (cheatsheet)
1) defimpl argument order (breaking change)
defimpl ImplName Trait ...
- First argument is the impl record name.
- Second argument is the trait value (symbol) or a tag.
Examples:
defimpl MyFooImpl MyFoo
:foo $ fn (p) (str-spaced |foo (:name p))
defimpl :MyFooImpl :MyFoo
:foo $ fn (p) (str-spaced |foo (:name p))
2) Method pair forms
All of the following are accepted and equivalent:
defimpl MyFooImpl MyFoo
:foo (fn (p) ...)
:bar (fn (p) ...)
defimpl MyFooImpl MyFoo
:: :foo (fn (p) ...)
:: :bar (fn (p) ...)
3) Tag-based impl (no concrete trait value)
If you need a pure marker and don’t want to bind to a real trait value, use tags:
defimpl :MyMarkerImpl :MyMarker
:dummy nil
This is also a safe replacement for the old self-referential pattern
defimpl X X, which can cause recursion in new builds.
Implementation notes:
defimplcreates an “impl record” that stores the trait as its origin.- This origin is used by
&trait-callto match the correct implementation when method names overlap.
Attach impls to struct/enum definitions
impl-traits attaches impl records to a struct/enum type. For user values, later impls override earlier impls for the same method name ("last-wins").
Constraints:
impl-traitsonly accepts struct/enum values.- Record/tuple instances must be created from a struct/enum that already has impls attached (
%{}or%::).
Syntax:
impl-traits StructOrEnumDef ImplA ImplB
Public vs internal API boundary
- Prefer public API in app/library code:
deftrait,defimpl,impl-traits,.method,&trait-call. - Treat internal
&...helpers as runtime-level details; they may change more frequently and are not the stable user contract.
defstruct Person0
:name :string
def Person $ impl-traits Person0 MyFooImpl
let
p $ %{} Person (:name |Alice)
.foo p
deftrait ResultTrait
:describe :fn
defimpl ResultImpl ResultTrait
:describe $ fn (x)
tag-match x
(:ok v) (str |ok: v)
(:err v) (str |err: v)
defenum Result0
:ok :string
:err :string
def Result $ impl-traits Result0 ResultImpl
let
r $ %:: Result :ok |done
.describe r
Static analysis boundary
For preprocess to resolve impls and inline methods, keep struct/enum definitions and impl-traits at top-level ns/def. If they are created inside defn/defmacro bodies, preprocess only sees dynamic values and method dispatch cannot be specialized.
When running warn-dyn-method, preprocess emits extra diagnostics for:
.methodcall sites that have multiple trait candidates with the same method name.impl-traitsused inside function/macro bodies (non-top-level attachment).
Docs as tests
Key trait docs examples are mirrored by executable smoke cases in calcit/test-doc-smoke.cirru, including:
defimplargument order (ImplNamethenTrait)assert-traitslocal-first requirementimpl-traitsonly accepting struct/enum definitions
Method call vs explicit trait call
Normal method invocation uses .method dispatch. If multiple traits provide the same method name, .method resolves by impl precedence.
When you want to disambiguate (or bypass .method resolution), use &trait-call.
&trait-call
Usage:
&trait-call Trait :method receiver & args
&trait-call matches by the impl record's trait origin, not just by trait name text. This avoids accidental dispatch when two different trait values share the same printed name.
Example with two traits sharing the same method name:
deftrait MyZapA
:zap (:: :fn ('T) ('T) :string)
deftrait MyZapB
:zap (:: :fn ('T) ('T) :string)
defimpl MyZapAImpl MyZapA
:zap $ fn (_x) |zapA
defimpl MyZapBImpl MyZapB
:zap $ fn (_x) |zapB
defstruct Person0
:name :string
def Person $ impl-traits Person0 MyZapAImpl MyZapBImpl
let
p $ %{} Person (:name |Alice)
; `.zap` follows normal dispatch (last-wins for user impls)
.zap p
; explicitly pick a trait’s implementation
&trait-call MyZapA :zap p
&trait-call MyZapB :zap p
Debugging / introspection
Two helpers are useful when debugging trait + method dispatch:
&methods-ofreturns a list of available method names (strings, including the leading dot).&inspect-methodsprints impl records and methods to stderr, and returns the value unchanged.&impl:originreturns the trait origin stored on an impl record (or nil).
let
xs $ [] 1 2
&methods-of xs
&inspect-methods xs "|list"
You can also inspect impl origins directly when validating trait dispatch:
let
impls $ &tuple:impls some-tuple
any? impls $ fn (impl)
= (&impl:origin impl) MyFoo
Checking trait requirements
assert-traits checks at runtime that a value implements a trait (i.e. it provides all required methods). It returns the value unchanged if the check passes.
Notes:
assert-traitsis syntax (expanded to&assert-traits) and its first argument must be a local.- For built-in values (list/map/set/string/number/...),
assert-traitsonly validates default implementations. It does not extend methods at runtime. - Static analysis and runtime checks may diverge for built-ins due to limited compile-time information; this mismatch is currently allowed.
assert-traits p MyFoo
Examples (verified with cr eval)
cargo run --bin cr -- demos/compact.cirru eval 'let ((xs ([] 1 2 3))) (assert= xs (assert-traits xs calcit.core/Len)) (.len xs)'
Expected output:
3
cargo run --bin cr -- demos/compact.cirru eval 'let ((xs ([] 1 2 3))) (assert= xs (assert-traits xs calcit.core/Mappable)) (.map xs inc)'
Expected output:
([] 2 3 4)
Static Type Analysis
Calcit includes a built-in static type analysis system that performs compile-time checks to catch common errors before runtime. This system operates during the preprocessing phase and provides warnings for type mismatches and other potential issues.
Overview
The static analysis system provides:
- Type inference - Automatically infers types from literals and expressions
- Type annotations - Optional type hints for function parameters and return values
- Compile-time warnings - Catches errors before code execution
- Zero runtime overhead - All checks happen during preprocessing
Type Annotations
Function Parameter Types
Annotate function parameters using assert-type within the function body.
Runnable Example:
let
calculate-total $ fn (items)
assert-type items :list
reduce items 0
fn (acc item) (+ acc item)
calculate-total $ [] 1 2 3
Return Type Annotations
There are two ways to specify return types:
1. Formal Hint (hint-fn)
Use hint-fn with return-type at the start of the function body:
let
get-name $ fn (user)
hint-fn $ return-type :string
|demo
get-name nil
2. Compact Hint (Trailing Label)
For defn and fn, you can place a type label immediately after the parameters:
let
add $ fn (a b) :number
+ a b
add 10 20
Multiple Annotations
let
add $ fn (a b) :number
assert-type a :number
assert-type b :number
+ a b
add 1 2
Supported Types
The following type tags are supported:
| Tag | Calcit Type |
|---|---|
:nil | Nil |
:bool | Boolean |
:number | Number |
:string | String |
:symbol | Symbol |
:tag | Tag (Keyword) |
:list | List |
:map | Hash Map |
:set | Set |
:tuple | Tuple (general) |
:fn | Function |
:ref | Atom / Ref |
:any / :dynamic | Any type (wildcard) |
Complex Types
Optional Types
Represent values that can be nil. Use the :: :optional <type> syntax:
let
greet $ fn (name)
assert-type name $ :: :optional :string
str "|Hello " (or name "|Guest")
greet nil
Variadic Types
Represent variable arguments in & parameters:
let
sum $ fn (& xs)
assert-type xs $ :: :& :number
reduce xs 0 &+
sum 1 2 3
Record and Enum Types
Use the name defined by defrecord or defenum:
let
User $ new-record :User :name
get-name $ fn (u)
assert-type u User
:name u
get-name $ %{} User (:name |Alice)
Built-in Type Checks
Function Arity Checking
The system validates that function calls have the correct number of arguments:
defn greet (name age)
str "|Hello " name "|, you are " age
; Error: expects 2 args but got 1
; greet |Alice
Record Field Access
Validates that record fields exist:
defrecord User :name :age
defn get-user-email (user)
.-email user
; Warning: field 'email' not found in record User
; Available fields: name, age
Tuple Index Bounds
Checks tuple index access at compile time:
let
point (%:: :Point 10 20 30)
&tuple:nth point 5 ; Warning: index 5 out of bounds, tuple has 4 elements
Enum Variant Validation
Validates enum construction and pattern matching:
defenum Result
:Ok :any
:Error :string
; Warning: variant 'Failure' not found in enum Result
%:: Result :Failure "|something went wrong"
; Available variants: Ok, Error
; Warning: variant 'Ok' expects 1 payload but got 2
%:: Result :Ok 42 |extra
Method Call Validation
Checks that methods exist for the receiver type:
defn process-list (xs)
; .unknown-method xs
println "|demo code"
; "Warning: unknown method .unknown-method for :list"
; Available methods: .map, .filter, .count, ...
Recur Arity Checking
Validates that recur calls have the correct number of arguments:
defn factorial (n acc)
if (<= n 1) acc
recur (dec n) (* n acc)
; Warning: recur expects 2 args but got 3
; recur (dec n) (* n acc) 999
Note: Recur arity checking automatically skips:
- Functions with variadic parameters (
&rest args) - Functions with optional parameters (
?markers) - Macro-generated functions (e.g., from
loopmacro) calcit.corenamespace functions
Type Inference
The system infers types from various sources:
Literal Types
let
x 42 ; inferred as :number
y |hello ; inferred as :string
z true ; inferred as :bool
w nil ; inferred as :nil
println "|demo code"
Function Return Types
let
numbers (range 10) ; inferred as :list
first-num (&list:first numbers) ; inferred as :number
println "|demo code"
Record and Struct Types
defstruct Point :x :y
let
p (%:: Point :x 10 :y 20) ; inferred as Point record
x-val (.:x p) ; inferred from field type
println "|demo code"
Type Assertions
Use assert-type to explicitly check types during preprocessing:
defn process-data (data)
assert-type data :list
&list:map data transform-fn
Note: assert-type is evaluated during preprocessing and removed at runtime, so there's no performance penalty.
Type Inspection Tool
Use &inspect-type to debug type inference. Pass a symbol name and the inferred type is printed to stderr during preprocessing:
defn demo ()
let
x 10
nums $ [] 1 2 3
assert-type nums :list
&inspect-type x ; Prints: [&inspect-type] x => number type
&inspect-type nums ; Prints: [&inspect-type] nums => list type
let
first $ &list:nth nums 0
&inspect-type first ; Prints: [&inspect-type] first => dynamic type
assert-type first :number
&inspect-type first ; Prints: [&inspect-type] first => number type
Note: This is a development tool - remove it in production code. Returns nil at runtime.
Optional Types
Calcit supports optional type annotations for nullable values:
defn find-user (id)
hint-fn $ return-type $ :: :optional :record
; May return nil if user not found
println "|demo code"
Variadic Types
Functions with rest parameters use variadic type annotations:
defn sum (& numbers)
hint-fn $ return-type :number
assert-type numbers $ :: :& :number
reduce numbers 0 +
Function Types
Functions can be typed as :fn. You can also assert input types:
defn apply-twice (f x)
assert-type f :fn
assert-type x :number
f (f x)
Disabling Checks
Per-Function
Skip checks for specific functions by naming them with special markers:
- Functions with
%in the name (macro-generated) - Functions with
$in the name (special markers) - Functions starting with
__(internal functions)
Per-Namespace
Checks are automatically skipped for:
calcit.corenamespace (external library)- Functions with variadic or optional parameters (complex arity rules)
Best Practices
1. Use Type Annotations for Public APIs
defn public-api-function (input)
hint-fn $ return-type :string
assert-type input :map
process-input input
2. Leverage Type Inference
Let the system infer types from literals and function calls:
defn calculate-area (width height)
; Types inferred from arithmetic operations
* width height
3. Add Assertions for Critical Code
defn critical-operation (data)
assert-type data :list
; Ensure data is a list before processing
dangerous-operation data
4. Document Complex Types
; Function that takes a map with specific keys
defn process-user (user-map)
assert-type user-map :map
; Expected keys: :name :email :age
println "|demo code"
Limitations
- Dynamic Code: Type checks don't apply to dynamically generated code
- JavaScript Interop: JS function calls are not type-checked
- Macro Expansion: Some macros may generate code that bypasses checks
- Runtime Polymorphism: Type checks are conservative with polymorphic code
Error Messages
Type check warnings include:
- Location information: namespace, function, and code location
- Expected vs actual types: clear description of the mismatch
- Available options: list of valid fields/methods/variants
Example warning:
[Warn] Tuple index out of bounds: tuple has 3 element(s), but trying to access index 5, at my-app.core/process-point
Advanced Topics
Custom Type Predicates
While Calcit doesn't support custom type predicates in the static analysis system yet, you can use runtime checks:
defn is-positive? (n)
and (number? n) (> n 0)
Type-Driven Development
- Write function signatures with type annotations
- Let the compiler guide implementation
- Use warnings to catch edge cases
- Add assertions for invariants
Performance
Static type analysis:
- Runs during preprocessing phase
- Zero runtime overhead
- Only checks functions that are actually called
- Cached between hot reloads (incremental)
See Also
- Polymorphism - Object-oriented programming patterns
- Macros - Metaprogramming and code generation
- Data - Data types and structures
Common Patterns
This document provides practical examples and patterns for common programming tasks in Calcit.
Working with Collections
Filtering and Transforming Lists
; Filter even numbers and square them
-> (range 20)
filter $ fn (n)
= 0 $ &number:rem n 2
map $ fn (n)
* n n
; => ([] 0 4 16 36 64 100 144 196 256 324)
Grouping Data
let
group-by-length $ fn (words)
group-by words count
group-by-length ([] |apple |pear |banana |kiwi)
; => {}
; 4 $ [] |pear |kiwi
; 5 $ [] |apple
; 6 $ [] |banana
Finding Elements
let
result1 $ find ([] 1 2 3 4 5) $ fn (x) (> x 3)
result2 $ index-of ([] :a :b :c :d) :c
result3 $ any? ([] 1 2 3) $ fn (x) (> x 2)
result4 $ every? ([] 2 4 6) $ fn (x) (= 0 $ &number:rem x 2)
println result1
; => 4
println result2
; => 2
println result3
; => true
println result4
; => true
Error Handling
Using Result Type
let
Result $ defenum Result
:ok
:err :string
safe-divide $ fn (a b)
if (= b 0)
%:: Result :err |Division by zero
%:: Result :ok (/ a b)
handle-result $ fn (result)
tag-match result
(:ok v) (println $ str |Result: v)
(:err msg) (println $ str |Error: msg)
handle-result $ safe-divide 10 2
Using Option Type
let
Option $ defenum Option
:some :dynamic
:none
find-user $ fn (users id)
let
user $ find users $ fn (u)
= (&record:get u :id) id
if (nil? user)
%:: Option :none
%:: Option :some user
find-user
[] ({} (:id |001) (:name |Alice))
, |001
Working with Maps
Nested Map Operations
let
data $ {} (:a $ {} (:b $ {} (:c 1)))
result1 $ get-in data $ [] :a :b :c
result2 $ assoc-in data $ [] :a :b :c 100
result3 $ update-in data $ [] :a :b :c inc
println result1
; => 1
println result2
; => {} (:a $ {} (:b $ {} (:c 100)))
println result3
; => {} (:a $ {} (:b $ {} (:c 2)))
Merging Maps
let
result1 $ merge
{} (:a 1) (:b 2)
{} (:b 3) (:c 4)
{} (:d 5)
result2 $ &merge-non-nil
{} (:a 1) (:b nil)
{} (:b 2) (:c 3)
println result1
; => {} (:a 1) (:b 3) (:c 4) (:d 5)
println result2
; => {} (:a 1) (:b 2) (:c 3)
String Manipulation
String Syntax
Calcit has two ways to write strings:
|text- for strings without spaces (shorthand)"|text with spaces"- for strings with spaces (must use quotes)
let
s1 |HelloWorld
s2 |hello-world
s3 "|hello world"
s4 "|error in module"
println s1
; => |HelloWorld
println s2
; => |hello-world
println s3
; => "|hello world"
println s4
; => "|error in module"
Building Strings
let
result1 $ str |Hello | |World
result2 $ join-str ([] :a :b :c) |,
result3 $ str-spaced :error |in :module
println result1
; => |HelloWorld
println result2
; => |a,b,c
println result3
; => "|error in module"
Parsing Strings
let
result1 $ split |hello-world-test |-|
result2 $ split-lines |line1\nline2\nline3
result3 $ parse-float |3.14159
println result1
; => ([] |hello |world |test)
println result2
; => ([] |line1 |line2 |line3)
println result3
; => 3.14159
String Inspection
let
result1 $ starts-with? |hello-world |hello
result2 $ ends-with? |hello-world |world
result3 $ &str:find-index |hello-world |world
result4 $ &str:contains? |hello-world |llo
println result1
; => true
println result2
; => true
println result3
; => 6
println result4
; => true
State Management
Using Atoms
let
counter $ atom 0
println $ deref counter
; => 0
reset! counter 10
; => 10
swap! counter inc
; => 11
Managing Collections in State
let
todos $ atom ([])
add-todo! $ fn (text)
swap! todos $ fn (todos)
append todos $ {} (:id $ generate-id!) (:text text) (:done false)
toggle-todo! $ fn (id)
swap! todos $ fn (todos)
map todos $ fn (todo)
if (= (&record:get todo :id) id)
&record:with todo (:done $ not (&record:get todo :done))
, todo
remove-todo! $ fn (id)
swap! todos $ fn (todos)
filter todos $ fn (todo)
not= (&record:get todo :id) id
add-todo! |Buy milk
add-todo! |Write documentation
deref todos
Control Flow Patterns
Early Return Pattern
defn process-data (data)
if (empty? data)
:: :err |Empty data
let
validated $ validate-data data
if (nil? validated)
:: :err |Invalid data
let
result $ transform-data validated
:: :ok result
Pipeline Pattern
defn process-user-input (input)
-> input
trim
&str:slice 0 100 (; Truncate)
validate-input
parse-input
transform-to-command
Loop with Recur
; Factorial with loop/recur
defn factorial (n)
apply-args (1 n)
fn (acc n)
if (&<= n 1) acc
recur
* acc n
&- n 1
; Fibonacci with loop/recur
defn fibonacci (n)
apply-args (0 1 n)
fn (a b n)
if (&<= n 0) a
recur b (&+ a b) (&- n 1)
Working with Files
Reading and Writing
let
content $ read-file |data.txt
lines $ split-lines content
println content
&doseq (line lines)
process-line line
Math Operations
Common Calculations
let
round-to $ fn (n places)
let
factor $ pow 10 places
/ (round $ * n factor) factor
clamp $ fn (x min-val max-val)
-> x
&max min-val
&min max-val
average $ fn (numbers)
/ (apply + numbers) (count numbers)
println $ round-to 3.14159 2
; => 3.14
println $ clamp 15 0 10
; => 10
println $ average ([] 1 2 3 4 5)
; => 3
Debugging
Inspecting Values
let
data $ {} (:x 1) (:y 2)
result $ -> data
tap $ fn (x) (println |Step 1: x)
transform-1
tap $ fn (x) (println |Step 2: x)
transform-2
x 5
assert "|Should be positive" $ > x 0
assert= 4 (+ 2 2)
&display-stack
println result
Performance Tips
Lazy Evaluation
let
result $ foldl-shortcut
range 1000
, nil nil
fn (acc x)
if (> x 100)
:: true x
:: false nil
println result
Avoiding Intermediate Collections
let
items $ [] ({} (:value 1)) ({} (:value 2)) ({} (:value 3))
result1 $ reduce items 0 $ fn (acc item)
+ acc (&record:get item :value)
result2 $ apply +
map items $ fn (item)
&record:get item :value
println result1
; => 6
println result2
; => 6
Testing
Writing Tests
let
test-addition $ fn ()
assert= 4 (+ 2 2)
assert= 0 (+ 0 0)
assert= -5 (+ -2 -3)
test-with-setup $ fn ()
let
input $ {} (:name |test) (:value 42)
true
test-addition
Best Practices
- Use type annotations for function parameters and return values
- Prefer immutable data - use
swap!instead of manual mutation - Use pattern matching (
tag-match,record-match) for control flow - Leverage threading macros (
->,->>) for data pipelines - Use enums for result types instead of exceptions
- Keep functions small and focused on a single responsibility
Structural Editor
Deprecated: As Calcit shifts toward LLM-generated code workflows, command-line operations and type annotations have become more important. The structural editor approach is no longer recommended. Agent interfaces are preferred over direct user interaction.
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?
Tools:
- Calcit Editor - Structural editor for Calcit (Web-based)
- Calcit IR viewer - Visualize IR representation
- Calcit Error viewer - Enhanced error display
- Calcit Paint - Experimental 2D shape editor
- cr-mcp - MCP server for tool integration
- calcit-bundler - Bundle indentation syntax to compact format
- caps-cli - Dependency management tool
VS Code Integration:
- Calcit VS Code Extension - Syntax highlighting and snippets
- GitHub Copilot integration via cr-mcp server
Package Registry:
- libs.calcit-lang.org - Official package index and documentation