Skip to main content

sim_lib_numbers_rational/implementation/
value.rs

1//! The `Rational` value (a numerator/denominator pair of integer values) and
2//! its value class: reduction, citizen encoding, and the read-constructor that
3//! rebuilds rationals.
4
5use std::sync::Arc;
6use std::sync::atomic::{AtomicU32, Ordering};
7
8use num_bigint::{BigInt, Sign};
9use sim_kernel::{
10    Args, Callable, Class, ClassId, ClassRef, Cx, DefaultFactory, Error, Expr, Factory,
11    NumberLiteral, NumberValue, Object, ObjectEncode, ObjectEncoding, ReadConstructor,
12    ReadConstructorRef, Result, ShapeRef, Symbol, TableRef, Value,
13};
14
15use crate::implementation::number_domain;
16use sim_lib_numbers_core::domains;
17
18use super::domain::{rational_value_class_symbol, value_shape_symbol};
19use super::integer::{
20    compact_canonical, is_integer_domain, parse_integer_literal, parse_integer_value,
21};
22
23/// An exact rational value: a numerator/denominator pair of integer values over
24/// bigint, displayed in compact `num/den` form when both parts are compact.
25#[derive(Clone)]
26pub struct Rational {
27    /// The numerator, an integer-domain value carrying the rational's sign.
28    pub num: Value,
29    /// The denominator, a positive integer-domain value.
30    pub den: Value,
31}
32
33impl Rational {
34    pub(crate) fn new(num: Value, den: Value) -> Self {
35        Self { num, den }
36    }
37}
38
39impl Object for Rational {
40    fn display(&self, cx: &mut Cx) -> Result<String> {
41        if let Some(canonical) = compact_canonical(cx, &self.num, &self.den)? {
42            return Ok(canonical);
43        }
44        Ok(format!(
45            "#<rational {}/{}>",
46            self.num.object().display(cx)?,
47            self.den.object().display(cx)?
48        ))
49    }
50
51    fn as_any(&self) -> &dyn std::any::Any {
52        self
53    }
54}
55
56impl sim_kernel::ObjectCompat for Rational {
57    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
58        if let Some(value) = cx
59            .registry()
60            .class_by_symbol(&rational_value_class_symbol())
61        {
62            return Ok(value.clone());
63        }
64        DefaultFactory.class_stub(
65            sim_kernel::CORE_NUMBER_CLASS_ID,
66            Symbol::qualified("core", "Number"),
67        )
68    }
69    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
70        if let Some(canonical) = compact_canonical(cx, &self.num, &self.den)? {
71            return Ok(Expr::Number(NumberLiteral {
72                domain: number_domain(),
73                canonical,
74            }));
75        }
76        Ok(Expr::Extension {
77            tag: rational_value_class_symbol(),
78            payload: Box::new(Expr::Vector(vec![
79                self.num.object().as_expr(cx)?,
80                self.den.object().as_expr(cx)?,
81            ])),
82        })
83    }
84    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
85        cx.factory().table(vec![
86            (
87                Symbol::new("kind"),
88                cx.factory().string("rational".to_owned())?,
89            ),
90            (Symbol::new("num"), self.num.clone()),
91            (Symbol::new("den"), self.den.clone()),
92        ])
93    }
94    fn as_number_value(&self) -> Option<&dyn NumberValue> {
95        Some(self)
96    }
97    fn as_object_encoder(&self) -> Option<&dyn ObjectEncode> {
98        Some(self)
99    }
100}
101
102impl NumberValue for Rational {
103    fn number_domain(&self, _cx: &mut Cx) -> Result<Symbol> {
104        Ok(number_domain())
105    }
106
107    fn number_literal(&self, cx: &mut Cx) -> Result<Option<NumberLiteral>> {
108        Ok(
109            compact_canonical(cx, &self.num, &self.den)?.map(|canonical| NumberLiteral {
110                domain: number_domain(),
111                canonical,
112            }),
113        )
114    }
115}
116
117impl ObjectEncode for Rational {
118    fn object_encoding(&self, cx: &mut Cx) -> Result<ObjectEncoding> {
119        Ok(ObjectEncoding::Constructor {
120            class: rational_value_class_symbol(),
121            args: vec![
122                self.num.object().as_expr(cx)?,
123                self.den.object().as_expr(cx)?,
124            ],
125        })
126    }
127}
128
129impl sim_citizen::Citizen for Rational {
130    fn citizen_symbol() -> Symbol {
131        rational_value_class_symbol()
132    }
133
134    fn citizen_version() -> u32 {
135        0
136    }
137
138    fn citizen_arity() -> usize {
139        2
140    }
141
142    fn citizen_fields() -> &'static [&'static str] {
143        &["num", "den"]
144    }
145}
146
147pub(crate) struct RationalValueClass {
148    id: AtomicU32,
149}
150
151impl RationalValueClass {
152    pub(crate) fn new() -> Self {
153        Self {
154            id: AtomicU32::new(0),
155        }
156    }
157
158    pub(crate) fn set_id(&self, id: ClassId) {
159        self.id.store(id.0, Ordering::Relaxed);
160    }
161}
162
163impl Object for RationalValueClass {
164    fn display(&self, _cx: &mut Cx) -> Result<String> {
165        Ok(format!("#<class {}>", rational_value_class_symbol()))
166    }
167
168    fn as_any(&self) -> &dyn std::any::Any {
169        self
170    }
171}
172
173impl sim_kernel::ObjectCompat for RationalValueClass {
174    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
175        if let Some(value) = cx
176            .registry()
177            .class_by_symbol(&Symbol::qualified("core", "Class"))
178        {
179            return Ok(value.clone());
180        }
181        DefaultFactory.class_stub(
182            sim_kernel::CORE_CLASS_CLASS_ID,
183            Symbol::qualified("core", "Class"),
184        )
185    }
186    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
187        Ok(Expr::Symbol(rational_value_class_symbol()))
188    }
189    fn as_callable(&self) -> Option<&dyn Callable> {
190        Some(self)
191    }
192    fn as_class(&self) -> Option<&dyn Class> {
193        Some(self)
194    }
195    fn as_read_constructor(&self) -> Option<&dyn ReadConstructor> {
196        Some(self)
197    }
198}
199
200impl Callable for RationalValueClass {
201    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
202        let values = args.into_vec();
203        let [num, den] = values.as_slice() else {
204            return Err(Error::Eval(format!(
205                "class {} expects exactly two arguments",
206                rational_value_class_symbol()
207            )));
208        };
209        make_rational(cx, num.clone(), den.clone())
210    }
211}
212
213impl Class for RationalValueClass {
214    fn id(&self) -> ClassId {
215        ClassId(self.id.load(Ordering::Relaxed))
216    }
217
218    fn symbol(&self) -> Symbol {
219        rational_value_class_symbol()
220    }
221
222    fn constructor_shape(&self, cx: &mut Cx) -> Result<ShapeRef> {
223        cx.factory().nil()
224    }
225
226    fn instance_shape(&self, cx: &mut Cx) -> Result<ShapeRef> {
227        Ok(cx
228            .registry()
229            .shape_by_symbol(&value_shape_symbol())
230            .cloned()
231            .unwrap_or(cx.factory().symbol(value_shape_symbol())?))
232    }
233
234    fn read_constructor(&self, cx: &mut Cx) -> Result<Option<ReadConstructorRef>> {
235        Ok(cx
236            .registry()
237            .class_by_symbol(&rational_value_class_symbol())
238            .cloned())
239    }
240
241    fn members(&self, cx: &mut Cx) -> Result<TableRef> {
242        cx.factory().table(Vec::new())
243    }
244}
245
246impl ReadConstructor for RationalValueClass {
247    fn symbol(&self) -> Symbol {
248        rational_value_class_symbol()
249    }
250
251    fn args_shape(&self, cx: &mut Cx) -> Result<ShapeRef> {
252        cx.factory().nil()
253    }
254
255    fn construct_read(&self, cx: &mut Cx, args: Vec<Value>) -> Result<Value> {
256        self.call(cx, Args::new(args))
257    }
258}
259
260pub(crate) fn make_rational(cx: &mut Cx, num: Value, den: Value) -> Result<Value> {
261    build_rational(cx, num, den, false)
262}
263
264pub(crate) fn make_reduced_rational(cx: &mut Cx, num: Value, den: Value) -> Result<Value> {
265    build_rational(cx, num, den, true)
266}
267
268fn build_rational(cx: &mut Cx, num: Value, den: Value, cross_reduce: bool) -> Result<Value> {
269    let (num, den) = normalize_integer_parts(cx, num, den, cross_reduce)?;
270    if let Some(canonical) = compact_canonical(cx, &num, &den)? {
271        return cx.factory().number_literal(number_domain(), canonical);
272    }
273    cx.factory().opaque(Arc::new(Rational::new(num, den)))
274}
275
276pub(crate) fn expect_rational_parts(cx: &mut Cx, value: Value, side: &str) -> Result<Rational> {
277    let Some(number) = cx.number_value_ref(value.clone())? else {
278        return Err(Error::Eval(format!(
279            "{side} operand expected number domain {}, found non-number",
280            number_domain()
281        )));
282    };
283    if number.domain != number_domain() {
284        return Err(Error::Eval(format!(
285            "{side} operand expected number domain {}, found {}",
286            number_domain(),
287            number.domain
288        )));
289    }
290    if let Some(rational) = value.object().downcast_ref::<Rational>() {
291        return Ok(rational.clone());
292    }
293    let literal = number.literal.ok_or_else(|| {
294        Error::Eval(format!(
295            "{side} operand in {} does not have a canonical rational form",
296            number_domain()
297        ))
298    })?;
299    let (num_text, den_text) = literal.canonical.split_once('/').ok_or_else(|| {
300        Error::Eval(format!(
301            "{side} operand was not a valid rational literal: {}",
302            literal.canonical
303        ))
304    })?;
305    Ok(Rational::new(
306        parse_integer_value(cx, num_text)?,
307        parse_integer_value(cx, den_text)?,
308    ))
309}
310
311fn normalize_integer_parts(
312    cx: &mut Cx,
313    num: Value,
314    den: Value,
315    cross_reduce: bool,
316) -> Result<(Value, Value)> {
317    require_integer_value(cx, &num, "numerator")?;
318    require_integer_value(cx, &den, "denominator")?;
319    let Some(den_literal) = cx
320        .number_value_ref(den.clone())?
321        .and_then(|number| number.literal)
322    else {
323        return Err(Error::Eval(
324            "denominator integer value does not have a canonical literal form".to_owned(),
325        ));
326    };
327    let den_big = parse_integer_literal(&den_literal)?;
328    if den_big == BigInt::from(0_u8) {
329        return Err(Error::Eval(
330            "rational denominator must not be zero".to_owned(),
331        ));
332    }
333
334    let (mut num, mut den) = if den_big.sign() == Sign::Minus {
335        (
336            negate_integer_value(cx, num.clone())?,
337            negate_integer_value(cx, den.clone())?,
338        )
339    } else {
340        (num, den)
341    };
342
343    if let Some((common_num, common_den)) =
344        coerce_for_reduction(cx, num.clone(), den.clone(), cross_reduce)?
345    {
346        let gcd = gcd_integer_values(cx, common_num.clone(), common_den.clone())?;
347        if let Some(gcd_literal) = cx
348            .number_value_ref(gcd.clone())?
349            .and_then(|value| value.literal)
350            && parse_integer_literal(&gcd_literal)? != BigInt::from(1_u8)
351        {
352            num = exact_divide_integer_value(cx, common_num, gcd.clone())?;
353            den = exact_divide_integer_value(cx, common_den, gcd)?;
354            return Ok((num, den));
355        }
356        num = common_num;
357        den = common_den;
358    }
359
360    Ok((num, den))
361}
362
363fn require_integer_value(cx: &mut Cx, value: &Value, side: &str) -> Result<()> {
364    let Some(number) = cx.number_value_ref(value.clone())? else {
365        return Err(Error::Eval(format!(
366            "{side} expected integer number value, found non-number"
367        )));
368    };
369    if !is_integer_domain(&number.domain) {
370        return Err(Error::Eval(format!(
371            "{side} expected integer number domain, found {}",
372            number.domain
373        )));
374    }
375    Ok(())
376}
377
378fn negate_integer_value(cx: &mut Cx, value: Value) -> Result<Value> {
379    cx.apply_value_number_unary_op(&Symbol::qualified("math", "neg"), value)
380}
381
382fn coerce_for_reduction(
383    cx: &mut Cx,
384    num: Value,
385    den: Value,
386    cross_reduce: bool,
387) -> Result<Option<(Value, Value)>> {
388    let num_ref = cx.number_value_ref(num.clone())?.ok_or_else(|| {
389        Error::Eval("rational numerator lost numeric identity during normalization".to_owned())
390    })?;
391    let den_ref = cx.number_value_ref(den.clone())?.ok_or_else(|| {
392        Error::Eval("rational denominator lost numeric identity during normalization".to_owned())
393    })?;
394    if num_ref.domain == den_ref.domain {
395        return Ok(Some((num, den)));
396    }
397    if !cross_reduce
398        || cx
399            .registry()
400            .number_domain_by_symbol(&domains::bigint())
401            .is_none()
402    {
403        return Ok(None);
404    }
405    let Some(num_literal) = num_ref.literal else {
406        return Ok(None);
407    };
408    let Some(den_literal) = den_ref.literal else {
409        return Ok(None);
410    };
411    let num_big = parse_integer_literal(&num_literal)?;
412    let den_big = parse_integer_literal(&den_literal)?;
413    Ok(Some((
414        cx.factory()
415            .number_literal(domains::bigint(), num_big.to_string())?,
416        cx.factory()
417            .number_literal(domains::bigint(), den_big.to_string())?,
418    )))
419}
420
421fn gcd_integer_values(cx: &mut Cx, left: Value, right: Value) -> Result<Value> {
422    let rem = Symbol::qualified("math", "rem");
423    let mut left = absolute_integer_value(cx, left)?;
424    let mut right = absolute_integer_value(cx, right)?;
425    loop {
426        let Some(right_literal) = cx
427            .number_value_ref(right.clone())?
428            .and_then(|value| value.literal)
429        else {
430            return Ok(left);
431        };
432        if parse_integer_literal(&right_literal)? == BigInt::from(0_u8) {
433            return Ok(left);
434        }
435        let next = cx.apply_value_number_binary_op(&rem, left, right.clone())?;
436        left = right;
437        right = next;
438    }
439}
440
441fn absolute_integer_value(cx: &mut Cx, value: Value) -> Result<Value> {
442    let Some(literal) = cx
443        .number_value_ref(value.clone())?
444        .and_then(|value| value.literal)
445    else {
446        return Ok(value);
447    };
448    let big = parse_integer_literal(&literal)?;
449    if big.sign() == Sign::Minus {
450        negate_integer_value(cx, value)
451    } else {
452        Ok(value)
453    }
454}
455
456fn exact_divide_integer_value(cx: &mut Cx, left: Value, right: Value) -> Result<Value> {
457    let left_ref = cx.number_value_ref(left)?.ok_or_else(|| {
458        Error::Eval("exact integer division expected a numeric left operand".to_owned())
459    })?;
460    let right_ref = cx.number_value_ref(right)?.ok_or_else(|| {
461        Error::Eval("exact integer division expected a numeric right operand".to_owned())
462    })?;
463    if left_ref.domain != right_ref.domain {
464        return Err(Error::Eval(format!(
465            "exact integer division requires a shared domain, found {} and {}",
466            left_ref.domain, right_ref.domain
467        )));
468    }
469    let left_literal = left_ref.literal.ok_or_else(|| {
470        Error::Eval("exact integer division requires a canonical left integer literal".to_owned())
471    })?;
472    let right_literal = right_ref.literal.ok_or_else(|| {
473        Error::Eval("exact integer division requires a canonical right integer literal".to_owned())
474    })?;
475    let left_big = parse_integer_literal(&left_literal)?;
476    let right_big = parse_integer_literal(&right_literal)?;
477    if right_big == BigInt::from(0_u8) {
478        return Err(Error::Eval(
479            "exact integer division encountered a zero divisor".to_owned(),
480        ));
481    }
482    if (&left_big % &right_big) != BigInt::from(0_u8) {
483        return Err(Error::Eval(format!(
484            "exact integer division found a non-divisible pair {}/{}",
485            left_big, right_big
486        )));
487    }
488    cx.factory()
489        .number_literal(left_ref.domain, (left_big / right_big).to_string())
490}