clients-macros 0.1.0

Procedural macros for the clients concrete dependency injection crate
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
//! Procedural macros that power the public `clients` API.
//!
//! The runtime crate intentionally keeps parsing dependencies small and local, so
//! this crate hand-rolls just enough token inspection to support the user-facing
//! `client!` and `#[derive(Depends)]` syntaxes without pulling in a larger macro
//! parsing stack.

use proc_macro::{Delimiter, Spacing, TokenStream, TokenTree};

/// Declares a concrete dependency client backed by raw function pointers.
///
/// The generated type is a plain `struct`, not a trait object or a trait-based
/// abstraction. Each declared method is stored as a function pointer field, and
/// the generated method bodies simply call through to those pointers.
///
/// The macro also generates a helper module whose name comes after `as`, along
/// with one nested helper module per declared method. Those helper modules are
/// what power [`clients::deps!`](https://docs.rs/clients/latest/clients/macro.deps.html)
/// and [`clients::test_deps!`](https://docs.rs/clients/latest/clients/macro.test_deps.html).
///
/// A method may provide a live implementation directly:
///
/// ```ignore
/// client! {
///     pub struct Clock as clock {
///         pub fn now_millis() -> u64 = || 1234;
///     }
/// }
/// ```
///
/// Or it may leave the live implementation unspecified, causing calls to panic
/// unless a test override supplies one:
///
/// ```ignore
/// client! {
///     pub struct UserClient as user_client {
///         pub fn fetch_user(id: u64) -> Result<User, DependencyError>;
///     }
/// }
/// ```
///
/// Async methods are supported directly:
///
/// ```ignore
/// client! {
///     pub struct AsyncClock as async_clock {
///         pub async fn now_millis() -> u64 = || async { 1234 };
///     }
/// }
/// ```
///
/// Current limitations:
///
/// - method implementations must be non-capturing closures or function items
/// - at most 4 method arguments are supported
#[proc_macro]
pub fn client(input: TokenStream) -> TokenStream {
    match expand_client(input) {
        Ok(stream) => stream,
        Err(message) => compile_error(message),
    }
}

/// Derives dependency-backed construction for a simple braced struct.
///
/// Fields marked with `#[dep]` are initialized with `::clients::get::<FieldType>()`.
/// All other fields are initialized with `Default::default()`.
///
/// The derive generates:
///
/// - an implementation of `Default`
/// - a `from_deps() -> Self` convenience constructor
///
/// Example:
///
/// ```ignore
/// #[derive(Depends)]
/// struct Greeter {
///     #[dep]
///     user_client: UserClient,
///     #[dep]
///     clock: Clock,
///     greeting_prefix: String,
/// }
///
/// let greeter = Greeter::from_deps();
/// ```
///
/// Current limitations:
///
/// - only braced structs are supported
/// - generics and where-clauses are not currently supported
#[proc_macro_derive(Depends, attributes(dep))]
pub fn derive_depends(input: TokenStream) -> TokenStream {
    match derive_depends_impl(input) {
        Ok(stream) => stream,
        Err(message) => compile_error(message),
    }
}

/// Expands a `client!` invocation into a concrete client struct plus helper
/// modules for runtime lookup and test overrides.
fn expand_client(input: TokenStream) -> Result<TokenStream, String> {
    let tokens = input.into_iter().collect::<Vec<_>>();
    let struct_index = tokens
        .iter()
        .position(|token| is_ident(token, "struct"))
        .ok_or_else(|| "client! expects `struct`".to_string())?;

    let visibility = tokens_to_string(&tokens[..struct_index]);
    let name = ident_at(&tokens, struct_index + 1, "a client name")?;

    if !matches!(tokens.get(struct_index + 2), Some(token) if is_ident(token, "as")) {
        return Err("client! expects `as <module_name>` after the struct name".into());
    }

    let module = ident_at(&tokens, struct_index + 3, "a module name after `as`")?;
    let body = match tokens.get(struct_index + 4) {
        Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => group.stream(),
        _ => return Err("client! expects a braced method body".into()),
    };

    if tokens.len() != struct_index + 5 {
        return Err("unexpected tokens after the client body".into());
    }

    let methods = parse_methods(body)?;
    if methods.is_empty() {
        return Err("client! requires at least one method".into());
    }

    let visibility_prefix = with_trailing_space(&visibility);
    let field_lines = methods
        .iter()
        .map(Method::render_field)
        .collect::<Vec<_>>()
        .join("\n");
    let method_lines = methods
        .iter()
        .map(Method::render_method)
        .collect::<Vec<_>>()
        .join("\n\n");
    let live_lines = methods
        .iter()
        .map(|method| format!("{}: {}", method.name, method.render_live_initializer(&name)))
        .collect::<Vec<_>>()
        .join(",\n                    ");
    let module_lines = methods
        .iter()
        .map(|method| method.render_module(&name))
        .collect::<Vec<_>>()
        .join("\n");

    let output = format!(
        "#[derive(Clone, Copy)]
        {visibility_prefix}struct {name} {{
            {field_lines}
        }}

        impl {name} {{
            {method_lines}
        }}

        impl ::clients::Dependency for {name} {{
            fn live() -> Self {{
                Self {{
                    {live_lines}
                }}
            }}
        }}

        impl ::core::default::Default for {name} {{
            fn default() -> Self {{
                <Self as ::clients::Dependency>::live()
            }}
        }}

        {visibility_prefix}mod {module} {{
            use super::*;

            pub fn get() -> super::{name} {{
                ::clients::get::<super::{name}>()
            }}

            {module_lines}
        }}"
    );

    output
        .parse::<TokenStream>()
        .map_err(|error| error.to_string())
}

/// Internal representation of one declared client method.
#[derive(Clone)]
struct Method {
    /// The Rust identifier used for both the field and the wrapper method.
    name: String,
    /// Any leading method tokens that need to be replayed, such as `pub` or
    /// attributes that were attached to the method declaration.
    visibility: String,
    /// The parsed argument list in declaration order.
    arguments: Vec<Argument>,
    /// The declared return type as source tokens.
    return_ty: String,
    /// The optional live implementation expression.
    implementation: Option<String>,
    /// Whether the declared method was marked `async`.
    is_async: bool,
}

/// Internal representation of one method argument.
#[derive(Clone)]
struct Argument {
    /// The argument binding name.
    name: String,
    /// The argument type as source tokens.
    ty: String,
}

impl Method {
    /// Returns the number of arguments declared by the method.
    fn arity(&self) -> usize {
        self.arguments.len()
    }

    /// Chooses the correct runtime eraser helper for the method shape.
    fn eraser_name(&self) -> String {
        if self.is_async {
            format!("::clients::erase_async_{}", self.arity())
        } else {
            format!("::clients::erase_sync_{}", self.arity())
        }
    }

    /// Renders the argument list in declaration form, for example
    /// `id: u64, name: String`.
    fn args_decl(&self) -> String {
        self.arguments
            .iter()
            .map(|argument| format!("{}: {}", argument.name, argument.ty))
            .collect::<Vec<_>>()
            .join(", ")
    }

    /// Renders only the argument types in declaration order.
    fn args_types(&self) -> String {
        self.arguments
            .iter()
            .map(|argument| argument.ty.clone())
            .collect::<Vec<_>>()
            .join(", ")
    }

    /// Renders only the argument names in declaration order.
    fn args_names(&self) -> String {
        self.arguments
            .iter()
            .map(|argument| argument.name.clone())
            .collect::<Vec<_>>()
            .join(", ")
    }

    /// Renders the function-pointer return type used by the stored field.
    fn fn_pointer_return(&self) -> String {
        if self.is_async {
            format!("::clients::BoxFuture<{}>", self.return_ty)
        } else {
            self.return_ty.clone()
        }
    }

    /// Renders the struct field that stores the underlying function pointer.
    fn render_field(&self) -> String {
        format!(
            "{}: fn({}) -> {},",
            self.name,
            self.args_types(),
            self.fn_pointer_return()
        )
    }

    /// Renders the ergonomic wrapper method that forwards to the stored
    /// function pointer.
    fn render_method(&self) -> String {
        let visibility = with_trailing_space(&self.visibility);
        let args_decl = self.args_decl();
        let call_args = self.args_names();

        if self.is_async {
            format!(
                "{visibility}async fn {}(&self{}{}) -> {} {{
                (self.{})({}).await
            }}",
                self.name,
                maybe_comma(&args_decl),
                args_decl,
                self.return_ty,
                self.name,
                call_args,
            )
        } else {
            format!(
                "{visibility}fn {}(&self{}{}) -> {} {{
                (self.{})({})
            }}",
                self.name,
                maybe_comma(&args_decl),
                args_decl,
                self.return_ty,
                self.name,
                call_args,
            )
        }
    }

    /// Renders the live initializer for the function-pointer field.
    ///
    /// When a live implementation is present, this selects the correct eraser
    /// helper. Otherwise it generates a panic-based placeholder used to surface
    /// missing live implementations with a readable dependency path.
    fn render_live_initializer(&self, client_name: &str) -> String {
        if let Some(implementation) = &self.implementation {
            format!("{}({implementation})", self.eraser_name())
        } else if self.is_async {
            format!(
                "{{
                        fn __dep_unimplemented({}) -> ::clients::BoxFuture<{}> {{
                            ::clients::boxed(async move {{
                                ::clients::unimplemented_dependency(\"{}.{}\")
                            }})
                        }}

                        __dep_unimplemented
                    }}",
                self.args_decl(),
                self.return_ty,
                client_name,
                self.name,
            )
        } else {
            format!(
                "{{
                        fn __dep_unimplemented({}) -> {} {{
                            ::clients::unimplemented_dependency(\"{}.{}\")
                        }}

                        __dep_unimplemented
                    }}",
                self.args_decl(),
                self.return_ty,
                client_name,
                self.name,
            )
        }
    }

    /// Renders the per-method helper module used by `deps!` and `test_deps!`.
    fn render_module(&self, client_name: &str) -> String {
        let args_types = self.args_types();
        let fn_pointer_return = self.fn_pointer_return();
        let eraser = self.eraser_name();

        if self.is_async {
            format!(
                "pub mod {} {{
                    use super::*;

                    pub fn get() -> fn({}) -> {} {{
                        super::get().{}
                    }}

                    pub fn override_with<F, Fut>(builder: &mut ::clients::OverrideBuilder, implementation: F)
                    where
                        F: Fn({}) -> Fut + Copy + 'static,
                        Fut: ::core::future::Future<Output = {}> + Send + 'static,
                    {{
                        builder.update::<super::super::{client_name}, _>(|mut dependency| {{
                            dependency.{} = {}(implementation);
                            dependency
                        }});
                    }}
                }}",
                self.name,
                args_types,
                fn_pointer_return,
                self.name,
                args_types,
                self.return_ty,
                self.name,
                eraser,
            )
        } else {
            format!(
                "pub mod {} {{
                    use super::*;

                    pub fn get() -> fn({}) -> {} {{
                        super::get().{}
                    }}

                    pub fn override_with<F>(builder: &mut ::clients::OverrideBuilder, implementation: F)
                    where
                        F: Fn({}) -> {} + Copy + 'static,
                    {{
                        builder.update::<super::super::{client_name}, _>(|mut dependency| {{
                            dependency.{} = {}(implementation);
                            dependency
                        }});
                    }}
                }}",
                self.name,
                args_types,
                fn_pointer_return,
                self.name,
                args_types,
                self.return_ty,
                self.name,
                eraser,
            )
        }
    }
}

/// Parses a method list from the body of a `client!` declaration.
fn parse_methods(stream: TokenStream) -> Result<Vec<Method>, String> {
    split_top_level(stream, ';')
        .into_iter()
        .map(|tokens| parse_method(&tokens))
        .collect()
}

/// Parses one declared client method.
fn parse_method(tokens: &[TokenTree]) -> Result<Method, String> {
    if tokens.is_empty() {
        return Err("empty method definition".into());
    }

    let fn_index = tokens
        .iter()
        .position(|token| is_ident(token, "fn"))
        .ok_or_else(|| "client methods must use `fn`".to_string())?;

    let mut leading = tokens[..fn_index].to_vec();
    let is_async = matches!(
        leading.last(),
        Some(TokenTree::Ident(ident)) if ident.to_string() == "async"
    );
    if is_async {
        leading.pop();
    }

    let visibility = tokens_to_string(&leading);
    let name = ident_at(tokens, fn_index + 1, "a method name")?;
    let arguments_group = match tokens.get(fn_index + 2) {
        Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => {
            group.stream()
        }
        _ => return Err(format!("method `{name}` is missing its argument list")),
    };

    let rest = &tokens[fn_index + 3..];
    if !matches!(rest.first(), Some(TokenTree::Punct(punct)) if punct.as_char() == '-')
        || !matches!(rest.get(1), Some(TokenTree::Punct(punct)) if punct.as_char() == '>')
    {
        return Err(format!("method `{name}` is missing `->`"));
    }

    let eq_index = rest
        .iter()
        .position(|token| matches!(token, TokenTree::Punct(punct) if punct.as_char() == '='));
    let return_tokens = match eq_index {
        Some(index) => &rest[2..index],
        None => &rest[2..],
    };
    if return_tokens.is_empty() {
        return Err(format!("method `{name}` is missing a return type"));
    }

    let implementation = eq_index.map(|index| tokens_to_string(&rest[index + 1..]));
    let arguments = parse_arguments(arguments_group)?;
    if arguments.len() > 4 {
        return Err(format!(
            "method `{name}` has {} arguments, but only up to 4 are supported right now",
            arguments.len()
        ));
    }

    Ok(Method {
        name,
        visibility,
        arguments,
        return_ty: tokens_to_string(return_tokens),
        implementation,
        is_async,
    })
}

/// Parses a parenthesized argument list into named arguments.
fn parse_arguments(stream: TokenStream) -> Result<Vec<Argument>, String> {
    split_top_level(stream, ',')
        .into_iter()
        .map(|tokens| {
            let colon_index = tokens
                .iter()
                .position(
                    |token| matches!(token, TokenTree::Punct(punct) if punct.as_char() == ':'),
                )
                .ok_or_else(|| "expected arguments to look like `name: Type`".to_string())?;

            let name = tokens[..colon_index]
                .iter()
                .rev()
                .find_map(|token| match token {
                    TokenTree::Ident(ident) => Some(ident.to_string()),
                    _ => None,
                })
                .ok_or_else(|| "expected an argument name".to_string())?;

            let ty = tokens_to_string(&tokens[colon_index + 1..]);
            if ty.is_empty() {
                return Err("expected an argument type".into());
            }

            Ok(Argument { name, ty })
        })
        .collect()
}

/// Parses the `Depends` derive input and routes to struct expansion.
fn derive_depends_impl(input: TokenStream) -> Result<TokenStream, String> {
    let mut tokens = input.into_iter().peekable();

    while let Some(token) = tokens.next() {
        if is_ident(&token, "struct") {
            return expand_struct(tokens);
        }
    }

    Err("Depends can only be derived for structs".into())
}

/// Expands a supported struct declaration into `Default` and `from_deps()`.
fn expand_struct<I>(mut tokens: I) -> Result<TokenStream, String>
where
    I: Iterator<Item = TokenTree>,
{
    let name = match tokens.next() {
        Some(TokenTree::Ident(ident)) => ident,
        _ => return Err("expected a struct name".into()),
    };

    let fields_group = match tokens.next() {
        Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => group,
        Some(_) => return Err("Depends does not support generics or where clauses yet".into()),
        None => return Err("expected a braced struct body".into()),
    };

    let fields = parse_fields(fields_group.stream())?;
    let initializers = fields
        .into_iter()
        .map(|field| {
            if field.injected {
                format!("{}: ::clients::get::<{}>()", field.name, field.ty)
            } else {
                format!("{}: ::core::default::Default::default()", field.name)
            }
        })
        .collect::<Vec<_>>()
        .join(", ");

    let output = format!(
        "impl ::core::default::Default for {name} {{
            fn default() -> Self {{
                Self {{ {initializers} }}
            }}
        }}

        impl {name} {{
            #[doc = \"Constructs `Self` by resolving every `#[dep]` field from the dependency system and initializing all other fields with `Default::default()`.\"]
            pub fn from_deps() -> Self {{
                ::core::default::Default::default()
            }}
        }}",
    );

    output
        .parse::<TokenStream>()
        .map_err(|error| error.to_string())
}

/// Internal representation of a struct field encountered by `Depends`.
struct Field {
    /// The field identifier.
    name: String,
    /// The field type as source tokens.
    ty: String,
    /// Whether the field carries `#[dep]`.
    injected: bool,
}

/// Parses a comma-separated field list from a braced struct body.
fn parse_fields(stream: TokenStream) -> Result<Vec<Field>, String> {
    split_top_level(stream, ',')
        .into_iter()
        .map(|tokens| parse_field(&tokens))
        .collect()
}

/// Parses one struct field and detects the `#[dep]` marker attribute.
fn parse_field(tokens: &[TokenTree]) -> Result<Field, String> {
    let mut injected = false;
    let mut colon_index = None;

    for (index, token) in tokens.iter().enumerate() {
        if matches_dep_attribute(tokens, index) {
            injected = true;
        }

        if let TokenTree::Punct(punct) = token
            && punct.as_char() == ':'
        {
            colon_index = Some(index);
            break;
        }
    }

    let colon_index = colon_index.ok_or_else(|| "expected a named struct field".to_string())?;

    let name = tokens[..colon_index]
        .iter()
        .rev()
        .find_map(|token| match token {
            TokenTree::Ident(ident) => Some(ident.to_string()),
            _ => None,
        })
        .ok_or_else(|| "expected a field name".to_string())?;

    let ty_tokens = tokens[colon_index + 1..]
        .iter()
        .cloned()
        .collect::<TokenStream>();
    if ty_tokens.is_empty() {
        return Err("expected a field type".into());
    }

    Ok(Field {
        name,
        ty: ty_tokens.to_string(),
        injected,
    })
}

/// Splits a token stream at a top-level punctuation separator.
///
/// Token groups already isolate parentheses, brackets, and braces for us, but
/// generic type arguments are represented with raw punctuation, so this helper
/// also tracks angle-bracket nesting in order to avoid splitting commas inside
/// types like `Vec<Result<T, E>>`.
fn split_top_level(stream: TokenStream, separator: char) -> Vec<Vec<TokenTree>> {
    let mut items = Vec::new();
    let mut current = Vec::new();
    let mut angle_depth = 0usize;

    for token in stream {
        let should_split = matches!(
            &token,
            TokenTree::Punct(punct)
                if punct.as_char() == separator
                    && punct.spacing() == Spacing::Alone
                    && angle_depth == 0
        );

        if should_split {
            if !current.is_empty() {
                items.push(current);
                current = Vec::new();
            }
            continue;
        }

        if let TokenTree::Punct(punct) = &token {
            match punct.as_char() {
                '<' => angle_depth += 1,
                '>' => angle_depth = angle_depth.saturating_sub(1),
                _ => {}
            }
        }

        current.push(token);
    }

    if !current.is_empty() {
        items.push(current);
    }

    items
}

/// Reads an identifier from a token slice at a specific index.
fn ident_at(tokens: &[TokenTree], index: usize, expected: &str) -> Result<String, String> {
    match tokens.get(index) {
        Some(TokenTree::Ident(ident)) => Ok(ident.to_string()),
        _ => Err(format!("expected {expected}")),
    }
}

/// Returns `true` when the token pair at `index` spells `#[dep]`.
fn matches_dep_attribute(tokens: &[TokenTree], index: usize) -> bool {
    let Some(TokenTree::Punct(pound)) = tokens.get(index) else {
        return false;
    };
    if pound.as_char() != '#' {
        return false;
    }

    let Some(TokenTree::Group(group)) = tokens.get(index + 1) else {
        return false;
    };

    if group.delimiter() != Delimiter::Bracket {
        return false;
    }

    let mut attribute_tokens = group.stream().into_iter();
    matches!(attribute_tokens.next(), Some(TokenTree::Ident(ident)) if ident.to_string() == "dep")
}

/// Returns `true` when the token is an identifier matching `expected`.
fn is_ident(token: &TokenTree, expected: &str) -> bool {
    matches!(token, TokenTree::Ident(ident) if ident.to_string() == expected)
}

/// Serializes a token slice into a whitespace-normalized Rust source fragment.
fn tokens_to_string(tokens: &[TokenTree]) -> String {
    tokens
        .iter()
        .map(TokenTree::to_string)
        .collect::<Vec<_>>()
        .join(" ")
}

/// Returns `value` plus a trailing space when it is non-empty.
fn with_trailing_space(value: &str) -> String {
    if value.is_empty() {
        String::new()
    } else {
        format!("{value} ")
    }
}

/// Returns `", "` when `value` is non-empty so rendered methods can place
/// commas between `&self` and declared parameters without branching inline.
fn maybe_comma(value: &str) -> &'static str {
    if value.is_empty() { "" } else { ", " }
}

/// Renders a compile-time error token stream from a friendly message.
fn compile_error(message: String) -> TokenStream {
    format!("compile_error!({message:?});")
        .parse()
        .expect("compile_error! should parse")
}