Skip to main content

forge_ir_bindgen/
convert.rs

1//! Conversions between bindgen-generated WIT types and [`forge_ir`].
2//!
3//! Both `ir-transformer` and `code-generator` worlds share the `types`,
4//! `host-api`, and `stage` interfaces, but `wasmtime::component::bindgen!`
5//! emits *separate* Rust modules per world (the types are structurally
6//! identical, nominally distinct). To avoid duplicating ~600 lines of
7//! conversion glue, we expand the conversions once per world via a
8//! `macro_rules!` macro keyed on the bindings root module.
9//!
10//! Public API:
11//!
12//! * `convert::transformer::*` — operates on
13//!   `crate::bindings::transformer::forge::plugin::types::*`.
14//! * `convert::generator::*` — operates on
15//!   `crate::bindings::generator::forge::plugin::types::*`.
16//!
17//! Both submodules expose:
18//!
19//! * `ir_to_wit(forge_ir::Ir) -> b::types::Ir`
20//! * `ir_from_wit(b::types::Ir) -> Result<forge_ir::Ir, BindgenError>`
21//! * `diagnostic_from_wit(b::types::Diagnostic) -> Result<forge_ir::Diagnostic, BindgenError>`
22//! * `plugin_info_from_wit(b::types::PluginInfo) -> forge_ir::PluginInfo`
23//! * `stage_error_from_wit(b::stage::StageError) -> StageErrorRepr`
24//!
25//! `StageErrorRepr` is a world-independent representation of the WIT
26//! `stage-error` variant; the host translates it to its own `StageError`
27//! type to avoid a circular dependency.
28
29use forge_ir as ir;
30
31/// Host-neutral representation of the WIT `stage-error` variant. Shared
32/// between both worlds; the host wraps it into its own `StageError`.
33#[derive(Debug, Clone, PartialEq)]
34pub enum StageErrorRepr {
35    Rejected {
36        reason: String,
37        diagnostics: Vec<ir::Diagnostic>,
38    },
39    PluginBug(String),
40    ConfigInvalid(String),
41    ResourceExceeded(ResourceKindRepr),
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum ResourceKindRepr {
46    Fuel,
47    Memory,
48    Time,
49    OutputSize,
50}
51
52macro_rules! define_world_conversions {
53    ($mod:ident, $world:ident) => {
54        pub mod $mod {
55            //! Conversions between `forge_ir` and the
56            #![doc = concat!(" `", stringify!($world), "` world's WIT types.")]
57
58            use super::{ResourceKindRepr, StageErrorRepr};
59            use crate::bindings::$world::forge::plugin::{stage as b_stage, types as b};
60            use crate::BindgenError;
61            use forge_ir as ir;
62
63            // -------------------------------------------------------------
64            // PluginInfo
65            // -------------------------------------------------------------
66
67            pub fn plugin_info_from_wit(p: b::PluginInfo) -> ir::PluginInfo {
68                ir::PluginInfo {
69                    name: p.name,
70                    version: p.version,
71                }
72            }
73
74            pub fn plugin_info_to_wit(p: ir::PluginInfo) -> b::PluginInfo {
75                b::PluginInfo {
76                    name: p.name,
77                    version: p.version,
78                }
79            }
80
81            // -------------------------------------------------------------
82            // SpecLocation
83            // -------------------------------------------------------------
84
85            fn loc_from(l: b::SpecLocation) -> ir::SpecLocation {
86                ir::SpecLocation {
87                    pointer: l.pointer,
88                    file: l.file,
89                }
90            }
91
92            fn loc_to(l: ir::SpecLocation) -> b::SpecLocation {
93                b::SpecLocation {
94                    pointer: l.pointer,
95                    file: l.file,
96                }
97            }
98
99            // -------------------------------------------------------------
100            // Value
101            // -------------------------------------------------------------
102
103            fn value_from(v: b::Value) -> ir::Value {
104                match v {
105                    b::Value::Null => ir::Value::Null,
106                    b::Value::Bool(value) => ir::Value::Bool { value },
107                    b::Value::Int(value) => ir::Value::Int { value },
108                    b::Value::Float(value) => ir::Value::Float { value },
109                    b::Value::String(value) => ir::Value::String { value },
110                    b::Value::List(items) => ir::Value::List { items },
111                    b::Value::Object(fields) => ir::Value::Object {
112                        fields: fields.into_iter().collect(),
113                    },
114                }
115            }
116
117            fn value_to(v: ir::Value) -> b::Value {
118                match v {
119                    ir::Value::Null => b::Value::Null,
120                    ir::Value::Bool { value } => b::Value::Bool(value),
121                    ir::Value::Int { value } => b::Value::Int(value),
122                    ir::Value::Float { value } => b::Value::Float(value),
123                    ir::Value::String { value } => b::Value::String(value),
124                    ir::Value::List { items } => b::Value::List(items),
125                    ir::Value::Object { fields } => b::Value::Object(fields),
126                }
127            }
128
129            // Extensions are `Vec<(String, ValueRef)>` — identity copy
130            // across the boundary since `ValueRef` is `u32` on both sides.
131            // Kept as functions so call sites stay readable.
132            fn extensions_from(xs: Vec<(String, ir::ValueRef)>) -> Vec<(String, ir::ValueRef)> {
133                xs
134            }
135
136            fn extensions_to(xs: Vec<(String, ir::ValueRef)>) -> Vec<(String, ir::ValueRef)> {
137                xs
138            }
139
140            // -------------------------------------------------------------
141            // Diagnostics
142            // -------------------------------------------------------------
143
144            pub fn diagnostic_from_wit(d: b::Diagnostic) -> Result<ir::Diagnostic, BindgenError> {
145                Ok(ir::Diagnostic {
146                    severity: severity_from(d.severity),
147                    code: d.code,
148                    message: d.message,
149                    location: d.location.map(loc_from),
150                    related: d.related.into_iter().map(related_from).collect(),
151                    suggested_fix: d.suggested_fix.map(fix_from),
152                })
153            }
154
155            pub fn diagnostic_to_wit(d: ir::Diagnostic) -> b::Diagnostic {
156                b::Diagnostic {
157                    severity: severity_to(d.severity),
158                    code: d.code,
159                    message: d.message,
160                    location: d.location.map(loc_to),
161                    related: d.related.into_iter().map(related_to).collect(),
162                    suggested_fix: d.suggested_fix.map(fix_to),
163                }
164            }
165
166            fn severity_from(s: b::Severity) -> ir::Severity {
167                match s {
168                    b::Severity::Error => ir::Severity::Error,
169                    b::Severity::Warning => ir::Severity::Warning,
170                    b::Severity::Info => ir::Severity::Info,
171                    b::Severity::Hint => ir::Severity::Hint,
172                }
173            }
174
175            fn severity_to(s: ir::Severity) -> b::Severity {
176                match s {
177                    ir::Severity::Error => b::Severity::Error,
178                    ir::Severity::Warning => b::Severity::Warning,
179                    ir::Severity::Info => b::Severity::Info,
180                    ir::Severity::Hint => b::Severity::Hint,
181                }
182            }
183
184            fn related_from(r: b::RelatedInfo) -> ir::RelatedInfo {
185                ir::RelatedInfo {
186                    message: r.message,
187                    location: r.location.map(loc_from),
188                }
189            }
190
191            fn related_to(r: ir::RelatedInfo) -> b::RelatedInfo {
192                b::RelatedInfo {
193                    message: r.message,
194                    location: r.location.map(loc_to),
195                }
196            }
197
198            fn fix_from(f: b::FixSuggestion) -> ir::FixSuggestion {
199                ir::FixSuggestion {
200                    message: f.message,
201                    edits: f
202                        .edits
203                        .into_iter()
204                        .map(|e| ir::FixEdit {
205                            location: loc_from(e.location),
206                            replacement: e.replacement,
207                        })
208                        .collect(),
209                }
210            }
211
212            fn fix_to(f: ir::FixSuggestion) -> b::FixSuggestion {
213                b::FixSuggestion {
214                    message: f.message,
215                    edits: f
216                        .edits
217                        .into_iter()
218                        .map(|e| b::FixEdit {
219                            location: loc_to(e.location),
220                            replacement: e.replacement,
221                        })
222                        .collect(),
223                }
224            }
225
226            // -------------------------------------------------------------
227            // StageError
228            // -------------------------------------------------------------
229
230            pub fn stage_error_from_wit(e: b_stage::StageError) -> StageErrorRepr {
231                match e {
232                    b_stage::StageError::Rejected(r) => StageErrorRepr::Rejected {
233                        reason: r.reason,
234                        // Diagnostics from a rejection are best-effort; if
235                        // any fail to convert, surface them as a plugin-bug
236                        // by collapsing to an empty list with the reason
237                        // preserved.
238                        diagnostics: r
239                            .diagnostics
240                            .into_iter()
241                            .filter_map(|d| diagnostic_from_wit(d).ok())
242                            .collect(),
243                    },
244                    b_stage::StageError::PluginBug(s) => StageErrorRepr::PluginBug(s),
245                    b_stage::StageError::ConfigInvalid(s) => StageErrorRepr::ConfigInvalid(s),
246                    b_stage::StageError::ResourceExceeded(k) => {
247                        StageErrorRepr::ResourceExceeded(match k {
248                            b_stage::ResourceKind::Fuel => ResourceKindRepr::Fuel,
249                            b_stage::ResourceKind::Memory => ResourceKindRepr::Memory,
250                            b_stage::ResourceKind::Time => ResourceKindRepr::Time,
251                            b_stage::ResourceKind::OutputSize => ResourceKindRepr::OutputSize,
252                        })
253                    }
254                }
255            }
256
257            // -------------------------------------------------------------
258            // Top-level Ir
259            // -------------------------------------------------------------
260
261            pub fn ir_to_wit(i: ir::Ir) -> b::Ir {
262                b::Ir {
263                    info: api_info_to(i.info),
264                    operations: i.operations.into_iter().map(operation_to).collect(),
265                    types: i.types.into_iter().map(named_type_to).collect(),
266                    security_schemes: i
267                        .security_schemes
268                        .into_iter()
269                        .map(security_scheme_to)
270                        .collect(),
271                    servers: i.servers.into_iter().map(server_to).collect(),
272                    webhooks: i.webhooks.into_iter().map(webhook_to).collect(),
273                    external_docs: i.external_docs.map(external_docs_to),
274                    tags: i.tags.into_iter().map(tag_to).collect(),
275                    json_schema_dialect: i.json_schema_dialect,
276                    self_url: i.self_url,
277                    values: i.values.into_iter().map(value_to).collect(),
278                }
279            }
280
281            pub fn ir_from_wit(i: b::Ir) -> Result<ir::Ir, BindgenError> {
282                let out = ir::Ir {
283                    info: api_info_from(i.info),
284                    operations: i
285                        .operations
286                        .into_iter()
287                        .map(operation_from)
288                        .collect::<Result<_, _>>()?,
289                    types: i
290                        .types
291                        .into_iter()
292                        .map(named_type_from)
293                        .collect::<Result<_, _>>()?,
294                    security_schemes: i
295                        .security_schemes
296                        .into_iter()
297                        .map(security_scheme_from)
298                        .collect(),
299                    servers: i.servers.into_iter().map(server_from).collect(),
300                    webhooks: i
301                        .webhooks
302                        .into_iter()
303                        .map(webhook_from)
304                        .collect::<Result<_, _>>()?,
305                    external_docs: i.external_docs.map(external_docs_from),
306                    tags: i.tags.into_iter().map(tag_from).collect(),
307                    json_schema_dialect: i.json_schema_dialect,
308                    self_url: i.self_url,
309                    values: i.values.into_iter().map(value_from).collect(),
310                };
311                Ok(out)
312            }
313
314            // -------------------------------------------------------------
315            // ApiInfo / Server
316            // -------------------------------------------------------------
317
318            fn api_info_from(a: b::ApiInfo) -> ir::ApiInfo {
319                ir::ApiInfo {
320                    title: a.title,
321                    version: a.version,
322                    summary: a.summary,
323                    description: a.description,
324                    terms_of_service: a.terms_of_service,
325                    contact: a.contact.map(contact_from),
326                    license_name: a.license_name,
327                    license_url: a.license_url,
328                    license_identifier: a.license_identifier,
329                    extensions: extensions_from(a.extensions),
330                }
331            }
332
333            fn api_info_to(a: ir::ApiInfo) -> b::ApiInfo {
334                b::ApiInfo {
335                    title: a.title,
336                    version: a.version,
337                    summary: a.summary,
338                    description: a.description,
339                    terms_of_service: a.terms_of_service,
340                    contact: a.contact.map(contact_to),
341                    license_name: a.license_name,
342                    license_url: a.license_url,
343                    license_identifier: a.license_identifier,
344                    extensions: extensions_to(a.extensions),
345                }
346            }
347
348            fn contact_from(c: b::Contact) -> ir::Contact {
349                ir::Contact {
350                    name: c.name,
351                    url: c.url,
352                    email: c.email,
353                }
354            }
355
356            fn contact_to(c: ir::Contact) -> b::Contact {
357                b::Contact {
358                    name: c.name,
359                    url: c.url,
360                    email: c.email,
361                }
362            }
363
364            fn server_from(s: b::Server) -> ir::Server {
365                ir::Server {
366                    url: s.url,
367                    description: s.description,
368                    name: s.name,
369                    variables: s
370                        .variables
371                        .into_iter()
372                        .map(|(k, v)| (k, server_var_from(v)))
373                        .collect(),
374                    extensions: extensions_from(s.extensions),
375                }
376            }
377
378            fn server_to(s: ir::Server) -> b::Server {
379                b::Server {
380                    url: s.url,
381                    description: s.description,
382                    name: s.name,
383                    variables: s
384                        .variables
385                        .into_iter()
386                        .map(|(k, v)| (k, server_var_to(v)))
387                        .collect(),
388                    extensions: extensions_to(s.extensions),
389                }
390            }
391
392            fn server_var_from(v: b::ServerVariable) -> ir::ServerVariable {
393                ir::ServerVariable {
394                    default: v.default,
395                    r#enum: v.enum_,
396                    description: v.description,
397                    extensions: extensions_from(v.extensions),
398                }
399            }
400
401            fn server_var_to(v: ir::ServerVariable) -> b::ServerVariable {
402                b::ServerVariable {
403                    default: v.default,
404                    enum_: v.r#enum,
405                    description: v.description,
406                    extensions: extensions_to(v.extensions),
407                }
408            }
409
410            // -------------------------------------------------------------
411            // Types
412            // -------------------------------------------------------------
413
414            fn named_type_from(n: b::NamedType) -> Result<ir::NamedType, BindgenError> {
415                Ok(ir::NamedType {
416                    id: n.id,
417                    original_name: n.original_name,
418                    title: n.title,
419                    description: n.description,
420                    deprecated: n.deprecated,
421                    read_only: n.read_only,
422                    write_only: n.write_only,
423                    external_docs: n.external_docs.map(external_docs_from),
424                    default: n.default,
425                    examples: examples_from(n.examples),
426                    xml: n.xml.map(xml_object_from),
427                    definition: type_def_from(n.definition)?,
428                    extensions: extensions_from(n.extensions),
429                    location: n.location.map(loc_from),
430                })
431            }
432
433            fn named_type_to(n: ir::NamedType) -> b::NamedType {
434                b::NamedType {
435                    id: n.id,
436                    original_name: n.original_name,
437                    title: n.title,
438                    description: n.description,
439                    deprecated: n.deprecated,
440                    read_only: n.read_only,
441                    write_only: n.write_only,
442                    external_docs: n.external_docs.map(external_docs_to),
443                    default: n.default,
444                    examples: examples_to(n.examples),
445                    xml: n.xml.map(xml_object_to),
446                    definition: type_def_to(n.definition),
447                    extensions: extensions_to(n.extensions),
448                    location: n.location.map(loc_to),
449                }
450            }
451
452            fn external_docs_from(d: b::ExternalDocs) -> ir::ExternalDocs {
453                ir::ExternalDocs {
454                    description: d.description,
455                    url: d.url,
456                }
457            }
458
459            fn external_docs_to(d: ir::ExternalDocs) -> b::ExternalDocs {
460                b::ExternalDocs {
461                    description: d.description,
462                    url: d.url,
463                }
464            }
465
466            fn example_from(e: b::Example) -> ir::Example {
467                ir::Example {
468                    summary: e.summary,
469                    description: e.description,
470                    value: e.value,
471                    external_value: e.external_value,
472                    data_value: e.data_value,
473                    serialized_value: e.serialized_value,
474                }
475            }
476
477            fn example_to(e: ir::Example) -> b::Example {
478                b::Example {
479                    summary: e.summary,
480                    description: e.description,
481                    value: e.value,
482                    external_value: e.external_value,
483                    data_value: e.data_value,
484                    serialized_value: e.serialized_value,
485                }
486            }
487
488            fn examples_from(xs: Vec<(String, b::Example)>) -> Vec<(String, ir::Example)> {
489                xs.into_iter().map(|(k, v)| (k, example_from(v))).collect()
490            }
491
492            fn examples_to(xs: Vec<(String, ir::Example)>) -> Vec<(String, b::Example)> {
493                xs.into_iter().map(|(k, v)| (k, example_to(v))).collect()
494            }
495
496            fn xml_object_from(x: b::XmlObject) -> ir::XmlObject {
497                ir::XmlObject {
498                    name: x.name,
499                    namespace: x.namespace,
500                    prefix: x.prefix,
501                    attribute: x.attribute,
502                    wrapped: x.wrapped,
503                    text: x.text,
504                    ordered: x.ordered,
505                    extensions: x.extensions,
506                }
507            }
508
509            fn xml_object_to(x: ir::XmlObject) -> b::XmlObject {
510                b::XmlObject {
511                    name: x.name,
512                    namespace: x.namespace,
513                    prefix: x.prefix,
514                    attribute: x.attribute,
515                    wrapped: x.wrapped,
516                    text: x.text,
517                    ordered: x.ordered,
518                    extensions: x.extensions,
519                }
520            }
521
522            fn link_from(l: b::Link) -> ir::Link {
523                ir::Link {
524                    operation_ref: l.operation_ref,
525                    operation_id: l.operation_id,
526                    parameters: l.parameters,
527                    request_body: l.request_body,
528                    description: l.description,
529                    server: l.server.map(server_from),
530                    extensions: l.extensions,
531                }
532            }
533
534            fn link_to(l: ir::Link) -> b::Link {
535                b::Link {
536                    operation_ref: l.operation_ref,
537                    operation_id: l.operation_id,
538                    parameters: l.parameters,
539                    request_body: l.request_body,
540                    description: l.description,
541                    server: l.server.map(server_to),
542                    extensions: l.extensions,
543                }
544            }
545
546            fn links_from(xs: Vec<(String, b::Link)>) -> Vec<(String, ir::Link)> {
547                xs.into_iter().map(|(k, v)| (k, link_from(v))).collect()
548            }
549
550            fn links_to(xs: Vec<(String, ir::Link)>) -> Vec<(String, b::Link)> {
551                xs.into_iter().map(|(k, v)| (k, link_to(v))).collect()
552            }
553
554            fn webhook_from(w: b::Webhook) -> Result<ir::Webhook, BindgenError> {
555                Ok(ir::Webhook {
556                    name: w.name,
557                    summary: w.summary,
558                    description: w.description,
559                    operations: w
560                        .operations
561                        .into_iter()
562                        .map(operation_from)
563                        .collect::<Result<_, _>>()?,
564                })
565            }
566
567            fn webhook_to(w: ir::Webhook) -> b::Webhook {
568                b::Webhook {
569                    name: w.name,
570                    summary: w.summary,
571                    description: w.description,
572                    operations: w.operations.into_iter().map(operation_to).collect(),
573                }
574            }
575
576            fn callback_from(c: b::Callback) -> ir::Callback {
577                ir::Callback {
578                    name: c.name,
579                    expression: c.expression,
580                    operation_ids: c.operation_ids,
581                    extensions: c.extensions,
582                }
583            }
584
585            fn callback_to(c: ir::Callback) -> b::Callback {
586                b::Callback {
587                    name: c.name,
588                    expression: c.expression,
589                    operation_ids: c.operation_ids,
590                    extensions: c.extensions,
591                }
592            }
593
594            fn tag_from(t: b::Tag) -> ir::Tag {
595                ir::Tag {
596                    name: t.name,
597                    summary: t.summary,
598                    description: t.description,
599                    external_docs: t.external_docs.map(external_docs_from),
600                    parent: t.parent,
601                    kind: t.kind,
602                    extensions: t.extensions,
603                }
604            }
605
606            fn tag_to(t: ir::Tag) -> b::Tag {
607                b::Tag {
608                    name: t.name,
609                    summary: t.summary,
610                    description: t.description,
611                    external_docs: t.external_docs.map(external_docs_to),
612                    parent: t.parent,
613                    kind: t.kind,
614                    extensions: t.extensions,
615                }
616            }
617
618            fn type_def_from(d: b::TypeDef) -> Result<ir::TypeDef, BindgenError> {
619                Ok(match d {
620                    b::TypeDef::Primitive(p) => ir::TypeDef::Primitive(prim_from(p)),
621                    b::TypeDef::Object(o) => ir::TypeDef::Object(object_from(o)),
622                    b::TypeDef::Array(a) => ir::TypeDef::Array(array_from(a)),
623                    b::TypeDef::EnumString(e) => ir::TypeDef::EnumString(enum_str_from(e)),
624                    b::TypeDef::EnumInt(e) => ir::TypeDef::EnumInt(enum_int_from(e)),
625                    b::TypeDef::Union(u) => ir::TypeDef::Union(union_from(u)),
626                    b::TypeDef::Null => ir::TypeDef::Null,
627                    b::TypeDef::Any => ir::TypeDef::Any,
628                })
629            }
630
631            fn type_def_to(d: ir::TypeDef) -> b::TypeDef {
632                match d {
633                    ir::TypeDef::Primitive(p) => b::TypeDef::Primitive(prim_to(p)),
634                    ir::TypeDef::Object(o) => b::TypeDef::Object(object_to(o)),
635                    ir::TypeDef::Array(a) => b::TypeDef::Array(array_to(a)),
636                    ir::TypeDef::EnumString(e) => b::TypeDef::EnumString(enum_str_to(e)),
637                    ir::TypeDef::EnumInt(e) => b::TypeDef::EnumInt(enum_int_to(e)),
638                    ir::TypeDef::Union(u) => b::TypeDef::Union(union_to(u)),
639                    ir::TypeDef::Null => b::TypeDef::Null,
640                    ir::TypeDef::Any => b::TypeDef::Any,
641                }
642            }
643
644            // ---- primitive ----
645
646            fn prim_kind_from(k: b::PrimitiveKind) -> ir::PrimitiveKind {
647                use b::PrimitiveKind as W;
648                use ir::PrimitiveKind as I;
649                match k {
650                    W::PrimString => I::String,
651                    W::PrimInteger => I::Integer,
652                    W::PrimNumber => I::Number,
653                    W::PrimBool => I::Bool,
654                }
655            }
656
657            fn prim_kind_to(k: ir::PrimitiveKind) -> b::PrimitiveKind {
658                use b::PrimitiveKind as W;
659                use ir::PrimitiveKind as I;
660                match k {
661                    I::String => W::PrimString,
662                    I::Integer => W::PrimInteger,
663                    I::Number => W::PrimNumber,
664                    I::Bool => W::PrimBool,
665                }
666            }
667
668            fn prim_constraints_from(c: b::PrimitiveConstraints) -> ir::PrimitiveConstraints {
669                ir::PrimitiveConstraints {
670                    minimum: c.minimum,
671                    maximum: c.maximum,
672                    exclusive_minimum: c.exclusive_minimum,
673                    exclusive_maximum: c.exclusive_maximum,
674                    multiple_of: c.multiple_of,
675                    min_length: c.min_length,
676                    max_length: c.max_length,
677                    pattern: c.pattern,
678                    format_extension: c.format_extension,
679                    content_encoding: c.content_encoding,
680                    content_media_type: c.content_media_type,
681                    content_schema: c.content_schema,
682                }
683            }
684
685            fn prim_constraints_to(c: ir::PrimitiveConstraints) -> b::PrimitiveConstraints {
686                b::PrimitiveConstraints {
687                    minimum: c.minimum,
688                    maximum: c.maximum,
689                    exclusive_minimum: c.exclusive_minimum,
690                    exclusive_maximum: c.exclusive_maximum,
691                    multiple_of: c.multiple_of,
692                    min_length: c.min_length,
693                    max_length: c.max_length,
694                    pattern: c.pattern,
695                    format_extension: c.format_extension,
696                    content_encoding: c.content_encoding,
697                    content_media_type: c.content_media_type,
698                    content_schema: c.content_schema,
699                }
700            }
701
702            fn prim_from(p: b::PrimitiveType) -> ir::PrimitiveType {
703                ir::PrimitiveType {
704                    kind: prim_kind_from(p.kind),
705                    constraints: prim_constraints_from(p.constraints),
706                }
707            }
708
709            fn prim_to(p: ir::PrimitiveType) -> b::PrimitiveType {
710                b::PrimitiveType {
711                    kind: prim_kind_to(p.kind),
712                    constraints: prim_constraints_to(p.constraints),
713                }
714            }
715
716            // ---- array ----
717
718            fn array_from(a: b::ArrayType) -> ir::ArrayType {
719                ir::ArrayType {
720                    items: a.items,
721                    constraints: ir::ArrayConstraints {
722                        min_items: a.constraints.min_items,
723                        max_items: a.constraints.max_items,
724                        unique_items: a.constraints.unique_items,
725                    },
726                }
727            }
728
729            fn array_to(a: ir::ArrayType) -> b::ArrayType {
730                b::ArrayType {
731                    items: a.items,
732                    constraints: b::ArrayConstraints {
733                        min_items: a.constraints.min_items,
734                        max_items: a.constraints.max_items,
735                        unique_items: a.constraints.unique_items,
736                    },
737                }
738            }
739
740            // ---- object ----
741
742            fn object_from(o: b::ObjectType) -> ir::ObjectType {
743                ir::ObjectType {
744                    properties: o
745                        .properties
746                        .into_iter()
747                        .map(|p| ir::Property {
748                            name: p.name,
749                            r#type: p.type_,
750                            required: p.required,
751                            title: p.title,
752                            description: p.description,
753                            deprecated: p.deprecated,
754                            read_only: p.read_only,
755                            write_only: p.write_only,
756                            external_docs: p.external_docs.map(external_docs_from),
757                            default: p.default,
758                            examples: examples_from(p.examples),
759                            extensions: p.extensions,
760                        })
761                        .collect(),
762                    pattern_properties: o
763                        .pattern_properties
764                        .into_iter()
765                        .map(|p| ir::PatternProperty {
766                            pattern: p.pattern,
767                            r#type: p.type_,
768                        })
769                        .collect(),
770                    additional_properties: match o.additional_properties {
771                        b::AdditionalProperties::Forbidden => ir::AdditionalProperties::Forbidden,
772                        b::AdditionalProperties::Any => ir::AdditionalProperties::Any,
773                        b::AdditionalProperties::Typed(t) => {
774                            ir::AdditionalProperties::Typed { r#type: t }
775                        }
776                    },
777                    property_names: o.property_names,
778                    constraints: ir::ObjectConstraints {
779                        min_properties: o.constraints.min_properties,
780                        max_properties: o.constraints.max_properties,
781                    },
782                }
783            }
784
785            fn object_to(o: ir::ObjectType) -> b::ObjectType {
786                b::ObjectType {
787                    properties: o
788                        .properties
789                        .into_iter()
790                        .map(|p| b::Property {
791                            name: p.name,
792                            type_: p.r#type,
793                            required: p.required,
794                            title: p.title,
795                            description: p.description,
796                            deprecated: p.deprecated,
797                            read_only: p.read_only,
798                            write_only: p.write_only,
799                            external_docs: p.external_docs.map(external_docs_to),
800                            default: p.default,
801                            examples: examples_to(p.examples),
802                            extensions: p.extensions,
803                        })
804                        .collect(),
805                    pattern_properties: o
806                        .pattern_properties
807                        .into_iter()
808                        .map(|p| b::PatternProperty {
809                            pattern: p.pattern,
810                            type_: p.r#type,
811                        })
812                        .collect(),
813                    additional_properties: match o.additional_properties {
814                        ir::AdditionalProperties::Forbidden => b::AdditionalProperties::Forbidden,
815                        ir::AdditionalProperties::Any => b::AdditionalProperties::Any,
816                        ir::AdditionalProperties::Typed { r#type } => {
817                            b::AdditionalProperties::Typed(r#type)
818                        }
819                    },
820                    property_names: o.property_names,
821                    constraints: b::ObjectConstraints {
822                        min_properties: o.constraints.min_properties,
823                        max_properties: o.constraints.max_properties,
824                    },
825                }
826            }
827
828            // ---- map ----
829
830            // ---- enums ----
831
832            fn enum_str_from(e: b::EnumStringType) -> ir::EnumStringType {
833                ir::EnumStringType {
834                    values: e
835                        .values
836                        .into_iter()
837                        .map(|v| ir::EnumStringValue { value: v.value })
838                        .collect(),
839                }
840            }
841
842            fn enum_str_to(e: ir::EnumStringType) -> b::EnumStringType {
843                b::EnumStringType {
844                    values: e
845                        .values
846                        .into_iter()
847                        .map(|v| b::EnumStringValue { value: v.value })
848                        .collect(),
849                }
850            }
851
852            fn enum_int_from(e: b::EnumIntType) -> ir::EnumIntType {
853                ir::EnumIntType {
854                    values: e
855                        .values
856                        .into_iter()
857                        .map(|v| ir::EnumIntValue { value: v.value })
858                        .collect(),
859                    kind: match e.kind {
860                        b::IntKind::Int32 => ir::IntKind::Int32,
861                        b::IntKind::Int64 => ir::IntKind::Int64,
862                    },
863                }
864            }
865
866            fn enum_int_to(e: ir::EnumIntType) -> b::EnumIntType {
867                b::EnumIntType {
868                    values: e
869                        .values
870                        .into_iter()
871                        .map(|v| b::EnumIntValue { value: v.value })
872                        .collect(),
873                    kind: match e.kind {
874                        ir::IntKind::Int32 => b::IntKind::Int32,
875                        ir::IntKind::Int64 => b::IntKind::Int64,
876                    },
877                }
878            }
879
880            // ---- union ----
881
882            fn union_from(u: b::UnionType) -> ir::UnionType {
883                ir::UnionType {
884                    variants: u
885                        .variants
886                        .into_iter()
887                        .map(|v| ir::UnionVariant {
888                            r#type: v.type_,
889                            tag: v.tag,
890                        })
891                        .collect(),
892                    discriminator: u.discriminator.map(|d| ir::Discriminator {
893                        property_name: d.property_name,
894                        mapping: d.mapping.into_iter().collect(),
895                        extensions: d.extensions,
896                    }),
897                    kind: match u.kind {
898                        b::UnionKind::OneOf => ir::UnionKind::OneOf,
899                        b::UnionKind::AnyOf => ir::UnionKind::AnyOf,
900                    },
901                }
902            }
903
904            fn union_to(u: ir::UnionType) -> b::UnionType {
905                b::UnionType {
906                    variants: u
907                        .variants
908                        .into_iter()
909                        .map(|v| b::UnionVariant {
910                            type_: v.r#type,
911                            tag: v.tag,
912                        })
913                        .collect(),
914                    discriminator: u.discriminator.map(|d| b::Discriminator {
915                        property_name: d.property_name,
916                        mapping: d.mapping,
917                        extensions: d.extensions,
918                    }),
919                    kind: match u.kind {
920                        ir::UnionKind::OneOf => b::UnionKind::OneOf,
921                        ir::UnionKind::AnyOf => b::UnionKind::AnyOf,
922                    },
923                }
924            }
925
926            // -------------------------------------------------------------
927            // Operations
928            // -------------------------------------------------------------
929
930            fn http_method_from(m: b::HttpMethod) -> ir::HttpMethod {
931                use b::HttpMethod as W;
932                use ir::HttpMethod as I;
933                match m {
934                    W::Get => I::Get,
935                    W::Put => I::Put,
936                    W::Post => I::Post,
937                    W::Delete => I::Delete,
938                    W::Options => I::Options,
939                    W::Head => I::Head,
940                    W::Patch => I::Patch,
941                    W::Trace => I::Trace,
942                    W::Other(s) => I::Other(s),
943                }
944            }
945
946            fn http_method_to(m: ir::HttpMethod) -> b::HttpMethod {
947                use b::HttpMethod as W;
948                use ir::HttpMethod as I;
949                match m {
950                    I::Get => W::Get,
951                    I::Put => W::Put,
952                    I::Post => W::Post,
953                    I::Delete => W::Delete,
954                    I::Options => W::Options,
955                    I::Head => W::Head,
956                    I::Patch => W::Patch,
957                    I::Trace => W::Trace,
958                    I::Other(s) => W::Other(s),
959                }
960            }
961
962            fn param_style_from(s: b::ParameterStyle) -> ir::ParameterStyle {
963                use b::ParameterStyle as W;
964                use ir::ParameterStyle as I;
965                match s {
966                    W::ParamForm => I::Form,
967                    W::ParamSimple => I::Simple,
968                    W::ParamLabel => I::Label,
969                    W::ParamMatrix => I::Matrix,
970                    W::ParamSpaceDelimited => I::SpaceDelimited,
971                    W::ParamPipeDelimited => I::PipeDelimited,
972                    W::ParamDeepObject => I::DeepObject,
973                }
974            }
975
976            fn param_style_to(s: ir::ParameterStyle) -> b::ParameterStyle {
977                use b::ParameterStyle as W;
978                use ir::ParameterStyle as I;
979                match s {
980                    I::Form => W::ParamForm,
981                    I::Simple => W::ParamSimple,
982                    I::Label => W::ParamLabel,
983                    I::Matrix => W::ParamMatrix,
984                    I::SpaceDelimited => W::ParamSpaceDelimited,
985                    I::PipeDelimited => W::ParamPipeDelimited,
986                    I::DeepObject => W::ParamDeepObject,
987                }
988            }
989
990            fn header_from(h: b::Header) -> ir::Header {
991                ir::Header {
992                    r#type: h.type_,
993                    required: h.required,
994                    description: h.description,
995                    deprecated: h.deprecated,
996                    examples: examples_from(h.examples),
997                    style: h.style.map(param_style_from),
998                    explode: h.explode,
999                    allow_reserved: h.allow_reserved,
1000                    allow_empty_value: h.allow_empty_value,
1001                    location: h.location.map(loc_from),
1002                }
1003            }
1004
1005            fn header_to(h: ir::Header) -> b::Header {
1006                b::Header {
1007                    type_: h.r#type,
1008                    required: h.required,
1009                    description: h.description,
1010                    deprecated: h.deprecated,
1011                    examples: examples_to(h.examples),
1012                    style: h.style.map(param_style_to),
1013                    explode: h.explode,
1014                    allow_reserved: h.allow_reserved,
1015                    allow_empty_value: h.allow_empty_value,
1016                    location: h.location.map(loc_to),
1017                }
1018            }
1019
1020            fn parameter_from(p: b::Parameter) -> ir::Parameter {
1021                ir::Parameter {
1022                    name: p.name,
1023                    r#type: p.type_,
1024                    required: p.required,
1025                    description: p.description,
1026                    deprecated: p.deprecated,
1027                    examples: examples_from(p.examples),
1028                    style: p.style.map(param_style_from),
1029                    explode: p.explode,
1030                    allow_empty_value: p.allow_empty_value,
1031                    allow_reserved: p.allow_reserved,
1032                    extensions: extensions_from(p.extensions),
1033                    location: p.location.map(loc_from),
1034                }
1035            }
1036
1037            fn parameter_to(p: ir::Parameter) -> b::Parameter {
1038                b::Parameter {
1039                    name: p.name,
1040                    type_: p.r#type,
1041                    required: p.required,
1042                    description: p.description,
1043                    deprecated: p.deprecated,
1044                    examples: examples_to(p.examples),
1045                    style: p.style.map(param_style_to),
1046                    explode: p.explode,
1047                    allow_empty_value: p.allow_empty_value,
1048                    allow_reserved: p.allow_reserved,
1049                    extensions: extensions_to(p.extensions),
1050                    location: p.location.map(loc_to),
1051                }
1052            }
1053
1054            fn body_from(b_: b::Body) -> ir::Body {
1055                ir::Body {
1056                    content: b_.content.into_iter().map(body_content_from).collect(),
1057                    required: b_.required,
1058                    description: b_.description,
1059                    extensions: extensions_from(b_.extensions),
1060                }
1061            }
1062
1063            fn body_to(b_: ir::Body) -> b::Body {
1064                b::Body {
1065                    content: b_.content.into_iter().map(body_content_to).collect(),
1066                    required: b_.required,
1067                    description: b_.description,
1068                    extensions: extensions_to(b_.extensions),
1069                }
1070            }
1071
1072            fn body_content_from(c: b::BodyContent) -> ir::BodyContent {
1073                ir::BodyContent {
1074                    media_type: c.media_type,
1075                    r#type: c.type_,
1076                    encoding: c
1077                        .encoding
1078                        .into_iter()
1079                        .map(|(k, v)| (k, encoding_from(v)))
1080                        .collect(),
1081                    item_schema: c.item_schema,
1082                    examples: examples_from(c.examples),
1083                    extensions: extensions_from(c.extensions),
1084                }
1085            }
1086
1087            fn body_content_to(c: ir::BodyContent) -> b::BodyContent {
1088                b::BodyContent {
1089                    media_type: c.media_type,
1090                    type_: c.r#type,
1091                    encoding: c
1092                        .encoding
1093                        .into_iter()
1094                        .map(|(k, v)| (k, encoding_to(v)))
1095                        .collect(),
1096                    item_schema: c.item_schema,
1097                    examples: examples_to(c.examples),
1098                    extensions: extensions_to(c.extensions),
1099                }
1100            }
1101
1102            fn encoding_from(e: b::Encoding) -> ir::Encoding {
1103                ir::Encoding {
1104                    content_type: e.content_type,
1105                    style: e.style.map(param_style_from),
1106                    explode: e.explode,
1107                    allow_reserved: e.allow_reserved,
1108                    headers: e
1109                        .headers
1110                        .into_iter()
1111                        .map(|(k, v)| (k, header_from(v)))
1112                        .collect(),
1113                    extensions: extensions_from(e.extensions),
1114                }
1115            }
1116
1117            fn encoding_to(e: ir::Encoding) -> b::Encoding {
1118                b::Encoding {
1119                    content_type: e.content_type,
1120                    style: e.style.map(param_style_to),
1121                    explode: e.explode,
1122                    allow_reserved: e.allow_reserved,
1123                    headers: e
1124                        .headers
1125                        .into_iter()
1126                        .map(|(k, v)| (k, header_to(v)))
1127                        .collect(),
1128                    extensions: extensions_to(e.extensions),
1129                }
1130            }
1131
1132            fn response_from(r: b::Response) -> ir::Response {
1133                ir::Response {
1134                    status: match r.status {
1135                        b::ResponseStatus::Explicit(code) => ir::ResponseStatus::Explicit { code },
1136                        b::ResponseStatus::Default => ir::ResponseStatus::Default,
1137                        b::ResponseStatus::Range(class) => ir::ResponseStatus::Range { class },
1138                    },
1139                    content: r.content.into_iter().map(body_content_from).collect(),
1140                    headers: r
1141                        .headers
1142                        .into_iter()
1143                        .map(|(k, v)| (k, header_from(v)))
1144                        .collect(),
1145                    summary: r.summary,
1146                    description: r.description,
1147                    links: links_from(r.links),
1148                    extensions: extensions_from(r.extensions),
1149                }
1150            }
1151
1152            fn response_to(r: ir::Response) -> b::Response {
1153                b::Response {
1154                    status: match r.status {
1155                        ir::ResponseStatus::Explicit { code } => b::ResponseStatus::Explicit(code),
1156                        ir::ResponseStatus::Default => b::ResponseStatus::Default,
1157                        ir::ResponseStatus::Range { class } => b::ResponseStatus::Range(class),
1158                    },
1159                    content: r.content.into_iter().map(body_content_to).collect(),
1160                    headers: r
1161                        .headers
1162                        .into_iter()
1163                        .map(|(k, v)| (k, header_to(v)))
1164                        .collect(),
1165                    summary: r.summary,
1166                    description: r.description,
1167                    links: links_to(r.links),
1168                    extensions: extensions_to(r.extensions),
1169                }
1170            }
1171
1172            fn operation_from(op: b::Operation) -> Result<ir::Operation, BindgenError> {
1173                Ok(ir::Operation {
1174                    id: op.id,
1175                    original_id: op.original_id,
1176                    method: http_method_from(op.method),
1177                    path_template: op.path_template,
1178                    path_params: op.path_params.into_iter().map(parameter_from).collect(),
1179                    query_params: op.query_params.into_iter().map(parameter_from).collect(),
1180                    header_params: op.header_params.into_iter().map(parameter_from).collect(),
1181                    cookie_params: op.cookie_params.into_iter().map(parameter_from).collect(),
1182                    querystring_params: op
1183                        .querystring_params
1184                        .into_iter()
1185                        .map(parameter_from)
1186                        .collect(),
1187                    request_body: op.request_body.map(body_from),
1188                    responses: op.responses.into_iter().map(response_from).collect(),
1189                    security: op
1190                        .security
1191                        .into_iter()
1192                        .map(|s| ir::SecurityRequirement {
1193                            scheme_id: s.scheme_id,
1194                            scopes: s.scopes,
1195                        })
1196                        .collect(),
1197                    tags: op.tags,
1198                    summary: op.summary,
1199                    description: op.description,
1200                    deprecated: op.deprecated,
1201                    external_docs: op.external_docs.map(external_docs_from),
1202                    extensions: op.extensions,
1203                    servers: op.servers.into_iter().map(server_from).collect(),
1204                    callbacks: op.callbacks.into_iter().map(callback_from).collect(),
1205                    location: op.location.map(loc_from),
1206                })
1207            }
1208
1209            fn operation_to(op: ir::Operation) -> b::Operation {
1210                b::Operation {
1211                    id: op.id,
1212                    original_id: op.original_id,
1213                    method: http_method_to(op.method),
1214                    path_template: op.path_template,
1215                    path_params: op.path_params.into_iter().map(parameter_to).collect(),
1216                    query_params: op.query_params.into_iter().map(parameter_to).collect(),
1217                    header_params: op.header_params.into_iter().map(parameter_to).collect(),
1218                    cookie_params: op.cookie_params.into_iter().map(parameter_to).collect(),
1219                    querystring_params: op
1220                        .querystring_params
1221                        .into_iter()
1222                        .map(parameter_to)
1223                        .collect(),
1224                    request_body: op.request_body.map(body_to),
1225                    responses: op.responses.into_iter().map(response_to).collect(),
1226                    security: op
1227                        .security
1228                        .into_iter()
1229                        .map(|s| b::SecurityRequirement {
1230                            scheme_id: s.scheme_id,
1231                            scopes: s.scopes,
1232                        })
1233                        .collect(),
1234                    tags: op.tags,
1235                    summary: op.summary,
1236                    description: op.description,
1237                    deprecated: op.deprecated,
1238                    external_docs: op.external_docs.map(external_docs_to),
1239                    extensions: op.extensions,
1240                    servers: op.servers.into_iter().map(server_to).collect(),
1241                    callbacks: op.callbacks.into_iter().map(callback_to).collect(),
1242                    location: op.location.map(loc_to),
1243                }
1244            }
1245
1246            // -------------------------------------------------------------
1247            // Security
1248            // -------------------------------------------------------------
1249
1250            fn security_scheme_from(s: b::SecurityScheme) -> ir::SecurityScheme {
1251                ir::SecurityScheme {
1252                    id: s.id,
1253                    kind: match s.kind {
1254                        b::SecuritySchemeKind::ApiKey(k) => {
1255                            ir::SecuritySchemeKind::ApiKey(ir::ApiKeyScheme {
1256                                name: k.name,
1257                                location: match k.location {
1258                                    b::ApiKeyLocation::Header => ir::ApiKeyLocation::Header,
1259                                    b::ApiKeyLocation::Query => ir::ApiKeyLocation::Query,
1260                                    b::ApiKeyLocation::Cookie => ir::ApiKeyLocation::Cookie,
1261                                },
1262                            })
1263                        }
1264                        b::SecuritySchemeKind::HttpBasic => ir::SecuritySchemeKind::HttpBasic,
1265                        b::SecuritySchemeKind::HttpBearer(f) => {
1266                            ir::SecuritySchemeKind::HttpBearer { bearer_format: f }
1267                        }
1268                        b::SecuritySchemeKind::MutualTls => ir::SecuritySchemeKind::MutualTls,
1269                        b::SecuritySchemeKind::Oauth2(o) => {
1270                            ir::SecuritySchemeKind::Oauth2(ir::OAuth2Scheme {
1271                                flows: o.flows.into_iter().map(oauth2_flow_from).collect(),
1272                            })
1273                        }
1274                        b::SecuritySchemeKind::OpenIdConnect(u) => {
1275                            ir::SecuritySchemeKind::OpenIdConnect { url: u }
1276                        }
1277                    },
1278                    description: s.description,
1279                    deprecated: s.deprecated,
1280                    extensions: extensions_from(s.extensions),
1281                }
1282            }
1283
1284            fn security_scheme_to(s: ir::SecurityScheme) -> b::SecurityScheme {
1285                b::SecurityScheme {
1286                    id: s.id,
1287                    kind: match s.kind {
1288                        ir::SecuritySchemeKind::ApiKey(k) => {
1289                            b::SecuritySchemeKind::ApiKey(b::ApiKeyScheme {
1290                                name: k.name,
1291                                location: match k.location {
1292                                    ir::ApiKeyLocation::Header => b::ApiKeyLocation::Header,
1293                                    ir::ApiKeyLocation::Query => b::ApiKeyLocation::Query,
1294                                    ir::ApiKeyLocation::Cookie => b::ApiKeyLocation::Cookie,
1295                                },
1296                            })
1297                        }
1298                        ir::SecuritySchemeKind::HttpBasic => b::SecuritySchemeKind::HttpBasic,
1299                        ir::SecuritySchemeKind::HttpBearer { bearer_format } => {
1300                            b::SecuritySchemeKind::HttpBearer(bearer_format)
1301                        }
1302                        ir::SecuritySchemeKind::MutualTls => b::SecuritySchemeKind::MutualTls,
1303                        ir::SecuritySchemeKind::Oauth2(o) => {
1304                            b::SecuritySchemeKind::Oauth2(b::Oauth2Scheme {
1305                                flows: o.flows.into_iter().map(oauth2_flow_to).collect(),
1306                            })
1307                        }
1308                        ir::SecuritySchemeKind::OpenIdConnect { url } => {
1309                            b::SecuritySchemeKind::OpenIdConnect(url)
1310                        }
1311                    },
1312                    description: s.description,
1313                    deprecated: s.deprecated,
1314                    extensions: extensions_to(s.extensions),
1315                }
1316            }
1317
1318            fn oauth2_flow_from(f: b::Oauth2Flow) -> ir::OAuth2Flow {
1319                ir::OAuth2Flow {
1320                    kind: match f.kind {
1321                        b::Oauth2FlowKind::Implicit => ir::OAuth2FlowKind::Implicit,
1322                        b::Oauth2FlowKind::Password => ir::OAuth2FlowKind::Password,
1323                        b::Oauth2FlowKind::ClientCredentials => {
1324                            ir::OAuth2FlowKind::ClientCredentials
1325                        }
1326                        b::Oauth2FlowKind::AuthorizationCode => {
1327                            ir::OAuth2FlowKind::AuthorizationCode
1328                        }
1329                    },
1330                    authorization_url: f.authorization_url,
1331                    token_url: f.token_url,
1332                    refresh_url: f.refresh_url,
1333                    scopes: f.scopes,
1334                    extensions: extensions_from(f.extensions),
1335                }
1336            }
1337
1338            fn oauth2_flow_to(f: ir::OAuth2Flow) -> b::Oauth2Flow {
1339                b::Oauth2Flow {
1340                    kind: match f.kind {
1341                        ir::OAuth2FlowKind::Implicit => b::Oauth2FlowKind::Implicit,
1342                        ir::OAuth2FlowKind::Password => b::Oauth2FlowKind::Password,
1343                        ir::OAuth2FlowKind::ClientCredentials => {
1344                            b::Oauth2FlowKind::ClientCredentials
1345                        }
1346                        ir::OAuth2FlowKind::AuthorizationCode => {
1347                            b::Oauth2FlowKind::AuthorizationCode
1348                        }
1349                    },
1350                    authorization_url: f.authorization_url,
1351                    token_url: f.token_url,
1352                    refresh_url: f.refresh_url,
1353                    scopes: f.scopes,
1354                    extensions: extensions_to(f.extensions),
1355                }
1356            }
1357        }
1358    };
1359}
1360
1361define_world_conversions!(transformer, transformer);
1362define_world_conversions!(generator, generator);
1363
1364#[cfg(test)]
1365mod tests {
1366    use super::*;
1367    use forge_ir::proptest_util::small_ir;
1368    use proptest::prelude::*;
1369
1370    proptest! {
1371        /// Roundtrip: forge_ir → wit (transformer) → forge_ir is identity.
1372        #[test]
1373        fn roundtrip_transformer(ir in small_ir()) {
1374            let wit = transformer::ir_to_wit(ir.clone());
1375            let back = transformer::ir_from_wit(wit).unwrap();
1376            prop_assert_eq!(ir, back);
1377        }
1378
1379        /// Roundtrip: forge_ir → wit (generator) → forge_ir is identity.
1380        #[test]
1381        fn roundtrip_generator(ir in small_ir()) {
1382            let wit = generator::ir_to_wit(ir.clone());
1383            let back = generator::ir_from_wit(wit).unwrap();
1384            prop_assert_eq!(ir, back);
1385        }
1386    }
1387}