Language Reference

Formal specification for the CalcMark language.

On this page

This is the complete and authoritative specification for the CalcMark language. The sidebar table of contents is generated automatically from headings.


Overview #

CalcMark is a calculation language that blends seamlessly with Markdown. It allows calculations to live naturally within prose documents.

Design Goals #

  • Familiar: Syntax feels like calculator/spreadsheet usage
  • Minimal: Only essential features, no unnecessary complexity
  • Unambiguous: One way to do things, clear error messages
  • Unicode-aware: Full international character support
  • Markdown-compatible: Works within existing Markdown documents

Key Characteristics #

  • Line-based: Each line is classified independently
  • Context-aware: Variables must be defined before use
  • Strongly typed: Minimal type coercion, clear type errors
  • Arbitrary precision: Decimal math for exact results

Philosophy #

Calculation by Exclusion #

CalcMark uses “calculation by exclusion” - if a line looks like Markdown, it’s Markdown. Only unambiguous calculations are treated as calculations.

# My Budget          -> MARKDOWN (header prefix)
salary = $5000       -> CALCULATION (assignment)
This is text         -> MARKDOWN (natural language)
5 + 3                -> CALCULATION (arithmetic)
- List item          -> MARKDOWN (bullet prefix)
-5 + 3               -> CALCULATION (negative number)
$100 budget          -> MARKDOWN (trailing text)

Explicit Over Implicit #

  • Spaces NOT allowed in identifiers (my_budget, not my budget)
  • Forward references not allowed (define before use)
  • Type mismatches are errors (no silent coercion)
  • Reserved keywords cannot be variable names

Document Model #

A CalcMark document is a sequence of lines. Each line is independently:

  1. Classified as BLANK, MARKDOWN, or CALCULATION
  2. Parsed (if CALCULATION)
  3. Validated (optional, produces diagnostics)
  4. Evaluated (if CALCULATION and valid)

Three Line Types #

TypeDescriptionExamples
BLANKEmpty or whitespace-only"", " ", "\t"
MARKDOWNProse, headers, lists, or invalid calculations"# Header", "Some text", "- Item"
CALCULATIONValid CalcMark expression or assignment"x = 5", "10 + 20", "salary * 12"

Frontmatter #

A CalcMark document can begin with a YAML frontmatter block delimited by ---. Frontmatter defines document-level configuration that is available to all calculations.

calendar_year_offset #

Selects which calendar year a fiscal-year label refers to. 'before' (default) — FY label = year FY ends in (Australian government year, US tax year, most publicly traded companies). 'after' — FY label = year FY starts in (some companies). Has no effect when fiscal_year_starts is January.

Syntax
calendar_year_offset: before|after
Example
calendar_year_offset: after

convert_to #

Convert quantity results to a measurement system (si or imperial). Accepts a system name or a map with system and unit_categories. Valid categories: Acceleration, Area, Currency, Custom, DataSize, Energy, Force, Frequency, Impulse, Length, Mass, Number, Power, Pressure, Speed, Temperature, Time, Volume.

Syntax
convert_to: <system>
Example
convert_to: si

exchange #

Define currency conversion rates. Keys use FROM_TO format with 3-letter ISO 4217 codes (e.g., USD_EUR).

Syntax
exchange: FROM_TO: rate
Example
exchange: USD_EUR: 0.92

fiscal_year_starts #

Anchors fiscal-period expressions (FQ1, FY26, 'this fiscal quarter') to a calendar start. Accepts a month name (january through december, or short forms jul/oct), optionally followed by a day of the month (defaults to 1). Without this key, all fiscal expressions error out.

Syntax
fiscal_year_starts: <Month> [<Day>]
Example
fiscal_year_starts: July 1

globals #

Define document-wide variables. Values are CalcMark expressions evaluated before the document body.

Syntax
globals: name: value
Example
globals: tax_rate: 0.32 base_price: $100

measurement #

Configure how ambiguous unit names are interpreted. Each axis is independent. Only axes that differ from US Customary defaults need to be specified. "standard" mass means avoirdupois (everyday weight: 1 oz = 28.35g). "troy" mass is for precious metals (1 troy oz = 31.10g). Optional strict: true/false controls whether formatter annotates bare ambiguous units in output.

Syntax
measurement: volume: us|imperial mass: standard|troy ton: short|long|metric
Example
measurement: volume: imperial mass: troy

scale #

Multiply quantity results by a factor. Accepts a number or a map with factor and unit_categories. Currency scales only when Currency is listed in unit_categories. Valid categories: Acceleration, Area, Currency, Custom, DataSize, Energy, Force, Frequency, Impulse, Length, Mass, Number, Power, Pressure, Speed, Temperature, Time, Volume. Temperature excluded by default.

Syntax
scale: <factor>
Example
scale: 2

Exchange Rates #

Define currency conversion rates using FROM_TO: rate format (underscore separator):

---
exchange:
  USD_EUR: 0.92
  EUR_GBP: 0.86
  USD_GBP: 0.79
  GBP_USD: 1.27
---

Rates are not automatically reversed. If you define USD_EUR, you must also define EUR_USD to convert in the other direction.

Global Variables #

Define values available throughout the document:

---
globals:
  tax_rate: 0.32
  base_price: $100
  start_date: Jan 15 2025
  bandwidth: 100 MB/s
---

Globals support all CalcMark literal types (numbers, currencies, quantities, dates, durations, rates, booleans, percentages). Expressions like 1 + 1 are not allowed – only literal values.

Scale #

Multiply all quantity results by a factor. Applied after evaluation, before display.

---
scale: 2
---

Scale accepts a number or a map with factor and optional unit_categories:

---
scale:
  factor: 4
  unit_categories: [Length, Mass]
---

Rules:

  • Scaling is explicit: you must specify unit_categories for any scaling to occur. A bare scale: 2 sets the factor but scales nothing.
  • Quantities in listed categories are multiplied by the factor
  • Currency scales only when Currency is listed in unit_categories
  • Number (unitless values) scales only when Number is listed in unit_categories
  • Boolean, Date, Duration, and Rate are always immune to scale
  • The special keyword All matches every category: unit_categories: [All]
  • Expressions containing @scale are exempt from scaling to prevent double-scaling

Valid categories: Acceleration, All, Area, Currency, Custom, DataSize, Energy, Force, Frequency, Impulse, Length, Mass, Number, Power, Pressure, Speed, Temperature, Volume.

@Directive References #

Use @scale and @globals.name to reference frontmatter values in expressions:

per_unit = total_cost / @scale
tax = income * @globals.tax_rate

@scale resolves to the numeric scale factor from frontmatter. Requires scale: to be defined.

@globals.name resolves to the typed value of a named global. Requires globals: with that key.

Validation rules:

ReferenceValid whenError otherwise
@scalescale: defined in frontmatter@scale requires 'scale:' in frontmatter
@globals.nameglobals: has name keyundefined global 'name'; defined globals: ...
@globals (no field)NeverParser error: @globals requires a field name
@globals.a.bNeverParser error: nested dots not supported
@exchange, @convert_to, @fooNevernot a supported directive; use @scale or @globals.name

@scale always resolves to a Number. @globals.name resolves to whatever type the global is (Number, Currency, Quantity, etc.).

Convert To #

Convert quantity results to a target measurement system. Applied after scale.

---
convert_to: si
---

Valid systems: si (metric) and imperial (US customary). Accepts a string or a map with system and optional unit_categories:

---
convert_to:
  system: imperial
  unit_categories: [Length, Volume]
---

Rules:

  • Quantities already in the target system are unchanged
  • Explicit in conversions override convert_to (the user chose the unit)
  • Custom units (e.g., eggs, servers) have no system mapping and are not converted
  • Currency, numbers, and other non-quantity types are unaffected
  • Rates have their amount converted, leaving the time denominator unchanged
  • Frequency units are unaffected by convert_to — hertz is universal across measurement systems

Valid categories: Acceleration, All, Area, Currency, Custom, DataSize, Energy, Force, Frequency, Impulse, Length, Mass, Number, Power, Pressure, Speed, Temperature, Volume.

Measurement Conventions #

Some unit names are ambiguous — a “gallon” in the US (3.785 L) is different from a “gallon” in the UK (4.546 L). Similarly, an “ounce” of gold (troy, 31.10g) differs from an “ounce” of flour (standard, 28.35g).

CalcMark defaults to US Customary definitions for backwards compatibility. Use measurement: to declare which conventions your document uses:

measurement:
  volume: imperial    # gallon, pint, fl oz → Imperial (UK) definitions
  mass: troy          # ounce, pound → troy (precious metals) definitions
  ton: long           # ton → Imperial long ton (2240 lb)

Each axis is independent — only specify axes that differ from the US defaults.

AxisOptionsDefaultAffected Units
volumeus, imperialusgallon, quart, pint, fl oz, cup
massstandard, troystandardounce, pound
tonshort, long, metricshortton

What does “standard” mass mean? Standard is the avoirdupois system — the everyday weight you encounter at the grocery store and on bathroom scales (1 oz = 28.35g, 1 lb = 16 oz). Troy weight is used for precious metals and gemstones (1 troy oz = 31.10g, 1 troy lb = 12 troy oz).

Inline Qualifiers #

Override the document convention for a single expression using explicit prefixes:

gold = 10 troy oz          # Always troy, regardless of frontmatter
milk = 2 imp pt            # Always imperial pint
shipping = 5 short ton     # Always US short ton

Available prefixes: us, imp/imperial, troy, short, long, metric.

Strict Mode #

By default, when measurement: is present, the formatter annotates bare ambiguous units in output so readers always know which definition is active:

SourceFrontmatterOutput
2 oz(none)2 us oz
2 ozmass: troy2 troy ounce
2 troy oz(any)2 troy oz (already explicit)

Set strict: false to suppress annotation:

measurement:
  volume: imperial
  strict: false       # bare units display without prefix

Precedence #

config.toml (global default) < frontmatter (per-document) < inline qualifier (per-expression).

Interaction with Other Directives #

  • convert_to: Independent. measurement controls input interpretation (which gallon definition is used). convert_to controls output system (convert everything to SI or Imperial). They compose cleanly.
  • scale: Unaffected. Scale multiplies values; measurement resolves unit identities.
  • locale: Independent. Locale controls number formatting (decimal/thousand separators), not unit definitions.

All three directives compose. Here’s how 1 gallon flows through each combination:

DirectivesResultWhat happened
(none)1 galUS gallon, default display
measurement: { volume: imperial }1 imperial gallonResolved as imperial
convert_to: si3.79 lUS gallon → liters
measurement: { volume: imperial } + convert_to: si4.55 lImperial gallon → liters
measurement: { volume: imperial } + scale: 3 + convert_to: si13.6 lImperial gallon × 3 → liters
1 us gal (inline) + measurement: { volume: imperial } + convert_to: si3.79 lInline qualifier overrides convention
1 gallon in ml (explicit) + convert_to: si3,785 mlExplicit in skips convert_to

Transform Order #

When frontmatter directives are present, they apply in this order:

  1. Resolve measurement conventions (bare unit names mapped to qualified forms)
  2. Evaluate all expressions
  3. Scale quantity results
  4. Convert to target measurement system
  5. Annotate ambiguous units in output (strict mode)

See the Recipe Scaling example for a complete walkthrough, and the User Guide — Frontmatter for a gentler introduction.

Template Interpolation #

Template variables embed calculated values into prose. After all calculations are evaluated, {{variable_name}} tags in text blocks are replaced with display-formatted results. Resolved values render bold in Markdown and are wrapped in <span class="cm-interpolated"> in HTML.

Forward References #

The primary use case is forward references — a summary at the top of a document that pulls in values computed below:

## Executive Summary

| Metric | Value |
|--------|-------|
| Revenue | {{total_rev}} |
| Gross Margin | {{gross_margin}} |
| Team | {{team_hc}} |


total_rev = $4.2M
gross_margin = 28%
team_hc = 14 people

The summary table renders with $4.2M, 28%, and 14 people — even though the calculations appear below the text.

Inline Formatting #

Combine {{var}} with Markdown formatting for emphasis, headings, and lists:

The grand total is {{total_cost}}.

_Team: {{headcount}}_

## Budget: {{annual_budget}}

- Revenue: {{revenue}}
- Costs: {{costs}}
- Margin: {{margin}}

Backticks around tags are consumed — `{{var}}` renders as value, not value. This prevents interpolated values from appearing as inline code.

Tables #

Interpolation works naturally in Markdown tables. Use it for dashboard-style summaries:

| Scenario | Revenue | Margin |
|----------|---------|--------|
| Baseline | {{base_rev}} | {{base_margin}} |
| Optimistic | {{opt_rev}} | {{opt_margin}} |
| Stressed | {{stress_rev}} | {{stress_margin}} |

Syntax #

{{variable_name}} — variable names use word characters (letters, digits, underscore). Whitespace inside braces is tolerated: {{ total_rev }} resolves the same as {{total_rev}}.

Rules #

BehaviorDetail
Resolved valuesDisplay-formatted with locale, currency symbols, units, K/M/B suffixes
Markdown renderingResolved values are bold; backticks around tags are stripped
HTML renderingResolved values wrapped in <span class="cm-interpolated">
Scale / convert_toApplied before formatting — interpolated values match CalcBlock display
Missing variables{{unknown}} is left as-is in the output
ScopeText blocks only — {{var}} inside a CalcBlock is a syntax error, not interpolation
Round-trip safetySaving a .cm file preserves raw {{var}} tags (interpolation is render-time only)
Empty / expression tags{{}} and {{a + b}} are not matched
Directives{{@scale}} and {{@globals.name}} are not currently supported — use a variable alias

JSON output: The JSON formatter includes both source (raw text with {{var}} tags) and interpolated_source (resolved text) for programmatic consumers.

See the Services P&L example for a production use of interpolated summary tables, and the Household Budget for inline results in prose.

Line Classification #

Classification Rules #

Lines are classified in this order:

  1. BLANK — Empty or only whitespace
  2. INDENTED CODE → MARKDOWN — Line starts with 4+ spaces or a tab
  3. FENCED CODE BLOCK → MARKDOWN — Lines between ``` or ~~~ fences (stateful; all content inside is Markdown regardless of what it looks like)
  4. MARKDOWN pattern — Matches a known CommonMark construct:
    • Block-level: # (ATX heading), > (blockquote), - / * / + (unordered list), digit. (ordered list), --- / *** / ___ (horizontal rule), === / --- (setext heading underline), ``` / ~~~ (fenced code fence)
    • Inline-level at start of line: ![ (image), [text](url) (inline link), [id]: url (link definition), **text** (bold formatting)
  5. CALCULATION — Attempt to parse and validate:
    • Starts with a literal (number, currency, boolean)
    • Contains assignment (=)
    • Is a valid expression
    • All variables are defined (context-aware)
  6. MARKDOWN (fallback) — Anything else

Context-Aware Classification #

x = 5               -> CALCULATION (assignment)
y = x + 10          -> CALCULATION (x is defined)
z = unknown * 2     -> MARKDOWN (unknown is undefined)

Edge Cases #

InputClassificationReason
$100 budgetMARKDOWNTrailing text after valid token
-5 + 3CALCULATIONNegative number (no space after -)
- 5MARKDOWNBullet list (space after -)
+ itemMARKDOWNUnordered list (+ marker with space)
x = 10MARKDOWNIndented code block (4-space prefix)
![alt](img.png)MARKDOWNImage syntax
[ref]: https://…MARKDOWNLink definition
x *MARKDOWNIncomplete expression
averageMARKDOWNNot reserved, not in context
avgMARKDOWNReserved keyword alone (not a valid expression)

Syntax & Grammar #

EBNF Grammar #

The grammar below covers the core expression hierarchy. Natural language function syntax (described in Natural Language Syntax) is parsed at the Primary level but omitted here for clarity.

Statement       ::= Assignment | Expression
Assignment      ::= IDENTIFIER "=" Expression
Expression      ::= Or
Or              ::= And ("or" And)*
And             ::= Comparison ("and" Comparison)*
Comparison      ::= Additive (ComparisonOp Additive)*
ComparisonOp    ::= ">" | "<" | ">=" | "<=" | "==" | "!="
Additive        ::= Multiplicative (("+"|"-") Multiplicative)*
                     ("as" ConversionTarget)?
ConversionTarget ::= "napkin" | "precise" | UnitName
Multiplicative  ::= Exponent (("*"|"/"|"%") Exponent)*
                     ("in" UnitName)?
                     ("per" TimeUnit)?
                     ("over" Expression)?
                     ("at" Expression "per" Expression ("with" Expression)?)?
Exponent        ::= Unary ("^" Unary)*
Unary           ::= ("not" | "-" | "+")* Postfix
Postfix         ::= Primary ("%" ("of" Expression)?)?
Primary         ::= Number | Currency | Quantity | Percentage
                   | Boolean | Date | Duration | Rate
                   | DirectiveRef | Identifier
                   | FunctionCall | "(" Expression ")"
FunctionCall    ::= FunctionName "(" (Expression ("," Expression)*)? ")"
DirectiveRef    ::= "@scale" | "@globals." IDENTIFIER

Operator Precedence #

From highest to lowest:

  1. Parentheses ()
  2. Exponentiation ^ (right-associative)
  3. Unary not, -, + (prefix)
  4. Postfix %, % of
  5. Multiplicative *, /, % (left-associative); postfix in, per, over, at
  6. Additive +, - (left-associative); postfix as
  7. Comparison >, <, >=, <=, ==, !=
  8. Logical AND and (left-associative)
  9. Logical OR or (left-associative)

Type System #

Data Types #

TypeExampleInternal
Number42, 3.14, 1,000Arbitrary-precision decimal
Percentage50%, 8.25%Fractional decimal (0.5, 0.0825)
Currency$100, €50.99Symbol + decimal
Booleantrue, false, yes, noBoolean
Quantity10 meters, 5 kg, 100 MBValue + unit
Duration5 days, 2 weeks, 1 yearValue + time unit
Rate100 MB/s, $50/hour, 1000 req/sNumerator / time unit
DateJan 15 2025, todayCalendar date
PeriodQ1, FQ2, this month, this fiscal quarterCalendar / fiscal span. Structural type — full type-system integration is forthcoming. Today, period-bearing keywords evaluate to Date (the period’s start day). The Period type is defined in spec/types/period.go for a future PR that will plumb it through the interpreter. Period vs Duration distinction in function arguments and Period arithmetic semantics (Period − Period, Period + Duration, equality) are tracked as open design questions — see issues against go-calcmark.

Type Compatibility #

Binary operations (preserve units):

Number + Number -> Number
Currency + Number -> Currency  (unit preserved)
Number + Currency -> Currency  (unit preserved)
Currency + Currency (same symbol) -> Currency
Currency + Currency (different symbols) -> Number  (units dropped)
Quantity + Quantity (same unit) -> Quantity
Date + Duration -> Date
Date - Date -> Duration
Currency / Duration -> Rate  ($1000 / 4 days = $250/day)
Rate * Duration -> Quantity  (via "over" keyword)
Speed * Duration -> Quantity  (bridge: 60 mph * 2 hours = 120 mi)
Number * Rate -> Rate        (scaling: 3 * 10 MB/s = 30 MB/s)
Rate * Number -> Rate        (commutative)
Rate * Quantity -> Quantity   (e.g., 10 MB/s * 500 MB = 5000 MB)
Quantity * Rate -> Quantity   (commutative)

Percentage widening:

When a percentage appears in addition or subtraction with another type, it widens to a proportion of that value:

$100 + 15% -> $115.00        (same as $100 * 1.15)
$100 - 15% -> $85.00         (same as $100 * 0.85)
10 kg + 50% -> 15 kg         (same as 10 kg * 1.5)
15% of 200 -> 30             ("of" syntax)

In multiplication or standalone use, percentages behave as their decimal value (50% = 0.5).

Functions (drop units when mixed):

avg($100, $200) -> $150.00  (same unit preserved)
avg($100, €200) -> 150  (Number, mixed units)
sqrt($100) -> $10.00  (single unit preserved)

Type errors:

Boolean + Number -> ERROR (no boolean arithmetic)
Quantity + Currency -> ERROR (incompatible types)

Literals #

Numbers #

42              Valid integer
3.14            Valid decimal
1,000           Thousands separator (comma)
1_000_000       Thousands separator (underscore)
0.5             Leading zero
.5              Invalid (must have leading zero)
1.2.3           Invalid (multiple decimal points)

Multiplier Suffixes #

10K             -> 10000
5M              -> 5000000
2B              -> 2000000000
1.5T            -> 1500000000000
1.5K            -> 1500

Scientific Notation #

1.2e10          -> 12000000000 (displayed as 12B)
5e3             -> 5000
2.5e-2          -> 0.025

Currency #

Currency literals use either a prefix symbol or a postfix ISO 4217 code.

Symbol syntax (prefix only):

$100            USD
$1,000.50       With separators
€50             EUR
£25.99          GBP
¥1000           JPY
100$            Invalid (symbol must be prefix)
$ 100           Invalid (no space between symbol and number)

Supported symbols: $ (USD), (EUR), £ (GBP), ¥ (JPY)

ISO code syntax (postfix):

Any 3-letter uppercase code works as a postfix currency identifier. Codes with a corresponding symbol display with that symbol; others display with the ISO code:

100 USD         -> $100.00
50 EUR          -> €50.00
25 GBP          -> £25.00
1000 JPY        -> ¥1,000
100 CHF         -> 100 CHF
50 CAD          -> 50 CAD

Semantic validation checks whether the code is a known ISO 4217 currency. Unknown codes produce an invalid_currency_code diagnostic. Currency conversion between different codes requires exchange: rates defined in Frontmatter.

Percentages #

Percentages are their own type, stored as a decimal fraction internally:

50%             -> Percentage (0.5 internally)
8.25%           -> Percentage (0.0825 internally)
15% of 200      -> 30  ("of" syntax)

See Type Compatibility for percentage widening rules.

Booleans #

Case-insensitive keywords:

true, false     Standard
yes, no         Natural language
t, f            Single letter
y, n            Single letter
True, FALSE     Any case

Quantities #

10 meters       Quantity: 10 in meters
5 kg            Quantity: 5 in kilograms
100 MB          Quantity: 100 in megabytes

Custom Units #

Any identifier following a number becomes a unit. CalcMark does not require units to be predefined — these are called custom units:

5 apples        Quantity: 5 apples
1000 req/s      Rate: 1000 requests per second
10 servers      Quantity: 10 servers

Arithmetic with matching custom units preserves the unit. Mismatched custom units produce an error:

5 apples + 3 apples    -> 8 apples
10 servers * 2         -> 20 servers
5 apples + 3 oranges   -> ERROR (incompatible units)

Rates #

100 MB/s        Rate: 100 megabytes per second
$50/hour        Rate: $50 per hour
1000 req/s      Rate: 1000 requests per second
$120000/year    Rate: $120,000 per year

Dates #

Jan 15 2025     Date literal
Dec 25 2025     Date literal
today           Current date
tomorrow        Tomorrow's date
yesterday       Yesterday's date

Durations #

5 days          Duration
2 weeks         Duration
3 months        Duration
1 year          Duration

Identifiers #

  • Must start with letter, underscore, or Unicode character (not digit)
  • Can contain letters, digits, underscores, Unicode, emoji
  • Cannot be reserved keywords or constants
  • Spaces NOT allowed (use underscores)
x               Valid
salary          Valid
tax_rate        Valid (use underscores, not spaces)
_private        Valid (underscore prefix)
123abc          Invalid (cannot start with digit)
my budget       Invalid (spaces not allowed, use my_budget)
avg             Invalid (reserved keyword)
PI              Invalid (reserved constant)

Mathematical Constants #

Built-in constants (read-only, case-insensitive):

ConstantValue
PI, pi3.141592653589793
E, e2.718281828459045

Constants cannot be assigned:

PI = 3          ERROR: Cannot assign to constant 'PI'
radius = 5
area = PI * radius ^ 2
Results
radius = 55
area = PI * radius ^ 278.5

Operators #

Arithmetic #

OperatorNameExampleResultAssociativity
^Exponent2 ^ 38Right
*Multiply3 * 412Left
/Divide10 / 25Left
%Modulus10 % 31Left
+Add5 + 38Left
-Subtract5 - 32Left

Comparison #

OperatorNameExampleResult
>Greater than5 > 3true
<Less than5 < 3false
>=Greater or equal5 >= 5true
<=Less or equal5 <= 3false
==Equal5 == 5true
!=Not equal5 != 3true

Logical #

OperatorNameExampleResult
andLogical ANDtrue and falsefalse
orLogical ORtrue or falsetrue
notLogical NOTnot truefalse

Case-insensitive: AND, and, And all work.

Unary #

OperatorNameExampleResult
-Negation-5-5
+Plus+55
notLogical NOTnot truefalse

Conversion and Postfix #

OperatorNameExampleResult
inUnit conversion10 meters in feet32.81 feet
asUnit conversion or display modifier1 mile as km, 1234567 as napkin1.61 km, ~1.2M
perRate denominator100 MB per day100 MB/day
overAccumulation100 MB/s over 1 day~8.64 TB
at...perCapacity10 TB at 2 TB per disk5 disk
% ofPercentage of15% of 20030
as % ofRatio as percentage$100 as % of $50020%
fromDate offset2 days from today(date)

Assignment #

OperatorNameExampleEffect
=Assignx = 5Stores 5 in variable x

Type Arithmetic Rules #

Not every combination of types can be multiplied or divided. CalcMark follows dimensional analysis: the result of an operation must have a meaningful type.

ExpressionResultWhy
$100 + $50$150Adding prices is natural
$100 - $50$50Subtracting prices is natural
$100 * 5$500Scaling a price by a number
$100 / 5$20Splitting a price evenly
$100 * $50error“Square dollars” isn’t a real unit
$100 / $50errorDollars divided by dollars isn’t dollars
10 kg + 5 kg15 kgAdding quantities is natural
10 kg * 330 kgScaling a quantity by a number
10 kg * 5 kgerror“Square kilograms” isn’t a real unit
10 kg / 5 kgerrorkg divided by kg isn’t kg

To get a percentage, use as % of:

profit_margin = operating_income as % of total_revenue

This returns a Percentage type (e.g., 41.01%). Both values must be the same type — you can’t compute $100 as % of 50 kg.

For a raw number ratio, use number() to strip the type from both sides:

ratio = number(operating_income) / number(total_revenue)

Which side you wrap matters. number() strips the unit or currency, returning a plain number. The arithmetic rules then determine the result type:

ExpressionResultWhy
$100 / number($50)$2.00currency / number = currency
number($100) / number($50)2number / number = plain number
number($100) / $50errornumber / currency is not defined

Don’t over-wrap. If a value is already a plain number (from a previous number() call, or because it was never typed), wrapping it again is harmless but noisy. Only wrap typed values (currency, quantity) that need their type stripped.

The same rule applies to fractions with units:

ExpressionResultWhy
2/3 cup + 1/4 cup11/12 cupAdding quantities
2/3 cup * 32 cupScaling by a number
2/3 cup * 1/3 cuperrorSquare cups isn’t real
1/3 * $200$66.67Dimensionless fraction scales currency

Fractions vs Division #

CalcMark uses whitespace to disambiguate fractions from division:

  • 1/3 — fraction (no spaces around /)
  • 1 / 3 — division (spaces around /)
  • a/b where a and b are variables — always division

Only literal integers without spaces produce fractions.


Reserved Keywords #

These words cannot be used as variable names.

Logical Operators #

and, or, not

Case-insensitive: AND, and, And all work.

Control Flow (Reserved for Future) #

if, then, else, elif, end
for, while
return, break, continue
let, const

Function Names #

All built-in function names are reserved:

avg, sum, sqrt, number, accumulate, convert_rate, capacity,
downtime, rtt, throughput, transfer_time,
read, seek, compress, compound, grow, depreciate

Language Keywords #

in, as, of, per, over, at, from, with, napkin, precise

Contextual Keywords #

These words have special meaning in specific syntactic positions but are not reserved as variable names:

by, compounded, to, monthly, quarterly, weekly, daily, yearly
compound $1000 by 5% monthly over 10 years         (by, monthly)
compound $1000 by 5% compounded monthly over 10    (by, compounded)
depreciate $50000 by 15% over 5 to $5000           (to)

Bare frequency adverbs (monthly, quarterly, weekly, daily, yearly) are shorthand for compounded monthly, compounded quarterly, etc.


Functions #

accumulate #

Total from a rate over time: rate × duration. Use for bandwidth, request volume, or throughput planning.

Syntax
accumulate(rate, duration)
Example
accumulate(100 req/s, 1 hour) → 360000 req

avg #

Calculate the average of values

Syntax
avg(value1, value2, ...)
Aliases
average, mean, average of (input syntax)
Example
avg(10, 20, 30) → 20

capacity #

How many units to handle a load: ceil(demand / capacity_per_unit). Optional buffer adds headroom.

Syntax
capacity(demand, capacity_per_unit, unit, buffer?)
Aliases
requires
Example
capacity(10000 req/s, 500 req/s, server) → 20 servers

compound #

Compound growth: principal × (1 + rate)^periods. Add period for monthly/quarterly compounding.

Syntax
compound(principal, rate, periods, period?)
Aliases
compound...by...over (input syntax)
Example
compound(1000, 5%, 10 years, monthly) → 1647.01

compress #

Compressed output size using typical ratios: gzip ~3x, lz4 ~2x, zstd ~3.5x, snappy ~1.5x.

Syntax
compress(size, compression_type)
Aliases
compress...using (input syntax)
Example
compress 1 GB using gzip

convert_rate #

Convert a rate to a different time unit. Use the 'per' keyword as a natural-language synonym: d per year is equivalent to convert_rate(d, year). Works with literal rates (5 MB/s per minute) and variables holding rates (r per hour). Supports all time units including sub-second: nanosecond (ns), microsecond (μs/us), millisecond (ms).

Syntax
convert_rate(rate, time_unit)
Aliases
per (input syntax)
Example
convert_rate(1000 req/s, minute) → 60000 req/min

depreciate #

Declining balance depreciation: value × (1 - rate)^periods. Optional salvage sets a floor value.

Syntax
depreciate(value, rate, periods, salvage?)
Aliases
depreciate...by...over...to (input syntax)
Example
depreciate(10000, 20%, 5) → 3276.80

downtime #

Downtime from an SLA: (1 - availability) × time_period. E.g., 99.9% over a month → 43 minutes.

Syntax
downtime(availability, duration)
Example
downtime(99.9%, month) → 43.2 minutes

grow #

Linear growth: amount + (increment × periods). Use for additive scaling like hiring or storage growth.

Syntax
grow(amount, increment, periods)
Aliases
grow...by...over (input syntax)
Example
grow(100, 20 GB, 5) → 200 GB

number #

Strip the unit or currency from a typed value, returning a plain number. Use when you need a dimensionless ratio from two typed values: number($500) / number($1000) → 0.5. Only wrap what's needed — if a value is already a plain number, don't wrap it again. Which side you wrap matters: $100 / number($50) → $2.00 (currency), number($100) / number($50) → 2 (plain number).

Syntax
number(value)
Example
number($500) / number($1000) → 0.5

read #

Sequential read time: size / read speed. NVMe ~3 GB/s, SSD ~500 MB/s, HDD ~100 MB/s.

Syntax
read(size, storage_type)
Aliases
read...from (input syntax)
Example
read 100 MB from ssd

rtt #

Typical network round-trip latency: local ~0.5ms, regional ~10ms, continental ~50ms, global ~150ms.

Syntax
rtt(scope)
Aliases
round trip time
Example
rtt(regional) → 10 ms

seek #

Random access latency: NVMe ~0.1ms, SSD ~0.1ms, HDD ~10ms.

Syntax
seek(storage_type)
Example
seek(hdd) → 10 ms

sqrt #

Calculate the square root

Syntax
sqrt(value)
Aliases
square root of (input syntax)
Example
sqrt(16) → 4

sum #

Calculate the sum of values

Syntax
sum(value1, value2, ...)
Aliases
sum of (input syntax), total
Example
sum($100, $200, $300) → $600

throughput #

Network bandwidth by type: wifi ~6 MB/s, gigabit ~125 MB/s, ten_gig ~1.25 GB/s.

Syntax
throughput(network_type)
Example
throughput(gigabit) → 125 MB/s

transfer_time #

Time to transfer data: size / throughput(network_type) + rtt(scope).

Syntax
transfer_time(size, scope, network_type)
Aliases
transfer...across (input syntax)
Example
transfer 1 GB across regional gigabit

For detailed examples of every function, including natural language syntax forms, see the User Guide: Function Reference.

Unit Handling in Functions #

Same units are preserved:

avg($100, $200, $300) -> $200.00
sqrt($100) -> $10.00

Mixed units are dropped:

avg($100, €200) -> 150  (no units)
average of $50, €100, £150 -> 100  (no units)

Natural Language Syntax #

CalcMark supports natural language forms for many functions. These are equivalent to the function-call syntax. Arguments can be literal values (100 MB) or variable references (data).

PatternEquivalent
average of X, Y, Zavg(X, Y, Z)
sum of X, Y, Zsum(X, Y, Z)
square root of Xsqrt(X)
X over Yaccumulate(X, Y)
X at Y per Zcapacity(X, Y, Z)
X at Y per Z with W% buffercapacity(X, Y, Z, W%)
read X from Yread(X, Y)
compress X using Ycompress(X, Y)
transfer X across Y Ztransfer_time(X, Y, Z)
compound X by Y% over Zcompound(X, Y%, Z)
compound X by Y% monthly over Zcompound(X, Y%, Z, monthly)
compound X by Y% per P over Zcompound(X, Y%, Z, P)
compound X by Y% compounded F over Zcompound(X, Y%, Z, compounded F)
grow X by Y over Zgrow(X, Y, Z)
depreciate X by Y% over Zdepreciate(X, Y%, Z)
depreciate X by Y% over Z to Wdepreciate(X, Y%, Z, W)

See the User Guide: Natural Language Syntax for the complete reference table with examples.


Napkin Math #

The as napkin modifier rounds results to 2 significant figures and normalizes units. See the User Guide: Napkin Math for usage examples.

Syntax: expression as napkin

Works with: Number, Quantity, Currency, Duration, Rate


Precise Display #

The as precise modifier shows full float precision, skipping all display rounding. This is the opposite of as napkin and is useful when you need exact values from unit conversions.

Syntax: expression as precise

Can be chained after a unit conversion: 10 meters as feet as precise

Works with: Number, Quantity, Currency, Duration, Rate


Rates #

Rates are defined using slash syntax (e.g., 100 MB/s, $50/hour). See the User Guide: Rates for rate accumulation with over and rate conversion.

Rate Arithmetic Widening #

When a rate appears on the right side of * or /, its time denominator is dropped and the rate’s amount is used instead. This is called widening — the rate widens into its underlying quantity.

When a rate appears on the left side, it stays a rate. This lets you scale rates naturally.

Operand order determines the result type:

ExpressionLeftRightResultWhy
rate * 3RateNumberRateRate on left → stays rate (scaling)
3 * rateNumberRateQuantityRate on right → widened
rate / 2RateNumberRateRate on left → stays rate
100 / rateNumberRateNumberRate on right → widened
rate * qtyRateQuantityQuantityCross-type, extracts amount
qty * rateQuantityRateQuantityRate on right → widened
rate / rateRateRateNumberSame-unit ratio (no widening)
posts_rate = 2 posts/week
scaled = posts_rate * 3           -> 6 posts/week  (Rate — rate on left)
total  = 3 * posts_rate           -> 6 posts       (Quantity — rate on right)
half   = posts_rate / 2           -> 1 posts/week  (Rate — rate on left)

This rule is asymmetric by design. The operand on the left is the “subject” of the expression:

  • read_rate * peak_multiplier — you are scaling a rate, so the result is a rate.
  • daily_users * posts_per_user_per_week — you are scaling a count by a rate, so the result is a quantity.

Rate widening only applies to binary * and /. It does not affect functions like accumulate(), over, or per.


Date Arithmetic #

CalcMark supports date literals (Jan 15 2025, today), duration arithmetic, and the from keyword.

CY #

Specific calendar year start (Jan 1 of that year). Two-digit forms (CY26) interpreted as 2000+NN.

Syntax
CY<NNNN>
Example
end of CY2026

FQ1 #

First day of fiscal quarter 1 (per fiscal_year_starts frontmatter)

Syntax
FQ1
Example
end of FQ1

FQ2 #

First day of fiscal quarter 2 (3 months after fiscal_year_starts)

Syntax
FQ2
Example
end of FQ2

FQ3 #

First day of fiscal quarter 3 (6 months after fiscal_year_starts)

Syntax
FQ3
Example
end of FQ3

FQ4 #

First day of fiscal quarter 4 (9 months after fiscal_year_starts; requires fiscal_year_starts frontmatter)

Syntax
FQ4
Example
end of FQ4

FY #

Specific fiscal year start. Default labeling is by the year the FY ENDS in (matches the Australian government year, US tax year, and most publicly traded companies): FY2027 with July start = Jul 1 2026 -> Jun 30 2027. Set 'calendar_year_offset: after' in frontmatter to label by start year instead. Requires fiscal_year_starts frontmatter.

Syntax
FY<NNNN>
Example
end of FY2027

Q1 #

First day of calendar quarter 1 (Jan 1 of the current year)

Syntax
Q1
Example
end of Q1

Q2 #

First day of calendar quarter 2 (Apr 1 of the current year)

Syntax
Q2
Example
end of Q2

Q3 #

First day of calendar quarter 3 (Jul 1 of the current year)

Syntax
Q3
Example
end of Q3

Q4 #

First day of calendar quarter 4 (Oct 1 of the current year)

Syntax
Q4
Example
end of Q4

ago #

Date/time in the past

Syntax
N units ago
Example
2 weeks ago

between #

Construct a custom Period spanning two dates (closed interval, inclusive). end >= start required.

Syntax
between <date1> and <date2>
Aliases
from A to B (input syntax)
Example
between Apr 15 2026 and Jul 4 2026

days #

Duration in days

Syntax
N days
Aliases
day (input syntax)
Example
today + 30 days

days in #

Number of days in a period (integer count, closed-interval). Returns Number for use in arithmetic.

Syntax
days in <period>
Example
days in April

fiscal quarter #

First day of the current fiscal quarter (requires fiscal_year_starts frontmatter)

Syntax
this fiscal quarter
Aliases
next fiscal quarter (input syntax), last fiscal quarter (input syntax), this FQ (input syntax), next FQ (input syntax), last FQ (input syntax)
Example
this fiscal quarter

fiscal year #

First day of the current fiscal year (requires fiscal_year_starts frontmatter)

Syntax
this fiscal year
Aliases
next fiscal year (input syntax), last fiscal year (input syntax), this FY (input syntax), next FY (input syntax), last FY (input syntax)
Example
this fiscal year

from #

Calculate date offset

Syntax
N units from date
Example
7 days from Dec 25

length of #

Length of a period as a Duration in days (closed-interval, inclusive). Composes with duration arithmetic.

Syntax
length of <period>
Example
length of Q1

months #

Duration in months

Syntax
N months
Aliases
month (input syntax)
Example
Dec 25 + 1 month

next month name #

First day of the next occurrence of a named month

Syntax
next <month>
Aliases
this month name (input syntax), last month name (input syntax)
Example
next April

next weekday #

The soonest future occurrence of a weekday

Syntax
next <weekday>
Aliases
this weekday (input syntax), last weekday (input syntax)
Example
next Friday

this month #

First day of the current calendar month

Syntax
this month
Aliases
next month (input syntax), last month (input syntax)
Example
this month + 14 days

this quarter #

First day of the current calendar quarter

Syntax
this quarter
Aliases
next quarter (input syntax), last quarter (input syntax), this CQ (input syntax), next CQ (input syntax), last CQ (input syntax)
Example
this quarter + 30 days

this week #

First day of the current week (Monday)

Syntax
this week
Aliases
next week (input syntax), last week (input syntax)
Example
this week + 2 days

this year #

First day of the current calendar year

Syntax
this year
Aliases
next year (input syntax), last year (input syntax), this CY (input syntax), next CY (input syntax), last CY (input syntax)
Example
this year + 6 months

today #

Current date

Syntax
today
Example
today + 7 days

tomorrow #

Tomorrow's date

Syntax
tomorrow
Example
tomorrow + 1 week

weeks #

Duration in weeks

Syntax
N weeks
Aliases
week (input syntax)
Example
2 weeks from today

years #

Duration in years

Syntax
N years
Aliases
year (input syntax), yr (input syntax), yrs (input syntax)
Example
today + 1 year

yesterday #

Yesterday's date

Syntax
yesterday
Example
yesterday - 3 days

See the User Guide: Date Arithmetic for details.


Network Functions #

CalcMark provides functions for network planning — latency estimation, throughput lookup, and transfer time calculation.

continental #

Cross-continent latency (~50ms)

Syntax
rtt(continental)
Example
rtt(continental) → 50 ms

five_g #

5G mobile network (~50 MB/s)

Syntax
throughput(five_g)
Example
throughput(five_g) → 50 MB/s

four_g #

4G mobile network (~2.5 MB/s)

Syntax
throughput(four_g)
Example
throughput(four_g) → 2.5 MB/s

gigabit #

1 Gbps network (~125 MB/s)

Syntax
throughput(gigabit)
Example
throughput(gigabit) → 125 MB/s

global #

Global latency (~150ms)

Syntax
rtt(global)
Example
rtt(global) → 150 ms

hundred_gig #

100 Gbps network (~12.5 GB/s)

Syntax
throughput(hundred_gig)
Example
throughput(hundred_gig) → 12500 MB/s

local #

Same datacenter latency (~0.5ms)

Syntax
rtt(local)
Example
rtt(local) → 0.5 ms

regional #

Same region latency (~10ms)

Syntax
rtt(regional)
Example
rtt(regional) → 10 ms

ten_gig #

10 Gbps network (~1.25 GB/s)

Syntax
throughput(ten_gig)
Example
throughput(ten_gig) → 1250 MB/s

wifi #

Typical WiFi (~12.5 MB/s)

Syntax
throughput(wifi)
Example
throughput(wifi) → 12.5 MB/s

See the User Guide: Network Functions for scope tables and examples.


Storage Functions #

CalcMark provides functions for storage planning — read throughput, seek latency, and compression estimation.

hdd #

7200 RPM HDD (~150 MB/s, 10ms seek)

Syntax
read(size, hdd) or seek(hdd)
Example
seek(hdd) → 10 ms

nvme #

NVMe SSD (~3.5 GB/s, 0.01ms seek)

Syntax
read(size, nvme) or seek(nvme)
Example
read(1 GB, nvme)

pcie_ssd #

PCIe Gen4 SSD (~7 GB/s, 0.01ms seek)

Syntax
read(size, pcie_ssd) or seek(pcie_ssd)
Example
read(1 GB, pcie_ssd)

ssd #

SATA SSD (~550 MB/s, 0.1ms seek)

Syntax
read(size, ssd) or seek(ssd)
Aliases
sata_ssd (input syntax)
Example
read(1 GB, ssd)

See the User Guide: Storage Functions for device type tables and examples.


Compression #

bzip2 #

Bzip2 compression (~4:1 ratio, slow)

Syntax
compress(size, bzip2)
Example
compress(1 GB, bzip2) → 250 MB

gzip #

Gzip compression (~3:1 ratio)

Syntax
compress(size, gzip)
Example
compress(1 GB, gzip) → 333 MB

lz4 #

LZ4 fast compression (~2:1 ratio)

Syntax
compress(size, lz4)
Example
compress(1 GB, lz4) → 500 MB

none #

No compression (1:1 ratio)

Syntax
compress(size, none)
Example
compress(1 GB, none) → 1 GB

snappy #

Snappy fast compression (~2.5:1 ratio)

Syntax
compress(size, snappy)
Example
compress(1 GB, snappy) → 400 MB

zstd #

Zstandard compression (~3.5:1 ratio)

Syntax
compress(size, zstd)
Example
compress(1 GB, zstd) → 286 MB

Growth Functions #

CalcMark provides compound, grow, and depreciate for modeling growth and depreciation over time.

compound #

Compound growth: principal × (1 + rate)^periods. Add period for monthly/quarterly compounding.

Syntax
compound(principal, rate, periods, period?)
Aliases
compound...by...over (input syntax)
Example
compound(1000, 5%, 10 years, monthly) → 1647.01

depreciate #

Declining balance depreciation: value × (1 - rate)^periods. Optional salvage sets a floor value.

Syntax
depreciate(value, rate, periods, salvage?)
Aliases
depreciate...by...over...to (input syntax)
Example
depreciate(10000, 20%, 5) → 3276.80

grow #

Linear growth: amount + (increment × periods). Use for additive scaling like hiring or storage growth.

Syntax
grow(amount, increment, periods)
Aliases
grow...by...over (input syntax)
Example
grow(100, 20 GB, 5) → 200 GB

Compound Growth #

Simple compound growthA = P(1+r)^t, compounding once per year. The 3rd argument is years:

compound($1000, 5%, 10)                              -> $1628.89
compound(500 customers, 20%, 12)                     -> 4458.05 customers

Financial compoundingA = P(1+r/n)^(nt), compounding multiple times per year. Triggered by a frequency adverb (monthly, quarterly, weekly, daily, yearly):

compound($1000, 5%, 10, monthly)                     -> $1647.01
compound($1000, 5%, 10, quarterly)                   -> $1643.62
compound($1000, 5%, 24 months, monthly)              -> $1103.43

Natural language forms:

compound $1000 by 5% over 10 years
compound $1000 by 5% monthly over 10 years
compound $1000 by 5% per month over 12 months
Results
compound $1000 by 5% over 10 yearscompound: periods must be a plain number (iteration count) — got duration; write the count without a time unit
compound $1000 by 5% monthly over 10 years$1,647.01
compound $1000 by 5% per month over 12 months$1,795.86

The frequency adverb controls how many times per year the rate is applied. Without it, the rate compounds once per year. With monthly, the annual rate is split into 12 applications per year.

Linear Growth #

grow($500, $100, 36)               -> $4100.00
grow 100 by 20 over 5 months       -> 200  (NL form)

Depreciation #

depreciate($50000, 15%, 5)                -> $22185.27
depreciate($50000, 15%, 20, $5000)        -> $5000.00  (salvage floor)
depreciate $50000 by 15% over 5 years     -> (NL form)
depreciate $50000 by 15% over 5 years to $5000
Results
depreciate $50000 by 15% over 5 years to $5000depreciate: periods must be a plain number (iteration count) — got duration; write the count without a time unit

See the User Guide: Growth Functions for the full argument reference.


Validation & Diagnostics #

Diagnostic Levels #

SeverityMeaning
ErrorPrevents evaluation, line becomes MARKDOWN
WarningLine evaluates but issue is noted
HintSuggestion or style recommendation

Diagnostic Codes #

CodeSeverityDescription
type_mismatchErrorIncompatible types in operation
division_by_zeroErrorDivision or modulus by zero
invalid_currency_codeErrorUnsupported currency symbol
incompatible_currenciesErrorMixed currency codes without exchange rate
incompatible_unitsErrorUnit mismatch in operation
unsupported_unitErrorUnit not recognized for conversion
invalid_dateErrorInvalid date literal
invalid_monthErrorMonth name or number out of range
invalid_dayErrorDay out of range for the given month
invalid_yearErrorInvalid year value
invalid_leap_yearErrorFeb 29 in a non-leap year
invalid_date_operationErrorInvalid operation on date types
invalid_directiveErrorInvalid @ directive reference
undefined_globalError@globals.name references undefined global
missing_frontmatterWarningDirective requires frontmatter section
undefined_variableWarningVariable used before definition
variable_redefinitionWarningVariable assigned more than once
mixed_base_unitsHintMixing binary (KiB) and decimal (KB) data units