Skip to content

CalcMark/go-calcmark

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

go-calcmark

Go implementation of the CalcMark calculation language.

Go Version Tests

Overview

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)

Installation

go get github.com/CalcMark/go-calcmark

Building the CLI

To 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 version

Commands available:

# Build WASM module and copy to destination
calcmark wasm [output-directory]

# Output the syntax highlighter spec
calcmark spec

# Show version
calcmark version

Usage

Basic Evaluation

import (
    "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)
}

Validation with Diagnostics

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)
        }
    }
}

Line Classification

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)
    }
}

Working with Diagnostic Codes

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)
        }
    }
}

Diagnostic Severity Levels

  • ERROR: Invalid syntax that prevents parsing (e.g., x * )
    • Code: syntax_error
  • WARNING: Valid syntax but evaluation failure (e.g., undefined variables)
    • Codes: undefined_variable, division_by_zero, type_mismatch
  • HINT: Style suggestions for valid code (e.g., blank line isolation)
    • Code: blank_line_isolation

See DIAGNOSTIC_LEVELS.md for complete details.

Development

Run Tests

go test ./...

Test with Coverage

go test -cover ./...

Run Specific Package Tests

go test ./evaluator -v
go test ./parser -v

Architecture

CalcMark 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)

Component Responsibilities

1. lexer/ - Tokenization

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)

2. parser/ - Syntax Analysis

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)

3a. validator/ - Semantic Analysis

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 undefined

Returns: 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

3b. evaluator/ - Execution

Purpose: Executes the AST and computes actual values.

How it works:

  1. Processes lines sequentially
  2. Maintains a Context (variable storage)
  3. Evaluates each expression using the context
  4. 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 aliases
    • avg(1, 2, 3) or average of 1, 2, 3
    • sqrt(16) or square 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)

Entry Point: evaluator.Evaluate(string, *Context) ([]types.Type, error)

4. types/ - Value System

Purpose: Represents CalcMark values with proper types.

Types:

  • Number: Arbitrary precision decimals (using shopspring/decimal)
    • Supports thousands separators: 1,000 or 1_000_000
  • Currency: Number + symbol ($, €, Β£, Β₯)
    • Example: $1,234.56, €500, Β£1,000,000, Β₯10000
  • 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, "$")    // Currency

5. classifier/ - Line Classification

Purpose: Determines if a line is a calculation or markdown text.

Classification Logic:

  1. Try to parse the line
  2. Check if all variables are defined in context
  3. Return: CALCULATION, MARKDOWN, or BLANK

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

6. ast/ - Abstract Syntax Tree

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 reference
  • Literal: Number, currency, or boolean value

All nodes include:

  • Range: Position information (line, column) for error messages

How Data Flows Through the System

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

Package Structure

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)

Test Coverage

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 βœ…

Dependencies

  • github.com/shopspring/decimal - Arbitrary precision decimals (only external dependency)
  • Go standard library

Documentation

Language Specification

Embedded Syntax Spec

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

Development

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

License

Same as CalcMark/CalcDown project.

Contributing

This library is used by:

When making changes, ensure all tests pass:

go test ./...

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •