Go implementation of the CalcMark calculation language.
CalcMark is a calculation language that blends seamlessly with markdown. This library provides the complete Go implementation with:
- Types - Number, Currency, Boolean with arbitrary precision decimals
- Lexer - Unicode-aware tokenization with reserved keywords
- Parser - Recursive descent parser with operator precedence
- Evaluator - Expression evaluation with context
- Validator - Semantic validation with diagnostics
- Classifier - Line classification (calculation vs markdown)
Version: v0.1.1 (Phase 1 complete - Reserved keywords and multi-token functions)
go get github.com/CalcMark/go-calcmarkTo build and install the calcmark command-line tool:
# From the project root
go build -o calcmark ./impl/cmd/calcmark
./calcmark version
# Or install globally
go install ./impl/cmd/calcmark
calcmark versionCommands available:
# Build WASM module and copy to destination
calcmark wasm [output-directory]
# Output the syntax highlighter spec
calcmark spec
# Show version
calcmark versionimport (
"fmt"
"github.com/CalcMark/go-calcmark/evaluator"
)
func main() {
content := `salary = $5000
rent = β¬1500
tax_rate = 0.08
monthly_tax = salary * tax_rate
net_income = salary - rent - monthly_tax
circle_area = PI * 5 ^ 2
average_expense = avg($500, β¬400, Β£300)`
context := evaluator.NewContext()
results, err := evaluator.Evaluate(content, context)
if err != nil {
panic(err)
}
for _, result := range results {
fmt.Println(result.String())
}
// Output:
// $5000.00
// β¬1500.00
// 0.08
// $400.00
// $3100.00
// 78.539816339744825
// 400 (Number - mixed currency units)
}import (
"fmt"
"github.com/CalcMark/go-calcmark/evaluator"
"github.com/CalcMark/go-calcmark/validator"
)
func main() {
content := `x = 5
y = z + 2
total = x + y`
context := evaluator.NewContext()
result := validator.ValidateDocument(content, context)
// Check validation status
if result.IsValid() {
fmt.Println("Document is valid!")
} else {
fmt.Printf("Found %d errors\n", len(result.Errors()))
}
// Process all diagnostics
for _, diagnostic := range result.Diagnostics {
fmt.Printf("[%s] %s: %s\n",
diagnostic.Severity,
diagnostic.Code,
diagnostic.Message)
if diagnostic.Range != nil {
fmt.Printf(" at line %d, column %d\n",
diagnostic.Range.Start.Line,
diagnostic.Range.Start.Column)
}
}
// Filter by severity
if result.HasErrors() {
fmt.Println("\nErrors:")
for _, err := range result.Errors() {
fmt.Printf(" - %s\n", err.Message)
}
}
if result.HasWarnings() {
fmt.Println("\nWarnings:")
for _, warn := range result.Warnings() {
fmt.Printf(" - %s\n", warn.Message)
}
}
if result.HasHints() {
fmt.Println("\nHints:")
for _, hint := range result.Hints() {
fmt.Printf(" - %s\n", hint.Message)
}
}
}import (
"fmt"
"github.com/CalcMark/go-calcmark/classifier"
"github.com/CalcMark/go-calcmark/evaluator"
)
func main() {
context := evaluator.NewContext()
lines := []string{
"x = 5",
"This is markdown text",
"x + 2",
"",
"- bullet point",
}
for _, line := range lines {
lineType := classifier.ClassifyLine(line, context)
fmt.Printf("%s: %s\n", lineType, line)
}
}import (
"encoding/json"
"fmt"
"github.com/CalcMark/go-calcmark/validator"
)
func main() {
content := "result = undefined_var * 2"
result := validator.ValidateDocument(content, nil)
for _, diagnostic := range result.Diagnostics {
// Access structured data
fmt.Printf("Code: %s\n", diagnostic.Code) // "undefined_variable"
fmt.Printf("Severity: %s\n", diagnostic.Severity) // "error"
// Convert to map for JSON serialization
diagMap := diagnostic.ToMap()
jsonData, _ := json.MarshalIndent(diagMap, "", " ")
fmt.Println(string(jsonData))
// Access variable name for undefined variable errors
if diagnostic.Code == validator.UndefinedVariable {
fmt.Printf("Undefined variable: %s\n", diagnostic.VariableName)
}
}
}- ERROR: Invalid syntax that prevents parsing (e.g.,
x *)- Code:
syntax_error
- Code:
- WARNING: Valid syntax but evaluation failure (e.g., undefined variables)
- Codes:
undefined_variable,division_by_zero,type_mismatch
- Codes:
- HINT: Style suggestions for valid code (e.g., blank line isolation)
- Code:
blank_line_isolation
- Code:
See DIAGNOSTIC_LEVELS.md for complete details.
go test ./...go test -cover ./...go test ./evaluator -v
go test ./parser -vCalcMark processes text through a series of stages, each handled by a specialized package. Here's how the components work together:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Input: "sales_tax = 0.08\nsales = 1000 * sales_tax" β
ββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββ
β 1. LEXER β Breaks text into tokens
β lexer/ β "sales_tax" "=" "0.08" "\n" "sales" ...
ββββββββββ¬ββββββββ
β
βΌ
ββββββββββββββββββ
β 2. PARSER β Builds Abstract Syntax Tree (AST)
β parser/ β Assignment(sales_tax, Literal(0.08))
β ast/ β Assignment(sales, BinaryOp(1000, *, sales_tax))
ββββββββββ¬ββββββββ
β
ββββββββββββββ΄βββββββββββββ
β β
βΌ βΌ
ββββββββββββββββββ ββββββββββββββββββ
β 3a. VALIDATOR β β 3b. EVALUATOR β
β validator/ β β evaluator/ β
β β β β
β Checks AST for β β Executes AST β
β semantic errorsβ β with Context β
β (undefined vars)β β β
β β β Line 1: β
β Returns: β β sales_tax=0.08 β
β Diagnostics β β Context: { β
β β β sales_tax: 0.08 }
ββββββββββββββββββ β β
β Line 2: β
β 1000*0.08=80 β
β Context: { β
β sales_tax: 0.08,
β sales: 80 } β
ββββββββββ¬ββββββββ
β
βΌ
ββββββββββββββββββ
β 4. TYPES β
β types/ β
β β
β Results stored β
β as typed valuesβ
β Number, Currency,
β Boolean β
ββββββββββββββββββ
Additional Component:
ββββββββββββββββββ
β CLASSIFIER β Determines if a line is CALCULATION, MARKDOWN, or BLANK
β classifier/ β Uses parser + context to classify each line
ββββββββββββββββββ (Used for syntax highlighting and rendering)
Purpose: Converts raw text into a stream of tokens (lexemes).
Example: sales = 1000 * sales_tax becomes:
[IDENTIFIER:"sales", ASSIGN:"=", NUMBER:"1000", MULTIPLY:"*", IDENTIFIER:"sales_tax", EOF]
Key Features:
- Unicode-aware (supports international characters, emojis)
- Recognizes numbers with thousands separators (
,or_) - Currency symbols:
$,β¬,Β£,Β₯ - Mathematical constants:
PI,E(case-insensitive, read-only) - Booleans:
true,false,yes,no,t,f,y,n(case-insensitive) - Tracks line/column positions for error reporting
Entry Point: lexer.Tokenize(string) ([]Token, error)
Purpose: Converts tokens into an Abstract Syntax Tree (AST) that represents the program structure.
Example: Tokens become:
Assignment {
Variable: "sales"
Value: BinaryOp {
Left: Literal(1000)
Op: "*"
Right: Identifier("sales_tax")
}
}
Key Features:
- Recursive descent parsing with precedence climbing
- Operator precedence:
()>^>*/%>+-> comparisons - Validates syntax (parentheses matching, valid expressions)
- Builds AST nodes with position information
Entry Point: parser.Parse(string) ([]ast.Node, error)
Purpose: Checks if code is semantically valid WITHOUT executing it.
What it checks:
- Undefined variables: References to variables not yet defined
- Semantic errors: Issues that would prevent evaluation
- Style hints: Suggestions like blank line isolation
Example:
sales = 1000 * sales_tax // ERROR: sales_tax undefinedReturns: Diagnostics with severity levels:
- ERROR: Syntax errors (parsing failed)
- WARNING: Semantic errors (undefined variables)
- HINT: Style suggestions (missing blank lines)
Entry Point: validator.ValidateDocument(string, *Context) *ValidationResult
Purpose: Executes the AST and computes actual values.
How it works:
- Processes lines sequentially
- Maintains a Context (variable storage)
- Evaluates each expression using the context
- Updates context with assignment results
Example Execution:
Line 1: sales_tax = 0.08
β Evaluates: 0.08
β Context: {sales_tax: 0.08}
β Returns: 0.08
Line 2: sales = 1000 * sales_tax
β Evaluates: 1000 * 0.08
β Context: {sales_tax: 0.08, sales: 80}
β Returns: 80
Key Rules:
- Variables must be defined before use (no forward references)
- Context flows between lines
- Mathematical constants (
PI,E) are always available and read-only - Functions:
avg(),sqrt()with natural language aliasesavg(1, 2, 3)oraverage of 1, 2, 3sqrt(16)orsquare root of 16
- Unit handling:
- Binary operations preserve units:
$200 + 0.1β$200.10 - Functions drop units when mixed:
avg($100, β¬200)β150(Number) - Same units preserved:
avg($100, $200)β$150.00(Currency)
- Binary operations preserve units:
Entry Point: evaluator.Evaluate(string, *Context) ([]types.Type, error)
Purpose: Represents CalcMark values with proper types.
Types:
- Number: Arbitrary precision decimals (using
shopspring/decimal)- Supports thousands separators:
1,000or1_000_000
- Supports thousands separators:
- Currency: Number + symbol (
$,β¬,Β£,Β₯)- Example:
$1,234.56,β¬500,Β£1,000,000,Β₯10000
- Example:
- Boolean: true/false (keywords:
true,false,yes,no,t,f,y,n- case-insensitive)
Example:
sales_tax := types.NewNumber(0.08) // Number
sales := types.NewCurrency(80, "$") // CurrencyPurpose: Determines if a line is a calculation or markdown text.
Classification Logic:
- Try to parse the line
- Check if all variables are defined in context
- Return:
CALCULATION,MARKDOWN, orBLANK
Context-Aware Example:
// Empty context:
"sales_tax" β MARKDOWN (undefined variable)
// With sales_tax defined:
"sales_tax" β CALCULATION (valid reference)
Entry Point: classifier.ClassifyLine(string, *Context) LineType
Purpose: Defines the node types that represent parsed code structure.
Node Types:
Assignment: Variable assignment (e.g.,x = 5)BinaryOp: Arithmetic operations (e.g.,+,-,*,/)ComparisonOp: Comparisons (e.g.,>,<,==)UnaryOp: Unary minus/plus (e.g.,-5)Identifier: Variable referenceLiteral: Number, currency, or boolean value
All nodes include:
Range: Position information (line, column) for error messages
Let's trace sales = 1000 * sales_tax through the entire system:
1. Lexer Input: "sales = 1000 * sales_tax"
2. Lexer Output (Tokens):
[IDENTIFIER:"sales", ASSIGN:"=", NUMBER:"1000", MULTIPLY:"*", IDENTIFIER:"sales_tax"]
3. Parser Output (AST):
Assignment{
Variable: Identifier{Name: "sales", Range: ...}
Value: BinaryOp{
Op: "*"
Left: Literal{Value: Number(1000), Range: ...}
Right: Identifier{Name: "sales_tax", Range: ...}
}
}4a. Validator (if sales_tax undefined):
ValidationResult{
Diagnostics: [
{
Severity: Warning,
Code: "undefined_variable",
Message: "Undefined variable: sales_tax",
Range: {Line: 1, Column: 14},
VariableName: "sales_tax"
}
]
}4b. Evaluator (if sales_tax = 0.08):
// Looks up sales_tax in context β 0.08
// Evaluates: 1000 * 0.08 = 80
// Stores: context.Set("sales", 80)
// Returns: Number(80)5. Result:
sales = 80
go-calcmark/
βββ types/ # Core types (Number, Currency, Boolean)
βββ lexer/ # Tokenization
βββ ast/ # Abstract syntax tree
βββ parser/ # Recursive descent parser
βββ evaluator/ # Expression evaluation
βββ validator/ # Semantic validation
βββ classifier/ # Line classification
βββ syntax/ # Embedded syntax highlighter spec
βββ cmd/calcmark/# CLI tool
βββ spec/ # Documentation (JSON spec, markdown guides)
Comprehensive test coverage across all packages:
- lexer: Number parsing, thousands separators, currency symbols, reserved keywords
- parser: Expression parsing, operator precedence, function calls
- evaluator: Arithmetic, functions, constants, mixed units, type handling
- validator: Undefined variables, semantic errors, diagnostics
- classifier: Line classification, context-aware detection
- types: Number, Currency, Boolean types
- spec validation: Syntax highlighter spec generation
All tests passing β
github.com/shopspring/decimal- Arbitrary precision decimals (only external dependency)- Go standard library
- spec/LANGUAGE_SPEC.md - Complete, authoritative CalcMark language specification
- spec/SYNTAX_HIGHLIGHTER_SPEC.json - Machine-readable spec for editor integrations (embedded in library)
- spec/SYNTAX_HIGHLIGHTER_README.md - TypeScript/JavaScript integration guide
The SYNTAX_HIGHLIGHTER_SPEC.json is embedded in the library and can be accessed programmatically:
import "github.com/CalcMark/go-calcmark/syntax"
// Get the JSON spec as a string
jsonSpec := syntax.SyntaxHighlighterSpec
// Or as bytes (useful for HTTP responses)
jsonBytes := syntax.SyntaxHighlighterSpecBytes()
// Example: Serve via HTTP endpoint
http.HandleFunc("/syntax", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write(syntax.SyntaxHighlighterSpecBytes())
})To regenerate the spec from the Go implementation:
calcmark generate
# Or: go generate ./syntax- CLAUDE.md - Development guide for future Claude Code sessions
The spec/ directory contains the platform-independent language specification. The Go implementation in this repository is one implementation of that spec.
Same as CalcMark/CalcDown project.
This library is used by:
- CalcMark Server - HTTP API server
- CalcMark Web - Web application
When making changes, ensure all tests pass:
go test ./...