Skip to main content

rustdv_macros/
rustdv_macros.rs

1//! Proc macros (design-doc §6). Two macros only, per the governing
2//! principle — a macro is justified only where Python used runtime
3//! dynamism Rust lacks:
4//!
5//! - `#[rustdv::test]` (§6.1): registers the annotated `async fn` in the
6//!   link-time test registry (the `inventory`/`linkme` technique, hand
7//!   rolled for ELF: `#[link_section]` + `__start_`/`__stop_` symbols).
8//! - `#[derive(Component)]` (§6.3): generates the `ComponentNode`
9//!   traversal over `#[component]` fields (`T`, `Option<T>`,
10//!   `Vec<T>`).
11//!
12//! **Implementation note (STATUS.md):** the zero-dependency constraint
13//! rules out syn/quote, so parsing walks raw token trees and code
14//! generation goes through string formatting + `.parse()`. Supported
15//! grammar is deliberately narrow (plain fns, non-generic structs with
16//! named fields); unsupported shapes produce compile errors.
17
18use proc_macro::{Delimiter, TokenStream, TokenTree};
19
20// ===========================================================================
21// #[rustdv::test]
22// ===========================================================================
23
24#[derive(Default)]
25struct TestOpts {
26    name: Option<String>,
27    timeout_time: Option<u64>,
28    timeout_unit: Option<String>,
29    skip: bool,
30    expect_fail: bool,
31    expect_error: Option<String>,
32}
33
34fn strip_quotes(s: &str) -> String {
35    s.trim_matches('"').to_string()
36}
37
38fn parse_test_opts(attr: TokenStream) -> Result<TestOpts, String> {
39    let mut opts = TestOpts::default();
40    let mut iter = attr.into_iter().peekable();
41    while let Some(tt) = iter.next() {
42        let key = match &tt {
43            TokenTree::Ident(i) => i.to_string(),
44            TokenTree::Punct(p) if p.as_char() == ',' => continue,
45            other => return Err(format!("unexpected token in #[rustdv::test(...)]: {other}")),
46        };
47        // Optional `= value`
48        let mut value: Option<String> = None;
49        if let Some(TokenTree::Punct(p)) = iter.peek() {
50            if p.as_char() == '=' {
51                iter.next(); // consume '='
52                match iter.next() {
53                    Some(TokenTree::Literal(l)) => value = Some(l.to_string()),
54                    Some(TokenTree::Ident(i)) => value = Some(i.to_string()),
55                    other => return Err(format!("expected value after '{key} =', got {other:?}")),
56                }
57            }
58        }
59        match key.as_str() {
60            "name" => opts.name = value.map(|v| strip_quotes(&v)),
61            "timeout_time" => {
62                let v = value.ok_or("timeout_time needs a value")?;
63                opts.timeout_time =
64                    Some(v.parse::<u64>().map_err(|_| format!("bad timeout_time '{v}'"))?);
65            }
66            "timeout_unit" => opts.timeout_unit = value.map(|v| strip_quotes(&v)),
67            "skip" => opts.skip = value.map(|v| v == "true").unwrap_or(true),
68            "expect_fail" => opts.expect_fail = value.map(|v| v == "true").unwrap_or(true),
69            // Pass only if the test fails with this cause — the port of
70            // pyuvm's `expect_error=SomeException` (D68).
71            "expect_error" => {
72                let v = value.ok_or("expect_error needs a value, e.g. expect_error = \"config_not_found\"")?;
73                opts.expect_error = Some(strip_quotes(&v));
74            }
75            other => return Err(format!("unknown #[rustdv::test] option '{other}'")),
76        }
77    }
78    Ok(opts)
79}
80
81/// Which of the two front doors this item is (D46).
82#[derive(Copy, Clone, PartialEq, Eq)]
83enum TestForm {
84    /// `async fn tb(ctx: RustdvCtx) -> Result<(), TestError>` — the
85    /// cocotb shape, `@cocotb.test()` on a coroutine function.
86    Function,
87    /// `struct RandomTest;` or `type RandomTest = AluTest<RandomTester>;`
88    /// implementing `Component` — the pyuvm shape, `@pyuvm.test()` on a
89    /// class.
90    Component,
91}
92
93/// The item's kind and name: the first top-level `fn` / `struct` / `type`
94/// keyword and the identifier after it. Attributes (`#[derive(..)]`) are
95/// bracket groups, not top-level idents, so they are skipped for free;
96/// `pub` and `async` are idents that simply are not the keyword.
97fn find_item(item: &TokenStream) -> Option<(TestForm, String)> {
98    let mut form: Option<TestForm> = None;
99    for tt in item.clone() {
100        if let TokenTree::Ident(i) = tt {
101            let s = i.to_string();
102            if let Some(f) = form {
103                return Some((f, s));
104            }
105            form = match s.as_str() {
106                "fn" => Some(TestForm::Function),
107                "struct" | "type" => Some(TestForm::Component),
108                _ => None,
109            };
110        }
111    }
112    None
113}
114
115fn compile_error(msg: &str) -> TokenStream {
116    format!("compile_error!({msg:?});").parse().unwrap()
117}
118
119/// Both front doors (design-doc §6.1, D46). Registers at link time either
120///
121/// - `async fn name(ctx: RustdvCtx) -> Result<(), TestError>` — the port of
122///   `@cocotb.test()`, decorating a coroutine *function*; or
123/// - `struct Name;` / `type Name = ..;` implementing `Component + Default`
124///   — the port of `@pyuvm.test()`, decorating a *class*.
125///
126/// Both expand to the same erased shim, so there is one registry and one
127/// execution path behind the two syntaxes.
128#[proc_macro_attribute]
129pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
130    let opts = match parse_test_opts(attr) {
131        Ok(o) => o,
132        Err(e) => return compile_error(&e),
133    };
134    let Some((form, item_name)) = find_item(&item) else {
135        return compile_error(
136            "#[rustdv::test] must be applied to an async fn, a struct, or a type alias",
137        );
138    };
139    let fn_name = item_name;
140    let test_name = opts.name.unwrap_or_else(|| fn_name.clone());
141    let timeout = match (opts.timeout_time, opts.timeout_unit) {
142        (Some(t), Some(u)) => format!("::core::option::Option::Some(({t}u64, \"{u}\"))"),
143        (Some(t), None) => format!("::core::option::Option::Some(({t}u64, \"ns\"))"),
144        _ => "::core::option::Option::None".to_string(),
145    };
146    let skip = opts.skip;
147    let expect_fail = opts.expect_fail;
148    let expect_error = match &opts.expect_error {
149        Some(k) => format!("::core::option::Option::Some(\"{k}\")"),
150        None => "::core::option::Option::None".to_string(),
151    };
152
153    // The two forms differ only in this body: call the function, or build
154    // the component and let the phaser drive its whole lifecycle (D51). The
155    // struct form no longer calls `run` directly — `run_component_test`
156    // runs build → connect → … → run → extract → check → report → final.
157    let body = match form {
158        TestForm::Function => format!("::std::boxed::Box::pin({fn_name}(ctx))"),
159        TestForm::Component => format!(
160            r#"::std::boxed::Box::pin(async move {{
161            let mut __ctx = ctx;
162            let mut __test = <{fn_name} as ::core::default::Default>::default();
163            ::rustdv::run_component_test(&mut __test, &mut __ctx).await
164        }})"#
165        ),
166    };
167
168    let reg = format!(
169        r#"
170const _: () = {{
171    fn __rustdv_shim(
172        ctx: ::rustdv::RustdvCtx,
173    ) -> ::std::pin::Pin<::std::boxed::Box<
174        dyn ::std::future::Future<Output = ::core::result::Result<(), ::rustdv::TestError>>,
175    >> {{
176        {body}
177    }}
178    #[used]
179    #[cfg_attr(not(target_vendor = "apple"), link_section = "rustdv_tests")]
180    #[cfg_attr(target_vendor = "apple", link_section = "__DATA,rustdv_tests")]
181    static __RUSTDV_TEST_REG: &'static ::rustdv::TestRegistration = &::rustdv::TestRegistration {{
182        name: "{test_name}",
183        module: ::core::module_path!(),
184        file: ::core::file!(),
185        line: ::core::line!(),
186        run: __rustdv_shim,
187        timeout: {timeout},
188        skip: {skip},
189        expect_fail: {expect_fail},
190        expect_error: {expect_error},
191    }};
192}};
193"#
194    );
195
196    let mut out = item;
197    out.extend(reg.parse::<TokenStream>().expect("rustdv-macros: generated code failed to parse"));
198    out
199}
200
201// ===========================================================================
202// #[derive(Component)]
203// ===========================================================================
204
205struct Field {
206    name: String,
207    ty: String,
208    is_child: bool,
209    /// `Some("put")` for `#[port(put)]`, etc. A port field is never a child:
210    /// it is a request for an interface, not a component in the tree.
211    port: Option<String>,
212}
213
214/// Parse `struct Name { ... }` from the derive input token stream.
215/// Supported: structs with named fields, including simple generics
216/// (`struct Env<T: Tester + 'static> { ... }`).
217fn parse_struct(input: TokenStream) -> Result<(String, String, String, Vec<Field>), String> {
218    let mut iter = input.into_iter().peekable();
219    let mut struct_name: Option<String> = None;
220
221    // Find `struct` then its name, then the brace group.
222    while let Some(tt) = iter.next() {
223        if let TokenTree::Ident(i) = &tt {
224            if i.to_string() == "struct" {
225                match iter.next() {
226                    Some(TokenTree::Ident(n)) => {
227                        struct_name = Some(n.to_string());
228                        break;
229                    }
230                    _ => return Err("expected struct name".into()),
231                }
232            }
233        }
234    }
235    let name = struct_name.ok_or("#[derive(Component)] supports only structs")?;
236
237    // Capture optional generics `<...>` (with bounds), then the brace group.
238    // A unit struct (`struct HelloWorldTest;`) has no brace group at all —
239    // ch23's tests are unit structs, since a test with no children has no
240    // fields to declare.
241    let mut fields_group = None;
242    let mut unit_struct = false;
243    let mut generics_tokens: Vec<TokenTree> = Vec::new();
244    let mut depth = 0i32;
245    for tt in iter {
246        match &tt {
247            TokenTree::Group(g) if g.delimiter() == Delimiter::Brace && depth == 0 => {
248                fields_group = Some(g.clone());
249                break;
250            }
251            TokenTree::Punct(p) if p.as_char() == ';' && depth == 0 => {
252                unit_struct = true;
253                break;
254            }
255            TokenTree::Punct(p) if p.as_char() == '<' => {
256                depth += 1;
257                generics_tokens.push(tt.clone());
258                continue;
259            }
260            TokenTree::Punct(p) if p.as_char() == '>' => {
261                depth -= 1;
262                generics_tokens.push(tt.clone());
263                continue;
264            }
265            _ => {}
266        }
267        if depth > 0 {
268            generics_tokens.push(tt.clone());
269        }
270    }
271    // impl generics: verbatim (`<T: Tester + 'static>`); type params: names only.
272    let impl_generics: String = {
273        let mut out = String::new();
274        for t in &generics_tokens {
275            let text = t.to_string();
276            if !out.is_empty() && !out.ends_with('\'') {
277                out.push(' ');
278            }
279            out.push_str(&text);
280        }
281        out
282    };
283    let type_params = {
284        // First ident (or lifetime) of each comma-separated part at depth 1.
285        let mut params: Vec<String> = Vec::new();
286        let mut d = 0i32;
287        let mut take_next_ident = true;
288        let mut lifetime = false;
289        for t in &generics_tokens {
290            match t {
291                TokenTree::Punct(p) if p.as_char() == '<' => d += 1,
292                TokenTree::Punct(p) if p.as_char() == '>' => d -= 1,
293                TokenTree::Punct(p) if p.as_char() == ',' && d == 1 => take_next_ident = true,
294                TokenTree::Punct(p) if p.as_char() == '\'' && d == 1 && take_next_ident => {
295                    lifetime = true
296                }
297                TokenTree::Ident(i) if d == 1 && take_next_ident => {
298                    let word = i.to_string();
299                    if word == "const" {
300                        continue; // the const param's name is the next ident
301                    }
302                    params.push(if lifetime { format!("'{word}") } else { word });
303                    take_next_ident = false;
304                    lifetime = false;
305                }
306                _ => {}
307            }
308        }
309        if params.is_empty() { String::new() } else { format!("< {} >", params.join(" , ")) }
310    };
311    if unit_struct {
312        return Ok((name, impl_generics, type_params, Vec::new()));
313    }
314    let group = fields_group
315        .ok_or("#[derive(Component)] requires named fields, or a unit struct")?;
316
317    // Split the group's tokens into fields at top-level commas.
318    let mut fields = Vec::new();
319    let mut pending_child = false;
320    let mut pending_port: Option<String> = None;
321    let mut current: Vec<TokenTree> = Vec::new();
322
323    let mut toks = group.stream().into_iter().peekable();
324    let mut angle_depth = 0i32;
325    while let Some(tt) = toks.next() {
326        match &tt {
327            TokenTree::Punct(p) if p.as_char() == '<' => angle_depth += 1,
328            TokenTree::Punct(p) if p.as_char() == '>' => angle_depth -= 1,
329            TokenTree::Punct(p) if p.as_char() == '#' => {
330                // attribute: #[ ... ]
331                if let Some(TokenTree::Group(g)) = toks.peek() {
332                    if g.delimiter() == Delimiter::Bracket {
333                        let text = g.stream().to_string();
334                        if text.starts_with("component") {
335                            // A `#[component]` FIFO or `#[component]`
336                            // sequencer is a child like any
337                            // other — it is a component, and it belongs in the
338                            // hierarchy. We now also allow a bare `#[component]`
339                            // for brevity.
340                            pending_child = true;
341                        }
342                        if text.starts_with("port") {
343                            pending_port = attr_arg(&text);
344                        }
345                        toks.next(); // consume the bracket group
346                        continue;
347                    }
348                }
349            }
350            TokenTree::Punct(p) if p.as_char() == ',' && angle_depth == 0 => {
351                if !current.is_empty() {
352                    fields.push(make_field(&current, pending_child, pending_port.take())?);
353                    current.clear();
354                    pending_child = false;
355                }
356                continue;
357            }
358            _ => {}
359        }
360        current.push(tt);
361    }
362    if !current.is_empty() {
363        fields.push(make_field(&current, pending_child, pending_port.take())?);
364    }
365
366    Ok((name, impl_generics, type_params, fields))
367}
368
369/// From tokens like `pub name : Type ...` extract name and type text.
370/// The single argument of an attribute like `port (put)`, if there is one.
371fn attr_arg(text: &str) -> Option<String> {
372    let open = text.find('(')?;
373    let close = text.rfind(')')?;
374    let arg = text[open + 1..close].trim();
375    if arg.is_empty() {
376        None
377    } else {
378        Some(arg.to_string())
379    }
380}
381
382fn make_field(
383    tokens: &[TokenTree],
384    is_child: bool,
385    port: Option<String>,
386) -> Result<Field, String> {
387    let mut name = None;
388    let mut colon_at = None;
389    for (i, tt) in tokens.iter().enumerate() {
390        if let TokenTree::Punct(p) = tt {
391            if p.as_char() == ':' && colon_at.is_none() {
392                colon_at = Some(i);
393                break;
394            }
395        }
396    }
397    let colon = colon_at.ok_or("field without ':' (tuple structs unsupported)")?;
398    // The ident immediately before ':' is the field name (skips pub/pub(..)).
399    for tt in tokens[..colon].iter().rev() {
400        if let TokenTree::Ident(i) = tt {
401            name = Some(i.to_string());
402            break;
403        }
404    }
405    let name = name.ok_or("could not find field name")?;
406    let ty: String = tokens[colon + 1..].iter().map(|t| t.to_string()).collect::<Vec<_>>().join(" ");
407    Ok(Field { name, ty, is_child, port })
408}
409
410/// Generates the `ComponentNode` impl (design-doc §6.3, revised per R2):
411/// traversal of `#[component]` fields, including `Option<T>` and
412/// `Vec<T>`; names synthesized from field names. Emits **no** factory
413/// registration (R5) — but R5 is reversed: the factory returns in ch29,
414/// and this derive is where its registration will land. The impl is
415/// hand-writable; the derive is convenience.
416#[proc_macro_derive(Component, attributes(component, port))]
417pub fn derive_component(input: TokenStream) -> TokenStream {
418    let (name, impl_generics, type_params, fields) = match parse_struct(input) {
419        Ok(v) => v,
420        Err(e) => return compile_error(&e),
421    };
422
423    let mut visits = String::new();
424    let mut resolves = String::new();
425    let mut takes = String::new();
426    let mut restores = String::new();
427    for f in fields.iter().filter(|f| f.is_child) {
428        let fname = &f.name;
429        let ty = f.ty.trim_start();
430        if ty.starts_with("RustdvComp") {
431            // A factory slot (D75): reach through to the held component if
432            // present, and let it resolve its override during the walk.
433            visits.push_str(&format!(
434                "if let ::core::option::Option::Some(__c) = self.{fname}.as_node_mut() {{ __out.push((::std::string::String::from(\"{fname}\"), __c)); }}\n"
435            ));
436            resolves.push_str(&format!("self.{fname}.resolve(__ctx, \"{fname}\");\n"));
437            // D82b: move the box out for the run phase, and put it back after.
438            // Taken and restored in field order, so slots land where they came
439            // from.
440            takes.push_str(&format!(
441                "if let ::core::option::Option::Some(__c) = self.{fname}.take_node() {{ __out.push((::std::string::String::from(\"{fname}\"), __c)); }}\n"
442            ));
443            restores.push_str(&format!(
444                "if __name == \"{fname}\" {{ self.{fname}.put_node(__node); continue; }}\n"
445            ));
446        } else if ty.starts_with("Option") {
447            // "declared but not yet built": a child created during `build`
448            // (D6) appears here only once it is `Some`.
449            visits.push_str(&format!(
450                "if let ::core::option::Option::Some(__c) = &mut self.{fname} {{ __out.push((::std::string::String::from(\"{fname}\"), __c as &mut (dyn ::rustdv::ComponentNode + 'static))); }}\n"
451            ));
452        } else if ty.starts_with("Vec") {
453            visits.push_str(&format!(
454                "for (__i, __c) in self.{fname}.iter_mut().enumerate() {{ __out.push((::std::format!(\"{fname}[{{}}]\", __i), __c as &mut (dyn ::rustdv::ComponentNode + 'static))); }}\n"
455            ));
456        } else {
457            visits.push_str(&format!(
458                "__out.push((::std::string::String::from(\"{fname}\"), &mut self.{fname} as &mut (dyn ::rustdv::ComponentNode + 'static)));\n"
459            ));
460        }
461    }
462
463    // ----- ports (D83) -------------------------------------------------
464    //
465    // Each `#[port(kind)]` field contributes three things: a match arm so the
466    // port can be reached by name through `dyn ComponentNode` (the cast Rust
467    // does not have), a line in the elaboration report, and a typed constant
468    // so the parent names the port without spelling a string.
469    let mut port_arms = String::new();
470    let mut port_items = String::new();
471    let mut port_consts = String::new();
472    for f in fields.iter() {
473        let Some(kind) = f.port.as_deref() else { continue };
474        if !matches!(kind, "put" | "get" | "peek" | "publish" | "subscribe" | "seq_item") {
475            return compile_error(&format!(
476                "#[port({kind})]: expected put, get, peek, peek, publish, subscribe or seq_item"
477            ));
478        }
479        // An analysis port may be left unconnected — a monitor nobody listens
480        // to is a legitimate testbench (D85). Every other port must be wired.
481        let required = !matches!(kind, "publish" | "subscribe");
482        let fname = &f.name;
483        let ty = f.ty.trim();
484        let konst = fname.to_uppercase();
485        port_arms.push_str(&format!(
486            "\"{fname}\" => ::core::option::Option::Some(::rustdv::PortField::slot_any(&self.{fname})),\n            "
487        ));
488        port_items.push_str(&format!(
489            "::rustdv::PortInfo {{ name: \"{fname}\", kind: \"{kind}\", required: {required}, connected: ::rustdv::PortField::bound(&self.{fname}) }},\n            "
490        ));
491        port_consts.push_str(&format!(
492            "    /// The `{fname}` port, for `connect`.\n    pub const {konst}: ::rustdv::PortName<<{ty} as ::rustdv::PortField>::Iface> = ::rustdv::PortName::new(\"{fname}\");\n"
493        ));
494    }
495
496    let port_impl = if port_arms.is_empty() {
497        String::new()
498    } else {
499        format!(
500            "    fn port_slot(&self, __name: &str) -> ::core::option::Option<::std::rc::Rc<dyn ::std::any::Any>> {{\n        \
501             match __name {{\n            {port_arms}_ => ::core::option::Option::None,\n        }}\n    }}\n\
502             \n    fn port_infos(&self) -> ::std::vec::Vec<::rustdv::PortInfo> {{\n        \
503             ::std::vec![\n            {port_items}]\n    }}\n"
504        )
505    };
506
507    // Every component is a `PortOwner`, ports or not: that uniformity is what
508    // lets `connect(self, ..)` and `connect(&self.child, ..)` be one call.
509    let owner_impl = format!(
510        r#"
511impl {impl_generics} ::rustdv::PortOwner for {name} {type_params} {{
512    fn owner_port_slot(&self, __name: &str) -> ::core::option::Option<::std::rc::Rc<dyn ::std::any::Any>> {{
513        ::rustdv::ComponentNode::port_slot(self, __name)
514    }}
515    fn owner_label(&self) -> &'static str {{ "{name}" }}
516}}
517"#
518    );
519
520    let const_impl = if port_consts.is_empty() {
521        String::new()
522    } else {
523        format!("\nimpl {impl_generics} {name} {type_params} {{\n{port_consts}}}\n")
524    };
525
526    // A resolver only if there is at least one factory slot.
527    let resolve_impl = if resolves.is_empty() {
528        String::new()
529    } else {
530        format!(
531            "    fn resolve_children(&mut self, __ctx: &::rustdv::RustdvCtx) {{\n        {resolves}    }}\n"
532        )
533    };
534
535    // take/restore only if there is at least one `RustdvComp` slot; otherwise
536    // the trait defaults (empty) keep the old in-place behaviour.
537    let take_impl = if takes.is_empty() {
538        String::new()
539    } else {
540        format!(
541            "    fn take_children(&mut self) -> ::std::vec::Vec<(::std::string::String, ::std::boxed::Box<dyn ::rustdv::ComponentNode>)> {{\n        \
542             let mut __out: ::std::vec::Vec<(::std::string::String, ::std::boxed::Box<dyn ::rustdv::ComponentNode>)> = ::std::vec::Vec::new();\n        \
543             {takes}        __out\n    }}\n\
544             \n    fn restore_children(&mut self, __taken: ::std::vec::Vec<(::std::string::String, ::std::boxed::Box<dyn ::rustdv::ComponentNode>)>) {{\n        \
545             for (__name, __node) in __taken {{\n            \
546             let __name: &str = &__name;\n            {restores}        }}\n    }}\n"
547        )
548    };
549
550    // Universal registration (D73): enrol non-generic components by name so
551    // the factory can build them by string. Generic components are skipped —
552    // a `static` cannot be generic, and their monomorphs are not by-name
553    // targets.
554    let registration = if type_params.is_empty() {
555        format!(
556            r#"
557const _: () = {{
558    fn __rustdv_comp_name() -> &'static str {{ "{name}" }}
559    fn __rustdv_comp_make() -> ::std::boxed::Box<dyn ::rustdv::ComponentNode> {{
560        ::std::boxed::Box::new(<{name} as ::core::default::Default>::default())
561    }}
562    #[used]
563    #[cfg_attr(not(target_vendor = "apple"), link_section = "rustdv_comps")]
564    #[cfg_attr(target_vendor = "apple", link_section = "__DATA,rustdv_comps")]
565    static __RUSTDV_COMP_REG: &::rustdv::ComponentReg = &::rustdv::ComponentReg {{
566        name: __rustdv_comp_name,
567        make: __rustdv_comp_make,
568    }};
569}};
570"#
571        )
572    } else {
573        String::new()
574    };
575
576    let out = format!(
577        r#"
578impl {impl_generics} ::rustdv::ComponentNode for {name} {type_params} {{
579    fn node_name(&self) -> &'static str {{ "{name}" }}
580    fn children_mut(&mut self) -> ::std::vec::Vec<(::std::string::String, &mut (dyn ::rustdv::ComponentNode + 'static))> {{
581        let mut __out: ::std::vec::Vec<(::std::string::String, &mut (dyn ::rustdv::ComponentNode + 'static))> = ::std::vec::Vec::new();
582        {visits}
583        __out
584    }}
585{port_impl}{resolve_impl}{take_impl}}}
586{owner_impl}{const_impl}{registration}"#
587    );
588    out.parse().expect("rustdv-macros: generated ComponentNode impl failed to parse")
589}