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.cirru for 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 the cr command, Calcit code can be written as an indentation-based language directly. 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-reload function
  • HUD for JavaScript errors

Also there are some differences:

FeatureCalcitClojure
Host LanguageRust, and use dylibs for extendingJava/Clojure, import Mavan packages
SyntaxIndentations / Syntax Tree EditorParentheses
Persistent dataunbalanced 2-3 Tree, with tricks from FingerTreeHAMT / RRB-tree
Package managergit clone to a folderClojars
bundle js modulesES Modules, with ESBuild/ViteGoogle Closure Compiler / Webpack
operand orderat firstat last
Polymorphismat runtime, slow .map ([] 1 2 3) fat compile time, also supports multi-arities
REPLonly at command line: cr eval "+ 1 2"a real REPL
[] syntax[] is a built-in functionbuiltin syntax
{} syntax{} (:a b) is macro, expands to &{} :a :bbuiltin 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:

  1. parse Cirru Syntax into vectors,
  2. turn Cirru vectors into Cirru EDN, which is a piece of data,
  3. build program data with quoted Calcit data(very similar to EDN, but got more data types),
  4. 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:

do
  ; Without $: explicit nesting
  let
      x 1
    str x
  ; Multiple $ chain right-to-left
  str $ &+ 1 2
  ; Equivalent to: (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 |: hello is 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:

let
    x (- 0 3)
  ; cond tests conditions in sequence, returning first matching result
  cond
    (&< x 0) |negative
    (&= x 0) |zero
    true |positive

5. Quasiquote, Unquote, Unquote-Splicing

For macros:

  • quasiquote or 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-type inside the body for parameters.
  • ✅ Return types can be specified with hint-fn or a trailing label after parameters:
let
    ; Parameter check inside body
    square $ defn square (n)
  hint-fn $ {} (:args ([] :number)) (:return :number)
      assert-type n :number
      &* n n
    ; Return type as trailing label
    get-pi $ defn get-pi () :number
      , 3.14159
    ; Mixed style
    add $ defn add (a b) :number
      assert-type a :number
      assert-type b :number
      + a b
  [] (square 5) (get-pi) (add 3 4)

5. $ and , Usage

  • Use $ to avoid parentheses on the same line.
  • Use , to separate multiline pairs in cond or case if 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:

  1. Everything is arrays or strings: ["defn", "name", ["args"], ["body"]]
  2. Numbers as strings: ["&+", "1", "2"] not ["&+", 1, 2]
  3. Preserve prefixes: "|string", "~var", "~@list"
  4. No objects: JSON {} cannot be converted to Cirru

Common Mistakes

❌ Wrong✅ CorrectReason
println helloprintln \|helloMissing \| for string
$ a b c at line starta b cA line is an expression, no need of $ for extra nesting
a$ba $ bMissing space around $
["&+", 1, 2]["&+", "1", "2"]Numbers in syntax tree must be strings in JSON
Tabs for indent2 spacesCirru 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 snippet
  • cr js - Generate JavaScript
  • cr ir - Generate IR representation
  • cr query ... - Query definitions/usages/search
  • cr docs ... - Read/search guidebook docs
  • cr libs ... - Search/read library docs
  • cr-mcp - Start MCP server for tool integration

CLI Options

  • --watch / -w - Watch files and rerun/rebuild on changes
  • --once / -1 - Run once (compatibility flag; default is already once)
  • --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

Markdown Checking

  • See CLI Options for check-md usage and mode guidance.

Docs Navigation (Fast)

  • cr docs list - list available chapters
  • cr docs read <file> - list headings in one chapter
  • cr docs read <file> <keyword...> - fuzzy jump by heading keywords
  • cr docs read-lines <file> -s <start> -n <lines> - precise line-range reading
  • cr docs search <keyword> - global keyword search

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
defn add (a b)
  + a b
; Conditional
let ((x 1))
  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

let
    ; Function with type annotations
    add $ defn add (a b)
      hint-fn $ {} (:args ([] :number :number)) (:return :number)
      assert-type a :number
      assert-type b :number
      + a b
    ; Variadic type
    sum $ defn sum (& xs)
      hint-fn $ {} (:return :number)
      assert-type xs $ :: :& :number
      apply + xs
    ; Struct definition
    User $ defstruct User (:name :string) (:age :number) (:email :string)
    x 42
  ; Type assertion (composable check, returns original value)
  assert-type x :number
  [] (add 3 4) (sum 1 2 3) x

Built-in Types

  • :number, :string, :bool, :nil, :dynamic
  • :list, :map, :set, :record, :fn, :tuple
  • :dynamic - wildcard type (default when no annotation)
  • Generic types (Cirru style):
let
    t1 $ :: :list :number
    t2 $ :: :map :string
    t3 $ :: :fn $ {}
      :args $ [] :number
      :return :string
  [] t1 t2 t3

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 &case and &extract-case
  • Method validation: Checks method names and class types
  • Recur arity: Validates recur argument count matches function params

Method & Access Syntax

  • Method call: .map xs inc (or shorthand xs.map inc)
  • Tag access (map key): prefer obj.:name over legacy (:name obj)
  • Trait/impl declarations prefer dot method keys like .foo; legacy tag keys like :foo remain compatible but emit a default warning in deftrait/defimpl

File Structure

  • calcit.cirru - Editor snapshot (source for structural editing)
  • compact.cirru - Runtime format (compiled, cr command actually uses this)
  • deps.cirru - Dependencies
  • .compact-inc.cirru - Hot reload trigger, including incremental changes

Common Functions

Math

  • +, -, *, / - arithmetic (variadic)
  • &+, &-, &*, &/ - binary arithmetic
  • inc, dec - increment/decrement
  • pow, sqrt, round, floor, ceil
  • sin, cos - trigonometric functions
  • &max, &min - binary min/max
  • &number:fract - fractional part
  • &number:rem - remainder
  • &number:format - format number
  • bit-shl, bit-shr, bit-and, bit-or, bit-xor, bit-not

List Operations

  • [] - create list
  • append, prepend - add elements
  • concat - concatenate lists
  • nth, first, rest, last - access elements
  • count, empty? - list properties
  • slice - extract sublist
  • reverse - reverse list
  • sort, sort-by - sorting
  • map, filter, reduce - functional operations
  • foldl, foldl-shortcut, foldr-shortcut - folding
  • range - generate number range
  • take, drop - slice operations
  • distinct - 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 membership
  • keys, vals - extract keys/values
  • to-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 set
  • include, exclude - add/remove elements
  • union, difference, intersection - set operations
  • &set:includes? - membership test
  • &set:to-list - convert to list

String Operations

  • str - concatenate to string
  • str-spaced - join with spaces
  • &str:concat - binary concatenation
  • trim, split, split-lines - string manipulation
  • starts-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 - padding
  • parse-float - parse number from string
  • get-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

  • defstruct - define a struct type with typed fields
  • %{} - create a record instance from a struct
  • %{}? - create a partial record (unset fields default to nil)
  • &%{} - low-level record constructor (flat key-value pairs, no type check)
  • record-with - update multiple fields, returns new record
  • &record:get - get field value
  • &record:assoc - set field value (low-level)
  • &record:struct - get the struct definition the record was created from
  • &record:matches? - type check
  • &record:from-map - convert from map
  • &record:to-map - convert to map
  • &record:get-name - get tag name of the record's struct
  • record?, struct? - predicates

Struct & Enum Operations

  • defstruct - define struct type
  • defenum - define enum type
  • &struct::new, &enum::new - create instances
  • struct?, enum? - predicates
  • &tuple:enum-has-variant? - check variant
  • &tuple:enum-variant-arity - get variant arity
  • tag-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 unchanged
  • assert-traits - runtime check that a value implements a trait, returns the value unchanged

Ref/Atom Operations

  • atom - create atom
  • &atom:deref or deref - read value
  • reset! - set value
  • swap! - update with function
  • add-watch, remove-watch - observe changes
  • ref? - predicate

Type Predicates

  • nil?, some? - nil checks
  • number?, string?, tag?, symbol?
  • list?, map?, set?, tuple?
  • record?, struct?, enum?, ref?
  • fn?, macro?

Control Flow

  • if - conditional
  • when, when-not - single-branch conditionals
  • cond - multi-way conditional
  • case - pattern matching on values
  • &case - internal case macro
  • tag-match - enum/tuple pattern matching
  • record-match - record pattern matching
  • list-match - list destructuring match
  • field-match - map field matching

Threading Macros

  • -> - thread first
  • ->> - thread last
  • ->% - thread with % placeholder
  • %<- - reverse thread

Other Macros

  • let - local bindings
  • defn - define function
  • defmacro - define macro
  • fn - anonymous function
  • quote, quasiquote - code as data
  • macroexpand, macroexpand-all - debug macros
  • assert, assert= - assertions
  • &doseq - side-effect iteration
  • for - list comprehension

Meta Operations

  • type-of - get type tag
  • turn-string, turn-symbol, turn-tag - type conversion
  • identical? - reference equality
  • recur - tail recursion
  • generate-id! - unique ID generation
  • cpu-time - timing
  • &get-os, &get-calcit-backend - environment info

EDN/Data Operations

  • parse-cirru-edn, format-cirru-edn - EDN serialization
  • parse-cirru, format-cirru - Cirru syntax
  • &data-to-code - convert data to code
  • pr-str - print to string

Effects/IO

  • echo, println - output
  • read-file, write-file - file operations
  • get-env - environment variables
  • raise - throw error
  • quit! - 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 programs
  • bundle_calcit: bundles Calcit code into a compact.cirru file
  • caps: downloads Calcit packages
  • cr-mcp: provides a Model Context Protocol (MCP) server for Calcit compact files
  • cr-sync: syncs changes from compact.cirru back to calcit.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:

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 .

Run Calcit

This page is a quick navigation hub. Detailed topics are split into dedicated chapters under run/.

Quick start

Run local project once (default behavior):

cr

Enable watch mode explicitly:

cr -w

Evaluate a snippet:

cr eval 'println "|Hello world"'

Emit JavaScript / IR once:

cr js
cr ir

Run guide map

Quick find by keyword

Use these keywords directly with cr docs read for faster section hits:

Typical navigation flow:

# 1) List headings in a chapter
cr docs read run.md

# 2) Jump by keyword(s)
cr docs read run.md quick find

# 3) Open the target chapter and narrow again
cr docs read query.md usages

Use this page for orientation, then jump to the specific chapter for complete examples and edge cases.

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

You can load external modules with repeatable --dep options:

cr demos/compact.cirru eval --dep ~/.config/calcit/modules/respo.calcit/ -- 'ns app.demo $ :require respo.util.detect :refer $ element?\n\nelement? nil'

If the first expression in a snippet is ns, its :require rules are merged into runtime ns app.main, so imported symbols can be used in the same snippet.

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.cirru for complete stack traces
  • Use cr cirru parse-oneliner to 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

For markdown snippet validation (docs check-md), see CLI Options.

CLI Options

Usage: cr [<input>] [-1] [-w] [--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        run once and quit (compatibility option)
  -w, --watch       watch files and rerun/rebuild on changes
  --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
  analyze           analyze code structure (call-graph, count-calls, check-examples)
  query             query project information (namespaces, definitions, configs)
  docs              documentation tools (guidebook)
  cirru             Cirru syntax tools (parse, format, edn)
  libs              fetch available Calcit libraries from registry
  edit              edit project code (definitions, namespaces, modules, configs)
  tree              fine-grained code tree operations (view and modify AST nodes)

Quick note: cr edit format rewrites the target snapshot using canonical serialization without changing semantics.

Detailed Option Descriptions

Input File

# Run default compact.cirru
cr

# Run specific file
cr demos/compact.cirru

Run Once (--once / -1)

By default, cr runs once and exits. Use --watch (-w) to enable watch mode:

cr --watch
cr -w demos/compact.cirru

--once is still available for compatibility:

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 watch mode
cr -w --reload-fn app.main/reload!

# Production build
cr js --emit-path dist/

# JS watch mode
cr js -w --emit-path dist/

# IR watch mode
cr ir -w

# Testing single run
cr --once --init-fn app.test/run-tests!

# Debug mode with full stack traces
cr --reload-libs

# CI/CD environment
cr --once --disable-stack

Markdown code checking

Use docs check-md to validate fenced code blocks in markdown files:

cr docs check-md README.md

Load module dependencies with repeatable --dep options:

cr docs check-md README.md --dep ./ --dep ~/.config/calcit/modules/memof/

Recommended block modes:

  • cirru: run + preprocess + parse (preferred)
  • cirru.no-run: preprocess + parse when runtime setup is unavailable
  • cirru.no-check: parse only for illustrative snippets

Querying Definitions

Calcit provides a powerful query subcommand to inspect code, find definitions, and analyze usages directly from the command line.

Core Query Commands

List Namespaces (ns)

# List all loaded namespaces
cr query ns

# Show definitions in a specific namespace
cr query ns calcit.core

Read Code (def)

# Show full source code of a definition
cr query def calcit.core/assoc

Peek Signature (peek)

# Show documentation and examples without the full body
cr query peek calcit.core/map

Check Examples (examples)

# Extract only the examples section
cr query examples calcit.core/let

Find Symbol (find)

# Search for a symbol across ALL loaded namespaces
cr query find assoc

Analyze Usages (usages)

# Find where a specific definition is used
cr query usages app.main/main!
# Search for raw text (leaf values) across project
cr query search hello

# Limit to one definition
cr query search hello -f app.main/main!

Search Expressions (search-expr)

# Search structural expressions (Cirru pattern)
cr query search-expr "fn (x)"

# Limit to one definition
cr query search-expr "fn (x)" -f app.main/main!

Quick Recipes (for fast locating)

Locate a symbol and jump to definition

cr query find assoc
cr query def calcit.core/assoc

Locate all call sites before refactor

cr query usages app.main/main!

Locate by text when you only remember a fragment

cr query search "reload"

Runtime Code Inspection

You can also use built-in functions to inspect live data and definitions:

let
    Point $ defstruct Point (:x :number) (:y :number)
    p (%{} Point (:x 1) (:y 2))
  do
    ; Get all methods/traits implemented by a value
    println $ &methods-of p
    ; Get tag name of a record or enum
    println $ &record:get-name p
    ; Describe any value's internal type
    println $ &inspect-type p

Getting Help

Use cr query --help for the full list of available query subcommands.

Documentation & Libraries

Calcit includes built-in commands to navigate the language guidebook and discover community libraries.

Guidebook Access (docs)

The docs subcommand allows you to read the language guidebook (like this one) without leaving the terminal.

Reading Chapters

# List all chapters in the guidebook
cr docs list

# Read a specific file (fuzzy matching supported)
cr docs read run.md

# List headings in a file (best first step before narrowing)
cr docs read run.md

# Jump by heading keyword(s)
cr docs read run.md quick start

# Search for keywords across all chapters
cr docs search "polymorphism"

Advanced Navigation (read)

cr docs read supports fuzzy heading matching to jump straight to a section:

# Display the "Quick start" section of run.md
cr docs read run.md "Quick start"

# Exclude subheadings from the output
cr docs read run.md "Quick start" --no-subheadings

Precision Reading (read-lines)

Use read-lines for large files where you need a specific range:

# Read 50 lines starting from line 100 of common-patterns.md
cr docs read-lines common-patterns.md --start 100 --lines 50

Fast Navigation Patterns

Pattern 1: Discover headings first, then narrow

cr docs read query.md
cr docs read query.md usages

Pattern 2: Search globally, then open exact chapter

cr docs search trait
cr docs read traits.md

Library Discovery (libs)

The libs subcommand helps you find and understand Calcit modules.

Searching Registry

# Search for libraries related to "web"
cr libs search web

Reading Readmes

You can read the documentation of any official library, even if not installed locally:

# Show README of 'respo' module
cr libs readme respo

# Read a specific markdown file inside package
cr libs readme respo -f Skills.md

Scanning for Documentation

# List all markdown files inside the local 'memof' module
cr libs scan-md memof

Collaborative validation (check-md)

docs check-md is used to verify that code blocks in your markdown documentation are correct and runnable:

cr docs check-md README.md

It supports specific block types:

  • cirru: Run and validate.
  • cirru.no-run: Validate syntax and preprocessing without running.
  • cirru.no-check: Skip checking (illustrative).

CLI Code Editing (edit & tree)

Calcit provides powerful CLI tools for modifying code directly without opening a text editor. These commands are optimized for both interactive use and automated scripts/agents.

Core Editing (cr edit)

The edit command handles high-level operations on namespaces and definitions.

# Refresh snapshot formatting without semantic changes
cr edit format

Managing Namespaces

# Move or rename a definition
cr edit mv app.main/old-name app.main/new-name

# Add a new namespace
cr edit add-ns app.util

# Remove a namespace
cr edit rm-ns app.util

Managing Imports

# Add an import to a namespace
cr edit add-import app.main -e 'respo.core :refer $ deftime'

# Bulk reset all imports for a namespace
cr edit imports app.main -f imports.cirru

Fine-grained AST Operations (cr tree)

The tree command allows precise manipulation of nodes within a definition's S-expression tree.

Viewing the Tree

# View the AST of a definition with indices
cr tree view app.main/main!

Target-based Replacement

target-replace is the safest way to modify a specific node by its content:

# Replace '1' with '10' inside the definition
cr tree target-replace app.main/main! -t 1 -e 10

Path-based Operations

You can use numeric paths to locate deep nodes:

# Replace the node at path [1 2 0]
cr tree replace app.main/main! -p 1 2 0 -e '(+ 1 2)'

# Insert before/after a node
cr tree insert app.main/main! -p 1 0 --at before -e 'println |started'

# Delete a node
cr tree delete app.main/main! -p 1 0

Copying across Definitions

# Copy a node from one definition to another
cr tree cp app.main/target-def --from app.main/source-def -p 1 0 --at append-child

Input Formats

Editing commands support several ways to provide new code:

  • -e 'code': Inline Cirru expression (one-liner).
  • -f file.cirru: Multi-line code from a file (recommended for complex structures).
  • -j 'json': Raw JSON-serialized Cirru representation.

Note: For multi-line text input, prefer using -f with a temporary file in .calcit-snippets/.

Best Practices

  1. Check first: Use cr query find or cr tree view to confirm the current state.
  2. From back to front: When performing multiple delete or insert operations at the same level, start from the highest index to avoid shifting indices.
  3. Use target-replace: It is usually safer than path-based replacement as it validates the current content.

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, only https:// 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: f64 in 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

All data structures are persistent and immutable, following functional programming principles. For detailed information about specific types, see:

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

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-edn
  • format-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):

let
    A $ defstruct A (:a :dynamic)
  ; Then create an instance in Calcit
  %{} 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

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.cirru format
  • 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

Quick Find by Task

Use this section as a keyword index for cr docs read:

  • Collections: list, map, set, tuple, record
  • Pattern Matching: enum, tag-match, tuple-match, result
  • Types: static-analysis, assert-type, optional, variadic
  • Methods: trait, impl-traits, method dispatch, trait-call
  • Interop: js interop, async, promise, js-await
  • Architecture: imports, namespace, module, dependency

Task-oriented jump map:

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 caps CLI tool
  • Hot module replacement - Fast iteration with live code updates
  • REPL integration - Interactive development with cr eval mode
  • 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 a persistent, immutable vector. In Rust it uses ternary-tree (optimized 2-3 tree with finger-tree tricks). In JavaScript it uses a similar structure with a fast-path CalcitSliceList for append-heavy workloads.

All list operations return new lists — the original is never mutated.

Quick Recipes

  • Create: [] 1 2 3 or range 5
  • Access: nth xs 0, first xs, last xs
  • Modify: append xs 4, prepend xs 0, assoc xs 1 99
  • Transform: map xs f, filter xs f, reduce xs 0 f
  • Combine: concat xs ys, slice xs 1 3

Creating Lists

let
    empty-list $ []
    nums $ [] 1 2 3 4 5
    words $ [] |foo |bar |baz
  println nums
  ; => ([] 1 2 3 4 5)

range generates a sequence:

let
    r1 $ range 5
    r2 $ range 2 7
  println r1
  ; => ([] 0 1 2 3 4)
  println r2
  ; => ([] 2 3 4 5 6)

Accessing Elements

let
    xs $ [] 10 20 30 40
  println $ nth xs 0
  ; => 10
  println $ first xs
  ; => 10
  println $ last xs
  ; => 40
  println $ count xs
  ; => 4

get is an alias for nth:

let
    xs $ [] :a :b :c
  println $ get xs 1
  ; => :b

Adding / Removing Elements

let
    xs $ [] 1 2 3
  println $ append xs 4
  ; => ([] 1 2 3 4)
  println $ prepend xs 0
  ; => ([] 0 1 2 3)
  println $ conj xs 4 5
  ; => ([] 1 2 3 4 5)
  println $ concat xs $ [] 4 5
  ; => ([] 1 2 3 4 5)

Update or remove by index:

let
    xs $ [] 1 2 3
  println $ assoc xs 1 99
  ; => ([] 1 99 3)
  println $ dissoc xs 1
  ; => ([] 1 3)

Slicing & Reordering

let
    xs $ [] 1 2 3 4 5
  println $ rest xs
  ; => ([] 2 3 4 5)
  println $ butlast xs
  ; => ([] 1 2 3 4)
  println $ slice xs 1 3
  ; => ([] 2 3)
  println $ take xs 3
  ; => ([] 1 2 3)
  println $ take-last xs 2
  ; => ([] 4 5)
  println $ drop xs 2
  ; => ([] 3 4 5)

Sort (default ascending):

let
    xs $ [] 3 1 4 1 5
  println $ sort xs
  ; => ([] 1 1 3 4 5)

Sort by key function (method-style):

let
    xs $ [] 1 2 3 4 5
  println $ .sort-by xs $ fn (x) (- 0 x)
  ; => ([] 5 4 3 2 1)

Reverse:

let
    xs $ [] 1 2 3 4 5
  println $ reverse xs
  ; => ([] 5 4 3 2 1)

Filtering & Finding

let
    xs $ [] 1 2 3 4 5
  println $ filter xs $ fn (x) (> x 3)
  ; => ([] 4 5)
  println $ filter-not xs $ fn (x) (> x 3)
  ; => ([] 1 2 3)
  println $ find xs $ fn (x) (> x 3)
  ; => 4
  println $ find-index xs $ fn (x) (> x 3)
  ; => 3
  println $ index-of xs 3
  ; => 2

Transforming

let
    xs $ [] 1 2 3 4 5
  println $ map xs $ fn (x) (* x 2)
  ; => ([] 2 4 6 8 10)
  println $ map-indexed xs $ fn (i x) ([] i x)
  ; => ([] ([] 0 1) ([] 1 2) ([] 2 3) ([] 3 4) ([] 4 5))

Flatten one level of nesting (method-style):

let
    nested $ [] ([] 1 2) ([] 3 4) ([] 5)
  println $ .flatten nested
  ; => ([] 1 2 3 4 5)

Aggregating

let
    xs $ [] 1 2 3 4 5
  println $ reduce xs 0 $ fn (acc x) (+ acc x)
  ; => 15
  println $ foldl xs 0 $ fn (acc x) (+ acc x)
  ; => 15
  println $ any? xs $ fn (x) (> x 4)
  ; => true
  println $ every? xs $ fn (x) (> x 0)
  ; => true

group-by partitions into a map keyed by the return value of the function:

let
    xs $ [] 1 2 3 4 5
  println $ group-by xs $ fn (x) (if (> x 3) :big :small)
  ; => ({} (:big ([] 4 5)) (:small ([] 1 2 3)))

Strings from Lists

let
    words $ [] |hello |world |foo
  println $ join-str words |,
  ; => hello,world,foo

Converting

let
    xs $ [] 1 2 2 3 3 3
  println $ .to-set xs
  ; => (#{} 1 2 3)

Thread Macro Pipelines

The -> thread macro is idiomatic for list transformations:

let
    result $ -> (range 10)
      filter $ fn (x) (> x 5)
      map $ fn (x) (* x x)
  println result
  ; => ([] 36 49 64 81)

Common Patterns

Building lists incrementally

let
    source $ [] 1 2 3 4 5
    init $ []
    result $ foldl source init $ fn (acc item)
      if (> item 2)
        append acc (* item 10)
        , acc
  println result
  ; => ([] 30 40 50)

Zip two lists together

let
    ks $ [] :a :b :c
    vs $ [] 1 2 3
    zipped $ map-indexed ks $ fn (i k) ([] k (nth vs i))
  println zipped
  ; => ([] ([] :a 1) ([] :b 2) ([] :c 3))

Deduplicate

Convert to set (removes duplicates, loses order):

let
    xs $ [] 1 2 2 3 3 3
  println $ .to-set xs
  ; => (#{} 1 2 3)

Implementation Notes

  • nth and get are O(log n) on the ternary tree structure.
  • append and prepend are amortized O(1) in the Rust implementation.
  • concat is O(m) where m is the size of the appended list.
  • Lists are zero-indexed.

HashMap

Calcit HashMap is a persistent, immutable hash map. In Rust it uses rpds::HashTrieMap. In JavaScript it is built on ternary-tree.

All map operations return new maps — the original is never mutated.

Quick Recipes

  • Create: {} (:a 1) (:b 2)
  • Access: get m :a, contains? m :a
  • Modify: assoc m :c 3, dissoc m :a, update m :a inc
  • Transform: map-kv m f, merge m1 m2
  • Keys/Values: keys m, vals m, to-pairs m

Creating Maps

{} is a macro that takes key-value pairs:

let
    m $ {}
      :a 1
      :b 2
      :c 3
  println m
  ; => ({} (:a 1) (:b 2) (:c 3))

Inline form:

let
    m $ {} (:x 10) (:y 20)
  println m

The low-level primitive &{} takes flat key-value pairs:

&{} :a 1 :b 2

Reading Values

let
    m $ {} (:a 1) (:b 2) (:c 3)
  println $ get m :a
  ; => 1
  println $ get m :missing
  ; => nil
  println $ contains? m :b
  ; => true
  println $ count m
  ; => 3
  println $ empty? m
  ; => false

Nested access with get-in

let
    nested $ {} (:user $ {} (:name |Alice) (:age 30))
  println $ get-in nested $ [] :user :name
  ; => Alice

Modifying Maps

All operations return a new map:

let
    m $ {} (:a 1) (:b 2)
    m2 $ assoc m :c 3
    m3 $ dissoc m2 :b
    m4 $ merge m $ {} (:d 4) (:e 5)
  println m2
  ; => ({} (:a 1) (:b 2) (:c 3))
  println m3
  ; => ({} (:a 1) (:c 3))
  println m4
  ; => ({} (:a 1) (:b 2) (:d 4) (:e 5))

Nested update with assoc-in

; update a deeply nested value
assoc-in config $ [] :server :port $ 8080

Iterating & Transforming

map-kv — transform entries

Returns a new map. If the callback returns nil, the entry is dropped (used as filter):

let
    m $ {} (:a 1) (:b 2) (:c 13)
    doubled $ map-kv m $ fn (k v) ([] k (* v 2))
    filtered $ map-kv m $ fn (k v)
      if (> v 10) nil
        [] k v
  println doubled
  ; => ({} (:a 2) (:b 4) (:c 26))
  println filtered
  ; => ({} (:a 1) (:b 2))

to-pairs — convert to set of pairs

let
    m $ {} (:a 1) (:b 2)
  println $ to-pairs m
  ; => (#{} ([] :a 1) ([] :b 2))

keys and vals

let
    m $ {} (:x 10) (:y 20)
  println $ keys m
  ; => (#{} :x :y)
  println $ vals m
  ; => (#{} 10 20)

each-kv — side-effect iteration

each-kv config $ fn (k v)
  println $ str k |: v

Querying

let
    m $ {} (:a 1) (:b 2) (:c 3)
  println $ includes? m 2
  ; => true  (checks values)
  println $ contains? m :a
  ; => true  (checks keys)

Building from Other Structures

; from a list of pairs
; each pair is [key value]
foldl my-pairs ({}) $ fn (acc pair)
  assoc acc (nth pair 0) (nth pair 1)

Using thread macro to build up a map (inserting as first arg to each step):

let
    base $ {} (:a 1) (:b 2)
    result $ merge base $ {} (:c 3) (:d 4)
  println result

Common Patterns

Default value on missing key

let
    m $ {} (:a 1) (:b 2)
    val $ get m :missing
  if (nil? val) :default val
  ; => :default

Counting occurrences

let
    words $ [] :a :b :a :c :a :b
    init $ {}
    freq $ foldl words init $ fn (acc w)
      let
          cur $ get acc w
          n $ if (nil? cur) 0 cur
        assoc acc w (inc n)
  println freq
  ; ({} (:a 3) (:b 2) (:c 1))

Merging with override

let
    defaults $ {} (:host |localhost) (:port 3000) (:debug false)
    overrides $ {} (:port 8080) (:debug true)
  merge defaults overrides
  ; => ({} (:host |localhost) (:port 8080) (:debug true))

Implementation Notes

HashMap key iteration order is not guaranteed. Use to-pairs + sort if you need stable order. Tags (:kw) are the most common key type; string keys also work but tags are faster for equality checks.

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.

Quick Recipes

  • Create: #{:a :b :c}
  • Add/Remove: include s :d, exclude s :a
  • Check: &set:includes? s :a
  • Operations: union s1 s2, difference s1 s2, intersection s1 s2
  • Convert: &set:to-list s

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.

Quick Recipes

  • Create: :: :point 10 20
  • Create Typed: %:: Shape :circle 5
  • Access: &tuple:nth t 1
  • Match: tag-match t ((:point x y) ...)
  • Update: &tuple:assoc t 1 99

Creating Tuples

Shorthand Syntax

Use :: to create a tuple with a tag:

let
    result 42
    message |error-occurred
  do
    :: :point 10 20
    :: :ok result
    :: :err message

With Class Syntax

Use %:: to create a typed tuple from an enum:

let
    Shape $ defenum Shape (:point :number :number) (:circle :number)
  %:: Shape :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+)
let
    t $ :: :point 10 20
  ; Index 0: :point, Index 1: 10, Index 2: 20
  [] (&tuple:nth t 0) (&tuple:nth t 1) (&tuple:nth t 2)

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

let
    t $ :: :point 10 20
  do
    ; count includes the tag
    &tuple:count (:: :a 1 2 3)
    ; => 4
    &tuple:params t
    ; => ([] 10 20)
    &tuple:enum t
    ; => nil (plain tuple, not from enum)

&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 returns nil.
do
  let
      plain $ :: :point 10 20
    nil? $ &tuple:enum plain
    ; => true
  let
      ApiResult $ defenum ApiResult (:ok :number) (:err :string)
      ok $ %:: ApiResult :ok 1
    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)

Pattern Matching with Tuples

tag-match

Pattern match on enum/tuple tags:

let
    MyResult $ defenum MyResult (:ok :number) (:err :string)
    result $ %:: MyResult :ok 42
  tag-match result
    (:ok v) (str |Success: v)
    (:err msg) (str |Error: msg)
    _ |Unknown

list-match

For simple list-like destructuring:

; list-match takes (head rest) branches — rest captures remaining elements as a list
list-match ([] :point 10 20)
  () |Empty
  (h tl) ([] h tl)

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

let
    MyResult $ defenum MyResult (:ok :number) (:err :string)
    divide $ defn divide (a b)
      if (= b 0)
        %:: MyResult :err |Division-by-zero
        %:: MyResult :ok (/ a b)
    result $ divide 10 2
  tag-match result
    (:ok value) (str |ok: value)
    (:err msg) (str |err: msg)

Optional Values

let
    MaybeInt $ defenum MaybeInt (:some :number) (:none)
    find-item $ fn (items target)
      reduce items (%:: MaybeInt :none)
        fn (acc x)
          if (= x target) (%:: MaybeInt :some x) acc
    result $ find-item ([] 1 2 3) 2
  tag-match result
    (:some v) v
    _ |not-found

Tagged Data

; Represent different message types
:: :greeting |Hello
:: :number 42
:: :list ([] 1 2 3)

Type Annotations

let
    ApiResult $ defenum ApiResult (:ok :string) (:err :string)
    process-result $ defn process-result (r)
  hint-fn $ {} (:args ([] :dynamic)) (:return :string)
      tag-match r
        (:ok v) (str v)
        (:err msg) msg
  process-result (%:: ApiResult :ok |done)

Tuple vs Record

FeatureTupleRecord
AccessBy indexBy field name
StructureTag + paramsNamed fields
MethodsVia classVia traits
Use caseTagged unionsStructured objects

Performance Notes

  • Tuples are immutable
  • Element access is O(1)
  • &tuple:assoc creates a new tuple
  • Use records for complex objects with named fields

Enums (defenum)

Calcit enums are tagged unions — each variant has a tag (keyword) and zero or more typed payload fields. Under the hood enums are represented as tuples with a class reference.

Quick Recipes

  • Define: defenum Shape (:circle :number) (:rect :number :number)
  • Create: %:: Shape :circle 5
  • Match: tag-match shape ((:circle r) ...) ((:rect w h) ...)
  • Type Check: assert-type shape :enum

Defining Enums

let
    Color $ defenum Color (:red) (:green) (:blue)
    c $ %:: Color :red
  println c
  ; => (%:: :red (:enum Color))

Variants with payloads:

let
    Shape $ defenum Shape (:circle :number) (:rect :number :number)
    c $ %:: Shape :circle 5
    r $ %:: Shape :rect 3 4
  println c
  ; => (%:: :circle 5 (:enum Shape))
  println r
  ; => (%:: :rect 3 4 (:enum Shape))

Creating Instances

Use %:: with the enum definition, the variant tag, and then the payload values:

let
    ApiResult $ defenum ApiResult (:ok :string) (:err :string)
    ok $ %:: ApiResult :ok |success
    err $ %:: ApiResult :err |network-error
  println ok
  println err

Pattern Matching with tag-match

tag-match branches on the variant tag and binds payload values to names:

let
    Shape $ defenum Shape (:circle :number) (:rect :number :number)
    c $ %:: Shape :circle 5
    area $ tag-match c
      (:circle radius)
        * radius radius 3.14159
      (:rect w h)
        * w h
  println area
  ; => 78.53975

Multi-line branch bodies (required when the body is more than a single call):

let
    ApiResult $ defenum ApiResult (:ok :string) (:err :string)
    ok $ %:: ApiResult :ok |success
    describe $ fn (r)
      tag-match r
        (:ok msg)
          str-spaced |OK: msg
        (:err msg)
          str-spaced |Error: msg
  println (describe ok)
  ; => OK: success

Zero-payload Variants

When a variant has no payload, the pattern is just the tag:

let
    MaybeInt $ defenum MaybeInt (:some :number) (:none)
    some-val $ %:: MaybeInt :some 42
    none-val $ %:: MaybeInt :none
    extracted $ tag-match some-val
      (:some v)
        * v 2
      (:none) nil
  println extracted
  ; => 84

Checking Enum Origin

Use &tuple:enum to verify a tuple belongs to a specific enum:

let
    ApiResult $ defenum ApiResult (:ok :number) (:err :string)
    x $ %:: ApiResult :ok 1
  println $ = (&tuple:enum x) ApiResult
  ; => true

Common Patterns

Result / Either type

let
    AppResult $ defenum AppResult (:ok :number) (:err :string)
    compute $ fn (x)
      if (> x 0)
        %:: AppResult :ok (* x 10)
        %:: AppResult :err |negative-input
    handle $ fn (r)
      tag-match r
        (:ok v)
          str-spaced |result: v
        (:err e)
          str-spaced |failed: e
  println $ handle (compute 5)
  ; => result: 50
  println $ handle (compute -1)
  ; => failed: negative-input

Compose enums with functions

let
    Status $ defenum Status (:pending) (:done :string) (:failed :string)
    pending $ %:: Status :pending
    done $ %:: Status :done |ok
    is-done $ fn (s)
      tag-match s
        (:done _) true
        (:pending) false
        (:failed _) false
  println (is-done pending)
  ; => false
  println (is-done done)
  ; => true

Type Annotations

Field types in defenum declarations participate in type checking:

; (:ok :string) means the :ok variant has one :string payload
defenum ApiResult (:ok :string) (:err :string)

; (:point :number :number) means :point has two :number payloads
defenum Shape (:point :number :number) (:circle :number)

; (:none) means no payload
defenum MaybeInt (:some :number) (:none)

Runtime type validation is enforced at instance creation — passing the wrong type to %:: will raise an error.

Notes

  • Enum instances are immutable tuples with a class reference.
  • tag-match is exhaustive match; unmatched tags raise a runtime error.
  • Use &tuple:nth to directly access payload values by index (0 = tag, 1+ = payloads).
  • Enums vs plain tuples: plain :: :tag val tuples have no class; %:: Enum :tag val tuples carry their enum class for origin checking.

See Also

  • Tuples — raw tagged tuples without a class
  • Records — named-field structs with defstruct
  • Static Analysis — type checking for enum payloads

Records

Calcit provides Records as a way to define structured data types with named fields, similar to structs in other languages. Records are defined with defstruct and instantiated with the %{} macro.

Quick Recipes

  • Define: defstruct Point (:x :number) (:y :number)
  • Create: %{} Point (:x 1) (:y 2)
  • Access: get p :x or (:x p)
  • Update: assoc p :x 10 or update p :x inc
  • Type Check: assert-type p :record

Defining a Struct Type

Use defstruct to declare a named type with typed fields:

defstruct Point (:x :number) (:y :number)

Each field is a pair of (:field-name :type). Supported types include :number, :string, :bool, :tag, :list, :map, :fn, and :dynamic (untyped).

defstruct Person (:name :string) (:age :number) (:position :tag)

Creating Records

Use the %{} macro to instantiate a struct:

let
    Point $ defstruct Point (:x :number) (:y :number)
    p $ %{} Point (:x 1) (:y 2)
  , p

Fields can also be written on separate lines:

let
    Point $ defstruct Point (:x :number) (:y :number)
    p $ %{} Point
      :x 1
      :y 2
  , p

Accessing Fields

Use get (or &record:get) to read a field:

let
    Point $ defstruct Point (:x :number) (:y :number)
    p $ %{} Point (:x 1) (:y 2)
  println $ get p :x
  ; => 1

Standard collection functions like keys, count, and contains? also work on records:

let
    Point $ defstruct Point (:x :number) (:y :number)
    p $ %{} Point (:x 1) (:y 2)
  println $ keys p
  ; => (#{} :x :y)
  println $ count p
  ; => 2
  println $ contains? p :x
  ; => true

Updating Fields

Records are immutable. Use assoc or record-with to produce an updated copy:

let
    Point $ defstruct Point (:x :number) (:y :number)
    p $ %{} Point (:x 1) (:y 2)
    p2 $ assoc p :x 10
  println p2
  ; => (%{} :Point (:x 10) (:y 2))
  println p
  ; p is unchanged: (%{} :Point (:x 1) (:y 2))
let
    Person $ defstruct Person (:name :string) (:age :number) (:position :tag)
    p $ %{} Person (:name |Chen) (:age 20) (:position :mainland)
    p2 $ record-with p (:age 21) (:position :shanghai)
  println p2
  ; p2 has updated :age and :position, :name is unchanged

&record:assoc is the low-level variant (no type checking):

let
    Point $ defstruct Point (:x :number) (:y :number)
    p $ %{} Point (:x 1) (:y 2)
  println $ &record:assoc p :x 100

Partial Records

Use %{}? to create a record with only some fields set (others default to nil):

let
    Person $ defstruct Person (:name :string) (:age :number) (:position :tag)
    p1 $ %{}? Person (:name |Chen)
  println $ get p1 :name
  ; => |Chen
  println $ get p1 :age
  ; => nil

The low-level &%{} form accepts fields as flat keyword-value pairs (no type checking):

let
    Person $ defstruct Person (:name :string) (:age :number) (:position :tag)
  println $ &%{} Person :name |Chen :age 20 :position :mainland

Type Checking

let
    Point $ defstruct Point (:x :number) (:y :number)
    p $ %{} Point (:x 1) (:y 2)
  ; check if a value is a record (struct instance)
  println $ record? p
  ; => true
  ; check if it matches a specific struct
  println $ &record:matches? p Point
  ; => true
  ; get the struct definition the record was created from
  println $ &record:struct p
  ; compare structs directly for origin check
  println $ = (&record:struct p) Point
  ; => true
  ; struct? checks struct definitions, not instances
  println $ struct? Point
  ; => true
  println $ struct? p
  ; => false

Pattern Matching

Use record-match to branch on record types:

let
    Circle $ defstruct Circle (:radius :number)
    Square $ defstruct Square (:side :number)
    shape $ %{} Circle (:radius 5)
  record-match shape
    Circle c $ * 3.14 (* (get c :radius) (get c :radius))
    Square s $ * (get s :side) (get s :side)
    _ _ nil
; => 78.5

Converting Records

To Map

let
    Point $ defstruct Point (:x :number) (:y :number)
    p $ %{} Point (:x 1) (:y 2)
  println $ &record:to-map p
  ; => {} (:x 1) (:y 2)

merge also works and returns a new record of the same struct:

let
    Person $ defstruct Person (:name :string) (:age :number) (:position :tag)
    p $ %{} Person (:name |Chen) (:age 20) (:position :mainland)
  println $ merge p $ {} (:age 23) (:name |Ye)

Record Name and Struct Inspection

let
    Person $ defstruct Person (:name :string) (:age :number) (:position :tag)
    p $ %{} Person (:name |Chen) (:age 20) (:position :mainland)
  ; get the tag name of the record
  println $ &record:get-name p
  ; => :Person
  ; check the struct behind a record value
  println $ &record:struct p

Struct Origin Check

Compare struct definitions directly when you need to confirm a record's origin:

let
    Cat $ defstruct Cat (:name :string) (:color :tag)
    Dog $ defstruct Dog (:name :string)
    v1 $ %{} Cat (:name |Mimi) (:color :white)
  if (= (&record:struct v1) Cat)
    println "|Handle Cat branch"
    println "|Not a Cat"

Polymorphism with Traits

Define a trait with deftrait, implement it with defimpl, and attach it to a struct with impl-traits:

let
    BirdTrait $ deftrait BirdTrait
      .show $ :: :fn $ {}
        :args $ [] 'T
        :return :nil
      .rename $ :: :fn $ {}
        :args $ [] 'T :string
        :return 'T
    BirdShape $ defstruct BirdShape (:name :string)
    BirdImpl $ defimpl BirdImpl BirdTrait
      .show $ fn (self)
        println $ get self :name
      .rename $ fn (self name)
        assoc self :name name
    Bird $ impl-traits BirdShape BirdImpl
    b $ %{} Bird (:name |Sparrow)
  .show b
  let
      b2 $ .rename b |Eagle
    .show b2

Common Use Cases

Configuration Objects

let
    Config $ defstruct Config (:host :string) (:port :number) (:debug :bool)
    config $ %{} Config (:host |localhost) (:port 3000) (:debug false)
  println $ get config :port
  ; => 3000

Domain Models

let
    Product $ defstruct Product (:id :string) (:name :string) (:price :number) (:discount :number)
    product $ %{} Product
      :id |P001
      :name |Widget
      :price 100
      :discount 0.9
  println $ * (get product :price) (get product :discount)
  ; => 90

Type Annotations

let
    User $ defstruct User (:name :string) (:age :number) (:email :string)
    get-user-name $ fn (user)
  hint-fn $ {} (:args ([] (:: :record User))) (:return :string)
      assert-type user $ :: :record User
      get user :name
  println $ get-user-name $ %{} User
    :name |John
    :age 30
    :email |john@example.com
; => John

Performance Notes

  • Records are immutable — updates create new records
  • Field access is O(1)
  • Use record-with to update multiple fields at once and minimize intermediate allocations

Error Handling

Calcit uses try / raise for exception-based error handling. Errors are string values (or tags) propagated up the call stack.

Quick Recipes

  • Catch Errors: try (risky-op) (fn (e) ...)
  • Throw String: raise |something-went-wrong
  • Throw Tag: raise :invalid-input
  • Match Error: if (= e :invalid-input) ...

Basic try / raise

try takes an expression body and a handler function. If the body raises an error, the handler receives the error message as a string:

let
    result $ try
        raise |something-went-wrong
      fn (e)
        str-spaced |caught: e
  println result
  ; => caught: something-went-wrong

Raising from a Function

let
    safe-div $ fn (a b)
      if (= b 0)
        raise |division-by-zero
        / a b
    result $ try
        safe-div 10 0
      fn (e)
        str-spaced |error: e
  println result
  ; => error: division-by-zero

Raising Tags as Error Codes

Tags are a clean way to represent error categories:

let
    validate-age $ fn (n)
      if (< n 0)
        raise :negative-age
        if (> n 150)
          raise :unrealistic-age
          n
    result $ try
        validate-age -5
      fn (e)
        str-spaced |validation-failed: e
  println result
  ; => validation-failed: :negative-age

Silent Success vs Error Paths

When an error is raised, execution jumps to the handler — intermediate values are not returned:

let
    might-fail $ fn (flag)
      if flag (raise |early-exit) 42
    a $ try (might-fail false) $ fn (e) -1
    b $ try (might-fail true) $ fn (e) -1
  println a
  ; => 42
  println b
  ; => -1

Nested try

Inner try handlers can re-raise or recover selectively:

try
    try
        risky-operation
      fn (e)
        if (= e :recoverable)
          default-value
          raise e
  fn (outer-e)
    log-error outer-e
    nil

Using Enums for Typed Results (Preferred Pattern)

Instead of exceptions, idiom Calcit code often uses a Result enum to represent success/failure without throwing:

let
    AppResult $ defenum AppResult (:ok :number) (:err :string)
    safe-compute $ fn (x)
      if (> x 0)
        %:: AppResult :ok (* x 10)
        %:: AppResult :err |negative-input
    handle $ fn (r)
      tag-match r
        (:ok v)
          str-spaced |result: v
        (:err msg)
          str-spaced |failed: msg
  println $ handle (safe-compute 5)
  ; => result: 50
  println $ handle (safe-compute -1)
  ; => failed: negative-input

This pattern avoids exceptions entirely and keeps error handling explicit in the type system.

Assertions

assert and assert= raise errors during preprocessing/testing:

; assert a condition is true
assert (> x 0) |expected-positive

; assert two values are equal
assert= (+ 1 2) 3

assert-type checks type at preprocessing time:

; assert x is a number before using it
assert-type x :number

Notes

  • raise accepts any value that can be converted to a string. String literals and tags work best.
  • Raising maps or complex data structures may produce unexpected results — use the Result enum pattern for structured error data.
  • try always produces a value: either the result of the body, or the result of the handler.
  • assert / assert= are for development-time invariants. They generate warnings (not runtime errors) during static analysis.

See Also

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:

Quick Recipes

  • Define: defmacro my-macro (x) ...
  • Template: quasiquote $ if ~x ~y ~z
  • Splice: ~@xs to unpack a list into the template
  • Fresh Symbols: gensym |name or with-gensyms (a b) ...
  • Local Bindings: &let (v ~item) ...
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-all to 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

Calcit keeps JS interop syntax intentionally small. This page covers the existing core patterns:

  • global access
  • property access
  • method call
  • array/object construction
  • constructor call with new

Access global values

Use js/... to read JavaScript globals and nested members:

do js/window.innerWidth

Access properties

Use .-name for property access:

let
    obj $ js-object (:name |Alice)
  .-name obj

This compiles to direct JS member access. For non-identifier keys, Calcit uses bracket access automatically.

Optional access is also supported with .?-name, which maps to optional chaining style access.

Call methods

Use .!name for native JS method calls (object first, then args):

.!setItem js/localStorage |key |value

Optional method call is supported with .?!name.

Note: .m and .!m are different. .m is Calcit method dispatch (traits/impls), while .!m is native JavaScript method invocation.

Construct arrays

Use js-array for JavaScript arrays:

let
    a $ js-array 1 2
  .!push a 3 4
  , a

Construct objects

Use js-object with key/value pairs:

js-object
  :a 1
  :b 2

js-object is a macro that validates input shape, so each entry must be a pair.

Equivalent single-line form:

js-object (:a 1) (:b 2)

Create instances with new

Use new with a constructor symbol:

new js/Date

With arguments:

new js/Array 3

Async interop patterns

Calcit provides async interop syntax for JS codegen.

Mark async functions

Use hint-fn $ {} (:async true) in function body when using js-await:

js-await should stay inside async-marked function bodies.

let
    fetch-data $ fn () nil
  fn ()
    hint-fn $ {} (:async true)
    js-await $ fetch-data

Await promises

Use js-await for Promise-like values:

fn ()
  hint-fn $ {} (:async true)
  let
      p $ new js/Promise $ fn (resolve _reject)
        js/setTimeout
          fn () (resolve |done)
          , 100
      result $ js-await p
    , result

Build Promise helpers

A common pattern is wrapping callback APIs with new js/Promise:

defn timeout (ms)
  new js/Promise $ fn (resolve _reject)
    js/setTimeout resolve ms

Then consume it inside async function:

let
    timeout $ fn (ms) $ new js/Promise $ fn (resolve _reject)
      js/setTimeout resolve ms
  fn ()
    hint-fn $ {} (:async true)
    js-await $ timeout 200

Async iteration

Use js-for-await with js-await for async iterables:

let
    gen $ fn () nil
  fn ()
    hint-fn $ {} (:async true)
    js-await $ js-for-await (gen)
      fn (item)
        new js/Promise $ fn (resolve _reject)
          js/setTimeout $ fn ()
            resolve item

Imports

Calcit loads namespaces from compact.cirru (the compiled representation of source files). Dependencies are tracked via ~/.config/calcit/modules/.

Quick Recipes

  • Alias: :require (app.lib :as lib)
  • Refer: :require (app.lib :refer $ f1 f2)
  • Core: calcit.core is auto-imported
  • CLI Add: cr edit add-import app.main -e 'app.lib :refer $ f1'

The ns Form

Every source file declares its namespace at the top with ns:

ns app.demo
  :require
    app.lib :as lib
    app.lib :refer $ f1 f2
    app.util :refer $ helper

The :require block accepts two kinds of rules:

FormEffect
mod.ns :as aliasImports namespace as alias; access via alias/fn
mod.ns :refer $ sym1 sym2Imports symbols directly into scope

Aliased Import

Use :as to import an entire namespace under a local alias:

ns app.main
  :require
    app.model :as model
    app.util :as util

; Then use as:
; model/make-user
; util/format-date

Direct Symbol Import

Use :refer to bring specific names into the current namespace:

ns app.main
  :require
    app.math :refer $ add subtract multiply
    app.string :refer $ capitalize trim-whitespace

calcit.core — Auto-Imported

All standard library functions (map, filter, reduce, +, println, defn, let, etc.) come from calcit.core and are available automatically without an explicit import. You do not need to require calcit.core.

JavaScript Interop Imports

When compiling to JavaScript, Calcit generates ES module import syntax. The NS form supports additional rules for JS:

ns app.demo
  :require
    ; Regular Calcit module
    app.lib :as lib

    ; NPM package with default export
    |chalk :default chalk

    ; NPM package with named exports
    |path :refer $ join dirname

Generated JS output:

import * as $app_DOT_lib from "./app.lib.mjs";
import chalk from "chalk";
import { join, dirname } from "path";

Note the | prefix on npm package names — this indicates a string literal (the module specifier) vs a Calcit namespace path.

Avoiding Circular Imports

Circular dependencies (A imports B, B imports A) will cause a compilation error. Structure your code with:

  • Core data types and pure functions in low-level namespaces
  • Side-effectful and orchestration code at higher levels

Using cr edit for Import Management

The cr edit CLI commands help manage imports safely:

# Add a new import to a namespace
cr app.cirru edit add-import app.demo -e 'app.util :refer $ helper'

# Override an existing import (same source namespace)
cr app.cirru edit add-import app.demo -e 'app.util :refer $ helper new-fn' -o

See cr edit --help for all available operations.

Checking Imports

Use cr docs search to look up what's available in a namespace before importing:

cr app.cirru docs search my-function

or query the examples for a specific definition:

cr app.cirru query examples calcit.core/map

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.

Quick Recipes

  • Define Trait: deftrait Show .show (:: :fn $ {} ...)
  • Implement: defimpl ShowImpl Show .show (fn (x) ...)
  • Attach: impl-traits MyStruct ShowImpl
  • Call: .show instance

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 $ {}
    :generics $ [] 'T
    :args $ [] 'T
    :return :string

deftrait Eq
  .eq? $ :: :fn $ {}
    :generics $ [] 'T
    :args $ [] 'T 'T
    :return :bool

Traits are values and can be referenced like normal symbols.

Implement a trait for a struct/enum definition

let
    MyFoo $ deftrait MyFoo
      .foo $ :: :fn $ {}
        :generics $ [] 'T
        :args $ [] 'T
        :return :string
    MyFooImpl $ defimpl MyFooImpl MyFoo
      .foo $ fn (p) (str "|foo " (:name p))
    Person0 $ defstruct Person (:name :string)
    Person $ impl-traits Person0 MyFooImpl
    p $ %{} Person (:name |Alice)
  .foo p

impl-traits returns a new struct/enum definition with trait implementations attached. You can also attach multiple traits at once:

let
    MyFoo $ deftrait MyFoo
      .foo $ :: :fn $ {}
        :generics $ [] 'T
        :args $ [] 'T
        :return :string
    ShowTrait $ deftrait ShowTrait
      .show $ :: :fn $ {}
        :generics $ [] 'T
        :args $ [] 'T
        :return :string
    EqTrait $ deftrait EqTrait
      .eq $ :: :fn $ {}
        :generics $ [] 'T
        :args $ [] 'T
        :return :string
    Person0 $ defstruct Person (:name :string)
    ShowImpl $ defimpl ShowImpl ShowTrait
      .show $ fn (p) (str |Person: (:name p))
    EqImpl $ defimpl EqImpl EqTrait
      .eq $ fn (p) (str |eq: (:name p))
    MyFooImpl $ defimpl MyFooImpl MyFoo
      .foo $ fn (p) (str |foo: (:name p))
    Person $ impl-traits Person0 ShowImpl EqImpl MyFooImpl
    p $ %{} Person (:name |Alice)
  [] (.show p) (.foo p)

Trait checks and type hints

assert-traits marks a local as having a trait and validates it at runtime:

let
    MyFoo $ deftrait MyFoo
      .foo $ :: :fn $ {}
        :generics $ [] 'T
        :args $ [] 'T
        :return :string
    Person0 $ defstruct Person (:name :string)
    MyFooImpl $ defimpl MyFooImpl MyFoo
      .foo $ fn (p) (str-spaced |foo (:name p))
    Person $ impl-traits Person0 MyFooImpl
    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-traits when 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)”.

Quick Recipes

  • Define Trait: deftrait MyTrait .method (:: :fn $ {} ...)
  • Implement Trait: defimpl MyImpl MyTrait .method (fn (x) ...)
  • Attach to Struct: impl-traits MyStruct MyImpl
  • Call Method: .method instance
  • Check Trait: assert-traits instance MyTrait

Define a trait

Use deftrait to define a trait and its method signatures (including type annotations).

deftrait MyFoo
  .foo $ :: :fn $ {}
    :generics $ [] 'T
    :args $ [] 'T
    :return :string

Implement a trait

Use defimpl to create an impl record for a trait.

let
    MyFoo $ deftrait MyFoo
      .foo $ :: :fn $ {}
        :generics $ [] 'T
        :args $ [] 'T
        :return :string
    Person0 $ defstruct Person (:name :string)
    MyFooImpl $ defimpl MyFooImpl MyFoo
      .foo $ fn (p)
        str-spaced |foo (:name p)
    Person $ impl-traits Person0 MyFooImpl
    p $ %{} Person (:name |Alice)
  .foo p

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:

do
  ; Form 1: symbol names for impl and trait
  let
      PersonA0 $ defstruct PersonA (:name :string)
      MyFooA $ deftrait MyFooA
        .foo $ :: :fn $ {}
          :generics $ [] 'T
          :args $ [] 'T
          :return :string
      MyFooImplA $ defimpl MyFooImplA MyFooA
        .foo $ fn (p) (str-spaced |foo (:name p))
      PersonA $ impl-traits PersonA0 MyFooImplA
      p $ %{} PersonA (:name |Alice)
    .foo p
  ; Form 2: tag keywords for impl and trait (no deftrait needed)
  let
      PersonB0 $ defstruct PersonB (:name :string)
      MyFooImplB $ defimpl :MyFooImplB :MyFooB
        .foo $ fn (p) (str-spaced |bar (:name p))
      PersonB $ impl-traits PersonB0 MyFooImplB
      p $ %{} PersonB (:name |Bob)
    .foo p

2) Method pair forms

Prefer dot-style keys (.foo). Legacy tag keys (:foo) are still accepted for compatibility.

; Both forms are accepted and equivalent:
do
  let
      MyFoo $ deftrait MyFoo
        .foo $ :: :fn $ {}
          :generics $ [] 'T
          :args $ [] 'T
          :return :string
      Person0 $ defstruct Person (:name :string)
      ; Form 1: preferred .method keys
      ImplA $ defimpl ImplA MyFoo
        .foo (fn (p) (str |A: (:name p)))
      PersonA $ impl-traits Person0 ImplA
      pa $ %{} PersonA (:name |Alice)
    .foo pa
  let
      MyFoo $ deftrait MyFoo
        .foo $ :: :fn $ {}
          :generics $ [] 'T
          :args $ [] 'T
          :return :string
      Person0 $ defstruct Person (:name :string)
      ; Form 2: legacy tag keys (compatible)
      ImplB $ defimpl ImplB MyFoo
        :: :foo (fn (p) (str |B: (:name p)))
      PersonB $ impl-traits Person0 ImplB
      pb $ %{} PersonB (:name |Bob)
    .foo pb

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:

  • defimpl creates an “impl record” that stores the trait as its origin.
  • This origin is used by &trait-call to 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-traits only accepts struct/enum values.
  • Record/tuple instances must be created from a struct/enum that already has impls attached (%{} or %::).

Syntax:

let
    MyFoo $ deftrait MyFoo
      .foo $ :: :fn $ {}
        :generics $ [] 'T
        :args $ [] 'T
        :return :string
    ImplA $ defimpl ImplA MyFoo
      .foo $ fn (p) (str |A: (:name p))
    ImplB $ defimpl :ImplB :ImplB-trait
      .bar $ fn (p) (str |B: (:name p))
    StructDef0 $ defstruct StructDef (:name :string)
    StructDef $ impl-traits StructDef0 ImplA ImplB
    x $ %{} StructDef (:name |test)
  .foo x

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.
do
  ; struct example
  let
      MyFoo $ deftrait MyFoo
        .foo $ :: :fn $ {}
          :generics $ [] 'T
          :args $ [] 'T
          :return :string
      MyFooImpl $ defimpl MyFooImpl MyFoo
        .foo $ fn (p) (str-spaced |foo (:name p))
      Person0 $ defstruct Person (:name :string)
      Person $ impl-traits Person0 MyFooImpl
      p $ %{} Person (:name |Alice)
    .foo p
  ; enum example
  let
      ResultTrait $ deftrait ResultTrait
        .describe $ :: :fn $ {}
          :generics $ [] 'T
          :args $ [] 'T
          :return :string
      ResultImpl $ defimpl ResultImpl ResultTrait
        .describe $ fn (x)
          tag-match x
            (:ok v) (str |ok: v)
            (:err v) (str |err: v)
      Result0 $ defenum Result0 (:ok :string) (:err :string)
      MyResult $ impl-traits Result0 ResultImpl
      r $ %:: MyResult :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:

  • .method call sites that have multiple trait candidates with the same method name.
  • impl-traits used 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:

  • defimpl argument order (ImplName then Trait)
  • assert-traits local-first requirement
  • impl-traits only 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:

let
    MyZapA $ deftrait MyZapA
      .zap $ :: :fn $ {}
        :generics $ [] 'T
        :args $ [] 'T
        :return :string
    MyZapB $ deftrait MyZapB
      .zap $ :: :fn $ {}
        :generics $ [] 'T
        :args $ [] 'T
        :return :string
    MyZapAImpl $ defimpl MyZapAImpl MyZapA
      .zap $ fn (_x) |zapA
    MyZapBImpl $ defimpl MyZapBImpl MyZapB
      .zap $ fn (_x) |zapB
    Person0 $ defstruct Person (:name :string)
    Person $ impl-traits Person0 MyZapAImpl MyZapBImpl
    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-of returns a list of available method names (strings, including the leading dot).
  • &inspect-methods prints impl records and methods to stderr, and returns the value unchanged.
  • &impl:origin returns 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
    MyFoo $ deftrait MyFoo
      .foo $ :: :fn ('T) ('T) :string
    Shape0 $ defenum Shape (:point :number :number)
    MyFooImpl $ defimpl MyFooImpl MyFoo
      .foo $ fn (t) (str |shape: (&tuple:nth t 0))
    Shape $ impl-traits Shape0 MyFooImpl
    some-tuple $ %:: Shape :point 10 20
    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-traits is syntax (expanded to &assert-traits) and its first argument must be a local.
  • For built-in values (list/map/set/string/number/...), assert-traits only 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.
let
    MyFoo $ deftrait MyFoo
      .foo $ :: :fn $ {}
        :generics $ [] 'T
        :args $ [] 'T
        :return :string
    Person0 $ defstruct Person (:name :string)
    MyFooImpl $ defimpl MyFooImpl MyFoo
      .foo $ fn (p) (str-spaced |foo (:name p))
    Person $ impl-traits Person0 MyFooImpl
    p $ %{} Person (:name |Alice)
  assert-traits p MyFoo
  .foo p

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.

Quick Recipes

  • Assert Type: assert-type x :number
  • Return Type: hint-fn $ {} (:return :string)
  • Compact Hint: defn my-fn (x) :string ...
  • Check Traits: assert-traits x MyTrait
  • Ignore Warning: &core:ignore-type-warning

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
  • Composable runtime assertions - assert-type and assert-traits can validate values at runtime and return original values for chaining

Type Annotations

Function Parameter Types

Annotate function parameters using assert-type within the function body.

assert-type is composable: it returns the checked value when validation passes. For local symbols, preprocess also records type info for static inference. For non-local expressions, the runtime assertion path still works and keeps expression composition intact.

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 schema map at the start of the function body:

Legacy clause syntax such as (hint-fn (return-type ...)), (generics ...), and (type-vars ...) is no longer supported and now fails during preprocessing.

let
    get-name $ fn (user)
      hint-fn $ {} (:args ([] :dynamic)) (:return :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:

TagCalcit Type
:nilNil
:boolBoolean
:numberNumber
:stringString
:symbolSymbol
:tagTag (Keyword)
:listList
:mapHash Map
:setSet
:tupleTuple (general)
:fnFunction
:refAtom / Ref
:any / :dynamicAny 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 defstruct or defenum:

let
    User $ defstruct User (:name :string)
    get-name $ fn (u)
      assert-type u User
      get u :name
  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:

defstruct User (:name :string) (:age :number)

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 loop macro)
  • calcit.core namespace functions

Type Inference

The system infers types from various sources:

Literal Types

let
    ; inferred as :number
    x 42
    ; inferred as :string
    y |hello
    ; inferred as :bool
    z true
    ; inferred as :nil
    w nil
  [] x y z w

Function Return Types

let
    ; inferred as :list
    numbers $ range 10
    ; inferred as :number
    n $ &list:first numbers
  [] n numbers

Record and Struct Types

let
    Point $ defstruct Point (:x :number) (:y :number)
    p $ %{} Point (:x 10) (:y 20)
    x-val (:x p)
  ; x-val inferred as :number from field type
  assert= x-val 10

Type Assertions

Use assert-type to explicitly check types during preprocessing:

let
    transform-fn $ fn (x) (* x 2)
    process-data $ defn process-data (data)
      assert-type data :list
      &list:map data transform-fn
  process-data ([] 1 2 3)

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:

let
    x 10
    nums $ [] 1 2 3
  assert-type nums :list
  ; Prints: [&inspect-type] x => number type
  &inspect-type x
  ; Prints: [&inspect-type] nums => list type
  &inspect-type nums
  let
      item $ &list:nth nums 0
    ; Prints: [&inspect-type] item => dynamic type
    &inspect-type item
    assert-type item :number
    ; Prints: [&inspect-type] item => number type
    &inspect-type item

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 (:: :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 :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.core namespace (external library)
  • Functions with variadic or optional parameters (complex arity rules)

Best Practices

1. Use Type Annotations for Public APIs

let
    process-input $ fn (input) (assoc input :processed true)
    public-api-function $ defn public-api-function (input)
      hint-fn $ {} (:args ([] :map)) (:return :string)
      assert-type input :map
      str $ process-input input
  public-api-function ({} (:data |hello))

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

let
    dangerous-operation $ fn (data) (map data (fn (x) (* x 2)))
    critical-operation $ defn critical-operation (data)
      assert-type data :list
      ; Ensure data is a list before processing
      dangerous-operation data
  critical-operation ([] 1 2 3)

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

  1. Dynamic Code: Type checks don't apply to dynamically generated code
  2. JavaScript Interop: JS function calls are not type-checked
  3. Macro Expansion: Some macros may generate code that bypasses checks
  4. 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

  1. Write function signatures with type annotations
  2. Let the compiler guide implementation
  3. Use warnings to catch edge cases
  4. 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.

Quick Recipes

  • Filter/Map: -> xs (filter f) (map g)
  • Group: group-by xs f
  • Find: find xs f, index-of xs v
  • Check All/Any: every? xs f, any? xs f
  • State: defatom state 0, reset! state 1, swap! state inc

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
    MyResult $ defenum MyResult
      :ok :dynamic
      :err :string
    safe-divide $ fn (a b)
      if (= b 0)
        %:: MyResult :err "|Division by zero"
        %:: MyResult :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
  handle-result $ safe-divide 10 0

Using Option Type

let
    MyOption $ defenum MyOption
      :some :dynamic
      :none
    find-user $ fn (users id)
      let
          user $ find users $ fn (u)
            = (get u :id) id
        if (nil? user)
          %:: MyOption :none
          %:: MyOption :some user
  println $ 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
  ; result1 => true
  ; result2 => true
  ; result3 => 6 (index of |world in |hello-world)
  [] result1 result2 result3

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 (items)
        append items $ {} (:id $ generate-id!) (:text text) (:done false)
    toggle-todo! $ fn (id)
      swap! todos $ fn (items)
        map items $ fn (todo)
          if (= (get todo :id) id)
            assoc todo :done $ not (get todo :done)
            , todo
  add-todo! |buy-milk
  add-todo! |write-docs
  println $ deref todos

Control Flow Patterns

Early Return Pattern

let
    ; stub implementations for demonstration
    validate-data $ fn (data) (if (= (count data) 0) nil data)
    transform-data $ fn (validated) (map validated (fn (x) (* x 2)))
    process-data $ 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
  process-data ([] 1 2 3)

Pipeline Pattern

let
    ; stub implementations for demonstration
    validate-input $ fn (s) s
    parse-input $ fn (s) s
    transform-to-command $ fn (s) (str |cmd/ s)
    process-user-input $ defn process-user-input (input)
      -> input
        trim
        &str:slice 0 100
        validate-input
        parse-input
        transform-to-command
  process-user-input "|hello world"

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)
    println 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
    ; stub implementations for demonstration
    transform-1 $ fn (x) (assoc x :step1 true)
    transform-2 $ fn (x) (assoc x :step2 true)
    data $ {} (:x 1) (:y 2)
    result $ -> data transform-1 transform-2
    x 5
  assert |Should-be-positive $ > x 0
  assert= 4 (+ 2 2)
  , 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 (get item :value)
    result2 $ apply +
      map items $ fn (item)
        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

  1. Use type annotations for function parameters and return values
  2. Prefer immutable data - use swap! instead of manual mutation
  3. Use pattern matching (tag-match, record-match) for control flow
  4. Leverage threading macros (->, ->>) for data pipelines
  5. Use enums for result types instead of exceptions
  6. 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

Calcit Editor

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

Hovernia Editor

Ecosystem

Libraries:

Useful libraries are maintained at https://libs.calcit-lang.org/base.cirru.

Frameworks:

Tools:

VS Code Integration:

Package Registry: