Skip to main content

zerodds_ami4ccm/
transform.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Implied-IDL-Transformation — Spec §7.3 + §7.5.
5//!
6//! Eingabe: [`InterfaceDef`] eines normalen IDL-Interfaces.
7//! Ausgabe: zwei `InterfaceKind::Local` Interfaces (Spec §7.3 + §7.5):
8//!
9//! 1. `AMI4CCM_<Iface>ReplyHandler` (Spec §7.5) — Type-spezifische
10//!    Reply-Handler mit normal-reply + `_excep`-Operations.
11//! 2. `AMI4CCM_<Iface>` (Spec §7.3) — Async-Operations mit
12//!    `sendc_`-Praefix.
13//!
14//! Spec §7.3.1 (S. 7) — Naming-Conflict-Aufloesung: wenn der generierte
15//! `sendc_<op>`-Name bereits im Interface existiert, werden `ami_`-
16//! Praefixe zwischen `sendc_` und `<op>` eingefuegt, bis der Name
17//! eindeutig ist. Dieselbe Regel gilt fuer `<op>_excep` im
18//! ReplyHandler (Spec §7.5.2, S. 10).
19
20use alloc::collections::BTreeSet;
21use alloc::format;
22use alloc::string::{String, ToString};
23use alloc::vec::Vec;
24
25use zerodds_idl::ast::{
26    AttrDecl, Export, Identifier, InterfaceDef, InterfaceKind, OpDecl, ParamAttribute, ParamDecl,
27    ScopedName, TypeSpec,
28};
29use zerodds_idl::errors::Span;
30
31/// Ergebnis der Transformation: zwei zusaetzliche Local-Interfaces.
32#[derive(Debug, Clone, PartialEq)]
33pub struct Ami4CcmInterfaces {
34    /// `AMI4CCM_<Iface>ReplyHandler` (Spec §7.5).
35    pub reply_handler: InterfaceDef,
36    /// `AMI4CCM_<Iface>` (Spec §7.3 + §7.6 ami4ccm_provides).
37    pub ami_interface: InterfaceDef,
38}
39
40/// Compilation-Kontext fuer den AMI4CCM-Transform.
41///
42/// Spec-Referenzen:
43/// * §7.5 — abgeleitete Interfaces erben ihren ReplyHandler vom
44///   ReplyHandler des Base-Interfaces (`AMI4CCM_<Base>ReplyHandler`).
45///   Dazu braucht der Transformer eine Map der bereits transformierten
46///   Bases (`known_bases`).
47/// * §7.5 / §7.3.1 — wenn der naive `AMI4CCM_<Iface>`-/-`ReplyHandler`-
48///   Name mit einem bestehenden Identifier im Compilation-Unit
49///   kollidiert, muessen `AMI_`-Prefixe ergaenzt werden, bis der Name
50///   eindeutig ist (`known_symbols`).
51#[derive(Debug, Clone, Default)]
52pub struct TransformContext {
53    /// Set von Original-Interface-Namen, deren ReplyHandler bereits
54    /// generiert wurde. Wird bei `transform_interface` einer Iface mit
55    /// `iface.bases = [Base]` konsultiert: ist `Base.text` enthalten,
56    /// erbt der neue ReplyHandler von `AMI4CCM_<Base>ReplyHandler`
57    /// statt vom generischen `CCM_AMI::ReplyHandler`.
58    pub known_bases: BTreeSet<String>,
59    /// Set aller Identifier im aktuellen Compilation-Scope. Wird vor
60    /// dem Emittieren der AMI4CCM-Interface-Namen gegen den
61    /// Konflikt-Resolver gepruescht (`AMI_AMI4CCM_<Iface>` etc.).
62    pub known_symbols: BTreeSet<String>,
63}
64
65impl TransformContext {
66    /// Neuer leerer Kontext (gleichbedeutend mit `default()`).
67    #[must_use]
68    pub fn new() -> Self {
69        Self::default()
70    }
71
72    /// Markiert einen Iface-Namen als bereits-transformiert, sodass
73    /// abgeleitete Interfaces ihren ReplyHandler korrekt erben.
74    pub fn mark_transformed(&mut self, iface_name: &str) {
75        self.known_bases.insert(iface_name.to_string());
76        self.known_symbols.insert(format!("AMI4CCM_{iface_name}"));
77        self.known_symbols
78            .insert(format!("AMI4CCM_{iface_name}ReplyHandler"));
79    }
80
81    /// Registriert einen bereits-existierenden Identifier, gegen den
82    /// der AMI4CCM-Naming-Resolver pruefen muss.
83    pub fn add_known_symbol(&mut self, name: &str) {
84        self.known_symbols.insert(name.to_string());
85    }
86}
87
88/// Transformiert ein normales [`InterfaceDef`] in das implied AMI4CCM-
89/// Paar. Spec §7.3 + §7.5.
90///
91/// Convenience-Wrapper ohne Kontext (kein Inheritance-Lookup, keine
92/// Symbol-Tabelle). Fuer voll spec-konforme Transformation aufrufen
93/// von `transform_interface_in_context`.
94#[must_use]
95pub fn transform_interface(iface: &InterfaceDef) -> Ami4CcmInterfaces {
96    transform_interface_in_context(iface, &TransformContext::default())
97}
98
99/// Spec-konforme Variante mit Compilation-Kontext.
100///
101/// **Naming Convention (Spec §7.3.1 + §7.5):**
102/// * Reply-Handler-Name: `AMI4CCM_<original-iface-name>ReplyHandler`,
103///   bei Konflikt mit `ctx.known_symbols`: `AMI_AMI4CCM_<...>` etc.
104/// * Async-Interface-Name: `AMI4CCM_<original-iface-name>`.
105///
106/// **Reply-Handler-Inheritance (Spec §7.5):**
107/// Wenn `iface.bases` einen Eintrag hat, dessen `last`-Identifier in
108/// `ctx.known_bases` enthalten ist, erbt der ReplyHandler von
109/// `AMI4CCM_<Base>ReplyHandler`. Andernfalls von
110/// `CCM_AMI::ReplyHandler` (Default).
111///
112/// **`InterfaceKind::Local`** (Spec §7.3 + Annex A): beide generierten
113/// Interfaces sind `local interface` (nicht remotable).
114#[must_use]
115pub fn transform_interface_in_context(
116    iface: &InterfaceDef,
117    ctx: &TransformContext,
118) -> Ami4CcmInterfaces {
119    let span = Span::SYNTHETIC;
120    let original_name = iface.name.text.clone();
121
122    // Sammle alle Original-Operation-Namen + Attribute-Setter/Getter-
123    // Namen, um Naming-Conflicts (Spec §7.3.1) aufzuloesen.
124    let mut original_op_names: Vec<String> = Vec::new();
125    for export in &iface.exports {
126        match export {
127            Export::Op(op) => original_op_names.push(op.name.text.clone()),
128            Export::Attr(attr) => {
129                original_op_names.push(format!("get_{}", attr.name.text));
130                if !attr.readonly {
131                    original_op_names.push(format!("set_{}", attr.name.text));
132                }
133            }
134            _ => {}
135        }
136    }
137
138    // Spec §7.5 — Reply-Handler-Inheritance fuer abgeleitete Interfaces.
139    // Wir konsultieren ctx.known_bases: der erste base in iface.bases,
140    // dessen letzter Identifier-Part in ctx.known_bases enthalten ist,
141    // bestimmt den Parent-ReplyHandler.
142    let reply_handler_base = iface
143        .bases
144        .iter()
145        .find_map(|b| {
146            b.parts
147                .last()
148                .map(|i| &i.text)
149                .filter(|n| ctx.known_bases.contains(n.as_str()))
150                .cloned()
151        })
152        .map_or_else(
153            || scoped_name(&["CCM_AMI", "ReplyHandler"], span),
154            |base| ScopedName::single(Identifier::new(format!("AMI4CCM_{base}ReplyHandler"), span)),
155        );
156
157    // Spec §7.3.1 / §7.5 — Naming-Conflict mit dem Compilation-Scope:
158    // wenn AMI4CCM_<Iface>[ReplyHandler] bereits in ctx.known_symbols
159    // ist, prefix mit AMI_ (rekursiv).
160    let ami_iface_name =
161        resolve_unique_iface_name(&format!("AMI4CCM_{original_name}"), &ctx.known_symbols);
162    let reply_handler_name = resolve_unique_iface_name(
163        &format!("AMI4CCM_{original_name}ReplyHandler"),
164        &ctx.known_symbols,
165    );
166
167    let reply_handler = build_reply_handler(
168        &original_name,
169        &reply_handler_name,
170        reply_handler_base,
171        &iface.exports,
172        &original_op_names,
173        span,
174    );
175    let ami_interface = build_ami_interface(
176        &original_name,
177        &ami_iface_name,
178        &iface.exports,
179        &original_op_names,
180        span,
181    );
182
183    Ami4CcmInterfaces {
184        reply_handler,
185        ami_interface,
186    }
187}
188
189/// Resolver fuer den Naming-Konflikt aus Spec §7.3.1 / §7.5.
190///
191/// Wenn `name` in `known` enthalten ist, schiebt einen `AMI_`-Prefix
192/// vor und versucht erneut. Im pathologischen Fall (alle Varianten
193/// belegt) liefert die Funktion den letzten kandidaten zurueck — der
194/// Caller faellt dann auf die unvermeidbare Doublette zurueck.
195fn resolve_unique_iface_name(base: &str, known: &BTreeSet<String>) -> String {
196    let mut candidate = base.to_string();
197    for _ in 0..16 {
198        if !known.contains(&candidate) {
199            return candidate;
200        }
201        candidate = format!("AMI_{candidate}");
202    }
203    candidate
204}
205
206/// Spec §7.3 (S. 6-7) — `AMI4CCM_<Iface>` Async-Interface mit
207/// `sendc_<op>` Operations.
208fn build_ami_interface(
209    original_name: &str,
210    iface_name: &str,
211    exports: &[Export],
212    original_op_names: &[String],
213    span: Span,
214) -> InterfaceDef {
215    let handler_type = handler_type_spec(original_name);
216    // Verfolge bereits ausgegebene Namen, um interne Kollisionen
217    // (z.B. zwei "ami_..."-Aufstockungen) zu vermeiden.
218    let mut emitted: Vec<String> = Vec::new();
219    let mut new_exports: Vec<Export> = Vec::new();
220
221    for export in exports {
222        match export {
223            Export::Op(op) => {
224                let name = resolve_sendc_name(&op.name.text, original_op_names, &emitted);
225                emitted.push(name.clone());
226                new_exports.push(Export::Op(build_sendc_op(&name, &handler_type, op, span)));
227            }
228            Export::Attr(attr) => {
229                // Getter — Spec §7.3.1.2 (S. 7).
230                let getter_name = resolve_sendc_name(
231                    &format!("get_{}", attr.name.text),
232                    original_op_names,
233                    &emitted,
234                );
235                emitted.push(getter_name.clone());
236                new_exports.push(Export::Op(build_sendc_attr_get(
237                    &getter_name,
238                    &handler_type,
239                    span,
240                )));
241                if !attr.readonly {
242                    let setter_name = resolve_sendc_name(
243                        &format!("set_{}", attr.name.text),
244                        original_op_names,
245                        &emitted,
246                    );
247                    emitted.push(setter_name.clone());
248                    new_exports.push(Export::Op(build_sendc_attr_set(
249                        &setter_name,
250                        &handler_type,
251                        attr,
252                        span,
253                    )));
254                }
255            }
256            // Spec §7.3 — types/consts/exceptions sind im Async-
257            // Interface nicht relevant; sie bleiben im Original-Iface
258            // sichtbar.
259            _ => {}
260        }
261    }
262
263    InterfaceDef {
264        kind: InterfaceKind::Local,
265        name: Identifier::new(iface_name, span),
266        bases: Vec::new(),
267        exports: new_exports,
268        annotations: Vec::new(),
269        span,
270    }
271}
272
273/// Spec §7.5 (S. 9-10) — `AMI4CCM_<Iface>ReplyHandler` mit normal-
274/// Reply + `_excep`-Operations.
275fn build_reply_handler(
276    original_name: &str,
277    handler_name: &str,
278    handler_base: ScopedName,
279    exports: &[Export],
280    original_op_names: &[String],
281    span: Span,
282) -> InterfaceDef {
283    let _ = original_name; // unused now; reserved for future extensions
284    let exc_holder_type = exception_holder_type_spec();
285    let mut emitted: Vec<String> = Vec::new();
286    let mut new_exports: Vec<Export> = Vec::new();
287
288    for export in exports {
289        match export {
290            Export::Op(op) => {
291                // Normal-Reply (Spec §7.5.1).
292                let normal_name = op.name.text.clone();
293                emitted.push(normal_name.clone());
294                new_exports.push(Export::Op(build_handler_normal_op(&normal_name, op, span)));
295                // Exception-Reply (Spec §7.5.2).
296                let excep_name = resolve_excep_name(&op.name.text, original_op_names, &emitted);
297                emitted.push(excep_name.clone());
298                new_exports.push(Export::Op(build_handler_excep_op(
299                    &excep_name,
300                    &exc_holder_type,
301                    span,
302                )));
303            }
304            Export::Attr(attr) => {
305                // Getter normal — `void get_<attr>(in <type> ami_return_val);`.
306                let get_name = format!("get_{}", attr.name.text);
307                emitted.push(get_name.clone());
308                new_exports.push(Export::Op(build_handler_attr_get(
309                    &get_name,
310                    &attr.type_spec,
311                    span,
312                )));
313                // Getter excep.
314                let get_excep = resolve_excep_name(&get_name, original_op_names, &emitted);
315                emitted.push(get_excep.clone());
316                new_exports.push(Export::Op(build_handler_excep_op(
317                    &get_excep,
318                    &exc_holder_type,
319                    span,
320                )));
321                if !attr.readonly {
322                    // Setter normal — `void set_<attr>();` (no args).
323                    let set_name = format!("set_{}", attr.name.text);
324                    emitted.push(set_name.clone());
325                    new_exports.push(Export::Op(build_handler_attr_set_ack(&set_name, span)));
326                    // Setter excep.
327                    let set_excep = resolve_excep_name(&set_name, original_op_names, &emitted);
328                    emitted.push(set_excep.clone());
329                    new_exports.push(Export::Op(build_handler_excep_op(
330                        &set_excep,
331                        &exc_holder_type,
332                        span,
333                    )));
334                }
335            }
336            _ => {}
337        }
338    }
339
340    InterfaceDef {
341        kind: InterfaceKind::Local,
342        name: Identifier::new(handler_name, span),
343        // Spec §7.5 (S. 9): "is derived from the generic CCM_AMI::ReplyHandler"
344        // — bei abgeleiteten Interfaces mit bekannter Base wird der Parent-
345        // Handler des Base-Interfaces eingesetzt (siehe TransformContext).
346        bases: alloc::vec![handler_base],
347        exports: new_exports,
348        annotations: Vec::new(),
349        span,
350    }
351}
352
353/// Spec §7.3.1.1 (S. 7) — `void sendc_<op>(in handler, in/inout-args)`.
354fn build_sendc_op(name: &str, handler_type: &TypeSpec, op: &OpDecl, span: Span) -> OpDecl {
355    let mut params = Vec::new();
356    params.push(handler_param(handler_type, span));
357    for p in &op.params {
358        // Spec §7.3.1.1: "Each of the in and inout arguments [...] all
359        // with a parameter attribute of in [...]. out arguments are
360        // ignored."
361        if matches!(p.attribute, ParamAttribute::In | ParamAttribute::InOut) {
362            params.push(ParamDecl {
363                attribute: ParamAttribute::In,
364                type_spec: p.type_spec.clone(),
365                name: p.name.clone(),
366                annotations: Vec::new(),
367                span,
368            });
369        }
370    }
371    OpDecl {
372        name: Identifier::new(name, span),
373        oneway: false,
374        return_type: None,
375        params,
376        // Spec §7.3 (S. 5/6) — async-Operations werfen keine User-
377        // Exceptions; nur `INV_OBJREF` System-Exception, die nicht in
378        // `raises()` kommt.
379        raises: Vec::new(),
380        annotations: Vec::new(),
381        span,
382    }
383}
384
385/// Spec §7.3.1.2 (S. 7) — `void sendc_get_<attr>(in handler);`.
386fn build_sendc_attr_get(name: &str, handler_type: &TypeSpec, span: Span) -> OpDecl {
387    OpDecl {
388        name: Identifier::new(name, span),
389        oneway: false,
390        return_type: None,
391        params: alloc::vec![handler_param(handler_type, span)],
392        raises: Vec::new(),
393        annotations: Vec::new(),
394        span,
395    }
396}
397
398/// Spec §7.3.1.2 (S. 7) — `void sendc_set_<attr>(in handler, in
399/// <attrType> attr_<attrName>);`.
400fn build_sendc_attr_set(
401    name: &str,
402    handler_type: &TypeSpec,
403    attr: &AttrDecl,
404    span: Span,
405) -> OpDecl {
406    let arg_name = format!("attr_{}", attr.name.text);
407    OpDecl {
408        name: Identifier::new(name, span),
409        oneway: false,
410        return_type: None,
411        params: alloc::vec![
412            handler_param(handler_type, span),
413            ParamDecl {
414                attribute: ParamAttribute::In,
415                type_spec: attr.type_spec.clone(),
416                name: Identifier::new(arg_name, span),
417                annotations: Vec::new(),
418                span,
419            },
420        ],
421        raises: Vec::new(),
422        annotations: Vec::new(),
423        span,
424    }
425}
426
427/// Spec §7.5.1 (S. 9-10) — `void <op>(in <ret> ami_return_val,
428/// in/inout-args)`.
429fn build_handler_normal_op(name: &str, op: &OpDecl, span: Span) -> OpDecl {
430    let mut params = Vec::new();
431    if let Some(ret) = &op.return_type {
432        params.push(ParamDecl {
433            attribute: ParamAttribute::In,
434            type_spec: ret.clone(),
435            name: Identifier::new("ami_return_val", span),
436            annotations: Vec::new(),
437            span,
438        });
439    }
440    for p in &op.params {
441        // Spec §7.5.1: "Each inout/out type name and argument name as
442        // they were declared in IDL." — alle als `in`.
443        if matches!(p.attribute, ParamAttribute::InOut | ParamAttribute::Out) {
444            params.push(ParamDecl {
445                attribute: ParamAttribute::In,
446                type_spec: p.type_spec.clone(),
447                name: p.name.clone(),
448                annotations: Vec::new(),
449                span,
450            });
451        }
452    }
453    OpDecl {
454        name: Identifier::new(name, span),
455        oneway: false,
456        return_type: None,
457        params,
458        // Spec §7.5.1 (S. 10): "These operations do not raise any
459        // exceptions because they are never invoked by a client and
460        // have no client to respond to such an exception."
461        raises: Vec::new(),
462        annotations: Vec::new(),
463        span,
464    }
465}
466
467/// Spec §7.5.1 (S. 10) — `void get_<attr>(in <attrType> ami_return_val);`.
468fn build_handler_attr_get(name: &str, attr_type: &TypeSpec, span: Span) -> OpDecl {
469    OpDecl {
470        name: Identifier::new(name, span),
471        oneway: false,
472        return_type: None,
473        params: alloc::vec![ParamDecl {
474            attribute: ParamAttribute::In,
475            type_spec: attr_type.clone(),
476            name: Identifier::new("ami_return_val", span),
477            annotations: Vec::new(),
478            span,
479        }],
480        raises: Vec::new(),
481        annotations: Vec::new(),
482        span,
483    }
484}
485
486/// Spec §7.5.1 (S. 10) — `void set_<attr>();` (Setter-Acknowledgement,
487/// keine Argumente).
488fn build_handler_attr_set_ack(name: &str, span: Span) -> OpDecl {
489    OpDecl {
490        name: Identifier::new(name, span),
491        oneway: false,
492        return_type: None,
493        params: Vec::new(),
494        raises: Vec::new(),
495        annotations: Vec::new(),
496        span,
497    }
498}
499
500/// Spec §7.5.2 (S. 10) — `void <op>_excep(in CCM_AMI::ExceptionHolder
501/// excep_holder);`.
502fn build_handler_excep_op(name: &str, exc_holder_type: &TypeSpec, span: Span) -> OpDecl {
503    OpDecl {
504        name: Identifier::new(name, span),
505        oneway: false,
506        return_type: None,
507        params: alloc::vec![ParamDecl {
508            attribute: ParamAttribute::In,
509            type_spec: exc_holder_type.clone(),
510            name: Identifier::new("excep_holder", span),
511            annotations: Vec::new(),
512            span,
513        }],
514        raises: Vec::new(),
515        annotations: Vec::new(),
516        span,
517    }
518}
519
520/// Spec §7.3.1 (S. 7) — Naming-Conflict-Aufloesung fuer `sendc_<op>`:
521/// "If this implied-IDL operation name conflicts with existing
522/// operations on the interface or any of the interface's base
523/// interfaces, `ami_` strings are inserted between `sendc_` and the
524/// original operation name until the implied-IDL operation name is
525/// unique."
526fn resolve_sendc_name(
527    original_op_name: &str,
528    forbidden_in_iface: &[String],
529    already_emitted: &[String],
530) -> String {
531    let mut prefix = String::from("sendc_");
532    loop {
533        let candidate = format!("{prefix}{original_op_name}");
534        if !forbidden_in_iface.iter().any(|n| n == &candidate)
535            && !already_emitted.iter().any(|n| n == &candidate)
536        {
537            return candidate;
538        }
539        prefix.push_str("ami_");
540    }
541}
542
543/// Spec §7.5.2 (S. 10) — Analog fuer `<op>_excep` im ReplyHandler:
544/// "If the name generated by the method described above clashes with
545/// a name that already exists in the interface, `_ami` strings are
546/// inserted immediately preceding the `_excep` repeatedly, until
547/// generated IDL operation name is unique in the interface."
548fn resolve_excep_name(
549    original_op_name: &str,
550    forbidden_in_iface: &[String],
551    already_emitted: &[String],
552) -> String {
553    let mut suffix = String::from("_excep");
554    loop {
555        let candidate = format!("{original_op_name}{suffix}");
556        if !forbidden_in_iface.iter().any(|n| n == &candidate)
557            && !already_emitted.iter().any(|n| n == &candidate)
558        {
559            return candidate;
560        }
561        // Spec sagt "_ami" (kein "ami_" wie bei sendc) — hier
562        // Spec-getreu beibehalten.
563        suffix = format!("_ami{suffix}");
564    }
565}
566
567fn handler_param(handler_type: &TypeSpec, span: Span) -> ParamDecl {
568    ParamDecl {
569        attribute: ParamAttribute::In,
570        type_spec: handler_type.clone(),
571        name: Identifier::new("ami_handler", span),
572        annotations: Vec::new(),
573        span,
574    }
575}
576
577fn handler_type_spec(original_name: &str) -> TypeSpec {
578    TypeSpec::Scoped(scoped_name(
579        &[&format!("AMI4CCM_{original_name}ReplyHandler")],
580        Span::SYNTHETIC,
581    ))
582}
583
584fn exception_holder_type_spec() -> TypeSpec {
585    TypeSpec::Scoped(scoped_name(
586        &["CCM_AMI", "ExceptionHolder"],
587        Span::SYNTHETIC,
588    ))
589}
590
591fn scoped_name(parts: &[&str], span: Span) -> ScopedName {
592    ScopedName {
593        absolute: false,
594        parts: parts
595            .iter()
596            .map(|p| Identifier::new((*p).to_string(), span))
597            .collect(),
598        span,
599    }
600}
601
602/// Helper fuer `void`-Type Pruefung in Tests.
603#[cfg(test)]
604fn assert_void_return(op: &OpDecl) {
605    assert!(
606        op.return_type.is_none(),
607        "expected void return on {}",
608        op.name.text
609    );
610}
611
612#[cfg(test)]
613#[allow(clippy::panic, clippy::expect_used)]
614mod tests {
615    use super::*;
616    use zerodds_idl::ast::{IntegerType, PrimitiveType, StringType};
617
618    fn primitive(p: PrimitiveType) -> TypeSpec {
619        TypeSpec::Primitive(p)
620    }
621
622    fn op(
623        name: &str,
624        return_type: Option<TypeSpec>,
625        params: Vec<(ParamAttribute, TypeSpec, &str)>,
626    ) -> Export {
627        let span = Span::SYNTHETIC;
628        Export::Op(OpDecl {
629            name: Identifier::new(name, span),
630            oneway: false,
631            return_type,
632            params: params
633                .into_iter()
634                .map(|(attr, ty, n)| ParamDecl {
635                    attribute: attr,
636                    type_spec: ty,
637                    name: Identifier::new(n, span),
638                    annotations: Vec::new(),
639                    span,
640                })
641                .collect(),
642            raises: Vec::new(),
643            annotations: Vec::new(),
644            span,
645        })
646    }
647
648    fn attr(name: &str, ty: TypeSpec, readonly: bool) -> Export {
649        let span = Span::SYNTHETIC;
650        Export::Attr(AttrDecl {
651            name: Identifier::new(name, span),
652            type_spec: ty,
653            readonly,
654            get_raises: Vec::new(),
655            set_raises: Vec::new(),
656            annotations: Vec::new(),
657            span,
658        })
659    }
660
661    fn iface(name: &str, exports: Vec<Export>) -> InterfaceDef {
662        InterfaceDef {
663            kind: InterfaceKind::Plain,
664            name: Identifier::new(name, Span::SYNTHETIC),
665            bases: Vec::new(),
666            exports,
667            annotations: Vec::new(),
668            span: Span::SYNTHETIC,
669        }
670    }
671
672    fn string_ty() -> TypeSpec {
673        TypeSpec::String(StringType {
674            wide: false,
675            bound: None,
676            span: Span::SYNTHETIC,
677        })
678    }
679
680    fn double_ty() -> TypeSpec {
681        primitive(PrimitiveType::Floating(
682            zerodds_idl::ast::FloatingType::Double,
683        ))
684    }
685
686    fn boolean_ty() -> TypeSpec {
687        primitive(PrimitiveType::Boolean)
688    }
689
690    fn long_ty() -> TypeSpec {
691        primitive(PrimitiveType::Integer(IntegerType::Long))
692    }
693
694    #[test]
695    fn produces_two_local_interfaces_with_correct_names() {
696        // Spec §7.3 + §7.5 — Naming.
697        let i = iface("StockManager", alloc::vec![]);
698        let out = transform_interface(&i);
699        assert_eq!(out.ami_interface.name.text, "AMI4CCM_StockManager");
700        assert_eq!(
701            out.reply_handler.name.text,
702            "AMI4CCM_StockManagerReplyHandler"
703        );
704        assert_eq!(out.ami_interface.kind, InterfaceKind::Local);
705        assert_eq!(out.reply_handler.kind, InterfaceKind::Local);
706    }
707
708    #[test]
709    fn reply_handler_inherits_from_ccm_ami_replyhandler() {
710        // Spec §7.5 (S. 9): "is derived from the generic CCM_AMI::
711        // ReplyHandler".
712        let i = iface("Foo", alloc::vec![]);
713        let out = transform_interface(&i);
714        assert_eq!(out.reply_handler.bases.len(), 1);
715        let base = &out.reply_handler.bases[0];
716        assert_eq!(
717            base.parts
718                .iter()
719                .map(|p| p.text.as_str())
720                .collect::<Vec<_>>(),
721            alloc::vec!["CCM_AMI", "ReplyHandler"]
722        );
723    }
724
725    #[test]
726    fn sendc_op_has_handler_first_then_in_inout_only() {
727        // Spec §7.3.1.1 — out-Args werden ignoriert; inout wird zu in.
728        let i = iface(
729            "I",
730            alloc::vec![op(
731                "remove_stock",
732                None,
733                alloc::vec![
734                    (ParamAttribute::In, string_ty(), "symbol"),
735                    (ParamAttribute::Out, double_ty(), "quote"),
736                ],
737            )],
738        );
739        let out = transform_interface(&i);
740        let Export::Op(o) = &out.ami_interface.exports[0] else {
741            panic!("expected op");
742        };
743        assert_eq!(o.name.text, "sendc_remove_stock");
744        assert_void_return(o);
745        // Erwartet: ami_handler + symbol; out-arg "quote" ist gedroppt.
746        assert_eq!(o.params.len(), 2);
747        assert_eq!(o.params[0].name.text, "ami_handler");
748        assert_eq!(o.params[0].attribute, ParamAttribute::In);
749        assert_eq!(o.params[1].name.text, "symbol");
750        assert_eq!(o.params[1].attribute, ParamAttribute::In);
751    }
752
753    #[test]
754    fn sendc_inout_becomes_in() {
755        // Spec §7.3.1.1 — "all with a parameter attribute of in".
756        let i = iface(
757            "I",
758            alloc::vec![op(
759                "find_closest_symbol",
760                Some(boolean_ty()),
761                alloc::vec![(ParamAttribute::InOut, string_ty(), "symbol")],
762            )],
763        );
764        let out = transform_interface(&i);
765        let Export::Op(o) = &out.ami_interface.exports[0] else {
766            panic!()
767        };
768        assert_eq!(o.params.len(), 2);
769        // Original inout -> in.
770        assert_eq!(o.params[1].attribute, ParamAttribute::In);
771        assert_eq!(o.params[1].name.text, "symbol");
772    }
773
774    #[test]
775    fn handler_op_has_return_value_then_inout_out() {
776        // Spec §7.5.1 (S. 9-10): ami_return_val first, dann inout/out.
777        let i = iface(
778            "I",
779            alloc::vec![op(
780                "remove_stock",
781                Some(double_ty()),
782                alloc::vec![
783                    (ParamAttribute::In, string_ty(), "symbol"),
784                    (ParamAttribute::Out, double_ty(), "quote"),
785                ],
786            )],
787        );
788        let out = transform_interface(&i);
789        // Erstes Reply-Handler-Op = "remove_stock" (normal), zweites =
790        // "remove_stock_excep".
791        let Export::Op(o) = &out.reply_handler.exports[0] else {
792            panic!()
793        };
794        assert_eq!(o.name.text, "remove_stock");
795        // ami_return_val + quote (out wird zu in im Handler).
796        assert_eq!(o.params.len(), 2);
797        assert_eq!(o.params[0].name.text, "ami_return_val");
798        assert_eq!(o.params[0].attribute, ParamAttribute::In);
799        assert_eq!(o.params[1].name.text, "quote");
800        assert_eq!(o.params[1].attribute, ParamAttribute::In);
801    }
802
803    #[test]
804    fn handler_excep_op_takes_exception_holder() {
805        // Spec §7.5.2 (S. 10): `void <op>_excep(in CCM_AMI::
806        // ExceptionHolder excep_holder);`.
807        let i = iface(
808            "I",
809            alloc::vec![op("get_quote", Some(double_ty()), alloc::vec![])],
810        );
811        let out = transform_interface(&i);
812        // Index 0 = get_quote, Index 1 = get_quote_excep.
813        let Export::Op(o) = &out.reply_handler.exports[1] else {
814            panic!()
815        };
816        assert_eq!(o.name.text, "get_quote_excep");
817        assert_eq!(o.params.len(), 1);
818        assert_eq!(o.params[0].name.text, "excep_holder");
819        let TypeSpec::Scoped(sn) = &o.params[0].type_spec else {
820            panic!("expected ScopedName for ExceptionHolder")
821        };
822        assert_eq!(
823            sn.parts.iter().map(|p| p.text.as_str()).collect::<Vec<_>>(),
824            alloc::vec!["CCM_AMI", "ExceptionHolder"]
825        );
826    }
827
828    #[test]
829    fn attribute_get_set_generated_in_both_interfaces() {
830        // Spec §7.3.1.2 (S. 7) + §7.5.1 (S. 10) — getter+setter Pfad
831        // fuer writable Attribute.
832        let i = iface(
833            "I",
834            alloc::vec![attr("stock_exchange_name", string_ty(), false)],
835        );
836        let out = transform_interface(&i);
837
838        // AMI-Iface: sendc_get_..., sendc_set_...
839        let names: Vec<String> = out
840            .ami_interface
841            .exports
842            .iter()
843            .map(|e| match e {
844                Export::Op(o) => o.name.text.clone(),
845                _ => String::new(),
846            })
847            .collect();
848        assert!(names.contains(&String::from("sendc_get_stock_exchange_name")));
849        assert!(names.contains(&String::from("sendc_set_stock_exchange_name")));
850
851        // Reply-Handler: get_x, get_x_excep, set_x, set_x_excep.
852        let h_names: Vec<String> = out
853            .reply_handler
854            .exports
855            .iter()
856            .map(|e| match e {
857                Export::Op(o) => o.name.text.clone(),
858                _ => String::new(),
859            })
860            .collect();
861        assert!(h_names.contains(&String::from("get_stock_exchange_name")));
862        assert!(h_names.contains(&String::from("get_stock_exchange_name_excep")));
863        assert!(h_names.contains(&String::from("set_stock_exchange_name")));
864        assert!(h_names.contains(&String::from("set_stock_exchange_name_excep")));
865    }
866
867    #[test]
868    fn readonly_attribute_only_generates_getter() {
869        // Spec §7.3.1.2 (S. 7): "Setter operations are only generated
870        // for attributes that are not defined readonly".
871        let i = iface("I", alloc::vec![attr("price", double_ty(), true)]);
872        let out = transform_interface(&i);
873        let names: Vec<String> = out
874            .ami_interface
875            .exports
876            .iter()
877            .map(|e| match e {
878                Export::Op(o) => o.name.text.clone(),
879                _ => String::new(),
880            })
881            .collect();
882        assert!(names.contains(&String::from("sendc_get_price")));
883        assert!(!names.iter().any(|n| n.starts_with("sendc_set_")));
884    }
885
886    #[test]
887    fn sendc_attr_setter_takes_attr_prefixed_arg() {
888        // Spec §7.3.1.2: "in <attrType> attr_<attributeName>".
889        let i = iface(
890            "I",
891            alloc::vec![attr("stock_exchange_name", string_ty(), false)],
892        );
893        let out = transform_interface(&i);
894        // Index 1 = sendc_set_stock_exchange_name (Index 0 = getter).
895        let Export::Op(o) = &out.ami_interface.exports[1] else {
896            panic!()
897        };
898        assert_eq!(o.name.text, "sendc_set_stock_exchange_name");
899        assert_eq!(o.params.len(), 2);
900        assert_eq!(o.params[1].name.text, "attr_stock_exchange_name");
901    }
902
903    #[test]
904    fn handler_attr_setter_ack_has_no_args() {
905        // Spec §7.5.1 (Beispiel S. 10): "void set_stock_exchange_name();".
906        let i = iface(
907            "I",
908            alloc::vec![attr("stock_exchange_name", string_ty(), false)],
909        );
910        let out = transform_interface(&i);
911        // Reihenfolge: get_x, get_x_excep, set_x, set_x_excep.
912        let Export::Op(o) = &out.reply_handler.exports[2] else {
913            panic!()
914        };
915        assert_eq!(o.name.text, "set_stock_exchange_name");
916        assert!(o.params.is_empty());
917    }
918
919    #[test]
920    fn naming_conflict_resolved_with_ami_prefix() {
921        // Spec §7.3.1 (S. 7) — wenn `sendc_<op>` schon im Iface
922        // existiert, "ami_" einfuegen bis eindeutig.
923        let i = iface(
924            "I",
925            alloc::vec![
926                op("foo", None, alloc::vec![]),
927                op("sendc_foo", None, alloc::vec![]),
928            ],
929        );
930        let out = transform_interface(&i);
931        let names: Vec<String> = out
932            .ami_interface
933            .exports
934            .iter()
935            .map(|e| match e {
936                Export::Op(o) => o.name.text.clone(),
937                _ => String::new(),
938            })
939            .collect();
940        // "sendc_foo" existiert -> "sendc_ami_foo" wird benutzt.
941        assert!(names.contains(&String::from("sendc_ami_foo")));
942        assert!(names.contains(&String::from("sendc_sendc_foo")));
943    }
944
945    #[test]
946    fn excep_naming_conflict_resolved_with_ami_suffix() {
947        // Spec §7.5.2 (S. 10) — `_ami_excep` etc.
948        let i = iface(
949            "I",
950            alloc::vec![
951                op("foo", None, alloc::vec![]),
952                op("foo_excep", None, alloc::vec![]),
953            ],
954        );
955        let out = transform_interface(&i);
956        let h_names: Vec<String> = out
957            .reply_handler
958            .exports
959            .iter()
960            .map(|e| match e {
961                Export::Op(o) => o.name.text.clone(),
962                _ => String::new(),
963            })
964            .collect();
965        // "foo_excep" existiert -> "foo_ami_excep" wird verwendet.
966        assert!(h_names.contains(&String::from("foo_ami_excep")));
967        // foo_excep_excep wird NICHT (das waere ein Original-_excep
968        // ohne Konflikt fuer das Original-foo_excep).
969        assert!(h_names.contains(&String::from("foo_excep_excep")));
970    }
971
972    #[test]
973    fn full_stockmanager_running_example_yields_spec_signatures() {
974        // Spec §7.2 (S. 5-6) Running Example -> §7.3.1.3 (S. 8) +
975        // §7.5.3 (S. 11) erwartete Signaturen.
976        let i = iface(
977            "StockManager",
978            alloc::vec![
979                attr("stock_exchange_name", string_ty(), false),
980                op(
981                    "set_stock",
982                    None,
983                    alloc::vec![
984                        (ParamAttribute::In, string_ty(), "symbol"),
985                        (ParamAttribute::In, double_ty(), "new_quote"),
986                    ],
987                ),
988                op(
989                    "remove_stock",
990                    None,
991                    alloc::vec![
992                        (ParamAttribute::In, string_ty(), "symbol"),
993                        (ParamAttribute::Out, double_ty(), "quote"),
994                    ],
995                ),
996                op(
997                    "find_closest_symbol",
998                    Some(boolean_ty()),
999                    alloc::vec![(ParamAttribute::InOut, string_ty(), "symbol")],
1000                ),
1001                op(
1002                    "get_quote",
1003                    Some(double_ty()),
1004                    alloc::vec![(ParamAttribute::In, string_ty(), "symbol")],
1005                ),
1006            ],
1007        );
1008        let out = transform_interface(&i);
1009
1010        // §7.3.1.3 (S. 8) — AMI-Interface enthaelt diese sendc_-Ops.
1011        let ami_names: Vec<String> = out
1012            .ami_interface
1013            .exports
1014            .iter()
1015            .map(|e| match e {
1016                Export::Op(o) => o.name.text.clone(),
1017                _ => String::new(),
1018            })
1019            .collect();
1020        for expected in [
1021            "sendc_get_stock_exchange_name",
1022            "sendc_set_stock_exchange_name",
1023            "sendc_set_stock",
1024            "sendc_remove_stock",
1025            "sendc_find_closest_symbol",
1026            "sendc_get_quote",
1027        ] {
1028            assert!(
1029                ami_names.contains(&String::from(expected)),
1030                "missing {expected} in {ami_names:?}"
1031            );
1032        }
1033
1034        // §7.5.3 (S. 11) — ReplyHandler-Signaturen.
1035        let h_names: Vec<String> = out
1036            .reply_handler
1037            .exports
1038            .iter()
1039            .map(|e| match e {
1040                Export::Op(o) => o.name.text.clone(),
1041                _ => String::new(),
1042            })
1043            .collect();
1044        for expected in [
1045            "get_stock_exchange_name",
1046            "get_stock_exchange_name_excep",
1047            "set_stock_exchange_name",
1048            "set_stock_exchange_name_excep",
1049            "set_stock",
1050            "set_stock_excep",
1051            "remove_stock",
1052            "remove_stock_excep",
1053            "find_closest_symbol",
1054            "find_closest_symbol_excep",
1055            "get_quote",
1056            "get_quote_excep",
1057        ] {
1058            assert!(
1059                h_names.contains(&String::from(expected)),
1060                "missing {expected} in {h_names:?}"
1061            );
1062        }
1063    }
1064
1065    #[test]
1066    fn operation_with_no_return_no_args_yields_handler_op_with_no_params() {
1067        // Spec §7.5.1 (S. 10) "first case" — void return + no params.
1068        let i = iface("I", alloc::vec![op("acknowledge", None, alloc::vec![])]);
1069        let out = transform_interface(&i);
1070        let Export::Op(o) = &out.reply_handler.exports[0] else {
1071            panic!()
1072        };
1073        assert_eq!(o.name.text, "acknowledge");
1074        assert!(o.params.is_empty());
1075    }
1076
1077    #[test]
1078    fn long_typed_attribute_propagates_type_to_handler_param() {
1079        let i = iface("I", alloc::vec![attr("counter", long_ty(), true)]);
1080        let out = transform_interface(&i);
1081        let Export::Op(o) = &out.reply_handler.exports[0] else {
1082            panic!()
1083        };
1084        assert_eq!(o.name.text, "get_counter");
1085        assert_eq!(o.params[0].type_spec, long_ty());
1086    }
1087
1088    // ---- Phase-B Cluster 3 — Spec §7.5 ReplyHandler-Inheritance ----
1089
1090    fn iface_with_base(name: &str, base: &str, exports: Vec<Export>) -> InterfaceDef {
1091        InterfaceDef {
1092            kind: InterfaceKind::Plain,
1093            name: Identifier::new(name, Span::SYNTHETIC),
1094            bases: alloc::vec![ScopedName::single(Identifier::new(base, Span::SYNTHETIC))],
1095            exports,
1096            annotations: Vec::new(),
1097            span: Span::SYNTHETIC,
1098        }
1099    }
1100
1101    #[test]
1102    fn derived_iface_handler_inherits_from_base_handler_when_known() {
1103        // Spec §7.5: ReplyHandler eines abgeleiteten Interfaces erbt
1104        // von AMI4CCM_<Base>ReplyHandler statt von CCM_AMI::ReplyHandler.
1105        let mut ctx = TransformContext::new();
1106        ctx.mark_transformed("Base");
1107        let derived = iface_with_base(
1108            "Derived",
1109            "Base",
1110            alloc::vec![op(
1111                "ping",
1112                Some(long_ty()),
1113                alloc::vec![(ParamAttribute::In, long_ty(), "v")]
1114            )],
1115        );
1116        let out = transform_interface_in_context(&derived, &ctx);
1117        assert_eq!(out.reply_handler.bases.len(), 1);
1118        let parts = out.reply_handler.bases[0]
1119            .parts
1120            .iter()
1121            .map(|i| i.text.as_str())
1122            .collect::<Vec<_>>();
1123        assert_eq!(
1124            parts,
1125            alloc::vec!["AMI4CCM_BaseReplyHandler"],
1126            "expected derived ReplyHandler to inherit from AMI4CCM_BaseReplyHandler, got {parts:?}"
1127        );
1128    }
1129
1130    #[test]
1131    fn derived_iface_falls_back_to_ccm_ami_when_base_unknown() {
1132        // Wenn die Base nicht in known_bases vermerkt ist, behaelt
1133        // die Default-Inheritance-Regel.
1134        let ctx = TransformContext::new();
1135        let derived = iface_with_base("Derived", "UnknownBase", alloc::vec![]);
1136        let out = transform_interface_in_context(&derived, &ctx);
1137        let parts = out.reply_handler.bases[0]
1138            .parts
1139            .iter()
1140            .map(|i| i.text.as_str())
1141            .collect::<Vec<_>>();
1142        assert_eq!(parts, alloc::vec!["CCM_AMI", "ReplyHandler"]);
1143    }
1144
1145    // ---- Phase-B Cluster 3 — Spec §7.5 AMI_-Prefix Naming-Resolver ----
1146
1147    #[test]
1148    fn ami4ccm_prefix_collision_inserts_ami_prefix() {
1149        // Spec §7.5 / §7.3.1: wenn AMI4CCM_<Iface> oder AMI4CCM_<Iface>
1150        // ReplyHandler bereits als Identifier im Compilation-Scope
1151        // existiert, prefix mit AMI_ rekursiv.
1152        let mut ctx = TransformContext::new();
1153        ctx.add_known_symbol("AMI4CCM_Order");
1154        ctx.add_known_symbol("AMI4CCM_OrderReplyHandler");
1155        let i = iface("Order", alloc::vec![op("submit", None, alloc::vec![])]);
1156        let out = transform_interface_in_context(&i, &ctx);
1157        assert_eq!(out.ami_interface.name.text, "AMI_AMI4CCM_Order");
1158        assert_eq!(out.reply_handler.name.text, "AMI_AMI4CCM_OrderReplyHandler");
1159    }
1160
1161    #[test]
1162    fn ami4ccm_prefix_collision_recurses_until_unique() {
1163        // Wenn auch die ersten Prefix-Stufen belegt sind, immer weiter
1164        // rekursiv prefixen.
1165        let mut ctx = TransformContext::new();
1166        for layer in [
1167            "AMI4CCM_Order",
1168            "AMI_AMI4CCM_Order",
1169            "AMI_AMI_AMI4CCM_Order",
1170        ] {
1171            ctx.add_known_symbol(layer);
1172        }
1173        let i = iface("Order", alloc::vec![op("submit", None, alloc::vec![])]);
1174        let out = transform_interface_in_context(&i, &ctx);
1175        assert_eq!(out.ami_interface.name.text, "AMI_AMI_AMI_AMI4CCM_Order");
1176    }
1177
1178    #[test]
1179    fn transform_context_mark_transformed_sets_known_symbols_and_bases() {
1180        let mut ctx = TransformContext::new();
1181        ctx.mark_transformed("Foo");
1182        assert!(ctx.known_bases.contains("Foo"));
1183        assert!(ctx.known_symbols.contains("AMI4CCM_Foo"));
1184        assert!(ctx.known_symbols.contains("AMI4CCM_FooReplyHandler"));
1185    }
1186}