Skip to content

Procedural macros interactions with constant evaluation #2279

Open

Description

@9il

From 1566-proc-macros.md:

Interactions with constant evaluation

Both procedural macros and constant evaluation are mechanisms for running Rust code at compile time. Currently, and under the proposed design, they are considered completely separate features. There might be some benefit in letting them interact.

It is terrific feature! It allows to do awesome language idioms such as D's static if/static foreach but in library code.

For example static if/ static foreach and #2000 are required to port ndslice to Rust from D. I am aware about ndarray cargo package, ndslice implementation has more optimisation powers.

Related issues rust-lang/rust#38356

Activity

  1. eddyb commented on Jan 6, 2018

    @eddyb
    Contributor

    AFAIK, D uses the C++ approach to generics (aka "templates"), which is to type-check after monomorphizing them. The macro system is different in that it produces source and must be sequenced before the type-system can exist, for reasons of global reasoning (e.g. impl coherence).
    Even if we'd introduce more interactions, they'd require very careful design to prevent unsoundness.

    Now, for static if/foreach, what's the usecase #2000 or an extension thereof can't handle?

    Also, going all the way up to the macro system for an optimization, instead of pushing it down?
    I'd understand this better if this were about getting an algorithm on tuples/arrays to type-check.

  2. 9il commented on Jan 6, 2018

    @9il
    Author

    This is base struct for Rust's ndarray

    pub struct ArrayBase<S, D>
        where S: Data
    {
        /// Rc data when used as view, Uniquely held data when being mutated
        data: S,
        /// A pointer into the buffer held by data, may point anywhere
        /// in its range.
        ptr: *mut S::Elem,
        /// The size of each axis
        dim: D,
        /// The element count stride per axis. To be parsed as `isize`.
        strides: D,
    }

    #2000 allows to make dim a compile time constant.

    Assume you have an updated N-dimensional ArrayBase with fixed dimensions at compile time args.
    Then you have a ArrayBase's method, say atIndex for simplicity, that accepts M Args, M <= N. Based on M value and Args's types we can decide what return Type atIndex should have and how to compute it.

    In D it would look similar to (very simplified):

    struct ArrayBase(size_t N, T)
    {
        ...
        auto atIndex(Args...)(Args args)
        {
            enum M = Args.length;
            auto ptr = this._ptr;
            static foreach (i; 0 .. M)
            {
                  // move ptr to the element/subarray
            }
            static if (M == N && allIndexes!Args) // allIndexes!Args is a template the evaluates to false/true
            {
                   return *ptr;
            }
            else
           {
                  enum NewN = ... // compute new N using CTFE and information about types
                  return ArrayBase!(NewN, T)(*ptr, other_args);
           }
        }
    }

    #2000 introduces a compilte time constants, but they can not be used to generate/choose code to compile. D's CTFE is something recursive like, it allows to do CTFE, mix code, and do CTFE with mixed code again. #2000 makes me wonder if Rust will be able to do the same

  3. added
    T-langRelevant to the language team, which will review and decide on the RFC.
    on Jan 6, 2018
  4. Diggsey commented on Mar 14, 2018

    @Diggsey
    Contributor

    @9il with specialisation, you can change the implementation based on a constant number without requiring a macro.

  5. 9il commented on Mar 14, 2018

    @9il
    Author

    @Diggsey, could you please provide an example?

  6. Diggsey commented on Mar 14, 2018

    @Diggsey
    Contributor

    Well with both #2000 and specialization, you could do something like this:

    trait Algorithm {
        fn calculate(&self) -> Result;
    }
    
    impl<const N> Algorithm for Array<N> {
        default fn calculate(&self) -> Result {
            <general impl>
        }
    }
    
    impl Algorithm for Array<1> {
        fn calculate(&self) -> Result {
            <simple base case>
        }
    }
    
  7. Centril commented on Oct 9, 2018

    @Centril
    Contributor

    @9il are you satisfied with the provided solution? if so, please close this issue. :)

  8. 9il commented on Oct 11, 2018

    @9il
    Author

    Specialization solve's static if issue, but Rust still doesn't have static foreach alternative.

  9. eddyb commented on Nov 3, 2018

    @eddyb
    Contributor

    I think looping over tuples should be supplanted by VG (variadic generics).
    It can't literally be a for loop, but with generic closures, we can get really close.

    As for looping over integers and using them in const generics, you should be able to do it with a recursive impl, other than for the lack of a way to match on 0 vs N + 1 in the initial const generics.

    You can probably use specialization to handle N in general, vs 0?
    Or add a where clause that wouldn't hold for N = 0, such as where [(); N - 1]:
    (yes, no bounds, but the type must still be well-formed)

    And, again, sugar would use generic closures, e.g. for<const i: usize> |...| {...}.

  10. Centril commented on Nov 3, 2018

    @Centril
    Contributor

    It can't literally be a for loop, but with generic closures, we can get really close.

    And, again, sugar would use generic closures, e.g. for |...| {...}.

    I'm working on an RFC re. that. :)

  11. eddyb commented on Nov 3, 2018

    @eddyb
    Contributor

    @Centril heh, I was hoping we'd revive @Amanieu's #1650, glad to have someone on it!

  12. jacob-greenfield commented on Nov 15, 2019

    @jacob-greenfield

    Is there any progress on this? I think this should be revisited due to the recent progress for const generics. For example, now there are some really silly things that rely on this:

    foo!(u16::from_le_bytes([0x37, 0x13]));

    foo can't actually read the resulting number without adding in a special case for from_le_bytes, nevertheless any user-provided const fn.

    Side question: this is a bit of a weird question, but is const evaluation actually the one single thing that const fns can do and procedural macros can't? Unless I'm missing something, the only difference is that const fns create a const context, and this issue is just pointing out that procedural macros could also create a const context and evaluate a const value. Although there is the issue IIUC that macros are expanded long before const evaluation.

  13. Lokathor commented on Nov 15, 2019

    @Lokathor
    Contributor

    I think part of the problem is also that macros expand outside-in but const fn and other code contexts want to evaluate inside-out.

  14. Diggsey commented on Nov 15, 2019

    @Diggsey
    Contributor

    foo can't actually read the resulting number without adding in a special case for from_le_bytes, nevertheless any user-provided const fn.

    But foo could expand to something like this:

    const N: u16 = u16::from_le_bytes([0x37, 0x13]);
    
    use_n_in_some_way([24; N]);

    ... without needing any special cases for from_le_bytes. The macro can't actually use the value because macro expansion happens long before const evaluation, but it can generate code that does use the value and that also runs at compile time. This is sufficient for the vast majority of cases.

  15. Lokathor commented on Nov 15, 2019

    @Lokathor
    Contributor

    Well, unless you need the proc-macro to know the value of N as part of the code generation decision.

  16. jacob-greenfield commented on Nov 16, 2019

    @jacob-greenfield

    Well, unless you need the proc-macro to know the value of N as part of the code generation decision.

    That's what I was getting at.

    I think part of the problem is also that macros expand outside-in but const fn and other code contexts want to evaluate inside-out.

    Hm, I didn't realize that. Still, maybe there's some way (maybe an attribute) that a procedural macro could require a const context for its body and also require that the body is pre-evaluated?

    Also, probably the biggest hurdle in order to do anything useful with the input is that you would need to accept an actual, typed value rather than a token tree, which is really weird. What I'm imagining is basically a normal const fn except it returns a token tree.

    The more I think about it, the more it sounds like it wouldn't work because type checking, const evaluation, and macro evaluation would be mutually recursive. I only think it might work because Rust can compile things in passes, even across files without forward declaration. One concern is, for example, a const procedural macro relies on impl const Trait1 for Foo to generate impl Trait2 for Foo, which might require running type checking before Foo has implemented all of its traits. There would likely need to be some limits if this even worked in the first place. In any case I wonder if this might be a separate issue than the original, if that's the case sorry for derailing.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-langRelevant to the language team, which will review and decide on the RFC.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions