Skip to main content

submilli_engine/runtime/prelude/
math.rs

1//! The Rust port of the prelude's built-in `Math` namespace.
2
3use crate::runtime::host::{abi_arg, abi_result};
4use std::collections::BTreeMap;
5use wasmtime::{Caller, FuncType, Global, GlobalType, Linker, Mutability, Store, Val, ValType};
6
7use crate::runtime::StoreData;
8use crate::runtime::host::register_host_fn;
9use crate::runtime::intrinsic_types::intrinsic_types;
10use crate::runtime::number::{to_int32, to_uint32};
11use crate::runtime::prelude::MODULE_NAME;
12use crate::{
13    MangledName, NamespaceSymbol, PackageDeclaration, Param, Span, Type, ValueKind, ValueSymbol,
14};
15
16type UnaryFn = fn(f64) -> f64;
17type BinaryFn = fn(f64, f64) -> f64;
18
19const CONSTANTS: &[(&str, f64)] = &[
20    ("PI", std::f64::consts::PI),
21    ("E", std::f64::consts::E),
22    ("LN2", std::f64::consts::LN_2),
23    ("LN10", std::f64::consts::LN_10),
24    ("LOG2E", std::f64::consts::LOG2_E),
25    ("LOG10E", std::f64::consts::LOG10_E),
26    ("SQRT2", std::f64::consts::SQRT_2),
27    ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2),
28];
29
30const UNARY: &[(&str, UnaryFn)] = &[
31    ("abs", f64::abs),
32    ("ceil", f64::ceil),
33    ("floor", f64::floor),
34    ("trunc", f64::trunc),
35    ("sqrt", f64::sqrt),
36    ("round", math_round),
37    ("sign", math_sign),
38    ("fround", math_fround),
39    ("clz32", math_clz32),
40    ("exp", f64::exp),
41    ("expm1", f64::exp_m1),
42    ("log", f64::ln),
43    ("log1p", f64::ln_1p),
44    ("log2", f64::log2),
45    ("log10", f64::log10),
46    ("cbrt", f64::cbrt),
47    ("sin", f64::sin),
48    ("cos", f64::cos),
49    ("tan", f64::tan),
50    ("asin", f64::asin),
51    ("acos", f64::acos),
52    ("atan", f64::atan),
53    ("sinh", f64::sinh),
54    ("cosh", f64::cosh),
55    ("tanh", f64::tanh),
56    ("asinh", f64::asinh),
57    ("acosh", f64::acosh),
58    ("atanh", f64::atanh),
59];
60
61const BINARY: &[(&str, BinaryFn)] = &[
62    ("imul", math_imul),
63    ("pow", crate::runtime::number::pow_js),
64    ("atan2", f64::atan2),
65];
66
67const VARIADIC: &[&str] = &["min", "max", "hypot"];
68
69pub(crate) fn math_key(name: &str) -> MangledName {
70    crate::mangle::extend(&crate::mangle::prelude("Math"), name)
71}
72
73pub(crate) fn namespace_symbol() -> NamespaceSymbol {
74    let math_prefix = crate::mangle::prelude("Math");
75    let mut math = NamespaceSymbol {
76        name: "Math".to_string(),
77        mangled_prefix: math_prefix.clone(),
78        declaration_span: Span::at(crate::FileId::MATH),
79        values: BTreeMap::new(),
80        types: BTreeMap::new(),
81        namespaces: BTreeMap::new(),
82        doc: None,
83    };
84
85    for (name, _) in CONSTANTS {
86        math.values.insert(
87            (*name).to_string(),
88            ValueSymbol {
89                name: (*name).to_string(),
90                mangled_name: math_key(name),
91                declaration_span: Span::at(crate::FileId::MATH),
92                kind: ValueKind::Const {
93                    ty: Type::Number,
94                    doc: None,
95                },
96            },
97        );
98    }
99    for (name, _) in UNARY {
100        math.values.insert(
101            (*name).to_string(),
102            ValueSymbol {
103                name: (*name).to_string(),
104                mangled_name: math_key(name),
105                declaration_span: Span::at(crate::FileId::MATH),
106                kind: ValueKind::Function {
107                    generics: Vec::new(),
108                    params: vec![Param::new("x", Type::Number)],
109                    ret: Type::Number,
110                    type_predicate: None,
111                    doc: None,
112                },
113            },
114        );
115    }
116    math.values.insert(
117        "random".to_string(),
118        ValueSymbol {
119            name: "random".to_string(),
120            mangled_name: math_key("random"),
121            declaration_span: Span::at(crate::FileId::MATH),
122            kind: ValueKind::Function {
123                generics: Vec::new(),
124                params: Vec::new(),
125                ret: Type::Number,
126                type_predicate: None,
127                doc: crate::doc(
128                    crate::FileId::MATH,
129                    "/** Pseudo-random number in `[0, 1)`. Not suitable for cryptographic use — reach for `submilli:crypto.randomBytes` instead. */",
130                ),
131            },
132        },
133    );
134    for (name, _) in BINARY {
135        let params = match *name {
136            "pow" => vec![
137                Param::new("base", Type::Number),
138                Param::new("exponent", Type::Number),
139            ],
140            "atan2" => vec![Param::new("y", Type::Number), Param::new("x", Type::Number)],
141            "imul" => vec![Param::new("a", Type::Number), Param::new("b", Type::Number)],
142            _ => vec![Param::new("a", Type::Number), Param::new("b", Type::Number)],
143        };
144        math.values.insert(
145            (*name).to_string(),
146            ValueSymbol {
147                name: (*name).to_string(),
148                mangled_name: math_key(name),
149                declaration_span: Span::at(crate::FileId::MATH),
150                kind: ValueKind::Function {
151                    generics: Vec::new(),
152                    params,
153                    ret: Type::Number,
154                    type_predicate: None,
155                    doc: None,
156                },
157            },
158        );
159    }
160    for name in VARIADIC {
161        math.values.insert(
162            (*name).to_string(),
163            ValueSymbol {
164                name: (*name).to_string(),
165                mangled_name: math_key(name),
166                declaration_span: Span::at(crate::FileId::MATH),
167                kind: ValueKind::Function {
168                    generics: Vec::new(),
169                    params: vec![Param::rest("values", Type::Array(Box::new(Type::Number)))],
170                    ret: Type::Number,
171                    type_predicate: None,
172                    doc: None,
173                },
174            },
175        );
176    }
177
178    math
179}
180
181pub fn declare(defs: &mut PackageDeclaration) {
182    defs.namespaces
183        .insert("Math".to_string(), namespace_symbol());
184}
185
186pub(crate) fn install_constants(
187    linker: &mut Linker<StoreData>,
188    store: &mut Store<StoreData>,
189) -> wasmtime::Result<()> {
190    for (name, value) in CONSTANTS {
191        let gty = GlobalType::new(ValType::F64, Mutability::Var);
192        let global = Global::new(&mut *store, gty, Val::F64(value.to_bits()))?;
193        let mangled = math_key(name);
194        linker.define(&mut *store, MODULE_NAME, mangled.as_str(), global)?;
195    }
196    Ok(())
197}
198
199pub fn install(linker: &mut Linker<StoreData>) -> wasmtime::Result<()> {
200    let engine = linker.engine().clone();
201    let unary_ty = FuncType::new(&engine, [ValType::F64], [ValType::F64]);
202    let binary_ty = FuncType::new(&engine, [ValType::F64, ValType::F64], [ValType::F64]);
203    let array_ref = ValType::Ref(wasmtime::RefType::new(
204        false,
205        wasmtime::HeapType::ConcreteStruct(
206            crate::runtime::intrinsic_types::build_intrinsic_types(&engine)?.array,
207        ),
208    ));
209    let variadic_ty = FuncType::new(&engine, [array_ref], [ValType::F64]);
210
211    for (name, op) in UNARY {
212        let op = *op;
213        register_host_fn(
214            linker,
215            MODULE_NAME,
216            math_key(name),
217            unary_ty.clone(),
218            /* deterministic = */ true,
219            move |_caller, params, results| -> wasmtime::Result<()> {
220                let x = read_f64(abi_arg(params, 0)?, "Math unary")?;
221                *abi_result(results, 0)? = Val::F64(op(x).to_bits());
222                Ok(())
223            },
224        )?;
225    }
226
227    for (name, op) in BINARY {
228        let op = *op;
229        register_host_fn(
230            linker,
231            MODULE_NAME,
232            math_key(name),
233            binary_ty.clone(),
234            /* deterministic = */ true,
235            move |_caller, params, results| -> wasmtime::Result<()> {
236                let a = read_f64(abi_arg(params, 0)?, "Math binary")?;
237                let b = read_f64(abi_arg(params, 1)?, "Math binary")?;
238                *abi_result(results, 0)? = Val::F64(op(a, b).to_bits());
239                Ok(())
240            },
241        )?;
242    }
243
244    for name in VARIADIC {
245        let op = *name;
246        register_host_fn(
247            linker,
248            MODULE_NAME,
249            math_key(name),
250            variadic_ty.clone(),
251            /* deterministic = */ true,
252            move |caller, params, results| -> wasmtime::Result<()> {
253                let values = read_variadic_numbers(caller, abi_arg(params, 0)?, op)?;
254                let result = match op {
255                    "min" => math_min(&values),
256                    "max" => math_max(&values),
257                    "hypot" => math_hypot(&values),
258                    _ => {
259                        return Err(crate::runtime::host::invariant_trap(
260                            "Math variadic: unknown operation",
261                        ));
262                    }
263                };
264                *abi_result(results, 0)? = Val::F64(result.to_bits());
265                Ok(())
266            },
267        )?;
268    }
269
270    register_host_fn(
271        linker,
272        MODULE_NAME,
273        math_key("random"),
274        FuncType::new(&engine, [], [ValType::F64]),
275        /* deterministic = */ false,
276        |_caller, _params, results| -> wasmtime::Result<()> {
277            let mut bytes = [0u8; 8];
278            getrandom::getrandom(&mut bytes)
279                .map_err(|e| wasmtime::Error::msg(format!("Math.random getrandom failed: {e}")))?;
280            let raw = u64::from_le_bytes(bytes);
281            let mantissa = raw & ((1u64 << 52) - 1);
282            let bits = (0x3FFu64 << 52) | mantissa;
283            *abi_result(results, 0)? = Val::F64((f64::from_bits(bits) - 1.0).to_bits());
284            Ok(())
285        },
286    )?;
287
288    Ok(())
289}
290
291fn read_f64(val: &Val, name: &str) -> wasmtime::Result<f64> {
292    match val {
293        Val::F64(bits) => Ok(f64::from_bits(*bits)),
294        other => Err(crate::runtime::host::type_error(format!(
295            "{name} expects f64, got {other:?}"
296        ))),
297    }
298}
299
300fn read_variadic_numbers(
301    caller: &mut Caller<'_, StoreData>,
302    val: &Val,
303    name: &str,
304) -> wasmtime::Result<Vec<f64>> {
305    let elements = crate::runtime::prelude::collection::read_array_vals(caller, val)?;
306    let boxed_number = intrinsic_types(&mut *caller)?.boxed_number.clone();
307    let mut out = Vec::with_capacity(elements.len());
308    for element in elements {
309        let Val::AnyRef(Some(any)) = element else {
310            return Err(crate::runtime::host::type_error(format!(
311                "Math.{name} expects boxed number arguments"
312            )));
313        };
314        let st = any.as_struct(&mut *caller)?.ok_or_else(|| {
315            crate::runtime::host::type_error(format!("Math.{name}: expected boxed number"))
316        })?;
317        if !st.matches_ty(&*caller, &boxed_number)? {
318            return Err(crate::runtime::host::type_error(format!(
319                "Math.{name}: expected boxed number"
320            )));
321        }
322        out.push(read_f64(&st.field(&mut *caller, 1)?, name)?);
323    }
324    Ok(out)
325}
326
327fn math_round(x: f64) -> f64 {
328    if x.is_nan() || x == 0.0 || x.is_infinite() {
329        return x;
330    }
331    if (-0.5..0.0).contains(&x) || x == -0.5 {
332        return -0.0;
333    }
334    (x + 0.5).floor()
335}
336
337fn math_sign(x: f64) -> f64 {
338    if x > 0.0 {
339        1.0
340    } else if x < 0.0 {
341        -1.0
342    } else {
343        x
344    }
345}
346
347fn math_fround(x: f64) -> f64 {
348    (x as f32) as f64
349}
350
351fn math_clz32(x: f64) -> f64 {
352    f64::from(to_uint32(x).leading_zeros())
353}
354
355fn math_imul(a: f64, b: f64) -> f64 {
356    let a = to_int32(a);
357    let b = to_int32(b);
358    f64::from(a.wrapping_mul(b))
359}
360
361fn math_min(values: &[f64]) -> f64 {
362    let mut out = f64::INFINITY;
363    for &value in values {
364        if value.is_nan() {
365            return f64::NAN;
366        }
367        if value < out || (value == 0.0 && out == 0.0 && value.is_sign_negative()) {
368            out = value;
369        }
370    }
371    out
372}
373
374fn math_max(values: &[f64]) -> f64 {
375    let mut out = f64::NEG_INFINITY;
376    for &value in values {
377        if value.is_nan() {
378            return f64::NAN;
379        }
380        if value > out || (value == 0.0 && out == 0.0 && value.is_sign_positive()) {
381            out = value;
382        }
383    }
384    out
385}
386
387fn math_hypot(values: &[f64]) -> f64 {
388    if values.iter().any(|v| v.is_infinite()) {
389        return f64::INFINITY;
390    }
391    if values.iter().any(|v| v.is_nan()) {
392        return f64::NAN;
393    }
394    values.iter().fold(0.0, |acc, value| acc.hypot(*value))
395}