Skip to main content

zerodds_idl/features/
gate.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Feature-Gate-Validator (A2 Spec-Check 2.0).
4//!
5//! Walks ein CST nach Recognition und prueft pro Production-ID ob das
6//! korrespondierende Feature in [`IdlFeatures`] aktiv ist. Wenn nicht,
7//! liefert er einen [`FeatureGateError`] mit klarer Fehlermeldung.
8//!
9//! # Architektur
10//!
11//! Der Validator betreibt eine statische Lookup-Tabelle `Production-ID
12//! → Required-Feature`. Productions ohne Feature-Gate werden ignoriert
13//! (die meisten Plain-DDS- und XTypes-Konstrukte sind feature-frei).
14//! CORBA/CCM-Konstrukte und Vendor-Erweiterungen sind feature-gated.
15//!
16//! # Fehlerbehandlung
17//!
18//! Der Validator akkumuliert *alle* Verletzungen — der Caller bekommt
19//! eine Liste, nicht nur den ersten Fehler. So sieht der User in einem
20//! Pass alle disabled Features auf einmal.
21
22use crate::cst::CstNode;
23use crate::errors::Span;
24use crate::features::IdlFeatures;
25use crate::grammar::ProductionId;
26
27/// Gate-Verletzung: ein verwendetes Konstrukt benoetigt ein Feature
28/// das in [`IdlFeatures`] aus ist.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct FeatureGateError {
31    /// Production-ID des verwendeten Konstrukts.
32    pub production_id: ProductionId,
33    /// Name der Production (Spec-BNF-Bezeichnung).
34    pub production_name: &'static str,
35    /// Name des inaktiven Features (Feld in [`IdlFeatures`]).
36    pub required_feature: &'static str,
37    /// Span im Source.
38    pub span: Span,
39}
40
41impl core::fmt::Display for FeatureGateError {
42    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
43        write!(
44            f,
45            "use of `{}` requires feature `{}` (currently disabled in ParserConfig::features)",
46            self.production_name, self.required_feature
47        )
48    }
49}
50
51impl std::error::Error for FeatureGateError {}
52
53/// Mappt eine `(Production-ID, Alternative-Index)`-Kombi auf das Feature
54/// das sie aktiv haben muss. `None` heisst "kein Gate".
55///
56/// Wird vom Validator konsultiert *nach* `production_required_feature`,
57/// um auch einzelne Alternativen einer ungated Production zu gaten
58/// (z.B. `oneway`-Variante von `op_dcl`).
59#[must_use]
60pub fn alternative_required_feature(prod_id: ProductionId, alt_idx: usize) -> Option<&'static str> {
61    use crate::grammar::idl42::{
62        ID_INTERFACE_KIND, ID_OP_DCL, ID_VALUE_HEADER, ID_VALUE_INHERITANCE_SPEC,
63    };
64    match prod_id {
65        // `op_dcl` Alts:
66        // 0 = oneway_with_raises
67        // 1 = oneway_no_raises
68        // 2 = with_raises
69        // 3 = no_raises
70        id if id == ID_OP_DCL && (alt_idx == 0 || alt_idx == 1) => Some("corba_oneway_op"),
71        // `interface_kind` Alts:
72        // 0 = abstract
73        // 1 = local
74        id if id == ID_INTERFACE_KIND && alt_idx == 0 => Some("corba_abstract_interface"),
75        id if id == ID_INTERFACE_KIND && alt_idx == 1 => Some("corba_local_interface"),
76        // `value_header` Alts (§7.4.5 Extras: custom + abstract valuetype):
77        // 0 = custom
78        // 1 = plain
79        // 2 = abstract
80        id if id == ID_VALUE_HEADER && (alt_idx == 0 || alt_idx == 2) => {
81            Some("corba_value_types_extras")
82        }
83        // `value_inheritance_spec` Alt 0 = extends_truncatable.
84        id if id == ID_VALUE_INHERITANCE_SPEC && alt_idx == 0 => Some("corba_value_types_extras"),
85        _ => None,
86    }
87}
88
89/// Mappt eine Production-ID auf das Feature, das sie aktiv haben muss.
90/// `None` heisst "kein Gate" (Plain-DDS-Konstrukt, immer erlaubt).
91#[must_use]
92pub fn production_required_feature(prod_id: ProductionId) -> Option<&'static str> {
93    use crate::grammar::idl42::{
94        ID_COMPONENT_BODY, ID_COMPONENT_DCL, ID_COMPONENT_DEF, ID_COMPONENT_EXPORT,
95        ID_COMPONENT_FORWARD_DCL, ID_COMPONENT_HEADER, ID_COMPONENT_INHERITANCE_SPEC,
96        ID_CONNECTOR_DCL, ID_CONNECTOR_EXPORT, ID_CONNECTOR_HEADER, ID_CONNECTOR_INHERIT_SPEC,
97        ID_CONSUMES_DCL, ID_EMITS_DCL, ID_EVENT_DCL, ID_EVENT_DEF, ID_EVENT_FORWARD_DCL,
98        ID_EVENT_HEADER, ID_FACTORY_DCL, ID_FACTORY_PARAM_DCLS, ID_FINDER_DCL, ID_HOME_BODY,
99        ID_HOME_DCL, ID_HOME_EXPORT, ID_HOME_HEADER, ID_HOME_INHERITANCE_SPEC, ID_IMPORT_DCL,
100        ID_IMPORTED_SCOPE, ID_INIT_DCL, ID_INIT_PARAM_DCL, ID_INIT_PARAM_DCLS, ID_INTERFACE_TYPE,
101        ID_NATIVE_DCL, ID_PORT_BODY, ID_PORT_DCL, ID_PORT_EXPORT, ID_PORT_REF, ID_PORTTYPE_DCL,
102        ID_PRIMARY_KEY_SPEC, ID_PROVIDES_DCL, ID_PUBLISHES_DCL, ID_STATE_MEMBER,
103        ID_SUPPORTED_INTERFACE_SPEC, ID_TEMPLATE_MODULE_DCL, ID_TEMPLATE_MODULE_INST,
104        ID_TYPE_ID_DCL, ID_TYPE_PREFIX_DCL, ID_USES_DCL, ID_VALUE_DEF, ID_VALUE_ELEMENT,
105        ID_VALUE_HEADER, ID_VALUE_INHERITANCE_NAME_LIST, ID_VALUE_INHERITANCE_SPEC,
106    };
107    match prod_id {
108        id if id == ID_NATIVE_DCL => Some("corba_native"),
109        // §7.4.5 Value Types Full
110        id if id == ID_VALUE_DEF
111            || id == ID_VALUE_HEADER
112            || id == ID_VALUE_INHERITANCE_SPEC
113            || id == ID_VALUE_INHERITANCE_NAME_LIST
114            || id == ID_VALUE_ELEMENT
115            || id == ID_STATE_MEMBER
116            || id == ID_INIT_DCL
117            || id == ID_INIT_PARAM_DCLS
118            || id == ID_INIT_PARAM_DCL =>
119        {
120            Some("corba_value_types_full")
121        }
122        // §7.4.6 Repository-IDs (typeid/typeprefix).
123        id if id == ID_TYPE_ID_DCL || id == ID_TYPE_PREFIX_DCL => Some("corba_repository_ids"),
124        // §7.4.6 Import.
125        id if id == ID_IMPORT_DCL || id == ID_IMPORTED_SCOPE => Some("corba_import"),
126        // §7.4.9 CCM Components-Basic
127        id if id == ID_COMPONENT_DCL
128            || id == ID_COMPONENT_FORWARD_DCL
129            || id == ID_COMPONENT_DEF
130            || id == ID_COMPONENT_HEADER
131            || id == ID_COMPONENT_INHERITANCE_SPEC
132            || id == ID_COMPONENT_BODY
133            || id == ID_COMPONENT_EXPORT
134            || id == ID_PROVIDES_DCL
135            || id == ID_USES_DCL
136            || id == ID_INTERFACE_TYPE
137            || id == ID_SUPPORTED_INTERFACE_SPEC =>
138        {
139            Some("corba_components")
140        }
141        // §7.4.9 CCM Homes
142        id if id == ID_HOME_DCL
143            || id == ID_HOME_HEADER
144            || id == ID_HOME_INHERITANCE_SPEC
145            || id == ID_PRIMARY_KEY_SPEC
146            || id == ID_HOME_BODY
147            || id == ID_HOME_EXPORT
148            || id == ID_FACTORY_DCL
149            || id == ID_FACTORY_PARAM_DCLS
150            || id == ID_FINDER_DCL =>
151        {
152            Some("corba_homes")
153        }
154        // §7.4.9 CCM Eventtypes (`emits`/`publishes`/`consumes` + event_def)
155        id if id == ID_EVENT_DCL
156            || id == ID_EVENT_FORWARD_DCL
157            || id == ID_EVENT_DEF
158            || id == ID_EVENT_HEADER
159            || id == ID_EMITS_DCL
160            || id == ID_PUBLISHES_DCL
161            || id == ID_CONSUMES_DCL =>
162        {
163            Some("corba_eventtypes")
164        }
165        // §7.4.9 CCM Ports + Connectors
166        id if id == ID_PORTTYPE_DCL
167            || id == ID_PORT_BODY
168            || id == ID_PORT_REF
169            || id == ID_PORT_EXPORT
170            || id == ID_PORT_DCL
171            || id == ID_CONNECTOR_DCL
172            || id == ID_CONNECTOR_HEADER
173            || id == ID_CONNECTOR_INHERIT_SPEC
174            || id == ID_CONNECTOR_EXPORT =>
175        {
176            Some("corba_ports")
177        }
178        // §7.4.12 Template Modules — nur Top-Level-Wrapper gaten,
179        // weil Sub-Productions (formal/actual_parameter) Earley-State-
180        // Knoten via type_spec/const_expr in nicht-Template-Kontexten
181        // produzieren koennen (false-positive auf normales `long x`).
182        id if id == ID_TEMPLATE_MODULE_DCL || id == ID_TEMPLATE_MODULE_INST => {
183            Some("corba_template_modules")
184        }
185        _ => None,
186    }
187}
188
189/// Prueft ein Feature-Flag in [`IdlFeatures`] anhand seines Namens.
190///
191/// Liefert `true` wenn das benannte Flag aktiv ist; `false` sonst
192/// inkl. unbekannte Namen (defensive: unbekannte Namen werden als
193/// "deaktiviert" interpretiert, sodass der Caller den Konflikt sieht).
194#[must_use]
195pub fn is_feature_enabled(features: &IdlFeatures, name: &str) -> bool {
196    match name {
197        "corba_value_types_full" => features.corba_value_types_full,
198        "corba_value_types_extras" => features.corba_value_types_extras,
199        "corba_repository_ids" => features.corba_repository_ids,
200        "corba_import" => features.corba_import,
201        "corba_local_interface" => features.corba_local_interface,
202        "corba_abstract_interface" => features.corba_abstract_interface,
203        "corba_object_base" => features.corba_object_base,
204        "corba_oneway_op" => features.corba_oneway_op,
205        "corba_context" => features.corba_context,
206        "corba_components" => features.corba_components,
207        "corba_homes" => features.corba_homes,
208        "corba_eventtypes" => features.corba_eventtypes,
209        "corba_ports" => features.corba_ports,
210        "corba_template_modules" => features.corba_template_modules,
211        "corba_native" => features.corba_native,
212        "preprocessor_full" => features.preprocessor_full,
213        "preprocessor_warning_line" => features.preprocessor_warning_line,
214        "vendor_rti" => features.vendor_rti,
215        "vendor_opensplice" => features.vendor_opensplice,
216        "vendor_opensplice_legacy" => features.vendor_opensplice_legacy,
217        "vendor_cyclonedds" => features.vendor_cyclonedds,
218        "vendor_fastdds" => features.vendor_fastdds,
219        _ => false,
220    }
221}
222
223/// Walks das CST und sammelt alle Feature-Gate-Verletzungen.
224///
225/// Pro CstNode wird die Production-ID auf [`production_required_feature`]
226/// gemapped; ist das Feature inaktiv → ein [`FeatureGateError`] wird
227/// emittiert.
228///
229/// Iterative Implementation (Stack-explicit), weil Earley-CSTs sehr
230/// tief verschachtelt sein koennen (Recursion-Limit-Risiko).
231#[must_use]
232pub fn validate<'a>(cst: &'a CstNode<'a>, features: &IdlFeatures) -> Vec<FeatureGateError> {
233    let mut errors = Vec::new();
234    let mut stack: Vec<&CstNode<'_>> = vec![cst];
235    while let Some(node) = stack.pop() {
236        if let Some(prod_id) = node.production() {
237            // Production-Level-Gate.
238            if let Some(required) = production_required_feature(prod_id) {
239                if !is_feature_enabled(features, required) {
240                    errors.push(FeatureGateError {
241                        production_id: prod_id,
242                        production_name: production_name_of(prod_id),
243                        required_feature: required,
244                        span: node.span,
245                    });
246                }
247            }
248            // Alternative-Level-Gate (z.B. `oneway`-Variante von op_dcl).
249            if let Some(alt_idx) = node.alternative_index() {
250                if let Some(required) = alternative_required_feature(prod_id, alt_idx) {
251                    if !is_feature_enabled(features, required) {
252                        errors.push(FeatureGateError {
253                            production_id: prod_id,
254                            production_name: production_name_of(prod_id),
255                            required_feature: required,
256                            span: node.span,
257                        });
258                    }
259                }
260            }
261        }
262        for child in &node.children {
263            stack.push(child);
264        }
265    }
266    errors
267}
268
269fn production_name_of(prod_id: ProductionId) -> &'static str {
270    crate::grammar::idl42::IDL_42
271        .production(prod_id)
272        .map(|p| p.name)
273        .unwrap_or("unknown")
274}
275
276#[cfg(test)]
277mod tests {
278    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
279
280    use super::*;
281    use crate::cst::build_cst;
282    use crate::engine::Engine;
283    use crate::grammar::idl42::IDL_42;
284    use crate::lexer::Tokenizer;
285
286    fn check(src: &str, features: IdlFeatures) -> Vec<FeatureGateError> {
287        // CST-Reconstruction kann sehr tief rekursiv sein (Earley-Engine);
288        // wir laufen daher in einem Thread mit grosszuegigem Stack.
289        let src = src.to_string();
290        std::thread::Builder::new()
291            .stack_size(64 * 1024 * 1024)
292            .spawn(move || {
293                let tokenizer = Tokenizer::for_grammar(&IDL_42);
294                let stream = tokenizer.tokenize(&src).expect("lex");
295                let engine = Engine::new(&IDL_42);
296                let result = engine.recognize(stream.tokens()).expect("recognize");
297                // compiled_grammar() liefert die desugarte Form mit
298                // synthetisierten `_rep_X`-Productions, identisch zum
299                // Pfad des Public-`parse()` API.
300                let cst =
301                    build_cst(engine.compiled_grammar(), stream.tokens(), &result).expect("cst");
302                validate(&cst, &features)
303            })
304            .expect("spawn")
305            .join()
306            .expect("join")
307    }
308
309    #[test]
310    fn no_corba_use_no_errors_in_dds_basic() {
311        // Plain-DDS-IDL ohne CORBA-Konstrukte → keine Gate-Verletzung
312        // selbst in dds_basic.
313        let src = "module svc { struct Topic { long id; }; };";
314        let errors = check(src, IdlFeatures::dds_basic());
315        assert!(errors.is_empty(), "got {errors:?}");
316    }
317
318    #[test]
319    fn no_corba_use_no_errors_in_dds_extensible() {
320        let src = "struct Foo { long x; long y; };";
321        let errors = check(src, IdlFeatures::dds_extensible());
322        assert!(errors.is_empty(), "got {errors:?}");
323    }
324
325    #[test]
326    fn dds_extensible_allows_native() {
327        // corba_native ist in dds_extensible an.
328        let errors = check("native Handle;", IdlFeatures::dds_extensible());
329        assert!(errors.is_empty(), "got {errors:?}");
330    }
331
332    #[test]
333    fn dds_basic_rejects_native() {
334        // corba_native ist in dds_basic aus → klare Fehlermeldung.
335        let errors = check("native Handle;", IdlFeatures::dds_basic());
336        assert!(
337            errors
338                .iter()
339                .any(|e| e.required_feature == "corba_native" && e.production_name == "native_dcl"),
340            "expected corba_native gate error, got {errors:?}"
341        );
342    }
343
344    #[test]
345    fn corba_full_allows_native() {
346        let errors = check("native Handle;", IdlFeatures::corba_full());
347        assert!(errors.is_empty(), "got {errors:?}");
348    }
349
350    #[test]
351    fn opensplice_legacy_allows_native() {
352        let errors = check("native Handle;", IdlFeatures::opensplice_legacy());
353        assert!(errors.is_empty(), "got {errors:?}");
354    }
355
356    #[test]
357    fn is_feature_enabled_matches_struct_fields() {
358        let f = IdlFeatures::all();
359        assert!(is_feature_enabled(&f, "corba_native"));
360        assert!(is_feature_enabled(&f, "vendor_rti"));
361        assert!(!is_feature_enabled(&f, "unknown_feature"));
362    }
363
364    #[test]
365    fn feature_gate_error_has_helpful_message() {
366        let errors = check("native Handle;", IdlFeatures::dds_basic());
367        let msg = errors[0].to_string();
368        assert!(msg.contains("native_dcl"), "got: {msg}");
369        assert!(msg.contains("corba_native"), "got: {msg}");
370        assert!(msg.contains("disabled"), "got: {msg}");
371    }
372
373    // -----------------------------------------------------------------
374    // §7.4.5 Value Types Full — Feature-Gate-Tests
375    // -----------------------------------------------------------------
376
377    #[test]
378    fn dds_extensible_rejects_value_def() {
379        // corba_value_types_full ist in dds_extensible aus.
380        let errors = check("valuetype V {};", IdlFeatures::dds_extensible());
381        assert!(
382            errors
383                .iter()
384                .any(|e| e.required_feature == "corba_value_types_full"),
385            "expected corba_value_types_full gate error, got {errors:?}"
386        );
387    }
388
389    #[test]
390    fn corba_full_allows_value_def() {
391        let errors = check("valuetype V { public long x; };", IdlFeatures::corba_full());
392        assert!(errors.is_empty(), "got {errors:?}");
393    }
394
395    #[test]
396    fn opensplice_legacy_allows_value_def_with_factory() {
397        let errors = check(
398            "valuetype V { exception E {}; factory create() raises (E); };",
399            IdlFeatures::opensplice_legacy(),
400        );
401        assert!(errors.is_empty(), "got {errors:?}");
402    }
403
404    #[test]
405    fn dds_basic_allows_value_box() {
406        // value_box ist nicht gated → bleibt erlaubt auch in dds_basic.
407        let errors = check("valuetype Box long;", IdlFeatures::dds_basic());
408        assert!(
409            errors.is_empty(),
410            "value_box bleibt ungated, got {errors:?}"
411        );
412    }
413
414    // -----------------------------------------------------------------
415    // §7.4.6 Repository-IDs / Import — Feature-Gate-Tests
416    // -----------------------------------------------------------------
417
418    #[test]
419    fn dds_extensible_rejects_typeid() {
420        let errors = check(
421            r#"typeid Foo "IDL:foo:1.0";"#,
422            IdlFeatures::dds_extensible(),
423        );
424        assert!(
425            errors
426                .iter()
427                .any(|e| e.required_feature == "corba_repository_ids"),
428            "expected corba_repository_ids gate error, got {errors:?}"
429        );
430    }
431
432    #[test]
433    fn dds_extensible_rejects_typeprefix() {
434        let errors = check(
435            r#"typeprefix Foo "company";"#,
436            IdlFeatures::dds_extensible(),
437        );
438        assert!(
439            errors
440                .iter()
441                .any(|e| e.required_feature == "corba_repository_ids"),
442            "got {errors:?}"
443        );
444    }
445
446    #[test]
447    fn dds_extensible_rejects_import() {
448        let errors = check("import Foo;", IdlFeatures::dds_extensible());
449        assert!(
450            errors.iter().any(|e| e.required_feature == "corba_import"),
451            "got {errors:?}"
452        );
453    }
454
455    #[test]
456    fn corba_full_allows_repository_ids_and_import() {
457        let errors = check(
458            r#"typeid Foo "IDL:foo:1.0"; typeprefix M "company"; import Bar;"#,
459            IdlFeatures::corba_full(),
460        );
461        assert!(errors.is_empty(), "got {errors:?}");
462    }
463
464    #[test]
465    fn opensplice_legacy_allows_repository_ids() {
466        let errors = check(
467            r#"typeid Foo "IDL:foo:1.0";"#,
468            IdlFeatures::opensplice_legacy(),
469        );
470        assert!(errors.is_empty(), "got {errors:?}");
471    }
472
473    // -----------------------------------------------------------------
474    // §7.4.7 CORBA-Interface-Erweiterungen (oneway, abstract, local)
475    // -----------------------------------------------------------------
476
477    #[test]
478    fn dds_extensible_rejects_oneway_op() {
479        let errors = check(
480            "interface S { oneway void log(in string m); };",
481            IdlFeatures::dds_extensible(),
482        );
483        assert!(
484            errors
485                .iter()
486                .any(|e| e.required_feature == "corba_oneway_op"),
487            "expected corba_oneway_op gate error, got {errors:?}"
488        );
489    }
490
491    #[test]
492    fn dds_extensible_rejects_abstract_interface() {
493        let errors = check(
494            "abstract interface IBase {};",
495            IdlFeatures::dds_extensible(),
496        );
497        assert!(
498            errors
499                .iter()
500                .any(|e| e.required_feature == "corba_abstract_interface"),
501            "got {errors:?}"
502        );
503    }
504
505    #[test]
506    fn dds_extensible_rejects_local_interface() {
507        let errors = check("local interface ILocal {};", IdlFeatures::dds_extensible());
508        assert!(
509            errors
510                .iter()
511                .any(|e| e.required_feature == "corba_local_interface"),
512            "got {errors:?}"
513        );
514    }
515
516    #[test]
517    fn corba_full_allows_oneway_and_abstract_local() {
518        let errors = check(
519            "abstract interface IA {}; local interface IL {}; \
520             interface S { oneway void log(in string m); };",
521            IdlFeatures::corba_full(),
522        );
523        assert!(errors.is_empty(), "got {errors:?}");
524    }
525
526    #[test]
527    fn dds_extensible_allows_plain_op() {
528        // Normal-Op ohne oneway → kein Gate, geht in jedem Profil.
529        let errors = check(
530            "interface S { void log(in string m); };",
531            IdlFeatures::dds_extensible(),
532        );
533        assert!(errors.is_empty(), "got {errors:?}");
534    }
535
536    #[test]
537    fn opensplice_legacy_allows_oneway_and_abstract_interface() {
538        let errors = check(
539            "abstract interface IA {}; interface S { oneway void log(in string m); };",
540            IdlFeatures::opensplice_legacy(),
541        );
542        assert!(errors.is_empty(), "got {errors:?}");
543    }
544
545    // -----------------------------------------------------------------
546    // §7.4.5 Extras — custom/abstract valuetype + truncatable (B4)
547    // -----------------------------------------------------------------
548
549    #[test]
550    fn corba_full_minus_extras_rejects_custom_valuetype() {
551        // Mit corba_full (extras=on) sollte custom gehen.
552        let mut f = IdlFeatures::corba_full();
553        f.corba_value_types_extras = false;
554        let errors = check("custom valuetype V {};", f);
555        assert!(
556            errors
557                .iter()
558                .any(|e| e.required_feature == "corba_value_types_extras"),
559            "expected corba_value_types_extras gate, got {errors:?}"
560        );
561    }
562
563    #[test]
564    fn corba_full_minus_extras_rejects_abstract_valuetype() {
565        let mut f = IdlFeatures::corba_full();
566        f.corba_value_types_extras = false;
567        let errors = check("abstract valuetype V {};", f);
568        assert!(
569            errors
570                .iter()
571                .any(|e| e.required_feature == "corba_value_types_extras"),
572            "got {errors:?}"
573        );
574    }
575
576    #[test]
577    fn corba_full_minus_extras_rejects_truncatable() {
578        let mut f = IdlFeatures::corba_full();
579        f.corba_value_types_extras = false;
580        let errors = check("valuetype Derived : truncatable Base {};", f);
581        assert!(
582            errors
583                .iter()
584                .any(|e| e.required_feature == "corba_value_types_extras"),
585            "got {errors:?}"
586        );
587    }
588
589    #[test]
590    fn corba_full_allows_custom_abstract_truncatable() {
591        let errors = check(
592            "abstract valuetype IA {}; \
593             custom valuetype V : truncatable Base {};",
594            IdlFeatures::corba_full(),
595        );
596        assert!(errors.is_empty(), "got {errors:?}");
597    }
598}