Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Directly Exportable dec!() as Macro by Example #688

Open
daniel-pfeiffer opened this issue Oct 24, 2024 · 4 comments
Open

Directly Exportable dec!() as Macro by Example #688

daniel-pfeiffer opened this issue Oct 24, 2024 · 4 comments

Comments

@daniel-pfeiffer
Copy link

daniel-pfeiffer commented Oct 24, 2024

Since the companion macro does very little to justify a separate crate and proc-macro, I’ve sketched a solution with macro by example. For fun I also propose dec!(a123, radix) and dec!(123 >> scale).

tldr: Parsing the normal, scientific and base 36 numbers at compile time, and hence in const seems feasible. That is probably the same as doing it only with core. However this restricts error messages and does not seem compatible with the current Error.

I’ve done a little dummy Decimal with a dummy const parser (that calculates a checksum just for feasability.) Instead of iterating over chars, it manually loops over bytes. This is good enough, as we’re only parsing Ascii chars.

Surprisingly, const fns aren’t evaluated at compile time – even when called with constant arguments. So I pack it into const {}.

mod rust_decimal {
    pub struct Decimal(&'static str, u8, u32); // just for demo
    type Error = &'static str; // just for demo
    impl Decimal {
        // num is 'static, only so it can be stored for dbg
        pub const fn try_parse_with_radix(num: &'static str, radix: u32) -> Result<Self, Error> {
            let mut sum = 0_u32;
            let mut src = num.as_bytes();
            while let [c, rest @ ..] = src {
                if let Some(digit) = (*c as char).to_digit(radix) {
                    sum += digit
                } else {
                    sum += match *c as char {
                        '.' => 37,
                        '-' => 38,
                        'b' | 'o' | 'x' => 39,
                        'e' => 40,
                        '_' => 0,
                        ' ' => 0, // stringify!(-$num) inserts a space
                        _ => return Err("Malformed number"),
                    }
                }
                src = rest
            }
            Ok(Self(num, radix as u8, sum))
        }

        pub const fn try_parse(num: &'static str) -> Result<Self, Error> {
            // must handle scientific when found, as num.contains(['e', 'E']) is not const
            Self::try_parse_with_radix(num, 16) // wrong radix, as 0x not parsed yet
        }

        pub const fn try_parse_with_scale(num: &'static str, scale: u32) -> Result<Self, Error> {
            // dummy impl
            Self::try_parse_with_radix(num, scale + 30)
        }

        pub fn dbg(&self) {
            println!("sum {:3}, radix {:2}, input \"{}\"", self.2, self.1, self.0);
        }
    }
}

macro_rules! dec {
    // inner helper
    (@ $dec:expr) => {
        const {
            match $dec {
                Ok(dec) => dec,
                // No args. All errors need to be formulated here.
                _ => panic!("Malformed number")
            }
        }
    };

    // While 1a is a literal, a1 is an ident. Negation is for num, so get it explicitly.
    (- $radix:literal # $num:ident) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_radix(stringify!(-$num), $radix))
    };
    ($radix:literal # $num:ident) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_radix(stringify!($num), $radix))
    };
    (- $radix:literal # $num:literal) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_radix(stringify!(-$num), $radix))
    };
    ($radix:literal # $num:literal) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_radix(stringify!($num), $radix))
    };

    ($num:literal >> $scale:literal) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_scale(stringify!($num), $scale))
    };

    ($radix:literal: $num:ident) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_radix(stringify!($num), $radix))
    };
    ($radix:literal: -$num:ident) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_radix(stringify!(-$num), $radix))
    };
    ($radix:literal: $num:ident >>> $scale:literal) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_radix(stringify!(scale_not_implemented_yet), $radix))
    };
    ($radix:literal: -$num:ident >>> $scale:literal) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_radix(stringify!(-scale_not_implemented_yet), $radix))
    };
    ($radix:literal: $num:literal) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_radix(stringify!($num), $radix))
    };
    ($radix:literal: $num:literal >>> $scale:literal) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_radix(stringify!(scale_not_implemented_yet), $radix))
    };

    (- $radix:literal # $num:ident >> $scale:literal) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_radix(stringify!(-scale_also_not_implemented_yet), $radix))
    };
    /* Old idea doesn't combine well with >>
    ($num:ident, $radix:literal) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_radix(stringify!($num), $radix))
    };
    (- $num:ident, $radix:literal) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_radix(stringify!(-$num), $radix))
    };
    ($num:literal, $radix:literal) => {
        dec!(@ rust_decimal::Decimal::try_parse_with_radix(stringify!($num), $radix))
    }; */

    ($num:literal) => {
        dec!(@ rust_decimal::Decimal::try_parse(stringify!($num)))
    };
}

fn _main() {
    println!("Integers in various bases:");
    dec!(1).dbg();
    dec!(-1).dbg();
    dec!(0b10).dbg();
    dec!(-0b10).dbg();
    dec!(0o755).dbg();
    dec!(-0o755).dbg();
    dec!(0x1f).dbg();
    dec!(-0x1f).dbg();

    println!("\nScientific notation:");
    dec!(1e6).dbg();
    dec!(-1e6).dbg();
    dec!(1e-6).dbg();
    dec!(-1e-6).dbg();

    println!("\nFloat-like notation:");
    dec!(1.2).dbg();
    dec!(-1.2).dbg();
    dec!(1.2e6).dbg();
    dec!(-1.2e6).dbg();
    dec!(1.2e-6).dbg();
    dec!(-1.2e-6).dbg();

    println!("\nStructured notation:");
    dec!(1_000_000_000).dbg();
    dec!(1_000.200_000).dbg();
    dec!(-1_000.200_000).dbg();

    println!("\nDecimal shift right (π, as demo abuse radix for scale):");
    dec!(314159 >> 5).dbg();

    println!("\nNew radix: number syntax with >>> decimal shift right:");
    dec!(36: z1).dbg();
    dec!(36: z1 >>> 5).dbg();
    dec!(36: -z1).dbg();
    dec!(36: -z1 >>> 5).dbg();
    dec!(36: 1z).dbg();
    dec!(36: 1z >>> 5).dbg();
    dec!(36: -1z).dbg();
    dec!(36: -1z >>> 5).dbg();

    println!("\nShell radix#number syntax:");
    dec!(2#10).dbg();
    dec!(-2#10).dbg();
    dec!(8#755).dbg();
    dec!(-8#755).dbg();
    dec!(16#f1).dbg();
    dec!(-16#1f).dbg();
    dec!(36#1z).dbg();
    dec!(-36#z1).dbg();

    println!("\nShell radix#number syntax with decimal shift right:");
    dec!(-36#z1 >> 5).dbg();
    /* dec!(1a, 16).dbg();
    dec!(-1a, 36).dbg();
    dec!(a1, 11).dbg();
    dec!(-a1, 16).dbg(); */
}
@Tony-Samuels
Copy link
Collaborator

I guess it probably is possible and I'd happily take a look at an MR that implemented it. Even if its not super efficient, it would probably still be faster than building a proc-macro.

@Tony-Samuels
Copy link
Collaborator

As for version compat, we promise 4 minor versions, so right now we need to provide support for 1.78+. Once 1.83 is released though, we'll be able to move to 1.79+; so any MR proposing using newer syntax can still be raised, it'll just have to wait before it can be merged.

@daniel-pfeiffer
Copy link
Author

The macro itself is almost 0-cost. The parser will be highly efficient. Haven’t looked at splitting into 3 parts yet, but it’ll be at least as good as what you do now.

Is there any plan for Decimal<const PARTS: usize = 3> { parts: [u32; PARTS] … }? While I’m at it, I could consider generalising the parser, even if for now the output would still be non-generic.

In const I can only produce empty Error::ConversionTo(String::new()), so that’s neither nice nor a showstopper. In the V2.x future, for meaningful errors, the parser should return differentiated Error::RadixOutOfBounds(u32) or Error::BadByteAt(usize). The macro can then turn these into nice messages.

I have updated my example. Since the discussion on IRLO about Rust syntax for arbitrary bases is going nowhere, I came up with a new syntax idea, with both radix and scale being optional: dec!(radix: num <<< scale)/ dec!(radix: num >>> scale). I propose decimal left/right shift to have a fatter op than binary, but I can also do <</ >>.

@Tony-Samuels
Copy link
Collaborator

There's been discussion over a const generic Decimal, but I wouldn't start doing any work around it yet.

For dec! it's reasonable to panic imo on error, given that we're enforcing this as a compile-time macro via const { ... } there's no way to handle the error anyway.

As for radix/scale, I'd be open to improvements, but it's probably simplest for a replacement macro to just be a drop-in to start; and it can be extended later down the line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants