Repository: tweag/nickel
Pull request #2004 (2024-07-22 18:20:54)

[Profiling] Track more metrics
This PR adds many more metrics to what's currently being tracked (when compiled with the `metrics` feature and run with `--metrics` argument). In particular, we track the number of times each primop is called individually, each contract is applied, the number of contract deduped, etc. We also sort the result to ease diffing across different revisions.

Metrics are mostly used by us maintainers for performance profiling and aren't user-facing (gated by both a feature flag and an argument).


Patch:
diff --git a/cli/src/metrics.rs b/cli/src/metrics.rs
index eca0ff48..30adb39e 100644
--- a/cli/src/metrics.rs
+++ b/cli/src/metrics.rs
@@ -111,20 +111,32 @@ impl Display for BucketStatistics {
 }
 
 impl Recorder {
-    /// Print out a report of all metrics that have been collected so far, with
-    /// their description if available.
+    /// Print out a report of all metrics that have been collected so far, with their description
+    /// if available. Metrics are sorted to make diffing between different runs easier.
     ///
     /// We expect this function to be called once at the end of the CLI, but it
     /// should be safe to call it in the middle of a Nickel execution as well.
     pub(super) fn report(&self) {
+        let mut lines = Vec::new();
+
         self.registry.visit_counters(|key, counter| {
-            eprintln!("{}: {}", key.name(), counter.load(Ordering::Relaxed))
+            lines.push((
+                key.name().to_string(),
+                counter.load(Ordering::Relaxed).to_string(),
+            ));
         });
+
         self.registry.visit_histograms(|key, bucket| {
             let mut stats = BucketStatistics::new();
             bucket.data_with(|data| stats.update(data));
-            eprintln!("{}: {}", key.name(), stats);
+            lines.push((key.name().to_string(), stats.to_string()));
         });
+
+        lines.sort_by(|a, b| a.0.cmp(&b.0));
+
+        for (key, value) in lines {
+            eprintln!("{}: {}", key, value);
+        }
     }
 }
 
@@ -133,7 +145,7 @@ mod tests {
     use super::*;
 
     #[test]
-    fn averge_tracking() {
+    fn average_tracking() {
         let mut overall_stats = BucketStatistics::new();
         let mut incremental_stats = BucketStatistics::new();
 
diff --git a/core/benches/arrays/primes.ncl b/core/benches/arrays/primes.ncl
index c37bf70f..7d06c7ff 100644
--- a/core/benches/arrays/primes.ncl
+++ b/core/benches/arrays/primes.ncl
@@ -18,7 +18,7 @@ let primes
   | doc "Generate `max` primes using Sieve of Eratosthenes."
   | Number -> Array Prime
   = fun max =>
-    let limit = std.number.pow max (1 / 2) in # sqrt(max)
+    let limit = std.number.sqrt max in
     let drop_multiples = fun x xs =>
       let to_drop =
         max
diff --git a/core/src/eval/mod.rs b/core/src/eval/mod.rs
index 5609e2fe..cd0c4dd3 100644
--- a/core/src/eval/mod.rs
+++ b/core/src/eval/mod.rs
@@ -81,6 +81,7 @@ use crate::{
     identifier::Ident,
     identifier::LocIdent,
     match_sharedterm,
+    metrics::increment,
     position::TermPos,
     program::FieldPath,
     term::{
@@ -775,6 +776,8 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                     }
                 }
                 Term::ResolvedImport(id) => {
+                    increment!(format!("import:{id:?}"));
+
                     if let Some(t) = self.import_resolver.get(id) {
                         Closure::atomic_closure(t)
                     } else {
@@ -838,6 +841,8 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                 // avoiding repeated contract application. Annotations could then be a good way of
                 // remembering which contracts have been applied to a value.
                 Term::Annotated(annot, inner) => {
+                    increment!("contract:free-standing(annotated)");
+
                     // We apply the contract coming from the static type annotation separately as
                     // it is optimized.
                     let static_contract = annot.static_contract();
diff --git a/core/src/eval/operation.rs b/core/src/eval/operation.rs
index 3ee4f6a0..68aa02c8 100644
--- a/core/src/eval/operation.rs
+++ b/core/src/eval/operation.rs
@@ -21,9 +21,12 @@ use crate::{
     error::{EvalError, IllegalPolymorphicTailAction},
     identifier::LocIdent,
     label::{ty_path, Polarity, TypeVarData},
-    match_sharedterm, mk_app, mk_fun, mk_record,
+    match_sharedterm,
+    metrics::increment,
+    mk_app, mk_fun, mk_record,
     parser::utils::parse_number_sci,
     position::TermPos,
+    pretty::PrettyPrintCap,
     serialize,
     serialize::ExportFormat,
     stdlib::internals,
@@ -173,6 +176,8 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
         arg_pos: TermPos,
         pos_op: TermPos,
     ) -> Result<Closure, EvalError> {
+        increment!(format!("primop:{u_op}"));
+
         let Closure {
             body: RichTerm { term: t, pos },
             env,
@@ -1312,6 +1317,85 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                     pos_op_inh,
                 )))
             }
+            UnaryOp::NumberArcCos => Self::process_unary_number_operation(
+                RichTerm { term: t, pos },
+                arg_pos,
+                pos_op,
+                "number/arccos",
+                f64::acos,
+            ),
+            UnaryOp::NumberArcSin => Self::process_unary_number_operation(
+                RichTerm { term: t, pos },
+                arg_pos,
+                pos_op,
+                "number/arcsin",
+                f64::asin,
+            ),
+            UnaryOp::NumberArcTan => Self::process_unary_number_operation(
+                RichTerm { term: t, pos },
+                arg_pos,
+                pos_op,
+                "number/arctan",
+                f64::atan,
+            ),
+            UnaryOp::NumberCos => Self::process_unary_number_operation(
+                RichTerm { term: t, pos },
+                arg_pos,
+                pos_op,
+                "number/cos",
+                f64::cos,
+            ),
+            UnaryOp::NumberSin => Self::process_unary_number_operation(
+                RichTerm { term: t, pos },
+                arg_pos,
+                pos_op,
+                "number/sin",
+                f64::sin,
+            ),
+            UnaryOp::NumberTan => Self::process_unary_number_operation(
+                RichTerm { term: t, pos },
+                arg_pos,
+                pos_op,
+                "number/tan",
+                f64::tan,
+            ),
+        }
+    }
+
+    fn process_unary_number_operation<Op>(
+        body: RichTerm,
+        arg_pos: TermPos,
+        pos_op: TermPos,
+        op_name: &str,
+        op: Op,
+    ) -> Result<Closure, EvalError>
+    where
+        Op: Fn(f64) -> f64,
+    {
+        if let Term::Num(ref n) = &*body.term {
+            let result_as_f64 = op(f64::rounding_from(n, RoundingMode::Nearest).0);
+            let result = Number::try_from_float_simplest(result_as_f64).map_err(|_| {
+                EvalError::Other(
+                    format!(
+                        "invalid arithmetic operation: \
+                        {op_name}({n}) returned {result_as_f64}, \
+                        but {result_as_f64} isn't representable in Nickel"
+                    ),
+                    pos_op,
+                )
+            })?;
+
+            Ok(Closure::atomic_closure(RichTerm::new(
+                Term::Num(result),
+                pos_op.into_inherited(),
+            )))
+        } else {
+            Err(EvalError::UnaryPrimopTypeError {
+                primop: String::from(op_name),
+                expected: String::from("Number"),
+                arg_pos,
+                arg_evaluated: body,
+            })
         }
     }
 
@@ -1328,6 +1412,8 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
         snd_pos: TermPos,
         pos_op: TermPos,
     ) -> Result<Closure, EvalError> {
+        increment!(format!("primop:{b_op}"));
+
         let Closure {
             body: RichTerm {
                 term: t1,
@@ -1470,6 +1556,72 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                     pos_op_inh,
                 )))
             }
+            BinaryOp::NumberArcTan2 => {
+                let Term::Num(ref n1) = *t1 else {
+                    return mk_type_error!("Number", 1, t1, pos1);
+                };
+
+                let Term::Num(ref n2) = *t2 else {
+                    return mk_type_error!("Number", 2, t2, pos2);
+                };
+
+                let y = f64::rounding_from(n1, RoundingMode::Nearest).0;
+                let x = f64::rounding_from(n2, RoundingMode::Nearest).0;
+
+                let result_as_f64 = y.atan2(x);
+
+                let result = Number::try_from_float_simplest(result_as_f64).map_err(|_| {
+                    EvalError::Other(
+                        format!(
+                            "invalid arithmetic operation: \
+                            number/arctan2({n1}, {n2}) returned {result_as_f64}, \
+                            but {result_as_f64} isn't representable in Nickel"
+                        ),
+                        pos_op,
+                    )
+                })?;
+
+                Ok(Closure::atomic_closure(RichTerm::new(
+                    Term::Num(result),
+                    pos_op_inh,
+                )))
+            }
+            BinaryOp::NumberLog => {
+                let Term::Num(ref n1) = *t1 else {
+                    return mk_type_error!("Number", 1, t1, pos1);
+                };
+
+                let Term::Num(ref n2) = *t2 else {
+                    return mk_type_error!("Number", 2, t2, pos2);
+                };
+
+                let n = f64::rounding_from(n1, RoundingMode::Nearest).0;
+
+                let result_as_f64 = if n2 == &Number::from(2) {
+                    n.log2()
+                } else if n2 == &Number::from(10) {
+                    n.log10()
+                } else {
+                    let base = f64::rounding_from(n2, RoundingMode::Nearest).0;
+                    n.log(base)
+                };
+
+                let result = Number::try_from_float_simplest(result_as_f64).map_err(|_| {
+                    EvalError::Other(
+                        format!(
+                            "invalid arithmetic operation: \
+                            number/log({n1}, {n2}) returned {result_as_f64}, \
+                            but {result_as_f64} isn't representable in Nickel"
+                        ),
+                        pos_op,
+                    )
+                })?;
+
+                Ok(Closure::atomic_closure(RichTerm::new(
+                    Term::Num(result),
+                    pos_op_inh,
+                )))
+            }
             BinaryOp::Pow => {
                 if let Term::Num(ref n1) = *t1 {
                     if let Term::Num(ref n2) = *t2 {
@@ -1535,6 +1687,11 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                 // tracking the argument and updating the label: this will be done by the next call
                 // to `contract/apply(_as_custom)`.
                 if let Term::Type(typ) = &*t1 {
+                    increment!(format!(
+                        "primop:contract/apply:{}",
+                        typ.pretty_print_cap(40)
+                    ));
+
                     return Ok(Closure {
                         body: mk_term::op2(
                             b_op,
@@ -1552,6 +1709,16 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                 let t2 = t2.into_owned();
 
                 if let Term::Lbl(mut label) = t2 {
+                    increment!(format!(
+                        "contract:originates_from_type {}",
+                        label.typ.pretty_print_cap(40)
+                    ));
+
+                    #[cfg(feature = "metrics")]
+                    if let Some(field) = label.field_name {
+                        increment!(format!("contract:originates_from_field {field}"));
+                    }
+
                     // Track the contract argument for better error reporting, and push back the
                     // label on the stack, so that it becomes the first argument of the contract.
                     let idx = self.stack.track_arg(&mut self.cache).ok_or_else(|| {
@@ -2643,6 +2810,8 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
         args: Vec<(Closure, TermPos)>,
         pos_op: TermPos,
     ) -> Result<Closure, EvalError> {
+        increment!(format!("primop:{n_op}"));
+
         let pos_op_inh = pos_op.into_inherited();
 
         let mk_type_error = |expected: &str,
diff --git a/core/src/parser/grammar.lalrpop b/core/src/parser/grammar.lalrpop
index ea8ffd9f..5cafedb5 100644
--- a/core/src/parser/grammar.lalrpop
+++ b/core/src/parser/grammar.lalrpop
@@ -1128,6 +1128,12 @@ UOp: UnaryOp = {
     "enum/is_variant" => UnaryOp::EnumIsVariant,
     "enum/get_tag" => UnaryOp::EnumGetTag,
     "contract/custom" => UnaryOp::ContractCustom,
+    "number/arccos" => UnaryOp::NumberArcCos,
+    "number/arcsin" => UnaryOp::NumberArcSin,
+    "number/arctan" => UnaryOp::NumberArcTan,
+    "number/cos" => UnaryOp::NumberCos,
+    "number/sin" => UnaryOp::NumberSin,
+    "number/tan" => UnaryOp::NumberTan,
 }
 
 PatternGuard: RichTerm = "if" <Term> => <>;
@@ -1312,6 +1318,8 @@ BOpPre: BinaryOp = {
     "hash" => BinaryOp::Hash,
     "serialize" => BinaryOp::Serialize,
     "deserialize" => BinaryOp::Deserialize,
+    "number/arctan2" => BinaryOp::NumberArcTan2,
+    "number/log" => BinaryOp::NumberLog,
     "pow" => BinaryOp::Pow,
     "string/split" => BinaryOp::StringSplit,
     "string/contains" => BinaryOp::StringContains,
@@ -1547,6 +1555,14 @@ extern {
         "record/fields" => Token::Normal(NormalToken::RecordFields),
         "record/fields_with_opts" => Token::Normal(NormalToken::RecordFieldsWithOpts),
         "record/values" => Token::Normal(NormalToken::RecordValues),
+        "number/arccos" => Token::Normal(NormalToken::NumberArcCos),
+        "number/arcsin" => Token::Normal(NormalToken::NumberArcSin),
+        "number/arctan" => Token::Normal(NormalToken::NumberArcTan),
+        "number/arctan2" => Token::Normal(NormalToken::NumberArcTan2),
+        "number/cos" => Token::Normal(NormalToken::NumberCos),
+        "number/sin" => Token::Normal(NormalToken::NumberSin),
+        "number/tan" => Token::Normal(NormalToken::NumberTan),
+        "number/log" => Token::Normal(NormalToken::NumberLog),
         "pow" => Token::Normal(NormalToken::Pow),
         "op rec_force" => Token::Normal(NormalToken::OpRecForce),
         "op rec_default" => Token::Normal(NormalToken::OpRecDefault),
diff --git a/core/src/parser/lexer.rs b/core/src/parser/lexer.rs
index b661faa4..80c48128 100644
--- a/core/src/parser/lexer.rs
+++ b/core/src/parser/lexer.rs
@@ -262,6 +262,22 @@ pub enum NormalToken<'input> {
     #[token("%record/values%")]
     RecordValues,
 
+    #[token("%number/arccos%")]
+    NumberArcCos,
+    #[token("%number/arcsin%")]
+    NumberArcSin,
+    #[token("%number/arctan%")]
+    NumberArcTan,
+    #[token("%number/arctan2%")]
+    NumberArcTan2,
+    #[token("%number/cos%")]
+    NumberCos,
+    #[token("%number/sin%")]
+    NumberSin,
+    #[token("%number/tan%")]
+    NumberTan,
+    #[token("%number/log%")]
+    NumberLog,
     #[token("%pow%")]
     Pow,
     #[token("%trace%")]
diff --git a/core/src/pretty.rs b/core/src/pretty.rs
index 9a616ff4..58ac14f9 100644
--- a/core/src/pretty.rs
+++ b/core/src/pretty.rs
@@ -1277,6 +1277,32 @@ macro_rules! impl_display_from_pretty {
     };
 }
 
+/// Provide a method to pretty-print a long term, type, etc. (anything that implements `ToString`,
+/// really) capped to a maximum length.
+pub trait PrettyPrintCap: ToString {
+    /// Pretty print an object capped to a given max length (in characters). Useful to limit the
+    /// size of terms reported e.g. in typechecking errors. If the output of pretty printing is
+    /// greater than the bound, the string is truncated to `max_width` and the last character after
+    /// truncate is replaced by the ellipsis unicode character U+2026.
+    fn pretty_print_cap(&self, max_width: usize) -> String {
+        let output = self.to_string();
+
+        if output.len() <= max_width {
+            output
+        } else {
+            let (end, _) = output.char_indices().nth(max_width).unwrap();
+            let mut truncated = String::from(&output[..end]);
+
+            if max_width >= 2 {
+                truncated.pop();
+                truncated.push('\u{2026}');
+            }
+
+            truncated
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use pretty::BoxAllocator;
diff --git a/core/src/repl/query_print.rs b/core/src/repl/query_print.rs
index 4097b0e1..e2b92cdb 100644
--- a/core/src/repl/query_print.rs
+++ b/core/src/repl/query_print.rs
@@ -1,9 +1,13 @@
 //! Rendering of the results of a metadata query.
-use crate::identifier::{Ident, LocIdent};
-use crate::term::{
-    record::{Field, FieldMetadata},
-    MergePriority, Term,
+use crate::{
+    identifier::{Ident, LocIdent},
+    pretty::PrettyPrintCap,
+    term::{
+        record::{Field, FieldMetadata},
+        MergePriority, Term,
+    },
 };
+
 use std::{io, io::Write};
 
 /// The maximum width for pretty-printing default values. Beyond this limit, the content is cut and
diff --git a/core/src/term/mod.rs b/core/src/term/mod.rs
index 9c20feab..cf9efbea 100644
--- a/core/src/term/mod.rs
+++ b/core/src/term/mod.rs
@@ -28,10 +28,13 @@ use crate::{
     label::{Label, MergeLabel},
     match_sharedterm,
     position::{RawSpan, TermPos},
+    pretty::PrettyPrintCap,
     typ::{Type, UnboundTypeVariableError},
     typecheck::eq::{contract_eq, type_eq_noenv},
 };
 
+use crate::metrics::increment;
+
 use codespan::FileId;
 
 pub use malachite::{
@@ -477,7 +480,10 @@ impl RuntimeContract {
         env2: &Environment,
     ) {
         for c in contracts.iter() {
+            increment!("contracts:equality-checks");
+
             if contract_eq(0, &c.contract, env1, &ctr.contract, env2) {
+                increment!("contracts:deduped");
                 return;
             }
         }
@@ -1494,6 +1500,24 @@ pub enum UnaryOp {
     /// Wrap a contract implementation as a [CustomContract]. You can think of this primop as a
     /// type constructor for custom contracts.
     ContractCustom,
+
+    /// The cosinus function.
+    NumberArcCos,
+
+    /// The sinus function.
+    NumberArcSin,
+
+    /// The tangent function.
+    NumberArcTan,
+
+    /// The cosinus function.
+    NumberCos,
+
+    /// The sinus function.
+    NumberSin,
+
+    /// The tangent function.
+    NumberTan,
 }
 
 impl fmt::Display for UnaryOp {
@@ -1556,6 +1580,13 @@ impl fmt::Display for UnaryOp {
 
             PatternBranch => write!(f, "pattern_branch"),
             ContractCustom => write!(f, "contract/custom"),
+
+            NumberArcCos => write!(f, "number/arccos"),
+            NumberArcSin => write!(f, "number/arcsin"),
+            NumberArcTan => write!(f, "number/arctan"),
+            NumberCos => write!(f, "number/cos"),
+            NumberSin => write!(f, "number/sin"),
+            NumberTan => write!(f, "number/tan"),
         }
     }
 }
@@ -1646,6 +1677,12 @@ pub enum BinaryOp {
     /// Modulo of numerals.
     Modulo,
 
+    /// Give the four quadrant arctangent of y and x.
+    NumberArcTan2,
+
+    /// Give the logarithm of a number.
+    NumberLog,
+
     /// Raise a number to a power.
     Pow,
 
@@ -1836,6 +1873,8 @@ impl fmt::Display for BinaryOp {
             Mult => write!(f, "(*)"),
             Div => write!(f, "(/)"),
             Modulo => write!(f, "(%)"),
+            NumberArcTan2 => write!(f, "number/arctan2"),
+            NumberLog => write!(f, "number/log"),
             Pow => write!(f, "pow"),
             StringConcat => write!(f, "string/concat"),
             Eq => write!(f, "(==)"),
@@ -2032,30 +2071,10 @@ impl RichTerm {
         self.pos = pos;
         self
     }
-
-    /// Pretty print a term capped to a given max length (in characters). Useful to limit the size
-    /// of terms reported e.g. in typechecking errors. If the output of pretty printing is greater
-    /// than the bound, the string is truncated to `max_width` and the last character after
-    /// truncate is replaced by the ellipsis unicode character U+2026.
-    pub fn pretty_print_cap(&self, max_width: usize) -> String {
-        let output = self.to_string();
-
-        if output.len() <= max_width {
-            output
-        } else {
-            let (end, _) = output.char_indices().nth(max_width).unwrap();
-            let mut truncated = String::from(&output[..end]);
-
-            if max_width >= 2 {
-                truncated.pop();
-                truncated.push('\u{2026}');
-            }
-
-            truncated
-        }
-    }
 }
 
+impl PrettyPrintCap for RichTerm {}
+
 /// Flow control for tree traverals.
 pub enum TraverseControl<S, U> {
     /// Normal control flow: continue recursing into the children.
diff --git a/core/src/term/pattern/compile.rs b/core/src/term/pattern/compile.rs
index 8bdc3ad4..bb4b645a 100644
--- a/core/src/term/pattern/compile.rs
+++ b/core/src/term/pattern/compile.rs
@@ -24,6 +24,7 @@
 //! we can generate inlined versions on-the-fly here).
 use super::*;
 use crate::{
+    metrics::increment,
     mk_app,
     term::{
         make, record::FieldMetadata, BinaryOp, MatchBranch, MatchData, NAryOp, RecordExtKind,
@@ -873,6 +874,8 @@ impl Compile for MatchData {
     //      # this primop evaluates body with an environment extended with bindings_id
     //      %pattern_branch% body bindings_id
     fn compile(mut self, value: RichTerm, pos: TermPos) -> RichTerm {
+        increment!("pattern_compile");
+
         if self.branches.iter().all(|branch| {
             // While we could get something working even with a guard, it's a bit more work and
             // there's no current incentive to do so (a guard on a tags-only match is arguably less
@@ -1034,6 +1037,8 @@ struct TagsOnlyMatch {
 
 impl Compile for TagsOnlyMatch {
     fn compile(self, value: RichTerm, pos: TermPos) -> RichTerm {
+        increment!("pattern_comile(tags_only_match)");
+
         // We simply use the corresponding specialized primop in that case.
         let match_op = mk_app!(
             make::op1(
diff --git a/core/src/typ.rs b/core/src/typ.rs
index 916a2f76..a7652c4b 100644
--- a/core/src/typ.rs
+++ b/core/src/typ.rs
@@ -46,8 +46,10 @@ use crate::{
     identifier::{Ident, LocIdent},
     impl_display_from_pretty,
     label::Polarity,
+    metrics::increment,
     mk_app, mk_fun,
     position::TermPos,
+    pretty::PrettyPrintCap,
     stdlib::internals,
     term::{
         array::Array, make as mk_term, record::RecordData, string::NickelString, IndexMap,
@@ -1334,6 +1336,8 @@ impl Type {
     /// Return the contract corresponding to a type. Said contract must then be applied using the
     /// `ApplyContract` primitive operation.
     pub fn contract(&self) -> Result<RichTerm, UnboundTypeVariableError> {
+        increment!(format!("gen_contract:{}", self.pretty_print_cap(40)));
+
         let mut sy = 0;
 
         self.subcontract(Environment::new(), Polarity::Positive, &mut sy)
@@ -1484,3 +1488,5 @@ impl_display_from_pretty!(EnumRow);
 impl_display_from_pretty!(EnumRows);
 impl_display_from_pretty!(RecordRow);
 impl_display_from_pretty!(RecordRows);
+
+impl PrettyPrintCap for Type {}
diff --git a/core/src/typecheck/operation.rs b/core/src/typecheck/operation.rs
index a4ed4d90..e9cd3142 100644
--- a/core/src/typecheck/operation.rs
+++ b/core/src/typecheck/operation.rs
@@ -273,6 +273,13 @@ pub fn get_uop_type(
         }
         // <custom_contract_type()> -> Dyn
         UnaryOp::ContractCustom => (custom_contract_type(), mk_uniftype::dynamic()),
+        // Num -> Num
+        UnaryOp::NumberCos
+        | UnaryOp::NumberSin
+        | UnaryOp::NumberTan
+        | UnaryOp::NumberArcCos
+        | UnaryOp::NumberArcSin
+        | UnaryOp::NumberArcTan => (mk_uniftype::num(), mk_uniftype::num()),
     })
 }
 
@@ -431,7 +438,9 @@ pub fn get_bop_type(
             mk_uniftype::dynamic(),
         ),
         // Num -> Num -> Num
-        BinaryOp::Pow => (mk_uniftype::num(), mk_uniftype::num(), mk_uniftype::num()),
+        BinaryOp::NumberArcTan2 | BinaryOp::NumberLog | BinaryOp::Pow => {
+            (mk_uniftype::num(), mk_uniftype::num(), mk_uniftype::num())
+        }
         // Str -> Str -> Bool
         BinaryOp::StringContains => (mk_uniftype::str(), mk_uniftype::str(), mk_uniftype::bool()),
         // Str -> Str -> <Lesser, Equal, Greater>
diff --git a/core/stdlib/std.ncl b/core/stdlib/std.ncl
index ec684639..49dc3f0e 100644
--- a/core/stdlib/std.ncl
+++ b/core/stdlib/std.ncl
@@ -2619,6 +2619,155 @@
         power operation, might incur rounding errors**.
       "%
       = fun x n => %pow% x n,
+
+    log
+      : Number -> Number -> Number
+      | doc m%"
+        `log x base` returns the logarithm of x with respect to the given base.
+
+        # Examples
+        ```nickel
+        std.number.log 2 1024 =>
+          10
+        std.number.log 100 10 =>
+          2
+        std.number.log 10 std.number.e =>
+          2.302585
+        ```
+        "%
+      = fun x b => %number/log% x b,
+
+    cos
+      : Number -> Number
+      | doc m%"
+        `cos x` returns the cosinus of x.
+
+        # Examples
+
+        ```nickel
+        std.number.cos (std.number.pi / 4) =>
+          0.70710678118
+        ```
+      "%
+      = fun x => %number/cos% x,
+
+    sin
+      : Number -> Number
+      | doc m%"
+        `sin x` returns the sinus of x.
+
+        # Examples
+
+        ```nickel
+        std.number.sin (std.number.pi / 2) =>
+          1
+        ```
+      "%
+      = fun x => %number/sin% x,
+
+    tan
+      : Number -> Number
+      | doc m%"
+        `tan x` returns the tangent of x.
+
+        # Examples
+
+        ```nickel
+        std.number.tan (std.number.pi / 4) =>
+          1
+        ```
+      "%
+      = fun x => %number/tan% x,
+
+    arccos
+      : Number -> Number
+      | doc m%"
+        `arccos x` returns the arccosinus of x.
+
+        # Examples
+
+        ```nickel
+        std.number.arccos 0 =>
+          1.5707963
+        std.number.arccos 1 =>
+          0
+        ```
+        "%
+      = fun x => %number/arccos% x,
+
+    arcsin
+      : Number -> Number
+      | doc m%"
+        `arcsin x` returns the arccsinus of x.
+
+        # Examples
+
+        ```nickel
+        std.number.arcsin 0 =>
+          0
+        std.number.arcsin 1 =>
+          1.5707963
+        ```
+        "%
+      = fun x => %number/arcsin% x,
+
+    arctan
+      : Number -> Number
+      | doc m%"
+        `arctan x` returns the arctangent of x.
+
+        # Examples
+
+        ```nickel
+        std.number.arctan 1 =>
+          0.78539816
+        ```
+        "%
+      = fun x => %number/arctan% x,
+
+    arctan2
+      : Number -> Number -> Number
+      | doc m%"
+      `arctan2 y x returns the four quadrant arctangent of y over x.
+
+      # Examples
+
+      ```nickel
+      std.number.arctan2 1 0 =>
+        1.5707963
+      std.number.arctan2 (-0.5) (-0.5) =>
+        -0.7853981633974483
+      ```
+      "%
+      = fun y x => %number/arctan2% y x,
+
+    sqrt
+      : Number -> Number
+      | doc m%"
+        `sqrt x` return the square root of x.
+
+        # Examples
+
+        ```nickel
+        std.number.sqrt 4 =>
+          2
+        ```
+      "%
+      = fun x => pow x (1 / 2),
+
+    pi
+      : Number
+      | doc m%"
+      The π mathematical constant.
+      "%
+      = 3.14159265358979323846,
+
+    e
+      : Number
+      | doc m%"
+      Euler's number, the e mathematical constant.
+      "%
+      = 2.7182818284590452354,
   },
 
   record = {


Test patch:
diff --git a/core/tests/integration/inputs/stdlib/maths.ncl b/core/tests/integration/inputs/stdlib/maths.ncl
new file mode 100644
index 00000000..d83dbb90
--- /dev/null
+++ b/core/tests/integration/inputs/stdlib/maths.ncl
@@ -0,0 +1,40 @@
+# test.type = 'pass'
+
+let number = std.number in
+let almost_equal = fun a b => std.number.abs (a - b) < 0.001 in
+
+[
+    almost_equal (number.log 1024 2) 10,
+    almost_equal (number.log 100 10) 2,
+    almost_equal (number.log 10 std.number.e) 2.302585,
+
+    almost_equal (number.cos 0) 1,
+    almost_equal (number.cos number.pi) (-1),
+    almost_equal (number.cos (number.pi / 2)) 0,
+
+    almost_equal (number.sin 0) 0,
+    almost_equal (number.sin (number.pi / 2)) 1,
+    almost_equal (number.sin (number.pi / 4)) ((number.sqrt 2) / 2),
+
+    almost_equal (number.tan 0) 0,
+    almost_equal (number.tan number.pi) 0,
+    almost_equal (number.tan (number.pi / 4)) 1,
+
+    almost_equal (number.arccos 0) (number.pi / 2),
+    almost_equal (number.arccos 1) 0,
+
+    almost_equal (number.arcsin 0) 0,
+    almost_equal (number.arcsin 1) (number.pi / 2),
+
+    almost_equal (number.arctan 0) 0,
+    almost_equal (number.arctan 1) (number.pi / 4),
+
+    almost_equal (number.arctan2 0 1) 0,
+    almost_equal (number.arctan2 1 0) (number.pi / 2),
+    almost_equal (number.arctan2 0.5 0.5) (number.pi / 4),
+    almost_equal (number.arctan2 0.5 (-0.5)) (3 * number.pi / 4),
+
+    almost_equal (number.sqrt 4) 2,
+    almost_equal (number.sqrt 2) 1.41421356237,
+]
+|> std.test.assert_all