Skip to main content

sim_lib_numbers_complex/implementation/
ops.rs

1//! Complex arithmetic rules, literal parsing and canonicalization, and the
2//! promotions registering `f64`, `i64`, and `rational` into the complex domain.
3
4use sim_kernel::{Cx, Expr, Linker, NumberLiteral, PromotionRule, Result, Value};
5use sim_lib_numbers_core::domains;
6
7use super::literal::{f64_domain, i64_domain, number_domain, rational_domain};
8
9pub(super) type ComplexRuleFn = fn(&mut Cx, NumberLiteral, NumberLiteral) -> Result<Value>;
10pub(super) type ValueRuleFn = fn(&mut Cx, Value, Value) -> Result<Value>;
11
12pub fn register_promotions(linker: &mut Linker<'_>) {
13    for rule in [
14        PromotionRule {
15            from_domain: f64_domain(),
16            to_domain: number_domain(),
17            cost: 1,
18            convert: promote_f64_to_complex,
19        },
20        PromotionRule {
21            from_domain: i64_domain(),
22            to_domain: number_domain(),
23            cost: 1,
24            convert: promote_i64_to_complex,
25        },
26        PromotionRule {
27            from_domain: rational_domain(),
28            to_domain: number_domain(),
29            cost: 1,
30            convert: promote_rational_to_complex,
31        },
32    ] {
33        linker.promotion_rule(rule);
34    }
35}
36
37pub(super) fn complex_add_rule(
38    cx: &mut Cx,
39    left: NumberLiteral,
40    right: NumberLiteral,
41) -> Result<Value> {
42    complex_rule(
43        cx,
44        left,
45        right,
46        |left, right| Some((left.0 + right.0, left.1 + right.1)),
47        "add",
48    )
49}
50
51pub(super) fn complex_sub_rule(
52    cx: &mut Cx,
53    left: NumberLiteral,
54    right: NumberLiteral,
55) -> Result<Value> {
56    complex_rule(
57        cx,
58        left,
59        right,
60        |left, right| Some((left.0 - right.0, left.1 - right.1)),
61        "sub",
62    )
63}
64
65pub(super) fn complex_mul_rule(
66    cx: &mut Cx,
67    left: NumberLiteral,
68    right: NumberLiteral,
69) -> Result<Value> {
70    complex_rule(
71        cx,
72        left,
73        right,
74        |left, right| {
75            Some((
76                left.0 * right.0 - left.1 * right.1,
77                left.0 * right.1 + left.1 * right.0,
78            ))
79        },
80        "mul",
81    )
82}
83
84pub(super) fn complex_div_rule(
85    cx: &mut Cx,
86    left: NumberLiteral,
87    right: NumberLiteral,
88) -> Result<Value> {
89    complex_rule(
90        cx,
91        left,
92        right,
93        |left, right| {
94            let denominator = right.0 * right.0 + right.1 * right.1;
95            if denominator == 0.0 {
96                return None;
97            }
98            Some((
99                (left.0 * right.0 + left.1 * right.1) / denominator,
100                (left.1 * right.0 - left.0 * right.1) / denominator,
101            ))
102        },
103        "div",
104    )
105}
106
107pub(super) fn complex_neg_rule(cx: &mut Cx, operand: NumberLiteral) -> Result<Value> {
108    let operand = parse_complex_number(operand, "operand")?;
109    cx.factory()
110        .number_literal(number_domain(), canonical_complex(-operand.0, -operand.1))
111}
112
113pub(super) fn complex_sum_rule(cx: &mut Cx, operands: Vec<NumberLiteral>) -> Result<Value> {
114    let mut acc = (0.0_f64, 0.0_f64);
115    for operand in operands {
116        let operand = parse_complex_number(operand, "operand")?;
117        acc = (acc.0 + operand.0, acc.1 + operand.1);
118    }
119    cx.factory()
120        .number_literal(number_domain(), canonical_complex(acc.0, acc.1))
121}
122
123pub(super) fn complex_product_rule(cx: &mut Cx, operands: Vec<NumberLiteral>) -> Result<Value> {
124    let mut acc = (1.0_f64, 0.0_f64);
125    for operand in operands {
126        let operand = parse_complex_number(operand, "operand")?;
127        acc = (
128            acc.0 * operand.0 - acc.1 * operand.1,
129            acc.0 * operand.1 + acc.1 * operand.0,
130        );
131    }
132    cx.factory()
133        .number_literal(number_domain(), canonical_complex(acc.0, acc.1))
134}
135
136pub(crate) fn complex_add_value_rule(cx: &mut Cx, left: Value, right: Value) -> Result<Value> {
137    let left = expect_domain_literal(cx, left, "left")?;
138    let right = expect_domain_literal(cx, right, "right")?;
139    complex_add_rule(cx, left, right)
140}
141
142pub(crate) fn complex_sub_value_rule(cx: &mut Cx, left: Value, right: Value) -> Result<Value> {
143    let left = expect_domain_literal(cx, left, "left")?;
144    let right = expect_domain_literal(cx, right, "right")?;
145    complex_sub_rule(cx, left, right)
146}
147
148pub(crate) fn complex_mul_value_rule(cx: &mut Cx, left: Value, right: Value) -> Result<Value> {
149    let left = expect_domain_literal(cx, left, "left")?;
150    let right = expect_domain_literal(cx, right, "right")?;
151    complex_mul_rule(cx, left, right)
152}
153
154pub(crate) fn complex_div_value_rule(cx: &mut Cx, left: Value, right: Value) -> Result<Value> {
155    let left = expect_domain_literal(cx, left, "left")?;
156    let right = expect_domain_literal(cx, right, "right")?;
157    complex_div_rule(cx, left, right)
158}
159
160pub(crate) fn complex_neg_value_rule(cx: &mut Cx, operand: Value) -> Result<Value> {
161    let operand = expect_domain_literal(cx, operand, "operand")?;
162    complex_neg_rule(cx, operand)
163}
164
165pub(crate) fn complex_sum_value_rule(cx: &mut Cx, operands: Vec<Value>) -> Result<Value> {
166    let operands = expect_domain_literals(cx, operands, "operand")?;
167    complex_sum_rule(cx, operands)
168}
169
170pub(crate) fn complex_product_value_rule(cx: &mut Cx, operands: Vec<Value>) -> Result<Value> {
171    let operands = expect_domain_literals(cx, operands, "operand")?;
172    complex_product_rule(cx, operands)
173}
174
175fn complex_rule(
176    cx: &mut Cx,
177    left: NumberLiteral,
178    right: NumberLiteral,
179    apply: impl FnOnce((f64, f64), (f64, f64)) -> Option<(f64, f64)>,
180    name: &str,
181) -> Result<Value> {
182    let left = parse_complex_number(left, "left")?;
183    let right = parse_complex_number(right, "right")?;
184    let out = apply(left, right).ok_or_else(|| {
185        sim_kernel::Error::Eval(format!(
186            "{name} failed in complex arithmetic (overflow or divide by zero)"
187        ))
188    })?;
189    cx.factory()
190        .number_literal(number_domain(), canonical_complex(out.0, out.1))
191}
192
193fn parse_complex_number(number: NumberLiteral, side: &str) -> Result<(f64, f64)> {
194    if number.domain != number_domain() {
195        return Err(sim_kernel::Error::Eval(format!(
196            "{side} operand expected number domain {}, found {}",
197            number_domain(),
198            number.domain
199        )));
200    }
201    parse_complex_literal(&number.canonical).ok_or_else(|| {
202        sim_kernel::Error::Eval(format!(
203            "{side} operand was not a valid complex literal: {}",
204            number.canonical
205        ))
206    })
207}
208
209/// Parses a canonical `a+bi` complex literal into its `(real, imag)` parts,
210/// returning `None` when the text is not a well-formed complex literal.
211///
212/// # Examples
213///
214/// ```
215/// use sim_lib_numbers_complex::parse_complex_literal;
216///
217/// assert_eq!(parse_complex_literal("3+4i"), Some((3.0, 4.0)));
218/// assert_eq!(parse_complex_literal("1.5-2.25i"), Some((1.5, -2.25)));
219/// assert_eq!(parse_complex_literal("not-complex"), None);
220/// ```
221pub fn parse_complex_literal(text: &str) -> Option<(f64, f64)> {
222    let trimmed = text.trim();
223    let imag_text = trimmed.strip_suffix('i')?;
224    let split = imag_text
225        .as_bytes()
226        .get(1..)?
227        .iter()
228        .rposition(|byte| *byte == b'+' || *byte == b'-')
229        .map(|index| index + 1)?;
230    let (real_text, imag_with_sign) = imag_text.split_at(split);
231    let real = real_text.parse::<f64>().ok()?;
232    let imag = imag_with_sign.parse::<f64>().ok()?;
233    Some((real, imag))
234}
235
236/// Renders `(real, imag)` parts into the canonical `a+bi` literal form, fixing
237/// the imaginary sign and normalizing negative zero via [`canonical_f64`].
238///
239/// # Examples
240///
241/// ```
242/// use sim_lib_numbers_complex::canonical_complex;
243///
244/// assert_eq!(canonical_complex(3.0, 4.0), "3+4i");
245/// assert_eq!(canonical_complex(1.5, -2.25), "1.5-2.25i");
246/// ```
247pub fn canonical_complex(real: f64, imag: f64) -> String {
248    let real = canonical_f64(real);
249    let imag = canonical_f64(imag);
250    if imag.starts_with('-') {
251        format!("{real}{imag}i")
252    } else {
253        format!("{real}+{imag}i")
254    }
255}
256
257/// Renders an `f64` part for inclusion in a complex literal, normalizing the
258/// `-0` rendering to `0`.
259pub fn canonical_f64(value: f64) -> String {
260    let rendered = value.to_string();
261    if rendered == "-0" {
262        "0".to_owned()
263    } else {
264        rendered
265    }
266}
267
268fn promote_f64_to_complex(_cx: &mut Cx, number: NumberLiteral) -> Result<NumberLiteral> {
269    if number.domain != f64_domain() {
270        return Err(sim_kernel::Error::Eval(format!(
271            "expected number domain {} for promotion, found {}",
272            f64_domain(),
273            number.domain
274        )));
275    }
276    let value = number.canonical.parse::<f64>().map_err(|err| {
277        sim_kernel::Error::Eval(format!("could not promote f64 literal to complex: {}", err))
278    })?;
279    Ok(NumberLiteral {
280        domain: number_domain(),
281        canonical: canonical_complex(value, 0.0),
282    })
283}
284
285fn promote_i64_to_complex(_cx: &mut Cx, number: NumberLiteral) -> Result<NumberLiteral> {
286    if number.domain != i64_domain() {
287        return Err(sim_kernel::Error::Eval(format!(
288            "expected number domain {} for promotion, found {}",
289            i64_domain(),
290            number.domain
291        )));
292    }
293    let value = number.canonical.parse::<i64>().map_err(|err| {
294        sim_kernel::Error::Eval(format!("could not promote i64 literal to complex: {}", err))
295    })?;
296    Ok(NumberLiteral {
297        domain: number_domain(),
298        canonical: canonical_complex(value as f64, 0.0),
299    })
300}
301
302fn promote_rational_to_complex(_cx: &mut Cx, number: NumberLiteral) -> Result<NumberLiteral> {
303    if number.domain != rational_domain() {
304        return Err(sim_kernel::Error::Eval(format!(
305            "expected number domain {} for promotion, found {}",
306            rational_domain(),
307            number.domain
308        )));
309    }
310    let value = parse_rational_as_f64(&number.canonical).ok_or_else(|| {
311        sim_kernel::Error::Eval(format!(
312            "could not promote rational literal to complex: {}",
313            number.canonical
314        ))
315    })?;
316    Ok(NumberLiteral {
317        domain: number_domain(),
318        canonical: canonical_complex(value, 0.0),
319    })
320}
321
322pub(crate) fn promote_f64_value_to_complex(cx: &mut Cx, value: Value) -> Result<Value> {
323    let number = expect_number_literal(cx, value, f64_domain(), "operand")?;
324    let promoted = promote_f64_to_complex(cx, number)?;
325    cx.factory()
326        .number_literal(promoted.domain, promoted.canonical)
327}
328
329pub(crate) fn promote_i64_value_to_complex(cx: &mut Cx, value: Value) -> Result<Value> {
330    let number = expect_number_literal(cx, value, i64_domain(), "operand")?;
331    let promoted = promote_i64_to_complex(cx, number)?;
332    cx.factory()
333        .number_literal(promoted.domain, promoted.canonical)
334}
335
336pub(crate) fn promote_rational_value_to_complex(cx: &mut Cx, value: Value) -> Result<Value> {
337    let Some(number) = cx.number_value_ref(value.clone())? else {
338        return Err(sim_kernel::Error::Eval(format!(
339            "operand expected number domain {}, found non-number",
340            rational_domain()
341        )));
342    };
343    if number.domain != rational_domain() {
344        return Err(sim_kernel::Error::Eval(format!(
345            "operand expected number domain {}, found {}",
346            rational_domain(),
347            number.domain
348        )));
349    }
350    if let Some(literal) = number.literal {
351        let promoted = promote_rational_to_complex(cx, literal)?;
352        return cx
353            .factory()
354            .number_literal(promoted.domain, promoted.canonical);
355    }
356
357    let value = noncompact_rational_as_f64(cx, value)?;
358    cx.factory()
359        .number_literal(number_domain(), canonical_complex(value, 0.0))
360}
361
362/// Parses a `num/den` rational literal to its `f64` value for the rational ->
363/// complex promotion, returning `None` on malformed text or a zero denominator.
364pub fn parse_rational_as_f64(text: &str) -> Option<f64> {
365    let (numerator, denominator) = text.split_once('/')?;
366    let numerator = numerator.parse::<f64>().ok()?;
367    let denominator = denominator.parse::<f64>().ok()?;
368    if denominator == 0.0 {
369        return None;
370    }
371    Some(numerator / denominator)
372}
373
374fn expect_domain_literal(cx: &mut Cx, value: Value, side: &str) -> Result<NumberLiteral> {
375    expect_number_literal(cx, value, number_domain(), side)
376}
377
378fn expect_number_literal(
379    cx: &mut Cx,
380    value: Value,
381    expected_domain: sim_kernel::Symbol,
382    side: &str,
383) -> Result<NumberLiteral> {
384    let Some(number) = cx.number_value_ref(value)? else {
385        return Err(sim_kernel::Error::Eval(format!(
386            "{side} operand expected number domain {}, found non-number",
387            expected_domain
388        )));
389    };
390    if number.domain != expected_domain {
391        return Err(sim_kernel::Error::Eval(format!(
392            "{side} operand expected number domain {}, found {}",
393            expected_domain, number.domain
394        )));
395    }
396    number.literal.ok_or_else(|| {
397        sim_kernel::Error::Eval(format!(
398            "{side} operand in {} does not have a canonical literal form",
399            expected_domain
400        ))
401    })
402}
403
404fn expect_domain_literals(
405    cx: &mut Cx,
406    values: Vec<Value>,
407    side: &str,
408) -> Result<Vec<NumberLiteral>> {
409    values
410        .into_iter()
411        .map(|value| expect_domain_literal(cx, value, side))
412        .collect()
413}
414
415fn noncompact_rational_as_f64(cx: &mut Cx, value: Value) -> Result<f64> {
416    let Expr::Extension { tag, payload } = value.object().as_expr(cx)? else {
417        return Err(sim_kernel::Error::Eval(
418            "operand rational value does not have a complex-compatible surface".to_owned(),
419        ));
420    };
421    if tag != domains::rational_value_class() {
422        return Err(sim_kernel::Error::Eval(format!(
423            "operand expected rational value extension, found {}",
424            tag
425        )));
426    }
427    let Expr::Vector(parts) = payload.as_ref() else {
428        return Err(sim_kernel::Error::Eval(
429            "operand rational extension payload must be a vector".to_owned(),
430        ));
431    };
432    let [num, den] = parts.as_slice() else {
433        return Err(sim_kernel::Error::Eval(
434            "operand rational extension must contain numerator and denominator".to_owned(),
435        ));
436    };
437    let num = number_expr_as_f64(num, "numerator")?;
438    let den = number_expr_as_f64(den, "denominator")?;
439    if den == 0.0 {
440        return Err(sim_kernel::Error::Eval(
441            "operand rational denominator must not be zero".to_owned(),
442        ));
443    }
444    Ok(num / den)
445}
446
447fn number_expr_as_f64(expr: &Expr, side: &str) -> Result<f64> {
448    let Expr::Number(number) = expr else {
449        return Err(sim_kernel::Error::Eval(format!(
450            "{side} expected numeric literal in rational extension"
451        )));
452    };
453    number.canonical.parse::<f64>().map_err(|err| {
454        sim_kernel::Error::Eval(format!(
455            "{side} could not be converted to f64 for complex promotion: {}",
456            err
457        ))
458    })
459}