steel decimal crate with a proper setup, needs so much work to be done to be finished
This commit is contained in:
@@ -1,207 +1,279 @@
|
||||
// src/lib.rs
|
||||
//! # Steel Decimal
|
||||
//!
|
||||
//! A Rust library that provides decimal arithmetic support for the Steel programming language.
|
||||
//! This crate transforms Steel scripts to use high-precision decimal operations and provides
|
||||
//! function registration utilities for Steel VMs.
|
||||
//!
|
||||
//! ## Quick Start
|
||||
//!
|
||||
//! ### Basic Usage
|
||||
//! ```rust
|
||||
//! use steel_decimal::SteelDecimal;
|
||||
//! use steel::steel_vm::engine::Engine;
|
||||
//!
|
||||
//! // Create a new Steel Decimal engine
|
||||
//! let steel_decimal = SteelDecimal::new();
|
||||
//!
|
||||
//! // Transform a script
|
||||
//! let script = "(+ 1.5 2.3)";
|
||||
//! let transformed = steel_decimal.transform(script);
|
||||
//! // Result: "(decimal-add \"1.5\" \"2.3\")"
|
||||
//!
|
||||
//! // Register functions with Steel VM
|
||||
//! let mut vm = Engine::new();
|
||||
//! steel_decimal.register_functions(&mut vm);
|
||||
//!
|
||||
//! // Now you can execute the transformed script
|
||||
//! let result = vm.compile_and_run_raw_program(transformed);
|
||||
//! ```
|
||||
//!
|
||||
//! ### With Variables
|
||||
//! ```rust
|
||||
//! use steel_decimal::SteelDecimal;
|
||||
//! use steel::steel_vm::engine::Engine;
|
||||
//! use std::collections::HashMap;
|
||||
//!
|
||||
//! let mut variables = HashMap::new();
|
||||
//! variables.insert("x".to_string(), "10.5".to_string());
|
||||
//! variables.insert("y".to_string(), "20.3".to_string());
|
||||
//!
|
||||
//! let steel_decimal = SteelDecimal::with_variables(variables);
|
||||
//!
|
||||
//! let script = "(+ $x $y)";
|
||||
//! let transformed = steel_decimal.transform(script);
|
||||
//!
|
||||
//! let mut vm = Engine::new();
|
||||
//! steel_decimal.register_functions(&mut vm);
|
||||
//! ```
|
||||
//!
|
||||
//! ### Selective Function Registration
|
||||
//! ```rust
|
||||
//! use steel_decimal::FunctionRegistryBuilder;
|
||||
//! use steel::steel_vm::engine::Engine;
|
||||
//!
|
||||
//! let mut vm = Engine::new();
|
||||
//! FunctionRegistryBuilder::new()
|
||||
//! .basic_arithmetic(true)
|
||||
//! .advanced_math(false)
|
||||
//! .trigonometric(false)
|
||||
//! .register(&mut vm);
|
||||
//! ```
|
||||
|
||||
pub mod functions;
|
||||
pub mod parser;
|
||||
pub mod registry;
|
||||
pub mod utils;
|
||||
|
||||
pub use functions::*;
|
||||
pub use parser::ScriptParser;
|
||||
pub use registry::{FunctionRegistry, FunctionRegistryBuilder};
|
||||
pub use utils::{TypeConverter, ScriptAnalyzer, DecimalPrecision, ConversionError};
|
||||
|
||||
use steel::steel_vm::engine::Engine;
|
||||
use steel::steel_vm::register_fn::RegisterFn;
|
||||
use steel::rvals::SteelVal;
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
use steel::steel_vm::engine::Engine;
|
||||
|
||||
mod decimal_math;
|
||||
mod syntax_parser;
|
||||
|
||||
pub use decimal_math::*;
|
||||
use syntax_parser::*;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SteelDecimalError {
|
||||
#[error("Script parsing failed: {0}")]
|
||||
ParseError(String),
|
||||
#[error("Script execution failed: {0}")]
|
||||
RuntimeError(String),
|
||||
#[error("Type conversion error: {0}")]
|
||||
TypeConversionError(String),
|
||||
/// Main entry point for the Steel Decimal library
|
||||
pub struct SteelDecimal {
|
||||
parser: ScriptParser,
|
||||
variables: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SteelContext {
|
||||
pub variables: HashMap<String, String>,
|
||||
pub current_table: String,
|
||||
}
|
||||
|
||||
impl SteelContext {
|
||||
pub fn new(current_table: String) -> Self {
|
||||
Self {
|
||||
variables: HashMap::new(),
|
||||
current_table,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_variables(current_table: String, variables: HashMap<String, String>) -> Self {
|
||||
Self {
|
||||
variables,
|
||||
current_table,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_variable(&mut self, key: String, value: String) {
|
||||
self.variables.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SteelResult {
|
||||
pub result: String,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct SteelDecimalEngine {
|
||||
parser: SyntaxParser,
|
||||
}
|
||||
|
||||
impl SteelDecimalEngine {
|
||||
impl SteelDecimal {
|
||||
/// Create a new SteelDecimal instance
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
parser: SyntaxParser::new(),
|
||||
parser: ScriptParser::new(),
|
||||
variables: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_script(&self, script: &str, context: &SteelContext) -> Result<String, SteelDecimalError> {
|
||||
Ok(self.parser.parse(script, &context.current_table))
|
||||
/// Create a new SteelDecimal instance with variables
|
||||
pub fn with_variables(variables: HashMap<String, String>) -> Self {
|
||||
Self {
|
||||
parser: ScriptParser::new(),
|
||||
variables,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_script(&self, script: &str, context: &SteelContext) -> Result<SteelResult, SteelDecimalError> {
|
||||
let transformed_script = self.parser.parse(script, &context.current_table);
|
||||
/// Transform a script by converting math operations to decimal functions
|
||||
pub fn transform(&self, script: &str) -> String {
|
||||
self.parser.transform(script)
|
||||
}
|
||||
|
||||
/// Register all decimal functions with a Steel VM
|
||||
pub fn register_functions(&self, vm: &mut Engine) {
|
||||
FunctionRegistry::register_all(vm);
|
||||
|
||||
if !self.variables.is_empty() {
|
||||
FunctionRegistry::register_variables(vm, self.variables.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Register functions selectively using a builder
|
||||
pub fn register_functions_with_builder(&self, vm: &mut Engine, builder: FunctionRegistryBuilder) {
|
||||
let builder = if !self.variables.is_empty() {
|
||||
builder.with_variables(self.variables.clone())
|
||||
} else {
|
||||
builder
|
||||
};
|
||||
|
||||
builder.register(vm);
|
||||
}
|
||||
|
||||
/// Add a variable to the context
|
||||
pub fn add_variable(&mut self, name: String, value: String) {
|
||||
self.variables.insert(name, value);
|
||||
}
|
||||
|
||||
/// Get all variables
|
||||
pub fn get_variables(&self) -> &HashMap<String, String> {
|
||||
&self.variables
|
||||
}
|
||||
|
||||
/// Extract dependencies from a script
|
||||
pub fn extract_dependencies(&self, script: &str) -> std::collections::HashSet<String> {
|
||||
self.parser.extract_dependencies(script)
|
||||
}
|
||||
|
||||
/// Parse and execute a script in one step (convenience method)
|
||||
pub fn parse_and_execute(&self, script: &str) -> Result<Vec<steel::rvals::SteelVal>, String> {
|
||||
let mut vm = Engine::new();
|
||||
self.register_decimal_functions(&mut vm);
|
||||
self.register_context_functions(&mut vm, context);
|
||||
|
||||
let results = vm.compile_and_run_raw_program(transformed_script)
|
||||
.map_err(|e| SteelDecimalError::RuntimeError(e.to_string()))?;
|
||||
|
||||
let result_string = self.convert_result_to_string(results)?;
|
||||
|
||||
Ok(SteelResult {
|
||||
result: result_string,
|
||||
warnings: Vec::new(),
|
||||
})
|
||||
self.register_functions(&mut vm);
|
||||
|
||||
let transformed = self.transform(script);
|
||||
|
||||
vm.compile_and_run_raw_program(transformed)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn register_decimal_functions(&self, vm: &mut Engine) {
|
||||
vm.register_fn("decimal-add", decimal_add);
|
||||
vm.register_fn("decimal-sub", decimal_sub);
|
||||
vm.register_fn("decimal-mul", decimal_mul);
|
||||
vm.register_fn("decimal-div", decimal_div);
|
||||
vm.register_fn("decimal-pow", decimal_pow);
|
||||
vm.register_fn("decimal-sqrt", decimal_sqrt);
|
||||
vm.register_fn("decimal-ln", decimal_ln);
|
||||
vm.register_fn("decimal-log10", decimal_log10);
|
||||
vm.register_fn("decimal-exp", decimal_exp);
|
||||
vm.register_fn("decimal-sin", decimal_sin);
|
||||
vm.register_fn("decimal-cos", decimal_cos);
|
||||
vm.register_fn("decimal-tan", decimal_tan);
|
||||
vm.register_fn("decimal-gt", decimal_gt);
|
||||
vm.register_fn("decimal-gte", decimal_gte);
|
||||
vm.register_fn("decimal-lt", decimal_lt);
|
||||
vm.register_fn("decimal-lte", decimal_lte);
|
||||
vm.register_fn("decimal-eq", decimal_eq);
|
||||
vm.register_fn("decimal-abs", decimal_abs);
|
||||
vm.register_fn("decimal-round", decimal_round);
|
||||
vm.register_fn("decimal-min", decimal_min);
|
||||
vm.register_fn("decimal-max", decimal_max);
|
||||
vm.register_fn("decimal-zero", || "0".to_string());
|
||||
vm.register_fn("decimal-one", || "1".to_string());
|
||||
vm.register_fn("decimal-pi", || "3.1415926535897932384626433833".to_string());
|
||||
vm.register_fn("decimal-e", || "2.7182818284590452353602874714".to_string());
|
||||
vm.register_fn("decimal-percentage", decimal_percentage);
|
||||
vm.register_fn("decimal-compound", decimal_compound);
|
||||
}
|
||||
|
||||
fn register_context_functions(&self, vm: &mut Engine, context: &SteelContext) {
|
||||
let variables = context.variables.clone();
|
||||
vm.register_fn("get-var", move |var_name: String| -> Result<String, String> {
|
||||
variables.get(&var_name)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("Variable '{}' not found", var_name))
|
||||
});
|
||||
|
||||
let variables_check = context.variables.clone();
|
||||
vm.register_fn("has-var?", move |var_name: String| -> bool {
|
||||
variables_check.contains_key(&var_name)
|
||||
});
|
||||
}
|
||||
|
||||
fn convert_result_to_string(&self, results: Vec<SteelVal>) -> Result<String, SteelDecimalError> {
|
||||
if results.is_empty() {
|
||||
return Ok(String::new());
|
||||
/// Validate that a script can be transformed without errors
|
||||
pub fn validate_script(&self, script: &str) -> Result<(), String> {
|
||||
// Basic validation - check if transformation succeeds
|
||||
let transformed = self.transform(script);
|
||||
|
||||
// Check if the script contains balanced parentheses
|
||||
let open_count = transformed.chars().filter(|c| *c == '(').count();
|
||||
let close_count = transformed.chars().filter(|c| *c == ')').count();
|
||||
|
||||
if open_count != close_count {
|
||||
return Err("Unbalanced parentheses in script".to_string());
|
||||
}
|
||||
|
||||
let last_result = &results[results.len() - 1];
|
||||
|
||||
match last_result {
|
||||
SteelVal::StringV(s) => Ok(s.to_string()),
|
||||
SteelVal::NumV(n) => Ok(n.to_string()),
|
||||
SteelVal::IntV(i) => Ok(i.to_string()),
|
||||
SteelVal::BoolV(b) => Ok(b.to_string()),
|
||||
SteelVal::VectorV(v) => {
|
||||
let string_values: Result<Vec<String>, _> = v.iter()
|
||||
.map(|val| match val {
|
||||
SteelVal::StringV(s) => Ok(s.to_string()),
|
||||
SteelVal::NumV(n) => Ok(n.to_string()),
|
||||
SteelVal::IntV(i) => Ok(i.to_string()),
|
||||
SteelVal::BoolV(b) => Ok(b.to_string()),
|
||||
_ => Err(SteelDecimalError::TypeConversionError(
|
||||
format!("Cannot convert vector element to string: {:?}", val)
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
match string_values {
|
||||
Ok(strings) => Ok(strings.join(",")),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
|
||||
// Check if all variables are defined
|
||||
let dependencies = self.extract_dependencies(script);
|
||||
for dep in dependencies {
|
||||
if !self.variables.contains_key(&dep) {
|
||||
return Err(format!("Undefined variable: {}", dep));
|
||||
}
|
||||
_ => Err(SteelDecimalError::TypeConversionError(
|
||||
format!("Cannot convert result to string: {:?}", last_result)
|
||||
))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SteelDecimalEngine {
|
||||
impl Default for SteelDecimal {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn decimal_percentage(amount: String, percentage: String) -> Result<String, String> {
|
||||
use rust_decimal::prelude::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
let amount_dec = Decimal::from_str(&amount)
|
||||
.map_err(|e| format!("Invalid amount: {}", e))?;
|
||||
let percentage_dec = Decimal::from_str(&percentage)
|
||||
.map_err(|e| format!("Invalid percentage: {}", e))?;
|
||||
let hundred = Decimal::from(100);
|
||||
|
||||
Ok((amount_dec * percentage_dec / hundred).to_string())
|
||||
/// Convenience functions for quick operations
|
||||
pub mod prelude {
|
||||
pub use crate::{SteelDecimal, FunctionRegistry, FunctionRegistryBuilder};
|
||||
pub use crate::functions::*;
|
||||
pub use crate::utils::{TypeConverter, ScriptAnalyzer, DecimalPrecision};
|
||||
}
|
||||
|
||||
fn decimal_compound(principal: String, rate: String, time: String) -> Result<String, String> {
|
||||
use rust_decimal::prelude::*;
|
||||
use rust_decimal::MathematicalOps;
|
||||
use std::str::FromStr;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
let principal_dec = Decimal::from_str(&principal)
|
||||
.map_err(|e| format!("Invalid principal: {}", e))?;
|
||||
let rate_dec = Decimal::from_str(&rate)
|
||||
.map_err(|e| format!("Invalid rate: {}", e))?;
|
||||
let time_dec = Decimal::from_str(&time)
|
||||
.map_err(|e| format!("Invalid time: {}", e))?;
|
||||
#[test]
|
||||
fn test_basic_transformation() {
|
||||
let steel_decimal = SteelDecimal::new();
|
||||
let script = "(+ 1.5 2.3)";
|
||||
let transformed = steel_decimal.transform(script);
|
||||
assert_eq!(transformed, "(decimal-add \"1.5\" \"2.3\")");
|
||||
}
|
||||
|
||||
let one = Decimal::ONE;
|
||||
let compound_factor = (one + rate_dec).checked_powd(time_dec)
|
||||
.ok_or("Compound calculation overflow")?;
|
||||
#[test]
|
||||
fn test_with_variables() {
|
||||
let mut variables = HashMap::new();
|
||||
variables.insert("x".to_string(), "10.5".to_string());
|
||||
variables.insert("y".to_string(), "20.3".to_string());
|
||||
|
||||
Ok((principal_dec * compound_factor).to_string())
|
||||
let steel_decimal = SteelDecimal::with_variables(variables);
|
||||
let script = "(+ $x $y)";
|
||||
let transformed = steel_decimal.transform(script);
|
||||
assert_eq!(transformed, "(decimal-add (get-var \"x\") (get-var \"y\"))");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_registration() {
|
||||
let steel_decimal = SteelDecimal::new();
|
||||
let mut vm = Engine::new();
|
||||
|
||||
steel_decimal.register_functions(&mut vm);
|
||||
|
||||
let script = "(decimal-add \"1.5\" \"2.3\")";
|
||||
let result = vm.compile_and_run_raw_program(script.to_string());
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_and_execute() {
|
||||
let steel_decimal = SteelDecimal::new();
|
||||
let script = "(+ 1.5 2.3)";
|
||||
|
||||
let result = steel_decimal.parse_and_execute(script);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_script_validation() {
|
||||
let steel_decimal = SteelDecimal::new();
|
||||
|
||||
// Valid script
|
||||
assert!(steel_decimal.validate_script("(+ 1.5 2.3)").is_ok());
|
||||
|
||||
// Invalid script - unbalanced parentheses
|
||||
assert!(steel_decimal.validate_script("(+ 1.5 2.3").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_variable_validation() {
|
||||
let steel_decimal = SteelDecimal::new();
|
||||
|
||||
// Script with undefined variable
|
||||
assert!(steel_decimal.validate_script("(+ $x 2.3)").is_err());
|
||||
|
||||
// Script with defined variable
|
||||
let mut variables = HashMap::new();
|
||||
variables.insert("x".to_string(), "10.5".to_string());
|
||||
let steel_decimal = SteelDecimal::with_variables(variables);
|
||||
assert!(steel_decimal.validate_script("(+ $x 2.3)").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_expressions() {
|
||||
let steel_decimal = SteelDecimal::new();
|
||||
|
||||
let script = "(+ (* 2.5 3.0) (/ 15.0 3.0))";
|
||||
let transformed = steel_decimal.transform(script);
|
||||
let expected = "(decimal-add (decimal-mul \"2.5\" \"3.0\") (decimal-div \"15.0\" \"3.0\"))";
|
||||
assert_eq!(transformed, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dependency_extraction() {
|
||||
let steel_decimal = SteelDecimal::new();
|
||||
let script = "(+ $x $y $z)";
|
||||
|
||||
let dependencies = steel_decimal.extract_dependencies(script);
|
||||
assert_eq!(dependencies.len(), 3);
|
||||
assert!(dependencies.contains("x"));
|
||||
assert!(dependencies.contains("y"));
|
||||
assert!(dependencies.contains("z"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user