Skip to main content

tancore/
range.rs

1use tan::{
2    context::Context,
3    error::Error,
4    expr::Expr,
5    util::{
6        args::{unpack_float_arg, unpack_float_range_arg},
7        module_util::require_module,
8    },
9};
10
11// #todo Do we need a separate Interval type?
12// #todo Range could be like in Rust and _not_ include a step, instead provide a step_by function?
13
14// #todo #hack No need for Range expressions, this is a temp hack.
15
16// #todo make some fields optional.
17// struct ForeignRange<T> {
18//     pub start: T,
19//     pub end: T,
20//     pub step: T,
21// }
22
23// #todo Range is Immutable, Copy.
24
25// #todo not used yet.
26// fn make_range<T: Send + Sync + 'static>(start: T, end: T, step: T) -> Expr {
27//     // #todo use IntRange, FloatRange.
28//     let foreign_range = ForeignRange { start, end, step };
29//     let expr = Expr::ForeignStruct(Arc::new(foreign_range));
30//     // #todo should annotate (Range Int) or (Range Float)
31//     annotate_type(expr, "Range")
32// }
33
34pub fn range_int_new(args: &[Expr]) -> Result<Expr, Error> {
35    // #todo make some of the arguments optional, e.g. step.
36    let [start, end, ..] = args else {
37        return Err(Error::invalid_arguments(
38            "requires `start`, `end` arguments",
39            None,
40        ));
41    };
42
43    // #todo create a helper.
44    let Some(start) = start.as_int() else {
45        return Err(Error::invalid_arguments(
46            &format!("start=`{start}` is not Int"),
47            start.range(),
48        ));
49    };
50
51    let Some(end) = end.as_int() else {
52        return Err(Error::invalid_arguments(
53            &format!("end=`{end}` is not Int"),
54            end.range(),
55        ));
56    };
57
58    let step = if let Some(step) = args.get(2) {
59        let Some(step) = step.as_int() else {
60            return Err(Error::invalid_arguments(
61                &format!("step=`{step}` is not Int"),
62                step.range(),
63            ));
64        };
65        step
66    } else if end >= start {
67        1
68    } else {
69        -1
70    };
71
72    // #todo use Expr::ForeignStruct
73    Ok(Expr::IntRange(start, end, step))
74}
75
76pub fn range_float_new(args: &[Expr]) -> Result<Expr, Error> {
77    // #todo make some of the arguments optional, e.g. step.
78    let [start, end, ..] = args else {
79        return Err(Error::invalid_arguments(
80            "requires `start`, `end`, and `step` arguments",
81            None,
82        ));
83    };
84
85    // #todo create a helper.
86    let Some(start) = start.as_float() else {
87        return Err(Error::invalid_arguments(
88            &format!("start=`{start}` is not Float"),
89            start.range(),
90        ));
91    };
92
93    let Some(end) = end.as_float() else {
94        return Err(Error::invalid_arguments(
95            &format!("end=`{end}` is not Float"),
96            end.range(),
97        ));
98    };
99
100    let step = if let Some(step) = args.get(2) {
101        let Some(step) = step.as_float() else {
102            return Err(Error::invalid_arguments(
103                &format!("step=`{step}` is not Float"),
104                step.range(),
105            ));
106        };
107        step
108    } else if end >= start {
109        1.0
110    } else {
111        -1.0
112    };
113
114    // #todo use Expr::ForeignStruct
115    Ok(Expr::FloatRange(start, end, step))
116}
117
118// #todo Implement those in Tan.
119
120// #todo Add contains.
121// #todo Add surrounds.
122// #todo Add size/length.
123// #todo Constants: empty, universe.
124// #todo The name `universe` is weird.
125
126// #todo Support open-ended and close-ended ranges.
127
128// #todo Add unit tests.
129// #todo Should take the step into account? Probably yes.
130// #todo The term `contains` is a bit ambiguous, could use `includes` or `surrounds`?
131// #todo Maybe `surrounds` should be like contains/includes but ignoring the step.
132pub fn range_float_contains(args: &[Expr]) -> Result<Expr, Error> {
133    let range = unpack_float_range_arg(args, 0, "range")?;
134    let value = unpack_float_arg(args, 1, "value")?;
135    Ok(Expr::Bool(range.contains(&value)))
136}
137
138pub fn setup_lib_range(context: &mut Context) {
139    // #todo put in 'range' path, and import selected functionality to prelude.
140    let module = require_module("prelude", context);
141
142    module.insert_invocable("Range", Expr::foreign_func(&range_int_new));
143    module.insert_invocable("Range$$Int$$Int", Expr::foreign_func(&range_int_new));
144    module.insert_invocable("Range$$Int$$Int$$Int", Expr::foreign_func(&range_int_new));
145    module.insert_invocable("Range$$Float$$Float", Expr::foreign_func(&range_float_new));
146    module.insert_invocable(
147        "Range$$Float$$Float$$Float",
148        Expr::foreign_func(&range_float_new),
149    );
150
151    // #todo Contains should probably take the step into account.
152    module.insert_invocable(
153        "contains?$$(Range Float)$$Float",
154        Expr::foreign_func(&range_float_contains),
155    );
156    // #todo Find a better name, originally from https://raytracing.github.io/
157    module.insert_invocable("surrounds?", Expr::foreign_func(&range_float_contains));
158    module.insert_invocable(
159        "surrounds?$$(Range Float)$$Float",
160        Expr::foreign_func(&range_float_contains),
161    );
162}