Skip to main content

sim_lib_numbers_rational/implementation/
domain.rs

1//! The `numbers/rational` domain object and `RationalNumbersLib`: the domain
2//! and operator symbols and the `Lib` that registers the domain, its shapes,
3//! value class, ops, and promotions to and from the integer and `f64` domains.
4
5use std::sync::Arc;
6
7use sim_kernel::{
8    AbiVersion, ClassId, DefaultFactory, Dependency, Export, Expr, Factory, Lib, LibManifest,
9    LibTarget, Linker, NumberDomain, NumberLiteral, Object, Result, Symbol, Value,
10    ValuePromotionRule, Version,
11};
12use sim_lib_numbers_core::{
13    DomainNumberValueShape, NumberDomainTableSpec, NumberLiteralClass, NumberLiteralShape,
14    ScalarBinaryOp, ScalarOps, ScalarReductionOp, ScalarUnaryOp, class_surface_or_symbol, domains,
15    install_scalar_ops, number_domain_table, shape_surface_or_symbol,
16};
17use sim_shape::shape_value;
18
19use super::integer::compact_canonical;
20use super::ops::{
21    RationalRuleFn, ValueRuleFn, promote_f64_literal_to_rational, promote_f64_value_to_rational,
22    promote_integer_literal_to_rational, promote_integer_value_to_rational,
23    promote_rational_literal_to_f64, promote_rational_value_to_f64, rational_add_rule,
24    rational_add_value_rule, rational_div_rule, rational_div_value_rule, rational_mul_rule,
25    rational_mul_value_rule, rational_neg_rule, rational_neg_value_rule, rational_pow_rule,
26    rational_pow_value_rule, rational_product_rule, rational_product_value_rule, rational_sub_rule,
27    rational_sub_value_rule, rational_sum_rule, rational_sum_value_rule,
28};
29use super::value::{Rational, RationalValueClass};
30
31/// The `numbers/rational` domain symbol shared by this crate's literals,
32/// values, and ops.
33pub fn number_domain() -> Symbol {
34    domains::rational()
35}
36
37/// The symbol of the rational literal class (the `Expr::Number` literal shape
38/// in canonical `num/den` form).
39pub fn literal_class_symbol() -> Symbol {
40    domains::literal_class("rational")
41}
42
43/// The symbol of the shape matching individual rational literals, derived from
44/// [`literal_class_symbol`].
45pub fn literal_instance_shape_symbol() -> Symbol {
46    Symbol::qualified(literal_class_symbol().to_string(), "instance-shape")
47}
48
49/// The symbol of the `numbers/rational` value class, used to register the class
50/// and to tag the extension encoding of non-compact rational values.
51pub fn rational_value_class_symbol() -> Symbol {
52    domains::rational_value_class()
53}
54
55pub(crate) fn value_shape_symbol() -> Symbol {
56    sim_lib_numbers_core::value_shape_symbol(&number_domain())
57}
58
59/// The `numbers/f64` domain symbol, the far end of the rational <-> f64
60/// promotion edges.
61pub fn f64_domain() -> Symbol {
62    domains::f64()
63}
64
65/// The `math/add` operator symbol this domain installs a rational rule for.
66pub fn add_symbol() -> Symbol {
67    Symbol::qualified("math", "add")
68}
69
70/// The `math/sub` operator symbol this domain installs a rational rule for.
71pub fn sub_symbol() -> Symbol {
72    Symbol::qualified("math", "sub")
73}
74
75/// The `math/mul` operator symbol this domain installs a rational rule for.
76pub fn mul_symbol() -> Symbol {
77    Symbol::qualified("math", "mul")
78}
79
80/// The `math/div` operator symbol this domain installs a rational rule for.
81pub fn div_symbol() -> Symbol {
82    Symbol::qualified("math", "div")
83}
84
85/// The `math/pow` operator symbol this domain installs a rational rule for.
86pub fn pow_symbol() -> Symbol {
87    Symbol::qualified("math", "pow")
88}
89
90#[sim_citizen_derive::non_citizen(
91    reason = "numbers/rational number-domain marker; reconstruct by loading the rational number lib",
92    kind = "marker"
93)]
94/// The exact rational number domain: parses `num/den` literals and declares the
95/// promotion edges to and from the integer and `f64` domains.
96pub struct RationalNumberDomain;
97
98impl NumberDomain for RationalNumberDomain {
99    fn symbol(&self) -> Symbol {
100        number_domain()
101    }
102
103    fn parse_literal(&self, cx: &mut sim_kernel::Cx, text: &str) -> Result<Option<Value>> {
104        let Some((numerator, denominator)) = super::ops::parse_rational_parts(text) else {
105            return Ok(None);
106        };
107        cx.factory()
108            .number_literal(number_domain(), format!("{numerator}/{denominator}"))
109            .map(Some)
110    }
111
112    fn encode_literal(
113        &self,
114        cx: &mut sim_kernel::Cx,
115        value: Value,
116    ) -> Result<Option<NumberLiteral>> {
117        match value.object().as_expr(cx)? {
118            Expr::Number(number) if number.domain == number_domain() => Ok(Some(number)),
119            _ => Ok(value
120                .object()
121                .downcast_ref::<Rational>()
122                .cloned()
123                .map(|rational| compact_canonical(cx, &rational.num, &rational.den))
124                .transpose()?
125                .flatten()
126                .map(|canonical| NumberLiteral {
127                    domain: number_domain(),
128                    canonical,
129                })),
130        }
131    }
132}
133
134impl Object for RationalNumberDomain {
135    fn display(&self, _cx: &mut sim_kernel::Cx) -> Result<String> {
136        Ok("#<number-domain numbers/rational>".to_owned())
137    }
138
139    fn as_any(&self) -> &dyn std::any::Any {
140        self
141    }
142}
143
144impl sim_kernel::ObjectCompat for RationalNumberDomain {
145    fn class(&self, cx: &mut sim_kernel::Cx) -> Result<sim_kernel::ClassRef> {
146        sim_lib_numbers_core::number_domain_class_stub(cx)
147    }
148    fn as_expr(&self, _cx: &mut sim_kernel::Cx) -> Result<Expr> {
149        Ok(Expr::Symbol(number_domain()))
150    }
151    fn as_table(&self, cx: &mut sim_kernel::Cx) -> Result<Value> {
152        let literal_class = class_surface_or_symbol(cx, literal_class_symbol())?;
153        let instance_shape = shape_surface_or_symbol(cx, literal_instance_shape_symbol())?;
154        let value_shape = shape_surface_or_symbol(cx, value_shape_symbol())?;
155        number_domain_table(
156            cx,
157            NumberDomainTableSpec::new(
158                number_domain(),
159                "rational",
160                "numerator/denominator",
161                0,
162                literal_class,
163                instance_shape,
164                value_shape,
165            ),
166        )
167    }
168    fn as_number_domain(&self) -> Option<&dyn NumberDomain> {
169        Some(self)
170    }
171}
172
173/// The library that installs the `numbers/rational` domain: its literal class
174/// and shapes, the `Rational` value class, the reduced arithmetic ops, and the
175/// promotion rules to and from the integer and `f64` domains.
176///
177/// # Examples
178///
179/// ```
180/// use std::sync::Arc;
181/// use sim_kernel::{Cx, DefaultFactory, NoopEvalPolicy};
182/// use sim_lib_numbers_rational::{RationalNumbersLib, number_domain};
183///
184/// let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
185/// cx.load_lib(&RationalNumbersLib::new()).unwrap();
186///
187/// // Rational literals parse into the domain even before any base scalar lib
188/// // is loaded; arithmetic over them additionally needs the integer libs.
189/// let value = cx
190///     .factory()
191///     .number_literal(number_domain(), "1/2".to_owned())
192///     .unwrap();
193/// let number = cx.number_value_ref(value).unwrap().unwrap();
194/// assert_eq!(number.domain, number_domain());
195/// ```
196pub struct RationalNumbersLib;
197
198impl RationalNumbersLib {
199    /// Creates a new `numbers/rational` domain library.
200    pub fn new() -> Self {
201        Self
202    }
203}
204
205impl Default for RationalNumbersLib {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211impl Lib for RationalNumbersLib {
212    fn manifest(&self) -> LibManifest {
213        LibManifest {
214            id: number_domain(),
215            version: Version(env!("CARGO_PKG_VERSION").to_owned()),
216            abi: AbiVersion { major: 0, minor: 1 },
217            target: LibTarget::HostRegistered,
218            requires: Vec::<Dependency>::new(),
219            capabilities: Vec::new(),
220            exports: vec![
221                Export::NumberDomain {
222                    symbol: number_domain(),
223                    number_domain_id: None,
224                },
225                Export::Class {
226                    symbol: literal_class_symbol(),
227                    class_id: None,
228                },
229                Export::Shape {
230                    symbol: literal_instance_shape_symbol(),
231                    shape_id: None,
232                },
233                Export::Shape {
234                    symbol: value_shape_symbol(),
235                    shape_id: None,
236                },
237                Export::Class {
238                    symbol: rational_value_class_symbol(),
239                    class_id: None,
240                },
241            ],
242        }
243    }
244
245    fn load(&self, _cx: &mut sim_kernel::LoadCx, linker: &mut Linker<'_>) -> Result<()> {
246        let instance_shape = Arc::new(NumberLiteralShape::new(
247            number_domain(),
248            "RationalLiteral",
249            [
250                "number literal in the numbers/rational domain",
251                "matches Expr::Number where domain == numbers/rational",
252            ],
253        ));
254        let literal_class = Arc::new(NumberLiteralClass::new(
255            literal_class_symbol(),
256            number_domain(),
257            "rational",
258            "numerator/denominator",
259            literal_instance_shape_symbol(),
260            instance_shape.clone(),
261        ));
262        let value_shape = Arc::new(DomainNumberValueShape::new(
263            number_domain(),
264            "RationalValue",
265            [
266                "number value in the numbers/rational domain",
267                "accepts any NumberValue where domain == numbers/rational",
268            ],
269        ));
270
271        linker.number_domain_value(
272            number_domain(),
273            DefaultFactory
274                .opaque(Arc::new(RationalNumberDomain))
275                .expect("number domain should be boxable"),
276        )?;
277        let literal_class_id = linker.class_value(
278            literal_class_symbol(),
279            DefaultFactory
280                .opaque(literal_class.clone())
281                .expect("number literal class should be boxable"),
282        )?;
283        literal_class.set_id(literal_class_id);
284        register_rational_value_class(linker)?;
285
286        linker.shape_value(
287            literal_instance_shape_symbol(),
288            shape_value(literal_instance_shape_symbol(), instance_shape),
289        )?;
290        linker.shape_value(
291            value_shape_symbol(),
292            shape_value(value_shape_symbol(), value_shape),
293        )?;
294
295        register_promotions(linker);
296        let binary = [
297            (
298                add_symbol(),
299                rational_add_rule as RationalRuleFn,
300                rational_add_value_rule as ValueRuleFn,
301            ),
302            (sub_symbol(), rational_sub_rule, rational_sub_value_rule),
303            (mul_symbol(), rational_mul_rule, rational_mul_value_rule),
304            (div_symbol(), rational_div_rule, rational_div_value_rule),
305            (pow_symbol(), rational_pow_rule, rational_pow_value_rule),
306        ]
307        .into_iter()
308        .map(|(operator, literal_apply, value_apply)| ScalarBinaryOp {
309            operator,
310            literal_cost: 0,
311            literal_apply,
312            value_cost: 1,
313            value_apply,
314        })
315        .collect();
316        let ops = ScalarOps {
317            domain: number_domain(),
318            binary,
319            unary: vec![ScalarUnaryOp {
320                operator: Symbol::qualified("math", "neg"),
321                literal_cost: 0,
322                literal_apply: rational_neg_rule,
323                value_cost: 1,
324                value_apply: rational_neg_value_rule,
325            }],
326            reduction: vec![
327                ScalarReductionOp {
328                    operator: Symbol::qualified("math", "sum"),
329                    literal_cost: 0,
330                    literal_apply: rational_sum_rule,
331                    value_cost: 1,
332                    value_apply: rational_sum_value_rule,
333                },
334                ScalarReductionOp {
335                    operator: Symbol::qualified("math", "product"),
336                    literal_cost: 0,
337                    literal_apply: rational_product_rule,
338                    value_cost: 1,
339                    value_apply: rational_product_value_rule,
340                },
341            ],
342        };
343        install_scalar_ops(linker, &ops);
344        Ok(())
345    }
346}
347
348fn register_rational_value_class(linker: &mut Linker<'_>) -> Result<ClassId> {
349    let rational_value_class = Arc::new(RationalValueClass::new());
350    let rational_class_id = linker.class_value(
351        rational_value_class_symbol(),
352        DefaultFactory
353            .opaque(rational_value_class.clone())
354            .expect("rational value class should be boxable"),
355    )?;
356    rational_value_class.set_id(rational_class_id);
357    Ok(rational_class_id)
358}
359
360fn install_rational_value_citizen(linker: &mut Linker<'_>) -> Result<()> {
361    register_rational_value_class(linker).map(|_| ())
362}
363
364fn conformance_rational_value_citizen(cx: &mut sim_kernel::Cx) -> Result<()> {
365    let num = cx
366        .factory()
367        .number_literal(domains::i64(), "3".to_owned())?;
368    let den = cx
369        .factory()
370        .number_literal(domains::bigint(), "5".to_owned())?;
371    let value = super::value::make_rational(cx, num, den)?;
372    sim_citizen::check_value_fixture(cx, value)
373}
374
375sim_citizen::inventory::submit! {
376    sim_citizen::CitizenInfo {
377        symbol: "numbers/Rational",
378        version: 0,
379        crate_name: env!("CARGO_PKG_NAME"),
380        arity: 2,
381        install: install_rational_value_citizen,
382        conformance: conformance_rational_value_citizen,
383    }
384}
385
386fn register_promotions(linker: &mut Linker<'_>) {
387    for domain in integer_domains() {
388        linker.promotion_rule(sim_kernel::PromotionRule {
389            from_domain: domain.clone(),
390            to_domain: number_domain(),
391            cost: 1,
392            convert: promote_integer_literal_to_rational,
393        });
394        linker.value_promotion_rule(ValuePromotionRule {
395            from_domain: domain,
396            to_domain: number_domain(),
397            cost: 1,
398            convert: promote_integer_value_to_rational,
399        });
400    }
401    linker.promotion_rule(sim_kernel::PromotionRule {
402        from_domain: f64_domain(),
403        to_domain: number_domain(),
404        cost: 1,
405        convert: promote_f64_literal_to_rational,
406    });
407    linker.value_promotion_rule(ValuePromotionRule {
408        from_domain: f64_domain(),
409        to_domain: number_domain(),
410        cost: 1,
411        convert: promote_f64_value_to_rational,
412    });
413    linker.promotion_rule(sim_kernel::PromotionRule {
414        from_domain: number_domain(),
415        to_domain: f64_domain(),
416        cost: 50,
417        convert: promote_rational_literal_to_f64,
418    });
419    linker.value_promotion_rule(ValuePromotionRule {
420        from_domain: number_domain(),
421        to_domain: f64_domain(),
422        cost: 50,
423        convert: promote_rational_value_to_f64,
424    });
425}
426
427fn integer_domains() -> Vec<Symbol> {
428    domains::integer_domains()
429}