Skip to content

Commit 024b680

Browse files
committed
Add pattern support in implicit multiplication and backtick precision notation
- Allow patterns (x_, c_., x_Head, etc.) as factors in implicit multiplication so expressions like `c_. x_^2` parse correctly - Add parser support for backtick precision notation (e.g. 0.1`1) with bare backtick for machine precision - Implement BigFloat arithmetic with error-propagation precision tracking in both binary_ops and plus_ast - Add BigFloat to expr_to_number for mixed-type numeric operations
1 parent e1556b9 commit 024b680

File tree

6 files changed

+295
-6
lines changed

6 files changed

+295
-6
lines changed

src/evaluator/binary_ops.rs

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,16 @@ pub fn thread_binary_op(
4343
};
4444
return Ok(crate::functions::math_ast::bigint_to_expr(result));
4545
}
46+
// BigFloat precision-tracked arithmetic
47+
if (matches!(l, Expr::BigFloat(_, _)) || matches!(r, Expr::BigFloat(_, _)))
48+
&& let Some(result) = bigfloat_binary_op(l, r, op)
49+
{
50+
return Ok(result);
51+
}
4652
let ln = expr_to_number(l);
4753
let rn = expr_to_number(r);
48-
let any_real = matches!(l, Expr::Real(_)) || matches!(r, Expr::Real(_));
54+
let any_real = matches!(l, Expr::Real(_) | Expr::BigFloat(_, _))
55+
|| matches!(r, Expr::Real(_) | Expr::BigFloat(_, _));
4956
match op {
5057
BinaryOperator::Plus => {
5158
if let (Some(a), Some(b)) = (ln, rn) {
@@ -185,6 +192,120 @@ pub fn thread_binary_op(
185192
}
186193
}
187194

195+
/// Extract (f64_value, precision_in_digits) from a numeric expression.
196+
/// For BigFloat, precision comes from the stored value.
197+
/// For Integer/BigInteger, precision is effectively infinite (use f64::INFINITY).
198+
/// For Real, precision is machine precision (~16 digits).
199+
fn bigfloat_value_prec(expr: &Expr) -> Option<(f64, f64)> {
200+
match expr {
201+
Expr::BigFloat(digits, prec) => {
202+
let v: f64 = digits.parse().unwrap_or(0.0);
203+
Some((v, *prec as f64))
204+
}
205+
Expr::Real(f) => Some((*f, 16.0)),
206+
Expr::Integer(n) => Some((*n as f64, f64::INFINITY)),
207+
Expr::BigInteger(n) => {
208+
use num_traits::ToPrimitive;
209+
n.to_f64().map(|v| (v, f64::INFINITY))
210+
}
211+
_ => None,
212+
}
213+
}
214+
215+
/// Compute result precision for addition/subtraction using error propagation.
216+
fn precision_for_add(lv: f64, lp: f64, rv: f64, rp: f64, result: f64) -> usize {
217+
let le = if lp.is_finite() {
218+
lv.abs() * 10f64.powf(-lp)
219+
} else {
220+
0.0
221+
};
222+
let re = if rp.is_finite() {
223+
rv.abs() * 10f64.powf(-rp)
224+
} else {
225+
0.0
226+
};
227+
let total_error = le + re;
228+
if result.abs() < 1e-300 || total_error <= 0.0 {
229+
if lp.is_finite() && rp.is_finite() {
230+
(lp.min(rp) as usize).max(1)
231+
} else if lp.is_finite() {
232+
(lp as usize).max(1)
233+
} else {
234+
(rp as usize).max(1)
235+
}
236+
} else {
237+
let p = result.abs().log10() - total_error.log10();
238+
(p.max(0.0).round() as usize).max(1)
239+
}
240+
}
241+
242+
/// Format an f64 value as a BigFloat string with the given number of significant digits.
243+
fn format_bigfloat_value(value: f64, sig_digits: usize) -> String {
244+
if value == 0.0 {
245+
return "0.".to_string();
246+
}
247+
let sign = if value < 0.0 { "-" } else { "" };
248+
let abs_val = value.abs();
249+
let magnitude = abs_val.log10().floor() as i32;
250+
let decimal_places = ((sig_digits as i32) - magnitude - 1).max(0) as usize;
251+
let formatted = format!("{}{:.prec$}", sign, abs_val, prec = decimal_places);
252+
// Ensure trailing dot if no decimal point
253+
if !formatted.contains('.') {
254+
format!("{}.", formatted)
255+
} else {
256+
formatted
257+
}
258+
}
259+
260+
/// Perform a binary operation on BigFloat operands with precision tracking.
261+
/// Returns None if operands are not numeric, so the caller can fall through.
262+
fn bigfloat_binary_op(l: &Expr, r: &Expr, op: BinaryOperator) -> Option<Expr> {
263+
let (lv, lp) = bigfloat_value_prec(l)?;
264+
let (rv, rp) = bigfloat_value_prec(r)?;
265+
266+
// If either operand is machine-precision Real (not BigFloat), produce Real
267+
if matches!(l, Expr::Real(_)) || matches!(r, Expr::Real(_)) {
268+
let result = match op {
269+
BinaryOperator::Plus => lv + rv,
270+
BinaryOperator::Minus => lv - rv,
271+
BinaryOperator::Times => lv * rv,
272+
BinaryOperator::Divide if rv != 0.0 => lv / rv,
273+
BinaryOperator::Power => lv.powf(rv),
274+
_ => return None,
275+
};
276+
return Some(Expr::Real(result));
277+
}
278+
279+
let result_value = match op {
280+
BinaryOperator::Plus => lv + rv,
281+
BinaryOperator::Minus => lv - rv,
282+
BinaryOperator::Times => lv * rv,
283+
BinaryOperator::Divide if rv != 0.0 => lv / rv,
284+
BinaryOperator::Power => lv.powf(rv),
285+
_ => return None,
286+
};
287+
288+
let result_prec = match op {
289+
BinaryOperator::Plus | BinaryOperator::Minus => {
290+
precision_for_add(lv, lp, rv, rp, result_value)
291+
}
292+
_ => {
293+
// For Times/Divide/Power, use min precision
294+
let p = if lp.is_finite() && rp.is_finite() {
295+
lp.min(rp)
296+
} else if lp.is_finite() {
297+
lp
298+
} else {
299+
rp
300+
};
301+
(p.round() as usize).max(1)
302+
}
303+
};
304+
305+
let result_str = format_bigfloat_value(result_value, result_prec);
306+
Some(Expr::BigFloat(result_str, result_prec))
307+
}
308+
188309
/// Extract raw string content from an Expr (without quotes for strings)
189310
pub fn expr_to_raw_string(expr: &Expr) -> String {
190311
match expr {

src/evaluator/type_helpers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub fn expr_to_number(expr: &Expr) -> Option<f64> {
1010
n.to_f64()
1111
}
1212
Expr::Real(f) => Some(*f),
13+
Expr::BigFloat(digits, _) => digits.parse::<f64>().ok(),
1314
Expr::Constant(name) => constant_to_f64(name),
1415
_ => None,
1516
}

src/functions/math_ast/arithmetic.rs

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,14 @@ pub fn plus_ast(args: &[Expr]) -> Result<Expr, InterpreterError> {
131131
});
132132
}
133133

134-
// Classify arguments: exact (Integer/Rational), real (Real), or symbolic
134+
// Classify arguments: exact (Integer/Rational), real (Real), bigfloat, or symbolic
135135
let mut has_real = false;
136+
let mut has_bigfloat = false;
136137
let mut all_numeric = true;
137138
for arg in &flat_args {
138139
match arg {
139140
Expr::Real(_) => has_real = true,
141+
Expr::BigFloat(_, _) => has_bigfloat = true,
140142
Expr::Integer(_) => {}
141143
Expr::FunctionCall { name, args: rargs }
142144
if name == "Rational"
@@ -149,8 +151,8 @@ pub fn plus_ast(args: &[Expr]) -> Result<Expr, InterpreterError> {
149151
}
150152
}
151153

152-
// If all numeric and no Reals, use exact rational arithmetic
153-
if all_numeric && !has_real {
154+
// If all numeric and no Reals/BigFloats, use exact rational arithmetic
155+
if all_numeric && !has_real && !has_bigfloat {
154156
// Sum as exact rational: (numer, denom)
155157
let mut sum_n: i128 = 0;
156158
let mut sum_d: i128 = 1;
@@ -172,6 +174,11 @@ pub fn plus_ast(args: &[Expr]) -> Result<Expr, InterpreterError> {
172174
return Ok(make_rational(sum_n, sum_d));
173175
}
174176

177+
// If all numeric with BigFloat (no machine Real), use precision-tracked arithmetic
178+
if all_numeric && has_bigfloat && !has_real {
179+
return bigfloat_plus(&flat_args);
180+
}
181+
175182
// If all numeric but has Reals, use f64
176183
if all_numeric {
177184
let mut sum = 0.0;
@@ -3257,3 +3264,75 @@ pub fn min_ast(args: &[Expr]) -> Result<Expr, InterpreterError> {
32573264
}
32583265
}
32593266
}
3267+
3268+
/// Sum BigFloat (precision-tagged) numbers with precision tracking.
3269+
/// Handles BigFloat + BigFloat and BigFloat + Integer/Rational.
3270+
fn bigfloat_plus(args: &[Expr]) -> Result<Expr, InterpreterError> {
3271+
// Extract (f64_value, precision) for each argument
3272+
// BigFloat: use stored precision; Integer/Rational: infinite precision
3273+
let mut sum_val: f64 = 0.0;
3274+
let mut sum_error: f64 = 0.0;
3275+
3276+
for arg in args {
3277+
match arg {
3278+
Expr::BigFloat(digits, prec) => {
3279+
let v: f64 = digits.parse().unwrap_or(0.0);
3280+
let p = *prec as f64;
3281+
sum_val += v;
3282+
sum_error += v.abs() * 10f64.powf(-p);
3283+
}
3284+
Expr::Integer(n) => {
3285+
sum_val += *n as f64;
3286+
// Integer has infinite precision, contributes 0 error
3287+
}
3288+
Expr::FunctionCall { name, args: rargs }
3289+
if name == "Rational" && rargs.len() == 2 =>
3290+
{
3291+
if let (Expr::Integer(n), Expr::Integer(d)) = (&rargs[0], &rargs[1]) {
3292+
sum_val += *n as f64 / *d as f64;
3293+
}
3294+
}
3295+
_ => {}
3296+
}
3297+
}
3298+
3299+
// Compute result precision
3300+
let result_prec = if sum_val.abs() < 1e-300 || sum_error <= 0.0 {
3301+
// Fallback: use min finite precision among BigFloat args
3302+
args
3303+
.iter()
3304+
.filter_map(|a| {
3305+
if let Expr::BigFloat(_, p) = a {
3306+
Some(*p)
3307+
} else {
3308+
None
3309+
}
3310+
})
3311+
.min()
3312+
.unwrap_or(1)
3313+
} else {
3314+
let p = sum_val.abs().log10() - sum_error.log10();
3315+
(p.max(0.0).round() as usize).max(1)
3316+
};
3317+
3318+
// Format result value with the right number of significant digits
3319+
let result_str = format_bigfloat_value(sum_val, result_prec);
3320+
Ok(Expr::BigFloat(result_str, result_prec))
3321+
}
3322+
3323+
/// Format an f64 value as a BigFloat digit string with the given significant digits.
3324+
fn format_bigfloat_value(value: f64, sig_digits: usize) -> String {
3325+
if value == 0.0 {
3326+
return "0.".to_string();
3327+
}
3328+
let sign = if value < 0.0 { "-" } else { "" };
3329+
let abs_val = value.abs();
3330+
let magnitude = abs_val.log10().floor() as i32;
3331+
let decimal_places = ((sig_digits as i32) - magnitude - 1).max(0) as usize;
3332+
let formatted = format!("{}{:.prec$}", sign, abs_val, prec = decimal_places);
3333+
if !formatted.contains('.') {
3334+
format!("{}.", formatted)
3335+
} else {
3336+
formatted
3337+
}
3338+
}

src/syntax.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,21 @@ pub fn pair_to_expr(pair: Pair<Rule>) -> Expr {
10721072
Expr::Real(s.parse().unwrap_or(0.0))
10731073
}
10741074
}
1075+
Rule::PrecisionReal | Rule::UnsignedPrecisionReal => {
1076+
let s = pair.as_str();
1077+
// Split on backtick: "0.1`5" → value="0.1", prec="5"
1078+
let backtick_pos = s.find('`').unwrap();
1079+
let value_str = &s[..backtick_pos];
1080+
let prec_str = &s[backtick_pos + 1..];
1081+
if prec_str.is_empty() {
1082+
// Bare backtick = machine precision, just parse as Real
1083+
Expr::Real(value_str.parse().unwrap_or(0.0))
1084+
} else {
1085+
let prec: f64 = prec_str.parse().unwrap_or(0.0);
1086+
let prec_usize = (prec.round() as usize).max(1);
1087+
Expr::BigFloat(value_str.to_string(), prec_usize)
1088+
}
1089+
}
10751090
Rule::BasePrefix => {
10761091
let s = pair.as_str();
10771092
// Parse base^^digits format (e.g. 16^^FF = 255, 2^^1010 = 10)

src/wolfram.pest

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,26 @@ UnsignedReal = @{
2828
(ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT* | "." ~ ASCII_DIGIT+)
2929
~ ("*^" ~ ("-")? ~ ASCII_DIGIT+)?
3030
}
31+
// Precision-tagged real: 0.1`1 means 0.1 with precision 1 digit
32+
// 0.1` means machine precision (bare backtick)
33+
// Precision value can be integer or real: 0.1`5, 0.1`5.2
34+
UnsignedPrecisionReal = @{
35+
(ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT* | "." ~ ASCII_DIGIT+)
36+
~ "`"
37+
~ (ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT*)? | "." ~ ASCII_DIGIT+)?
38+
}
39+
PrecisionReal = @{
40+
("-")?
41+
~ (ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT* | "." ~ ASCII_DIGIT+)
42+
~ "`"
43+
~ (ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT*)? | "." ~ ASCII_DIGIT+)?
44+
}
3145
UnsignedConstant = { ("Pi" | "Degree" | "E") ~ !(ASCII_ALPHANUMERIC | "_") }
3246
// Base-prefix literal: base^^digits (e.g. 16^^FF, 2^^1010)
3347
BasePrefix = @{ ASCII_DIGIT+ ~ "^^" ~ (ASCII_ALPHANUMERIC)+ }
3448
Constant = { ("-")? ~ ("Pi" | "Degree" | "E") ~ !(ASCII_ALPHANUMERIC | "_") }
35-
NumericValue = { BasePrefix | Real | Integer | Constant }
36-
UnsignedNumericValue = { BasePrefix | UnsignedReal | UnsignedInteger | UnsignedConstant }
49+
NumericValue = { BasePrefix | PrecisionReal | Real | Integer | Constant }
50+
UnsignedNumericValue = { BasePrefix | UnsignedPrecisionReal | UnsignedReal | UnsignedInteger | UnsignedConstant }
3751

3852
// PatternName is an identifier that will be followed by _ in a Pattern
3953
// It must start with a letter and can contain letters, digits, but NOT trailing underscores
@@ -263,6 +277,7 @@ SimpleTerm = _{
263277
| FunctionCall
264278
| DerivativeIdentifier
265279
| NamedCharIdentifier
280+
| PatternTest | PatternOptionalWithHead | PatternOptionalSimple | PatternOptionalDefaultWithHead | PatternOptionalDefaultSimple | PatternWithHead | PatternSimple
266281
| Identifier
267282
| SlotSequence
268283
| Slot

tests/interpreter_tests/math/misc.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,64 @@ mod min_symbolic {
182182
}
183183
}
184184

185+
mod implicit_times_with_patterns {
186+
use super::*;
187+
188+
#[test]
189+
fn pattern_optional_default_implicit_times() {
190+
// Regression: c_. x_^2 failed to parse as implicit multiplication
191+
assert_eq!(interpret("Hold[c_. x_^2]").unwrap(), "Hold[c_.*x_^2]");
192+
}
193+
194+
#[test]
195+
fn pattern_optional_default_implicit_times_with_power() {
196+
// c_. x_^2 should be Times[c_., Power[x_, 2]]
197+
assert_eq!(
198+
interpret("Hold[c_. x_^2] // FullForm").unwrap(),
199+
"FullForm[Hold[c_.*x_^2]]"
200+
);
201+
}
202+
203+
#[test]
204+
fn number_times_pattern_implicit() {
205+
assert_eq!(interpret("Hold[2 x_]").unwrap(), "Hold[2*x_]");
206+
}
207+
208+
#[test]
209+
fn complex_pattern_expression_implicit_times() {
210+
// The expression from compare_output.sh
211+
assert_eq!(
212+
interpret("Int[(a_.+b_.*x_+c_. x_^2)^n_,x_Symbol] := Foo[x]").unwrap(),
213+
"Null"
214+
);
215+
}
216+
}
217+
218+
mod precision_real {
219+
use super::*;
220+
221+
#[test]
222+
fn parse_precision_real() {
223+
assert_eq!(interpret("0.1`1").unwrap(), "0.1`1.");
224+
}
225+
226+
#[test]
227+
fn parse_bare_backtick_machine_precision() {
228+
// Bare backtick is machine precision, displayed as normal Real
229+
assert_eq!(interpret("0.1`").unwrap(), "0.1");
230+
}
231+
232+
#[test]
233+
fn precision_real_addition() {
234+
assert_eq!(interpret("0.1`1 + 0.2`1").unwrap(), "0.3`1.");
235+
}
236+
237+
#[test]
238+
fn precision_real_plus_integer() {
239+
assert_eq!(interpret("0.1`1 + 1").unwrap(), "1.1`2.");
240+
}
241+
}
242+
185243
mod max_min_flatten {
186244
use super::*;
187245

0 commit comments

Comments
 (0)