From 3bec39d181fe593f2a9241a04b02297f60b11694 Mon Sep 17 00:00:00 2001 From: ApsisRS contributors Date: Sat, 26 Sep 2026 13:48:53 +0800 Subject: [PATCH] foundation: record the built in macro capabilities --- crates/foundation/apsis-macros/src/lib.rs | 37 ++++++++++++++++ crates/foundation/apsis-macros/tests/probe.rs | 42 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 crates/foundation/apsis-macros/tests/probe.rs diff --git a/crates/foundation/apsis-macros/src/lib.rs b/crates/foundation/apsis-macros/src/lib.rs index 7f56ec1..b869e92 100644 --- a/crates/foundation/apsis-macros/src/lib.rs +++ b/crates/foundation/apsis-macros/src/lib.rs @@ -1 +1,38 @@ //! Macros that support the ApsisRS workspace. +//! +//! This crate currently defines no macros. It exists so that the workspace +//! has a single place for macro definitions if a need arises, and so that +//! adding one later does not require a new crate. +//! +//! The decision to leave it empty follows from what the workspace actually +//! needs. Rust's built in `macro_rules!` can already do everything the +//! foundation layer requires: +//! +//! - Substituting a type and a literal to build a quantity, as +//! `quantity!(Meters, 1.5)`. +//! - Dispatching on a trailing token to accept a unit suffix, as +//! `to_meters!(2.5 km)`. +//! +//! Both forms are demonstrated by the tests in `tests/probe.rs`. They can be +//! written directly in the crate that uses them, so no shared macro crate is +//! needed. +//! +//! Two limits are worth recording, because they explain why a macro based +//! unit system is not provided: +//! +//! - A `macro_rules!` macro cannot be generic over a unit suffix that is not +//! an identifier. `1.5 m` and `1.5 km` are accepted by separate rules, so +//! the macro must enumerate every suffix it supports. The unit newtypes in +//! `apsis-units` already give compile time unit safety without that list, +//! so the macro would add surface area without adding checking. +//! - A declarative macro cannot inspect the value of its arguments, so it +//! cannot fold a unit conversion at compile time beyond what constant +//! evaluation already does. A literal such as `2.5 * 1000.0` is folded by +//! the compiler anyway. +//! +//! If the workspace later needs macros that generate items, such as deriving +//! a set of conversions from one definition, that is the point at which this +//! crate should gain its first macro. + +#![forbid(unsafe_code)] +#![deny(missing_docs)] diff --git a/crates/foundation/apsis-macros/tests/probe.rs b/crates/foundation/apsis-macros/tests/probe.rs new file mode 100644 index 0000000..867c2fe --- /dev/null +++ b/crates/foundation/apsis-macros/tests/probe.rs @@ -0,0 +1,42 @@ +//! Probe of the built in macro facilities. +//! +//! These tests record what `macro_rules!` can express for the workspace. They +//! are kept as executable documentation of the decision to leave this crate +//! empty for now: see the crate documentation. + +#[test] +fn builtin_macros_can_substitute_a_type_and_a_literal() { + // A `macro_rules!` macro can paste a type and a literal together, which + // is the only form the workspace needs. + macro_rules! quantity { + ($quantity:ty, $value:literal) => { + <$quantity>::new($value) + }; + } + + // `f64` has no `new`, so the probe uses a type that does. + struct Meters(f64); + impl Meters { + const fn new(value: f64) -> Self { + Self(value) + } + } + + let value = quantity!(Meters, 1.5); + assert_eq!(value.0, 1.5); +} + +#[test] +fn builtin_macros_can_dispatch_on_a_trailing_token() { + macro_rules! to_meters { + ($value:literal m) => { + $value + }; + ($value:literal km) => { + $value * 1_000.0 + }; + } + + assert_eq!(to_meters!(2.5 m), 2.5); + assert_eq!(to_meters!(2.5 km), 2_500.0); +}