Skip to content

Commit 986c16a

Browse files
committed
Fix parsing, Apply, CosIntegral, and playground splitting (#60, #61, #63, #64)
- Fix #60: Allow Part extraction in increment/decrement operators (++x[[i]], --x[[i]], x[[i]]++, x[[i]]--) by updating grammar rules and adding Part evaluation support in the increment/decrement handler. - Fix #61: Implement CosIntegral and SinIntegral with special values (0, Infinity, -Infinity) and numeric evaluation via power series and asymptotic expansion. - Fix #63: Make Apply work with any expression type (BinaryOp, UnaryOp, Rule, Association) by adding expr_children() helper that extracts children in Wolfram canonical form. - Fix #64: Fix split_into_statements to recognize /; (Condition) and operator continuations at line endings, matching insert_statement_separators behavior for consistent playground/file parsing.
1 parent 8ac5750 commit 986c16a

File tree

11 files changed

+484
-51
lines changed

11 files changed

+484
-51
lines changed

functions.csv

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,7 @@ Cosh,Returns the hyperbolic cosine,✅,pure,1,259
11281128
CoshIntegral,,,,3,1347
11291129
CosineDistance,,,,6,2911
11301130
CosineWindow,,,,9,3862
1131-
CosIntegral,,,,2,1178
1131+
CosIntegral,Cosine integral Ci(z),,pure,2,1178
11321132
Cot,Returns the cotangent,,pure,1,358
11331133
CotDegrees,,,,14.1,
11341134
Coth,Returns the hyperbolic cotangent,,pure,1,592
@@ -4994,7 +4994,7 @@ SingularValueList,,,,5,1892
49944994
SingularValuePlot,,,,8,3755
49954995
Sinh,Returns the hyperbolic sine,,pure,1,278
49964996
SinhIntegral,,,,3,1309
4997-
SinIntegral,,,,2,1148
4997+
SinIntegral,Sine integral Si(z),,pure,2,1148
49984998
SixJSymbol,,,,2,1217
49994999
Skeleton,Represents omitted elements in output display as <<n>>,,none,1,4479
50005000
SkeletonTransform,,,,8,3522

src/evaluator/core_eval.rs

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,37 +1050,59 @@ pub fn evaluate_expr_to_expr_inner(
10501050
|| name == "PreIncrement"
10511051
|| name == "PreDecrement")
10521052
&& args.len() == 1
1053-
&& let Expr::Identifier(var_name) = &args[0]
10541053
{
1055-
let current = ENV.with(|e| e.borrow().get(var_name).cloned());
1056-
let current_val = match current {
1057-
Some(StoredValue::ExprVal(e)) => e,
1058-
Some(StoredValue::Raw(s)) => {
1059-
crate::syntax::string_to_expr(&s).unwrap_or(Expr::Integer(0))
1054+
if let Expr::Identifier(var_name) = &args[0] {
1055+
let current = ENV.with(|e| e.borrow().get(var_name).cloned());
1056+
let current_val = match current {
1057+
Some(StoredValue::ExprVal(e)) => e,
1058+
Some(StoredValue::Raw(s)) => {
1059+
crate::syntax::string_to_expr(&s).unwrap_or(Expr::Integer(0))
1060+
}
1061+
_ => Expr::Integer(0),
1062+
};
1063+
let delta = if name == "Increment" || name == "PreIncrement" {
1064+
Expr::Integer(1)
1065+
} else {
1066+
Expr::Integer(-1)
1067+
};
1068+
let new_val = evaluate_expr_to_expr(&Expr::BinaryOp {
1069+
op: crate::syntax::BinaryOperator::Plus,
1070+
left: Box::new(current_val.clone()),
1071+
right: Box::new(delta),
1072+
})?;
1073+
ENV.with(|e| {
1074+
e.borrow_mut().insert(
1075+
var_name.clone(),
1076+
StoredValue::Raw(crate::syntax::expr_to_string(&new_val)),
1077+
);
1078+
});
1079+
// Post-increment/decrement returns old value; pre returns new value
1080+
if name == "PreIncrement" || name == "PreDecrement" {
1081+
return Ok(new_val);
10601082
}
1061-
_ => Expr::Integer(0),
1062-
};
1063-
let delta = if name == "Increment" || name == "PreIncrement" {
1064-
Expr::Integer(1)
1065-
} else {
1066-
Expr::Integer(-1)
1067-
};
1068-
let new_val = evaluate_expr_to_expr(&Expr::BinaryOp {
1069-
op: crate::syntax::BinaryOperator::Plus,
1070-
left: Box::new(current_val.clone()),
1071-
right: Box::new(delta),
1072-
})?;
1073-
ENV.with(|e| {
1074-
e.borrow_mut().insert(
1075-
var_name.clone(),
1076-
StoredValue::Raw(crate::syntax::expr_to_string(&new_val)),
1077-
);
1078-
});
1079-
// Post-increment/decrement returns old value; pre returns new value
1080-
if name == "PreIncrement" || name == "PreDecrement" {
1081-
return Ok(new_val);
1083+
return Ok(current_val);
1084+
}
1085+
// Handle Part expressions: ++x[[i]], --x[[i]], x[[i]]++, x[[i]]--
1086+
if let Expr::Part { .. } = &args[0] {
1087+
// Get the current value at the part position
1088+
let current_val = evaluate_expr_to_expr(&args[0])?;
1089+
let delta = if name == "Increment" || name == "PreIncrement" {
1090+
Expr::Integer(1)
1091+
} else {
1092+
Expr::Integer(-1)
1093+
};
1094+
let new_val = evaluate_expr_to_expr(&Expr::BinaryOp {
1095+
op: crate::syntax::BinaryOperator::Plus,
1096+
left: Box::new(current_val.clone()),
1097+
right: Box::new(delta),
1098+
})?;
1099+
// Use Set to assign the new value back to the part
1100+
crate::evaluator::assignment::set_ast(&args[0], &new_val)?;
1101+
if name == "PreIncrement" || name == "PreDecrement" {
1102+
return Ok(new_val);
1103+
}
1104+
return Ok(current_val);
10821105
}
1083-
return Ok(current_val);
10841106
}
10851107
// Special handling for Unset - x =. (removes definition)
10861108
if name == "Unset" && args.len() == 1 {

src/evaluator/dispatch/math_functions.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,12 @@ pub fn dispatch_math_functions(
409409
"ExpIntegralE" if args.len() == 2 => {
410410
return Some(crate::functions::math_ast::exp_integral_e_ast(args));
411411
}
412+
"CosIntegral" if args.len() == 1 => {
413+
return Some(crate::functions::math_ast::cos_integral_ast(args));
414+
}
415+
"SinIntegral" if args.len() == 1 => {
416+
return Some(crate::functions::math_ast::sin_integral_ast(args));
417+
}
412418
"BesselI" if args.len() == 2 => {
413419
return Some(crate::functions::math_ast::bessel_i_ast(args));
414420
}

src/functions/list_helpers_ast/functional.rs

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,12 +298,69 @@ pub fn nest_while_list_ast(
298298
Ok(Expr::List(results))
299299
}
300300

301+
/// Extract the children of any expression in Wolfram canonical form.
302+
/// Returns None for atomic expressions (Integer, Real, String, Symbol).
303+
fn expr_children(expr: &Expr) -> Option<Vec<Expr>> {
304+
match expr {
305+
Expr::List(items) => Some(items.clone()),
306+
Expr::FunctionCall { args, .. } => Some(args.clone()),
307+
Expr::BinaryOp { op, left, right } => {
308+
use crate::syntax::BinaryOperator;
309+
match op {
310+
// a - b → Plus[a, Times[-1, b]]
311+
BinaryOperator::Minus => Some(vec![
312+
left.as_ref().clone(),
313+
Expr::FunctionCall {
314+
name: "Times".to_string(),
315+
args: vec![Expr::Integer(-1), right.as_ref().clone()],
316+
},
317+
]),
318+
// 1/b → Power[b, -1]; a/b → Times[a, Power[b, -1]]
319+
BinaryOperator::Divide => {
320+
if matches!(left.as_ref(), Expr::Integer(1)) {
321+
Some(vec![right.as_ref().clone(), Expr::Integer(-1)])
322+
} else {
323+
Some(vec![
324+
left.as_ref().clone(),
325+
Expr::FunctionCall {
326+
name: "Power".to_string(),
327+
args: vec![right.as_ref().clone(), Expr::Integer(-1)],
328+
},
329+
])
330+
}
331+
}
332+
// Plus, Times, Power, And, Or, etc.
333+
_ => Some(vec![left.as_ref().clone(), right.as_ref().clone()]),
334+
}
335+
}
336+
Expr::UnaryOp { operand, .. } => Some(vec![operand.as_ref().clone()]),
337+
Expr::Rule {
338+
pattern,
339+
replacement,
340+
}
341+
| Expr::RuleDelayed {
342+
pattern,
343+
replacement,
344+
} => Some(vec![pattern.as_ref().clone(), replacement.as_ref().clone()]),
345+
Expr::Association(pairs) => Some(
346+
pairs
347+
.iter()
348+
.map(|(k, v)| Expr::Rule {
349+
pattern: Box::new(k.clone()),
350+
replacement: Box::new(v.clone()),
351+
})
352+
.collect(),
353+
),
354+
// Atomic expressions have no children
355+
_ => None,
356+
}
357+
}
358+
301359
/// Apply[f, list] - applies f to the elements of list (f @@ list)
302360
pub fn apply_ast(func: &Expr, list: &Expr) -> Result<Expr, InterpreterError> {
303-
let items = match list {
304-
Expr::List(items) => items.clone(),
305-
Expr::FunctionCall { args, .. } => args.clone(),
306-
_ => {
361+
let items = match expr_children(list) {
362+
Some(items) => items,
363+
None => {
307364
return Err(InterpreterError::EvaluationError(
308365
"Apply expects a list or expression as second argument".into(),
309366
));

src/functions/math_ast/special_functions.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2467,6 +2467,187 @@ pub fn exp_integral_ei_numeric(x: f64) -> f64 {
24672467
}
24682468
}
24692469

2470+
/// CosIntegral[z] - Cosine integral Ci(z)
2471+
/// Ci(z) = γ + ln(z) + ∫₀ᶻ (cos(t)-1)/t dt
2472+
pub fn cos_integral_ast(args: &[Expr]) -> Result<Expr, InterpreterError> {
2473+
if args.len() != 1 {
2474+
return Err(InterpreterError::EvaluationError(
2475+
"CosIntegral expects exactly 1 argument".into(),
2476+
));
2477+
}
2478+
2479+
match &args[0] {
2480+
// CosIntegral[0] = -Infinity
2481+
Expr::Integer(0) => Ok(Expr::FunctionCall {
2482+
name: "Times".to_string(),
2483+
args: vec![Expr::Integer(-1), Expr::Identifier("Infinity".to_string())],
2484+
}),
2485+
// CosIntegral[Infinity] = 0
2486+
Expr::Identifier(s) if s == "Infinity" => Ok(Expr::Integer(0)),
2487+
// Numeric evaluation
2488+
Expr::Real(x) => Ok(Expr::Real(cos_integral_numeric(*x))),
2489+
// Check for -Infinity
2490+
other => {
2491+
if is_neg_infinity(other) {
2492+
// CosIntegral[-Infinity] = I*Pi
2493+
return Ok(Expr::FunctionCall {
2494+
name: "Times".to_string(),
2495+
args: vec![
2496+
Expr::Identifier("I".to_string()),
2497+
Expr::Constant("Pi".to_string()),
2498+
],
2499+
});
2500+
}
2501+
// Unevaluated
2502+
Ok(Expr::FunctionCall {
2503+
name: "CosIntegral".to_string(),
2504+
args: args.to_vec(),
2505+
})
2506+
}
2507+
}
2508+
}
2509+
2510+
/// Compute Ci(z) numerically for real z
2511+
/// Ci(z) = γ + ln|z| + ∫₀ᶻ (cos(t)-1)/t dt
2512+
/// = γ + ln|z| + Σ_{n=1}^∞ (-1)^n z^(2n) / (2n · (2n)!)
2513+
fn cos_integral_numeric(z: f64) -> f64 {
2514+
let euler_gamma = 0.5772156649015329;
2515+
2516+
if z.abs() < 40.0 {
2517+
// Power series: Ci(z) = γ + ln|z| + Σ (-1)^n z^(2n) / (2n · (2n)!)
2518+
let mut sum = euler_gamma + z.abs().ln();
2519+
let z2 = z * z;
2520+
let mut term = 1.0; // Will build up z^(2n) / (2n)!
2521+
for n in 1..200 {
2522+
let n2 = (2 * n) as f64;
2523+
// term *= z^2 / ((2n-1) * 2n)
2524+
term *= z2 / ((n2 - 1.0) * n2);
2525+
let contrib = if n % 2 == 1 { -term } else { term };
2526+
sum += contrib / n2;
2527+
if (contrib / n2).abs() < 1e-16 * sum.abs().max(1e-300) {
2528+
break;
2529+
}
2530+
}
2531+
sum
2532+
} else {
2533+
// Asymptotic expansion for large |z|
2534+
// Ci(z) ≈ sin(z)/z * f(z) - cos(z)/z * g(z)
2535+
// where f(z) = Σ (-1)^n (2n)! / z^(2n)
2536+
// g(z) = Σ (-1)^n (2n+1)! / z^(2n+1)
2537+
let mut f = 0.0;
2538+
let mut g = 0.0;
2539+
let mut f_term = 1.0;
2540+
let mut g_term = 1.0 / z;
2541+
for n in 0..100 {
2542+
f += if n % 2 == 0 { f_term } else { -f_term };
2543+
g += if n % 2 == 0 { g_term } else { -g_term };
2544+
let n2 = (2 * n + 1) as f64;
2545+
let n2p = (2 * n + 2) as f64;
2546+
f_term *= n2 * n2p / (z * z);
2547+
g_term *= n2p * (n2p + 1.0) / (z * z);
2548+
if f_term.abs() < 1e-16 && g_term.abs() < 1e-16 {
2549+
break;
2550+
}
2551+
// Divergent series: stop when terms start growing
2552+
if n > 0
2553+
&& (f_term.abs() > (n2 * n2p / (z * z) * f_term).abs()
2554+
|| g_term.abs() > 1e10)
2555+
{
2556+
break;
2557+
}
2558+
}
2559+
f / z * z.sin() - g * z.cos()
2560+
}
2561+
}
2562+
2563+
/// SinIntegral[z] - Sine integral Si(z)
2564+
/// Si(z) = ∫₀ᶻ sin(t)/t dt
2565+
pub fn sin_integral_ast(args: &[Expr]) -> Result<Expr, InterpreterError> {
2566+
if args.len() != 1 {
2567+
return Err(InterpreterError::EvaluationError(
2568+
"SinIntegral expects exactly 1 argument".into(),
2569+
));
2570+
}
2571+
2572+
match &args[0] {
2573+
// SinIntegral[0] = 0
2574+
Expr::Integer(0) => Ok(Expr::Integer(0)),
2575+
// SinIntegral[Infinity] = Pi/2
2576+
Expr::Identifier(s) if s == "Infinity" => Ok(Expr::FunctionCall {
2577+
name: "Times".to_string(),
2578+
args: vec![
2579+
crate::functions::math_ast::make_rational(1, 2),
2580+
Expr::Constant("Pi".to_string()),
2581+
],
2582+
}),
2583+
// Numeric evaluation
2584+
Expr::Real(x) => Ok(Expr::Real(sin_integral_numeric(*x))),
2585+
// Check for -Infinity
2586+
other => {
2587+
if is_neg_infinity(other) {
2588+
// SinIntegral[-Infinity] = -Pi/2
2589+
return Ok(Expr::FunctionCall {
2590+
name: "Times".to_string(),
2591+
args: vec![
2592+
crate::functions::math_ast::make_rational(-1, 2),
2593+
Expr::Constant("Pi".to_string()),
2594+
],
2595+
});
2596+
}
2597+
// Unevaluated
2598+
Ok(Expr::FunctionCall {
2599+
name: "SinIntegral".to_string(),
2600+
args: args.to_vec(),
2601+
})
2602+
}
2603+
}
2604+
}
2605+
2606+
/// Compute Si(z) numerically for real z
2607+
/// Si(z) = Σ_{n=0}^∞ (-1)^n z^(2n+1) / ((2n+1) · (2n+1)!)
2608+
fn sin_integral_numeric(z: f64) -> f64 {
2609+
if z.abs() < 40.0 {
2610+
let z2 = z * z;
2611+
let mut sum = z;
2612+
let mut term = z;
2613+
for n in 1..200 {
2614+
let n2 = (2 * n) as f64;
2615+
let n2p1 = n2 + 1.0;
2616+
term *= -z2 / (n2 * n2p1);
2617+
sum += term / n2p1;
2618+
if (term / n2p1).abs() < 1e-16 * sum.abs().max(1e-300) {
2619+
break;
2620+
}
2621+
}
2622+
sum
2623+
} else {
2624+
// Asymptotic expansion for large |z|
2625+
// Si(z) ≈ π/2 - cos(z)/z * f(z) - sin(z)/z * g(z)
2626+
let sign = if z > 0.0 { 1.0 } else { -1.0 };
2627+
let az = z.abs();
2628+
let mut f = 0.0;
2629+
let mut g = 0.0;
2630+
let mut f_term = 1.0;
2631+
let mut g_term = 1.0 / az;
2632+
for n in 0..100 {
2633+
f += if n % 2 == 0 { f_term } else { -f_term };
2634+
g += if n % 2 == 0 { g_term } else { -g_term };
2635+
let n2 = (2 * n + 1) as f64;
2636+
let n2p = (2 * n + 2) as f64;
2637+
let old_f = f_term;
2638+
f_term *= n2 * n2p / (az * az);
2639+
g_term *= n2p * (n2p + 1.0) / (az * az);
2640+
if f_term.abs() < 1e-16 && g_term.abs() < 1e-16 {
2641+
break;
2642+
}
2643+
if f_term.abs() > old_f.abs() {
2644+
break;
2645+
}
2646+
}
2647+
sign * (std::f64::consts::FRAC_PI_2 - f / az * az.cos() - g * az.sin())
2648+
}
2649+
}
2650+
24702651
/// ExpIntegralE[n, z] - Generalized exponential integral E_n(z)
24712652
pub fn exp_integral_e_ast(args: &[Expr]) -> Result<Expr, InterpreterError> {
24722653
if args.len() != 2 {

0 commit comments

Comments
 (0)