feat(engine): establish calcpad-engine workspace with Epic 1 modules

Cherry-picked and integrated the best code from 105 parallel epic
branches into a clean workspace structure:

- calcpad-engine/: Core Rust crate with lexer, parser, AST,
  interpreter, types, FFI (C ABI), pipeline, error handling,
  span tracking, eval context (from epic/1-5 base)
- calcpad-engine/src/number.rs: Arbitrary precision arithmetic
  via dashu crate, WASM-compatible, exact decimals (from epic/1-4)
- calcpad-engine/src/sheet_context.rs: SheetContext with dependency
  graph, dirty tracking, circular detection, cheap clone (from epic/1-8)
- calcpad-wasm/: Thin WASM wrapper crate via wasm-bindgen,
  delegates to calcpad-engine (from epic/1-6)
- Updated .gitignore for target/, node_modules/, build artifacts
- Fixed run-pipeline.sh for macOS compat and CalcPad phases

79 tests passing across workspace.
This commit is contained in:
C. Cassel
2026-03-17 07:54:17 -04:00
committed by C. Cassel
parent 922b591d68
commit 6a8fecd03e
26 changed files with 5046 additions and 877 deletions

214
calcpad-engine/src/types.rs Normal file
View File

@@ -0,0 +1,214 @@
use crate::span::Span;
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use std::fmt;
/// The result type tag for metadata purposes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResultType {
Number,
UnitValue,
CurrencyValue,
DateTime,
TimeDelta,
Boolean,
Error,
}
impl fmt::Display for ResultType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ResultType::Number => write!(f, "Number"),
ResultType::UnitValue => write!(f, "UnitValue"),
ResultType::CurrencyValue => write!(f, "CurrencyValue"),
ResultType::DateTime => write!(f, "DateTime"),
ResultType::TimeDelta => write!(f, "TimeDelta"),
ResultType::Boolean => write!(f, "Boolean"),
ResultType::Error => write!(f, "Error"),
}
}
}
/// Serializable span for JSON output.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SerializableSpan {
pub start: usize,
pub end: usize,
}
impl From<Span> for SerializableSpan {
fn from(s: Span) -> Self {
SerializableSpan {
start: s.start,
end: s.end,
}
}
}
/// Metadata attached to every evaluation result.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResultMetadata {
/// The source span of the expression that produced this result.
pub span: SerializableSpan,
/// The type tag of the result.
pub result_type: ResultType,
/// A display-formatted string of the result.
pub display: String,
/// The raw numeric value, if applicable.
pub raw_value: Option<f64>,
}
/// The value payload of a calculation result.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum CalcValue {
Number { value: f64 },
UnitValue { value: f64, unit: String },
CurrencyValue { amount: f64, currency: String },
DateTime { date: String },
TimeDelta { days: i64, description: String },
Boolean { value: bool },
Error { message: String, span: SerializableSpan },
}
/// A complete calculation result: value + metadata.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CalcResult {
pub value: CalcValue,
pub metadata: ResultMetadata,
}
impl CalcResult {
pub fn number(val: f64, span: Span) -> Self {
let display = format_number(val);
CalcResult {
value: CalcValue::Number { value: val },
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::Number,
display,
raw_value: Some(val),
},
}
}
pub fn unit_value(val: f64, unit: &str, span: Span) -> Self {
let display = format!("{} {}", format_number(val), unit);
CalcResult {
value: CalcValue::UnitValue {
value: val,
unit: unit.to_string(),
},
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::UnitValue,
display,
raw_value: Some(val),
},
}
}
pub fn currency_value(amount: f64, currency: &str, span: Span) -> Self {
let display = format_currency(amount, currency);
CalcResult {
value: CalcValue::CurrencyValue {
amount,
currency: currency.to_string(),
},
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::CurrencyValue,
display,
raw_value: Some(amount),
},
}
}
pub fn datetime(date: NaiveDate, span: Span) -> Self {
let display = date.format("%Y-%m-%d").to_string();
CalcResult {
value: CalcValue::DateTime {
date: display.clone(),
},
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::DateTime,
display,
raw_value: None,
},
}
}
pub fn time_delta(days: i64, description: &str, span: Span) -> Self {
let display = description.to_string();
CalcResult {
value: CalcValue::TimeDelta {
days,
description: description.to_string(),
},
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::TimeDelta,
display,
raw_value: Some(days as f64),
},
}
}
pub fn boolean(val: bool, span: Span) -> Self {
let display = val.to_string();
CalcResult {
value: CalcValue::Boolean { value: val },
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::Boolean,
display,
raw_value: None,
},
}
}
pub fn error(message: &str, span: Span) -> Self {
CalcResult {
value: CalcValue::Error {
message: message.to_string(),
span: span.into(),
},
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::Error,
display: format!("Error: {}", message),
raw_value: None,
},
}
}
pub fn result_type(&self) -> ResultType {
self.metadata.result_type
}
}
fn format_number(val: f64) -> String {
if val == val.floor() && val.abs() < 1e15 {
format!("{}", val as i64)
} else {
format!("{}", val)
}
}
fn format_currency(amount: f64, currency: &str) -> String {
let symbol = match currency {
"USD" => "$",
"EUR" => "",
"GBP" => "£",
"JPY" => "¥",
"BRL" => "R$",
_ => "",
};
if symbol.is_empty() {
format!("{:.2} {}", amount, currency)
} else {
format!("{}{:.2}", symbol, amount)
}
}