Skip to main content

fidius_wit/
generate.rs

1// Copyright 2026 Colliery, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Source-parsing WIT generator (FIDIUS-I-0023).
16//!
17//! Parses a plugin crate's Rust source, finds the `#[plugin_interface]` trait and
18//! every `#[derive(WitType)]` `struct`/`enum`, and produces (a) a complete `.wit`
19//! document (records/variants + funcs) and (b) the Rust source for
20//! generated↔author `From` conversions the wasm adapter includes. A proc-macro
21//! can't see external type definitions, so this runs from a `build.rs` helper and
22//! the `fidius wit` CLI, which read the source files.
23
24use std::collections::BTreeSet;
25
26use syn::{FnArg, Item, Pat, ReturnType, TraitItem, Type};
27
28use crate::{
29    enum_to_wit, return_to_wit_with, struct_to_record, to_kebab_case, wit_type_with, WitMethod,
30};
31
32/// The product of generating from a plugin crate's source.
33pub struct Generated {
34    /// The interface's Rust trait name (e.g. `Greeter`).
35    pub interface_name: String,
36    /// kebab-case interface name (the WIT package + interface name).
37    pub iface_kebab: String,
38    /// Rust idents of the `#[derive(WitType)]` user types found.
39    pub user_types: Vec<String>,
40    /// The complete `.wit` document.
41    pub wit: String,
42    /// Rust source for the generated↔author `From` conversions (to be `include!`d
43    /// inside the wasm adapter module). Empty when there are no user types.
44    pub conversions: String,
45}
46
47/// Generate WIT + conversions from a crate's source string (`lib.rs`). Inline
48/// modules (`mod m { .. }`) are walked; external `mod m;` files cannot be read
49/// from a bare string — use [`generate_from_path`] (the `build.rs` helper does).
50pub fn generate(src: &str) -> Result<Generated, String> {
51    let file = syn::parse_file(src).map_err(|e| format!("parse error: {e}"))?;
52    let mut acc = Collected::default();
53    collect(&file.items, &[], None, &mut acc)?;
54    assemble(acc)
55}
56
57/// Like [`generate`], but reads `lib_rs` and follows external `mod m;` files
58/// (resolving `m.rs` / `m/mod.rs`), so `#[derive(WitType)]` types and the
59/// `#[plugin_interface]` trait may live in submodules.
60pub fn generate_from_path(lib_rs: &std::path::Path) -> Result<Generated, String> {
61    let src = std::fs::read_to_string(lib_rs)
62        .map_err(|e| format!("reading {}: {e}", lib_rs.display()))?;
63    let file = syn::parse_file(&src).map_err(|e| format!("parse {}: {e}", lib_rs.display()))?;
64    let dir = lib_rs.parent().unwrap_or_else(|| std::path::Path::new("."));
65    let mut acc = Collected::default();
66    collect(&file.items, &[], Some(dir), &mut acc)?;
67    assemble(acc)
68}
69
70/// `#[derive(WitType)]` types (tagged with their Rust module path) + the
71/// `#[plugin_interface]` trait, gathered across the module tree.
72#[derive(Default)]
73struct Collected {
74    structs: Vec<(Vec<String>, syn::ItemStruct)>,
75    enums: Vec<(Vec<String>, syn::ItemEnum)>,
76    the_trait: Option<syn::ItemTrait>,
77}
78
79/// Recursively gather items, descending into inline `mod m { .. }` and (when
80/// `dir` is `Some`) external `mod m;` files (`m.rs` / `m/mod.rs`).
81fn collect(
82    items: &[Item],
83    mod_path: &[String],
84    dir: Option<&std::path::Path>,
85    acc: &mut Collected,
86) -> Result<(), String> {
87    for item in items {
88        match item {
89            Item::Struct(s) if has_derive(&s.attrs, "WitType") => {
90                acc.structs.push((mod_path.to_vec(), s.clone()));
91            }
92            Item::Enum(e) if has_derive(&e.attrs, "WitType") => {
93                acc.enums.push((mod_path.to_vec(), e.clone()));
94            }
95            Item::Trait(t) if has_attr(&t.attrs, "plugin_interface") => {
96                if acc.the_trait.is_some() {
97                    return Err("multiple #[plugin_interface] traits found".into());
98                }
99                acc.the_trait = Some(t.clone());
100            }
101            Item::Mod(m) => {
102                let mut child = mod_path.to_vec();
103                child.push(m.ident.to_string());
104                if let Some((_, items)) = &m.content {
105                    let sub = dir.map(|d| d.join(m.ident.to_string()));
106                    collect(items, &child, sub.as_deref(), acc)?;
107                } else if let Some(d) = dir {
108                    let name = m.ident.to_string();
109                    let candidates = [d.join(format!("{name}.rs")), d.join(&name).join("mod.rs")];
110                    let file = candidates.iter().find(|p| p.exists()).ok_or_else(|| {
111                        format!(
112                            "cannot find module file for `mod {name};` near {}",
113                            d.display()
114                        )
115                    })?;
116                    let src = std::fs::read_to_string(file)
117                        .map_err(|e| format!("reading {}: {e}", file.display()))?;
118                    let parsed = syn::parse_file(&src)
119                        .map_err(|e| format!("parse {}: {e}", file.display()))?;
120                    collect(&parsed.items, &child, Some(&d.join(&name)), acc)?;
121                }
122            }
123            _ => {}
124        }
125    }
126    Ok(())
127}
128
129/// Build the `.wit` + conversions from the collected items.
130fn assemble(acc: Collected) -> Result<Generated, String> {
131    let the_trait = acc
132        .the_trait
133        .ok_or("no #[plugin_interface] trait found in source")?;
134    let interface_name = the_trait.ident.to_string();
135    let iface_kebab = to_kebab_case(&interface_name);
136
137    let mut user_types: Vec<String> = Vec::new();
138    user_types.extend(acc.structs.iter().map(|(_, s)| s.ident.to_string()));
139    user_types.extend(acc.enums.iter().map(|(_, e)| e.ident.to_string()));
140    let known: BTreeSet<String> = user_types.iter().cloned().collect();
141
142    // Type defs: records (struct records + synthetic struct-variant payload
143    // records) before variants, so forward references resolve cleanly.
144    let mut type_defs: Vec<String> = Vec::new();
145    let mut variant_defs: Vec<String> = Vec::new();
146    for (_, s) in &acc.structs {
147        type_defs.push(struct_to_record(s, &known)?);
148    }
149    for (_, e) in &acc.enums {
150        let (synthetic, variant) = enum_to_wit(e, &known)?;
151        type_defs.extend(synthetic);
152        variant_defs.push(variant);
153    }
154    type_defs.extend(variant_defs);
155
156    let mut methods: Vec<WitMethod> = Vec::new();
157    for item in &the_trait.items {
158        let TraitItem::Fn(f) = item else { continue };
159        let mut params = Vec::new();
160        for arg in &f.sig.inputs {
161            let FnArg::Typed(pt) = arg else { continue }; // skip &self
162
163            // Client-/bidi-streaming: the `Stream<T>` input arg crosses as bincode via the
164            // `fidius:stream-pull` import, not a WIT param — exclude it (FIDIUS-T-0175). The
165            // return-side handling below renders a `Stream` *return* as a resource (bidi).
166            if crate::stream_item_type(&pt.ty).is_some() {
167                continue;
168            }
169            let name = match pt.pat.as_ref() {
170                Pat::Ident(id) => to_kebab_case(&id.ident.to_string()),
171                _ => "arg".to_string(),
172            };
173            let wt = wit_type_with(&pt.ty, &known)
174                .map_err(|e| format!("method `{}` arg `{name}`: {e}", f.sig.ident))?;
175            params.push((name, wt));
176        }
177        let ret_ty: Option<&Type> = match &f.sig.output {
178            ReturnType::Type(_, t) => Some(t.as_ref()),
179            ReturnType::Default => None,
180        };
181        // Server-streaming (`-> fidius::Stream<T>`): the func renders as a
182        // resource-returning export, not a value return (FIDIUS-I-0026).
183        let stream_item = ret_ty.and_then(crate::stream_item_type);
184        let (ret, stream_item) = match stream_item {
185            Some(item_ty) => {
186                let item_wit = wit_type_with(item_ty, &known)
187                    .map_err(|e| format!("method `{}` stream item: {e}", f.sig.ident))?;
188                (None, Some(item_wit))
189            }
190            None => {
191                let ret = return_to_wit_with(ret_ty, &known)
192                    .map_err(|e| format!("method `{}` return: {e}", f.sig.ident))?;
193                (ret, None)
194            }
195        };
196        methods.push(WitMethod {
197            name: to_kebab_case(&f.sig.ident.to_string()),
198            params,
199            ret,
200            stream_item,
201        });
202    }
203
204    let wit = crate::render_wit_full(&iface_kebab, &type_defs, &methods);
205    let conversions = render_conversions(&iface_kebab, &acc.structs, &acc.enums, &known);
206
207    Ok(Generated {
208        interface_name,
209        iface_kebab,
210        user_types,
211        wit,
212        conversions,
213    })
214}
215
216/// `crate::<mod::path>::<Name>` — the author-side path for a type at `mod_path`.
217fn author_path(mod_path: &[String], name: &str) -> String {
218    if mod_path.is_empty() {
219        format!("crate::{name}")
220    } else {
221        format!("crate::{}::{name}", mod_path.join("::"))
222    }
223}
224
225/// The keywords wit-bindgen's `to_rust_ident` escapes with a trailing `_` (its
226/// `wit-bindgen-rust` source, in turn the Rust reference keyword list). When a
227/// WIT field name collides with one of these, wit-bindgen names the generated
228/// struct field `<kw>_` (e.g. `type` → `type_`) — *not* the raw ident `r#type`
229/// the author wrote. The conversion code must address the generated field by
230/// that mangled name, or it fails to compile (FIDIUS-T-0177).
231const RUST_KEYWORDS: &[&str] = &[
232    "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for",
233    "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return",
234    "self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where",
235    "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final", "macro",
236    "override", "priv", "typeof", "unsized", "virtual", "yield", "try",
237];
238
239/// The author's field ident (`record`, or the raw `r#type`) paired with the name
240/// wit-bindgen gives the *generated* mirror field. They differ only when the
241/// field name is a Rust keyword: the author writes `r#type`, wit-bindgen emits
242/// `type_`. For every other name the two are identical (the author field is
243/// already snake_case, which is what wit-bindgen produces).
244fn field_idents(fl: &syn::Field) -> (String, String) {
245    let author = fl.ident.as_ref().unwrap().to_string();
246    let bare = author.strip_prefix("r#").unwrap_or(&author);
247    let generated = if RUST_KEYWORDS.contains(&bare) {
248        format!("{bare}_")
249    } else {
250        author.clone()
251    };
252    (author, generated)
253}
254
255/// Render `From` impls (both directions) between each user type and its
256/// wit-bindgen-generated mirror. Emitted into the adapter module, where the
257/// generated types live (flat) at `exports::fidius::<iface>::<iface>::<Type>`
258/// and the author types at `crate::<mod::path>::<Type>`.
259fn render_conversions(
260    iface_kebab: &str,
261    structs: &[(Vec<String>, syn::ItemStruct)],
262    enums: &[(Vec<String>, syn::ItemEnum)],
263    known: &BTreeSet<String>,
264) -> String {
265    if structs.is_empty() && enums.is_empty() {
266        return String::new();
267    }
268    let snake = iface_kebab.replace('-', "_");
269    let gen_path = format!("exports::fidius::{snake}::{snake}");
270    let mut out = String::new();
271    out.push_str("// Generated by fidius-wit: author <-> wit-bindgen conversions.\n");
272
273    for (path, s) in structs {
274        let name = s.ident.to_string();
275        let g = format!("{gen_path}::{name}");
276        let a = author_path(path, &name);
277        // (author-name, generated-name, type) per named field. The two names
278        // differ only for Rust-keyword fields (`r#type` vs `type_`).
279        let fields: Vec<(String, String, &Type)> = match &s.fields {
280            syn::Fields::Named(f) => f
281                .named
282                .iter()
283                .map(|fl| {
284                    let (au, gen) = field_idents(fl);
285                    (au, gen, &fl.ty)
286                })
287                .collect(),
288            _ => Vec::new(),
289        };
290        // generated -> author: build the author struct, reading the generated `v`.
291        let to_author: Vec<String> = fields
292            .iter()
293            .map(|(au, gen, ty)| format!("{au}: {}", conv_expr(&format!("v.{gen}"), ty, known)))
294            .collect();
295        out.push_str(&format!(
296            "impl ::core::convert::From<{g}> for {a} {{ fn from(v: {g}) -> Self {{ {a} {{ {} }} }} }}\n",
297            to_author.join(", ")
298        ));
299        // author -> generated: build the generated struct, reading the author `v`.
300        let to_gen: Vec<String> = fields
301            .iter()
302            .map(|(au, gen, ty)| format!("{gen}: {}", conv_expr(&format!("v.{au}"), ty, known)))
303            .collect();
304        out.push_str(&format!(
305            "impl ::core::convert::From<{a}> for {g} {{ fn from(v: {a}) -> Self {{ {g} {{ {} }} }} }}\n",
306            to_gen.join(", ")
307        ));
308    }
309
310    for (path, e) in enums {
311        let ename = e.ident.to_string();
312        let g = format!("{gen_path}::{ename}");
313        let a = author_path(path, &ename);
314
315        let mut g2a = Vec::new(); // generated -> author
316        let mut a2g = Vec::new(); // author -> generated
317        for v in &e.variants {
318            let case = v.ident.to_string();
319            match &v.fields {
320                syn::Fields::Unit => {
321                    g2a.push(format!("{g}::{case} => {a}::{case}"));
322                    a2g.push(format!("{a}::{case} => {g}::{case}"));
323                }
324                syn::Fields::Unnamed(u) if u.unnamed.len() == 1 => {
325                    let ty = &u.unnamed[0].ty;
326                    g2a.push(format!(
327                        "{g}::{case}(x) => {a}::{case}({})",
328                        conv_expr("x", ty, known)
329                    ));
330                    a2g.push(format!(
331                        "{a}::{case}(x) => {g}::{case}({})",
332                        conv_expr("x", ty, known)
333                    ));
334                }
335                syn::Fields::Named(f) => {
336                    // The gen side wraps the case payload in a synthesized record
337                    // (`<Enum><Case>`, e.g. `ShapeRect`); the author side is a
338                    // struct variant with inline fields.
339                    let gen_rec = format!("{gen_path}::{ename}{case}");
340                    // (author-name, generated-name, type) per payload field.
341                    let fnames: Vec<(String, String, &Type)> = f
342                        .named
343                        .iter()
344                        .map(|fl| {
345                            let (au, gen) = field_idents(fl);
346                            (au, gen, &fl.ty)
347                        })
348                        .collect();
349                    // generated -> author: read the generated record `__r`, build
350                    // the author struct variant.
351                    let g2a_inits = fnames
352                        .iter()
353                        .map(|(au, gen, ty)| {
354                            format!("{au}: {}", conv_expr(&format!("__r.{gen}"), ty, known))
355                        })
356                        .collect::<Vec<_>>()
357                        .join(", ");
358                    g2a.push(format!("{g}::{case}(__r) => {a}::{case} {{ {g2a_inits} }}"));
359
360                    // author -> generated: destructure the author variant (author
361                    // field names bind the locals), build the generated record.
362                    let binds = fnames
363                        .iter()
364                        .map(|(au, ..)| au.clone())
365                        .collect::<Vec<_>>()
366                        .join(", ");
367                    let a2g_inits = fnames
368                        .iter()
369                        .map(|(au, gen, ty)| format!("{gen}: {}", conv_expr(au, ty, known)))
370                        .collect::<Vec<_>>()
371                        .join(", ");
372                    a2g.push(format!(
373                        "{a}::{case} {{ {binds} }} => {g}::{case}({gen_rec} {{ {a2g_inits} }})"
374                    ));
375                }
376                // Multi-field tuple cases are rejected by `enum_to_wit`.
377                syn::Fields::Unnamed(_) => unreachable!("rejected by enum_to_wit"),
378            }
379        }
380        out.push_str(&format!(
381            "impl ::core::convert::From<{g}> for {a} {{ fn from(v: {g}) -> Self {{ match v {{ {} }} }} }}\n",
382            g2a.join(", ")
383        ));
384        out.push_str(&format!(
385            "impl ::core::convert::From<{a}> for {g} {{ fn from(v: {a}) -> Self {{ match v {{ {} }} }} }}\n",
386            a2g.join(", ")
387        ));
388    }
389    out
390}
391
392/// Conversion expression for a field/payload `access` of type `ty`. Identity
393/// (move) when the type holds no user type (generated and author types are then
394/// identical); otherwise `.into()` (user type), or a `map`/`into_iter().map()`
395/// recursing through `Option`/`Vec`. Symmetric — works generated→author and
396/// author→generated, since `From` is generated both ways. Public so the macro's
397/// adapter reuses the exact same boundary conversions.
398pub fn conv_expr(access: &str, ty: &Type, known: &BTreeSet<String>) -> String {
399    // Maps cross as `list<tuple<k, v>>` — a `Vec<(K, V)>` binding. Convert with
400    // `collect()` (bidirectional via type inference: `Vec`↔`HashMap`/`BTreeMap`).
401    // Handle before the user-type short-circuit so a `HashMap<String, u32>` (no
402    // `#[derive(WitType)]` inside) still converts to/from its binding.
403    if let Type::Path(p) = ty {
404        if let Some(seg) = p.path.segments.last() {
405            if matches!(seg.ident.to_string().as_str(), "HashMap" | "BTreeMap") {
406                if let Some((k, v)) = two_generics(seg) {
407                    let kc = conv_expr("k", k, known);
408                    let vc = conv_expr("v", v, known);
409                    if kc == "k" && vc == "v" {
410                        return format!("{access}.into_iter().collect()");
411                    }
412                    return format!("{access}.into_iter().map(|(k, v)| ({kc}, {vc})).collect()");
413                }
414            }
415        }
416    }
417    if !contains_user_type(ty, known) {
418        return access.to_string();
419    }
420    if let Type::Path(p) = ty {
421        if let Some(seg) = p.path.segments.last() {
422            let ident = seg.ident.to_string();
423            if let Some(inner) = single_generic(seg) {
424                match ident.as_str() {
425                    "Vec" => {
426                        return format!(
427                            "{access}.into_iter().map(|w| {}).collect()",
428                            conv_expr("w", inner, known)
429                        );
430                    }
431                    "Option" => {
432                        return format!("{access}.map(|w| {})", conv_expr("w", inner, known));
433                    }
434                    _ => {}
435                }
436            }
437            if known.contains(&ident) {
438                return format!("{access}.into()");
439            }
440        }
441    }
442    access.to_string()
443}
444
445/// Whether `ty` is, or contains (through `Vec`/`Option`/`Box`), a user type in
446/// `known`. Public so the macro can classify args/returns.
447pub fn contains_user_type(ty: &Type, known: &BTreeSet<String>) -> bool {
448    if let Type::Path(p) = ty {
449        if let Some(seg) = p.path.segments.last() {
450            let ident = seg.ident.to_string();
451            if known.contains(&ident) {
452                return true;
453            }
454            if matches!(ident.as_str(), "Vec" | "Option" | "Box") {
455                if let Some(inner) = single_generic(seg) {
456                    return contains_user_type(inner, known);
457                }
458            }
459            if matches!(ident.as_str(), "HashMap" | "BTreeMap") {
460                if let Some((k, v)) = two_generics(seg) {
461                    return contains_user_type(k, known) || contains_user_type(v, known);
462                }
463            }
464        }
465    }
466    false
467}
468
469fn single_generic(seg: &syn::PathSegment) -> Option<&Type> {
470    if let syn::PathArguments::AngleBracketed(ab) = &seg.arguments {
471        for a in &ab.args {
472            if let syn::GenericArgument::Type(t) = a {
473                return Some(t);
474            }
475        }
476    }
477    None
478}
479
480fn two_generics(seg: &syn::PathSegment) -> Option<(&Type, &Type)> {
481    if let syn::PathArguments::AngleBracketed(ab) = &seg.arguments {
482        let types: Vec<&Type> = ab
483            .args
484            .iter()
485            .filter_map(|a| match a {
486                syn::GenericArgument::Type(t) => Some(t),
487                _ => None,
488            })
489            .collect();
490        if types.len() >= 2 {
491            return Some((types[0], types[1]));
492        }
493    }
494    None
495}
496
497/// Does `attrs` contain `#[<name>(...)]` / `#[<path>::<name>]` (last segment match)?
498fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool {
499    attrs.iter().any(|a| {
500        a.path()
501            .segments
502            .last()
503            .map(|s| s.ident == name)
504            .unwrap_or(false)
505    })
506}
507
508/// Does `attrs` contain a `#[derive(... <name> ...)]`?
509fn has_derive(attrs: &[syn::Attribute], name: &str) -> bool {
510    for a in attrs {
511        if !a.path().is_ident("derive") {
512            continue;
513        }
514        let mut found = false;
515        let _ = a.parse_nested_meta(|m| {
516            if m.path
517                .segments
518                .last()
519                .map(|s| s.ident == name)
520                .unwrap_or(false)
521            {
522                found = true;
523            }
524            Ok(())
525        });
526        if found {
527            return true;
528        }
529    }
530    false
531}
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536
537    const SRC: &str = r#"
538        #[derive(WitType)]
539        pub struct Point { pub x: i32, pub y: i32 }
540
541        #[derive(WitType)]
542        pub enum Shape { Circle(u32), Rect(Point), Dot }
543
544        #[plugin_interface(version = 1, crate = "fidius_guest")]
545        pub trait Geo: Send + Sync {
546            fn midpoint(&self, a: Point, b: Point) -> Point;
547            fn classify(&self, pts: Vec<Point>) -> Shape;
548            fn name(&self, s: Shape) -> String;
549        }
550    "#;
551
552    #[test]
553    fn generates_wit_with_records_variants_and_funcs() {
554        let g = generate(SRC).unwrap();
555        assert_eq!(g.interface_name, "Geo");
556        assert_eq!(g.iface_kebab, "geo");
557        assert!(g.user_types.contains(&"Point".to_string()));
558        assert!(g.user_types.contains(&"Shape".to_string()));
559
560        assert!(g.wit.contains("record point {"));
561        assert!(g.wit.contains("variant shape {"));
562        assert!(g.wit.contains("rect(point),"));
563        assert!(g
564            .wit
565            .contains("midpoint: func(a: point, b: point) -> point;"));
566        assert!(g.wit.contains("classify: func(pts: list<point>) -> shape;"));
567        assert!(g.wit.contains("name: func(s: shape) -> string;"));
568        assert!(g.wit.contains("fidius-interface-hash: func() -> u64;"));
569    }
570
571    #[test]
572    fn generates_conversions_both_ways() {
573        let g = generate(SRC).unwrap();
574        let c = &g.conversions;
575        // struct record conversions
576        assert!(c.contains("From<exports::fidius::geo::geo::Point> for crate::Point"));
577        assert!(c.contains("From<crate::Point> for exports::fidius::geo::geo::Point"));
578        // enum variant conversions with nested .into() for Rect(Point)
579        assert!(c.contains("From<exports::fidius::geo::geo::Shape> for crate::Shape"));
580        assert!(
581            c.contains("Rect(x) => crate :: Shape :: Rect")
582                || c.contains("Rect(x) => crate::Shape::Rect")
583        );
584        assert!(c.contains(".into()"));
585    }
586
587    #[test]
588    fn primitive_only_interface_has_no_conversions() {
589        let src = r#"
590            #[plugin_interface(version = 1)]
591            pub trait Greeter { fn greet(&self, name: String) -> String; }
592        "#;
593        let g = generate(src).unwrap();
594        assert!(g.user_types.is_empty());
595        assert!(g.conversions.is_empty());
596        assert!(g.wit.contains("greet: func(name: string) -> string;"));
597    }
598
599    #[test]
600    fn unsupported_type_errors() {
601        // `Box<T>` has no WIT projection (maps/tuples are now supported, so use a
602        // type that genuinely isn't).
603        let src = r#"
604            #[plugin_interface(version = 1)]
605            pub trait T { fn f(&self, x: Box<String>) -> u32; }
606        "#;
607        assert!(generate(src).is_err());
608    }
609
610    /// FIDIUS-T-0177: a guest interface whose types, fields, cases, methods, and
611    /// params all use WIT reserved keywords. Before escaping, the generated WIT
612    /// is rejected by the parser (the `emit_wit` build failure); after escaping,
613    /// it both contains the `%`-escaped identifiers and round-trips through the
614    /// same parser wit-bindgen uses — without the wasm toolchain.
615    const KEYWORD_SRC: &str = r#"
616        #[derive(WitType)]
617        pub struct DeadLetter { pub record: String, pub stream: u64, pub from: bool, pub r#type: u8 }
618
619        #[derive(WitType)]
620        pub enum Variant { Stream, Record(u32), List { from: u8 } }
621
622        #[plugin_interface(version = 1, crate = "fidius_guest")]
623        pub trait Stream: Send + Sync {
624            fn record(&self, list: DeadLetter, option: Variant) -> DeadLetter;
625        }
626    "#;
627
628    #[test]
629    fn keyword_heavy_interface_escapes_every_identifier() {
630        let g = generate(KEYWORD_SRC).unwrap();
631        // interface name (trait `Stream`) is a keyword.
632        assert!(g.wit.contains("interface %stream {"), "{}", g.wit);
633        assert!(g.wit.contains("package fidius:%stream@0.1.0;"));
634        assert!(g.wit.contains("export %stream;"));
635        // record + keyword fields (`record`, `stream`, `from`, `r#type`).
636        assert!(g.wit.contains("record dead-letter {"));
637        assert!(g.wit.contains("%record: string,"));
638        assert!(g.wit.contains("%stream: u64,"));
639        assert!(g.wit.contains("%from: bool,"));
640        assert!(g.wit.contains("%type: u8,"));
641        // variant (type named `Variant`) + keyword cases; the synthetic struct-
642        // variant payload record (`variant-list`) is compound so it is not escaped.
643        assert!(g.wit.contains("variant %variant {"));
644        assert!(g.wit.contains("%stream,"));
645        assert!(g.wit.contains("%record(u32),"));
646        assert!(g.wit.contains("%list(variant-list),"));
647        assert!(g.wit.contains("record variant-list {"));
648        // method name + param names that are keywords, with a keyword type ref.
649        assert!(g
650            .wit
651            .contains("%record: func(%list: dead-letter, %option: %variant) -> dead-letter;"));
652    }
653
654    #[test]
655    fn keyword_field_conversions_use_wit_bindgen_mangling() {
656        // A Rust-keyword field (`r#type`) is named `type_` on the wit-bindgen
657        // side but `r#type` on the author side; the `From` impls must bridge the
658        // two (regression for the missing-`type` keyword bug).
659        let c = &generate(KEYWORD_SRC).unwrap().conversions;
660        // generated -> author: author field `r#type` reads generated `v.type_`.
661        assert!(c.contains("r#type: v.type_"), "{c}");
662        // author -> generated: generated field `type_` reads author `v.r#type`.
663        assert!(c.contains("type_: v.r#type"), "{c}");
664        // non-keyword fields stay identical on both sides.
665        assert!(c.contains("record: v.record") && c.contains("stream: v.stream"));
666    }
667
668    #[test]
669    fn keyword_heavy_interface_parses() {
670        let g = generate(KEYWORD_SRC).unwrap();
671        // Syntactic parse only (no dep resolution): exactly what fails today when
672        // the keywords are emitted bare.
673        wit_parser::UnresolvedPackageGroup::parse("keyword.wit", &g.wit)
674            .unwrap_or_else(|e| panic!("generated WIT must parse: {e}\n---\n{}", g.wit));
675    }
676}