Skip to main content

calc_rs

A strongly typed, spreadsheet-style expression engine — currency, tonnage, percentages, dates, durations, and tables are distinct first-class types, not just decorated numbers. A Rust rewrite of Attica-oss/calc, usable as a CLI, a Rust library, or a Python extension module.

Install

CLI — a prebuilt binary, no Rust toolchain needed:

uv tool install calc-rust          # then run: calc_rs
uvx --from calc-rust calc_rs '$5.00 * 3'
pipx install calc-rust

or from source:

cargo install --path . --bin calc_rs

Python library (import calc_rs) — also ships the CLI as python -m calc_rs, for locked-down environments that block standalone executables but allow pip packages:

pip install calc-rs-lang
python -m calc_rs '$5.00 * 3'
python -m calc_rs                 # REPL

(The pure-Python front end has no line editing or syntax highlighting, but the same value formatting — tables and all — via calc_rs.render.)

CLI usage

For the full language reference — every type, operator, cast, and built-in function, with worked examples — see docs/USAGE.md. The quick tour below covers the basics.

calc_rs '$5.00 * 3'            # $15.00  (currency)
calc_rs                        # start the REPL
calc_rs --bare '1 + 2 * 3'     # 7        (value only, for scripts)
calc_rs path/to/script.calc    # run a file — same as any other interpreter
calc_rs run main.calc          # run a project: main.calc can `import` its sibling files

That last form is what makes an editor's "run current file" binding a one-liner — see docs/LSP.md for editor setup (diagnostics, completion, hover) and this repo's own .zed/tasks.json for a worked Zed example (task: spawn → "calc: run current file").

A project is just a directory of .calc files — import "lib/tax" loads lib/tax.calc, relative to the entry file's folder, and everything it declares (fn, struct, enum, impl) lands in scope. See Projects and modules and the runnable examples/project/.

Statements are separated by ; or a newline, so scripts read naturally:

calc_rs $'let price = $12.50\nlet qty = 3\nprice * qty'   # $37.50

let is a one-time declaration for the rest of the session — let x = 1 a second time is an error. let mut x = 1 allows reassigning x later, to the same type freely or a different type only via an explicit cast (let x = x::TEXT). See docs/USAGE.md for the full rule.

struct/enum declarations persist the same way fn does, and a type name (like every enum variant) must start uppercase:

struct Person { name: text, age: int }
enum Status { Open, Pending, Closed }

let ada = Person { name: "Ada", age: 30 }
ada::name                              # "Ada"     — field access
match Status::Open { Status::Open => "new", _ => "old" }

A struct can grow methods in a separate impl block — Self stands for the enclosing struct, and receiver::method(args) calls one:

impl Person {
  fn is_adult(self: Self) -> boolean = self::age >= 18
}
ada::is_adult()                        # TRUE

See docs/USAGE.md for field-access rules, nominal typing, the struct-literal/match-subject disambiguation, and impl blocks and methods.

:: is genuinely overloaded between a scalar cast (x::DATE) and field access (t::date, ada::name) — the target's case, as written, decides which every time, and a column/field name is itself always normalized to lower_snake_case ("Date of Service" → date_of_service), so a real field can never be shaped like an uppercase cast target in the first place. See docs/USAGE.md.

range(5) is [0, 1, 2, 3, 4]; range(start, stop[, step]) and the half-open 1..10 (== range(1, 10)) give integer sequences to iterate or pipe: range(1, 101) |> sum. 1..=10 is the inclusive counterpart ([1, .., 10]); single-quoted 'a'..'d'/'a'..='d' do the same over chars (['a','b','c']/['a','b','c','d']) — a char literal like 'a' is distinct from the double-quoted text "a".

x! is postfix factorial (5! == 120, ints 0..=20), and A @ B is real matrix multiplication (distinct from A * B's elementwise one): matrix(array(1,2),array(3,4)) @ matrix(array(5,6),array(7,8)) == [[19,22],[43,50]].

Conditionals

if COND then A else B is an expression — it has a value, so it nests and pipes like any other. It's sugar for the lazy if(COND, A, B) builtin: only the taken branch is evaluated, and both branches must have the same type.

if score >= 90 then "A"
else if score >= 80 then "B"
else "C"

match compares one value against a list of alternatives and usually needs a _ catch-all last:

match status {
  "OPEN"    => 1,
  "PENDING" => 2,
  _         => 0,
}

Every arm must share a type and each pattern must be comparable to the subject with =. The subject (status, here) is evaluated exactly once no matter how many patterns it's checked against; patterns past the one that matches are never evaluated. (if, then, else, match, and _ are ordinary identifiers everywhere else — if(...) is still the builtin call.)

_ can be left out only when every case is provably covered some other way — currently just a match over an enum with every variant named by a literal Enum::Variant pattern:

enum Status { Open, Closed }
match Status::Open { Status::Open => 1, Status::Closed => 0 }   // no '_' needed

See docs/USAGE.md for why _ is otherwise mandatory (there's no general exhaustiveness checking) and what does/doesn't count as covering a variant.

Text

split("a,b,c", ",")            # ["a", "b", "c"]
concat(" / ", false, parts)    # "a / b / c"    (also works on a column)
replace("2026-01-02", "-", "") # "20260102"
contains(s, "err")  ·  starts_with(s, "S")  ·  ends_with(s, ".csv")
trim("  x  ")  ·  pad_left("7", 3)  ·  pad_right("7", 3)

Double-quoted strings interpolate ${…}: "Hi ${name}, ${n + 1} rows". Each ${expr} is cast to text; write a literal ${ as \${. The expression can hold balanced braces ("${match n { 1 => 10, _ => 0 }}") but not its own " — use concat(...) for that.

Sequences

map / filter / reduce / any / all / reverse work on an array or a column. Their expression argument is row-scoped like a table verb's, with [_] bound to the current element (and [acc] to the running total in reduce):

range(1, 11)
  |> filter([_] % 2 == 0)     # [2, 4, 6, 8, 10]
  |> map([_] * [_])           # [4, 16, 36, 64, 100]
  |> reduce(0, [acc] + [_])   # 220

any(scores, [_] < 50)         # is anyone failing?

map and filter return a plain array; reduce returns whatever its starting value's type is, and its expression must match that type. Equality is = (or ==); % and // stay integer when both operands are integers. sort works on arrays/columns too, ascending or sort(arr, "desc").

range(start, ∞) (or start..∞) gives a lazy sequence instead — nothing computed until take() asks for it. map/filter chain onto it without ever materializing an intermediate array:

1..∞ |> map(|x| x * 2) |> filter(|x| x % 3 == 0) |> take(5)   # [6, 12, 18, 24, 30]

Closure params in that position (|x|) take their type from the sequence and can't carry an annotation — same rule as [_]. reduce/any/all/ sort don't accept a lazy sequence yet; take() it into an array first.

Standard library

import "name" brings a bundled module of ordinary calc fns into scope — math (clamp/lerp/sign), stats (mean/median/ variance/stdev/zscore), finance (npv/fv/pv/pmt), and calendar (is_weekend/next_business_day/fiscal_quarter):

import "stats"
import "finance"

median(scores)
npv(0.08, cashflows)

See docs/USAGE.md for every function's signature — and its source, stdlib/*.calc, if you want to see map/filter/reduce used for real.

Prelude

fn and let definitions persist across a REPL session but not between them. To keep a library of helpers, put them in a .calc file: ~/.config/calc_rs/prelude.calc is loaded automatically, or pass --prelude FILE (--no-prelude skips it). It applies to one-shot runs too.

# ~/.config/calc_rs/prelude.calc
fn age(dob: date) -> int = today()::YEAR - dob::YEAR
let vat = 20%
calc_rs 'age(1990-06-15)'          # 35
calc_rs --prelude team.calc 'headcount |> avg'

The REPL is a plain line editor with live syntax highlighting. A newline inside an expression is insignificant (1 +⏎2 is 3), and the prompt keeps reading while a line has an open (/[ or ends on an operator — so multi-line table(...) / sort(filter(...)) just work, with each continuation line auto-indented to its bracket depth. Press Enter on a blank line twice to force-submit anyway and let the parser point at whatever's unbalanced. ↑/↓ recall previous input.

REPL commands start with : so they never shadow your own names — :help, :vars (what's bound), :clear, :reset (forget every variable), :quit (or Ctrl-D). A trailing ; on an expression hides its value and shows only its type.

Tab completes function names, plus the variables and fns you've defined this session (sel⇥ → select; several matches fill in as far as they agree). Once you open a call, a dimmed signature trails the cursor — round( → x: number, digits: int = 0) — shrinking as you fill in arguments.

Editor support

calc_rs --lsp runs the same binary as a Language Server (diagnostics, completion, hover) — cargo install --path ., then see docs/LSP.md for editor wiring.

Tables

Tables are ordinary values — build, edit, query, and persist them with functions, in the REPL or one-shot:

table("item:text, qty:int, price:currency")   # empty, typed
column("day", "Mon", "Tue")                    # a named column
table(column("x", 1, 2), column("y", 3, 4))    # from columns
table(Item, rows)                # from an array of Item rows, in one pass

append(t, "Bolt", 250, $0.08)     # add a row      (type-checked)
append(t, Item { item: "Bolt", qty: 250, price: $0.08 })   # or the whole row as a struct
extend(t, "total", [qty] * [price])   # add a computed column
setcell(t, 0, "qty", 5)           # replace one cell
droprow(t, 2)  ·  rename(t, "price", "unit_price")

select(t, "item", "total")  ·  filter(t, [qty] > 100)
sort(t, [total], "desc")  ·  groupby(t, "item", "total", "sum")
lookup(t, "port", ports, "code", "name")   # add a matched column

open("sales.json")                # read a JSON workbench file
save(sort(open("sales.json"), [total], "desc"), "ranked.json")

load("shipments.csv", "date:date, port:text, box:container, amount:currency")

load imports CSV against a declared schema and casts every cell to its type. A bad cell doesn't become text — it's reported with its line, column, raw value, and the reason, and load fails:

shipments.csv: 2 problem(s):
  row 45, column "date": "2026-13-01" is not a valid date.
  row 184, column "box": Invalid container check digit: expected 6, got 0.

A declared struct works in place of the schema string, for both table() and load() — write the shape once, reuse it everywhere that shape recurs, rather than retyping the same "name:type, ..." string every time:

struct Shipment { date: date, port: text, box: container, amount: currency }

table(Shipment)                     # an empty table of that shape
load("jan.csv", Shipment)           # and the same shape for every file
load("feb.csv", Shipment)

Each verb returns a new table, so they compose. [col] inside filter / sort / extend refers to that column in the current row.

The |> operator threads a value into the next call's first argument — x |> f(a) is f(x, a) — so a pipeline reads top-to-bottom instead of inside-out. In the REPL a line that opens with |> continues from the last result (|> f runs _ |> f), so you can grow a pipeline one line at a time:

open("sales.json")
  |> filter([region] == "EU")
  |> extend("net", [gross] - [tax])
  |> groupby("region", "net", "sum")
  |> sort([sum_net], "desc")
  |> save("eu_by_region.json")

Python usage

import calc_rs

calc_rs.evaluate("$5.00 * 3")          # Currency($15.00)
calc_rs.evaluate("1 + 2 * 3")          # 7
calc_rs.check("$5.00 * 3")             # 'currency'  (no evaluation)

calc = calc_rs.Calculator()
calc.set("price", calc_rs.Currency("12.50"))
calc.eval("let qty = 3")
calc.eval("price * qty")               # Currency($37.50)
calc.get("qty")                        # 3

Currency, tonnage, percent, duration, and complex results come back as dedicated classes — Currency/Tonnage/Percent expose .amount, Duration exposes .months/.days/.seconds, Complex exposes .real/.imag. Numbers, text, booleans, and dates/times come back as native Python types. The wheel ships a type stub and py.typed.

Rust library

[dependencies]
calc_rs = { git = "https://github.com/Attica-oss/calc_rs" }

Everything the common cases need is re-exported at the crate root (or grab it in one line with use calc_rs::prelude::*):

use calc_rs::prelude::*;

// one-shot: an expression or a full `let x = 5; x + 1` script
let out = evaluate("$5.00 * 3")?;
assert_eq!(out.to_string(), "$15.00");
assert_eq!(out.ty.to_string(), "currency");

// a session that remembers variables and `_`, like the REPL
let mut calc = Calculator::new();
calc.eval("let price = $12.50")?;
calc.eval("let qty = 3")?;
assert_eq!(calc.eval("price * qty")?.to_string(), "$37.50");

// type-check without evaluating
assert_eq!(calc.check("price * qty")?.to_string(), "currency");

evaluate and Calculator::eval return an Evaluated — value (a Value), ty (the inferred Type), bindings (every let the script made), assigned, and used_vars. For a custom function registry, drop to calc_rs::run_script(source, vars, functions).

Notebook

A marimo notebook backed by this engine lives in notebook/, replicating the expression-editor and table-builder workflow of the original Python project's notebook:

pip install -e '.[notebook]'
maturin develop --release
marimo edit notebook/calc_notebook.py

Development

cargo test                     # engine tests
maturin develop --release      # build + install the Python extension

License

MIT — see LICENSE.

Release files for calc-rs-lang 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for calc-rs-lang 0.2.1
File Size Uploaded
calc_rs_lang-0.2.1.tar.gz 352.5 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for calc-rs-lang 0.2.1
File
calc_rs_lang-0.2.1-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details
calc_rs_lang-0.2.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details
calc_rs_lang-0.2.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details
calc_rs_lang-0.2.1-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details
calc_rs_lang-0.2.1-cp310-abi3-macosx_10_12_x86_64.whl CPython 3.10 abi3 macOS 10.12+ x86-64 Details

Total release size: 9.0 MB

Release files / calc_rs_lang-0.2.1.tar.gz

Download URL calc_rs_lang-0.2.1.tar.gz
Size 352.5 kB
Tags Source
SHA-256 checksum
How to use checksums
5d6ead70bcb68b5a239bc470658f4a8bd7f91c441a3a3d33199a5bbde63b14e5
BLAKE2b-256 checksum
How to use checksums
b9a1c3b9b99458924bac8cd6ac9ee96effdf3a693a11111c6ac2435aa7756f3a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release files / calc_rs_lang-0.2.1-cp310-abi3-win_amd64.whl

Download URL calc_rs_lang-0.2.1-cp310-abi3-win_amd64.whl
Size 1.5 MB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
a29103e6026464d8ea8aef2e6036f496204c42f01e25ba3529b16490a3ebf83d
BLAKE2b-256 checksum
How to use checksums
832c925c6fc611f9e402c7200964f493696436984eb09fb328f208a12a0b0eec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release files / calc_rs_lang-0.2.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL calc_rs_lang-0.2.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 1.9 MB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
3784e6ce7f8f316a8c7828e10d7126c6725522a974ae671997782f2529016c9b
BLAKE2b-256 checksum
How to use checksums
c1e82152ea8d42d78b9124842aed680b2a28d3b72460bebd4c2e031991225863
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release files / calc_rs_lang-0.2.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL calc_rs_lang-0.2.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 1.9 MB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
b64757c52c7154791a0e906c4a98774ad1c8506c389aef6a8c18fc45e5b90c3a
BLAKE2b-256 checksum
How to use checksums
c1771e0b0b76267c49a582a6115f65c2f063ac76528ffa48da8fce21fd74e162
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release files / calc_rs_lang-0.2.1-cp310-abi3-macosx_11_0_arm64.whl

Download URL calc_rs_lang-0.2.1-cp310-abi3-macosx_11_0_arm64.whl
Size 1.7 MB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
86b8be6f5ed7b82099bba04148848a3f714dc69d30b41a11056914576d7af85c
BLAKE2b-256 checksum
How to use checksums
cd099a9916b508470c8eaf8bab42fec5d9833e2ea050cdeb5201cc926c4a170c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release files / calc_rs_lang-0.2.1-cp310-abi3-macosx_10_12_x86_64.whl

Download URL calc_rs_lang-0.2.1-cp310-abi3-macosx_10_12_x86_64.whl
Size 1.7 MB
Tags CPython 3.10 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
b394407a04369a9ac7b1b65afa9731569ef083e767ec90ed935339d874599db0
BLAKE2b-256 checksum
How to use checksums
d5b6efcd353f906e292d0757c82792767f1058a5ea96eaba772fc83e32396acf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.1 This release

6 release files

0.2.0

6 release files

0.1.0

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page