Introduction

Calcit scripting language.

an interpreter for calcit snapshot, and hot code swapping friendly.

Calcit is an interpreter built with Rust, and also a JavaScript code emitter. It's inspired mostly by ClojureScript. Calcit-js emits JavaScript in ES Modules syntax.

You can try Calcit WASM build online for simple snippets.

TODO

Features

  • Immutable Data

Values and states are represented in different data structures, which is the semantics from functional programming. Internally it's im in Rust and a custom finger tree in JavaScript.

  • Lisp(Code is Data)

Calcit-js was designed based on experiences from ClojureScript, with a bunch of builtin macros. It offers similar experiences to ClojureScript. So Calcit offers much power via macros, while keeping its core simple.

  • Indentations

With bundle_calcit command, Calcit code can be written as an indentation-based language. So you don't have to match parentheses like in Clojure. It also means now you need to handle indentations very carefully.

  • Hot code swapping

Calcit was built with hot swapping in mind. Combined with calcit-editor, it watches code changes by default, and re-runs program on updates. For calcit-js, it works with Vite and Webpack to reload, learning from Elm, ClojureScript and React.

  • ES Modules Syntax

To leverage the power of modern browsers with help of Vite, we need another ClojureScript that emits import/export for Vite. Calcit-js does this! And this page is built with Calcit-js as well, open Console to find out more.

Features from Clojure

Calcit is mostly a ClojureScript dialect. So it should also be considered a Clojure dialect.

There are some significant features Calcit is learning from Clojure,

  • Runtime persistent data by default, you can only simulate states with Refs.
  • Namespaces
  • Hygienic macros(although less powerful)
  • Higher order functions
  • Keywords
  • 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.

Installation

You need to install Rust first, then install Calcit with Rust:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

cargo install calcit_runner

then Calcit is available as a Rust command line:

cr -e "echo |done"

Modules directory

No package manager yet, need to manage modules with git tags.

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 git clone.

Rust bindings

A demo project can be found at https://github.com/calcit-lang/dylib-workflow

Currently two APIs are supported, based on Cirru EDN data.s

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.1")
}
}

(This feature is not stable enough yet.)

Run Calcit

There are several modes to run Calcit.

Eval

cr -e 'println "Hello world"'

which is actually:

cr --eval 'println "Hello world"'

Run program

For a local compact.cirru file, run:

cr

by default, Calcit has watcher launched. If you want to run without a watcher, use:

cr -1

Generating JavaScript

cr --emit-js

Generating IR

cr --emit-ir

Run in Eval mode

$ cr -e 'echo |demo'
1
took 0.07ms: nil

$ cr -e 'echo "|spaced string demo"'
spaced string demo
took 0.074ms: nil

CLI Options

$ cr --help
Calcit Runner 0.5.14
Jon. <jiyinyiyong@gmail.com>
Calcit Runner

USAGE:
    cr [FLAGS] [OPTIONS] [--] [input]

FLAGS:
        --emit-ir        emit EDN representation of program to program-ir.cirru
        --emit-js        emit js rather than interpreting
    -h, --help           Prints help information
    -1, --once           disable watching mode
        --reload-libs    reload libs data during code reload
    -V, --version        Prints version information

OPTIONS:
    -d, --dep <dep>...             add dependency
        --emit-path <emit-path>    emit directory for js, defaults to `js-out/`
        --entry <entry>            overwrite with config entry
    -e, --eval <eval>              eval a snippet
        --init-fn <init-fn>        overwrite `init_fn`
        --reload-fn <reload-fn>    overwrite `reload_fn`
        --watch-dir <watch-dir>    a folder of assets that also being watched

ARGS:
    <input>    entry file path, defaults to compact.cirru [default: compact.cirru]

Bundle Mode

By design, Calcit program is supposed to be written with calcit-editor. And you can try short snippets in eval mode.

If you want to code larger program with calcit-editor, it's also possible. Find example in minimal-calcit.

With bundle_calcit command, Calcit code can be written as an indentation-based language. So you don't have to match parentheses like in Clojure. It also means now you need to handle indentations very carefully.

Before you run Calcit, you need to bundle files first into a compact.cirru file. Then use cr command to run it. Hot code swapping is not available in this way since it requires a .compact-inc.cirru for detecting changes.

Data

  • Bool
  • Number, which is actuall f64 in Rust and Number in Rust
  • Keyword
  • String
  • Vector, serving the role as both List and Vector
  • HashMap
  • HashSet
  • Function.. maybe, internally there's also a "Proc" type.

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.

Run in Eval mode

$ cr -e 'echo |demo'
1
took 0.07ms: nil

$ cr -e 'echo "|spaced string demo"'
spaced string demo
took 0.074ms: nil

CLI Options

$ cr --help
Calcit Runner 0.5.14
Jon. <jiyinyiyong@gmail.com>
Calcit Runner

USAGE:
    cr [FLAGS] [OPTIONS] [--] [input]

FLAGS:
        --emit-ir        emit EDN representation of program to program-ir.cirru
        --emit-js        emit js rather than interpreting
    -h, --help           Prints help information
    -1, --once           disable watching mode
        --reload-libs    reload libs data during code reload
    -V, --version        Prints version information

OPTIONS:
    -d, --dep <dep>...             add dependency
        --emit-path <emit-path>    emit directory for js, defaults to `js-out/`
        --entry <entry>            overwrite with config entry
    -e, --eval <eval>              eval a snippet
        --init-fn <init-fn>        overwrite `init_fn`
        --reload-fn <reload-fn>    overwrite `reload_fn`
        --watch-dir <watch-dir>    a folder of assets that also being watched

ARGS:
    <input>    entry file path, defaults to compact.cirru [default: compact.cirru]

Ecosystem

Libraries:

Frameworks:

Tools: