Skip to main content

openbim_step/
express.rs

1//! Explicitly structural, partial EXPRESS declaration extraction.
2//!
3//! This module extracts schema names, entity headers, explicit positional
4//! attributes with their aggregate shape, the names of derived attributes,
5//! inverse attributes, uniqueness rules, `WHERE` rules as text, defined types,
6//! enumerations, and selects. It deliberately does **not** implement full
7//! EXPRESS semantics: expressions, rules, functions, procedures, constants,
8//! and complete type checking remain opaque. Derived attributes are reported
9//! by *name only* — their initialiser expressions are not evaluated, and
10//! neither are bound expressions or `WHERE` rules. Consumers needing language
11//! validation must use a complete EXPRESS implementation.
12//!
13//! Every declaration type is `#[non_exhaustive]`: extracting more of the
14//! language later adds fields without breaking callers. Build values with the
15//! constructors and `with_*` methods rather than struct literals.
16
17/// A structurally parsed schema.
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
19#[non_exhaustive]
20pub struct ParsedSchema {
21    /// Declared schema name, or an empty string when absent.
22    pub name: String,
23    /// Entity declarations in source order.
24    pub entities: Vec<EntityDef>,
25    /// Type declarations in source order.
26    pub types: Vec<TypeDef>,
27}
28
29impl ParsedSchema {
30    /// Creates a schema from its declarations.
31    #[must_use]
32    pub fn new(name: impl Into<String>, entities: Vec<EntityDef>, types: Vec<TypeDef>) -> Self {
33        Self {
34            name: name.into(),
35            entities,
36            types,
37        }
38    }
39}
40
41/// The four EXPRESS aggregation types (ISO 10303-11 §8.2).
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43#[non_exhaustive]
44pub enum AggregateKind {
45    /// `LIST`: ordered, duplicates allowed unless `UNIQUE`.
46    List,
47    /// `SET`: unordered, no duplicates.
48    Set,
49    /// `BAG`: unordered, duplicates allowed.
50    Bag,
51    /// `ARRAY`: fixed-size, indexed by its bounds.
52    Array,
53}
54
55impl AggregateKind {
56    fn from_keyword(keyword: &str) -> Option<Self> {
57        match keyword {
58            "LIST" => Some(Self::List),
59            "SET" => Some(Self::Set),
60            "BAG" => Some(Self::Bag),
61            "ARRAY" => Some(Self::Array),
62            _ => None,
63        }
64    }
65}
66
67/// One bound of an aggregation, as declared.
68#[derive(Debug, Clone, PartialEq, Eq, Hash)]
69#[non_exhaustive]
70pub enum Bound {
71    /// An integer literal.
72    Integer(u64),
73    /// `?`: no upper limit.
74    Unbounded,
75    /// Any other bound expression, as written with whitespace normalised,
76    /// e.g. `SELF\IfcBSplineCurve.UpperIndexOnControlPoints`. It is not
77    /// evaluated.
78    Expression(String),
79}
80
81impl Bound {
82    fn parse(text: &str) -> Self {
83        let text = text.trim();
84        if text == "?" {
85            Self::Unbounded
86        } else if let Ok(value) = text.parse() {
87            Self::Integer(value)
88        } else {
89            Self::Expression(text.split_whitespace().collect::<Vec<_>>().join(" "))
90        }
91    }
92}
93
94/// One aggregation level of a declared type: `LIST [1:3] OF ...`.
95#[derive(Debug, Clone, PartialEq, Eq, Hash)]
96#[non_exhaustive]
97pub struct Aggregation {
98    /// Which aggregation type.
99    pub kind: AggregateKind,
100    /// Lower bound. `0` when `LIST`, `SET` or `BAG` omit their bounds, which
101    /// ISO 10303-11 defines as `[0:?]`.
102    pub lower: Bound,
103    /// Upper bound. [`Bound::Unbounded`] when `LIST`, `SET` or `BAG` omit
104    /// their bounds.
105    pub upper: Bound,
106    /// Whether the elements are declared `UNIQUE` (`OF UNIQUE ...`).
107    pub unique: bool,
108    /// Whether an `ARRAY`'s elements are declared `OPTIONAL`
109    /// (`ARRAY [1:3] OF OPTIONAL ...`).
110    pub optional_elements: bool,
111}
112
113impl Aggregation {
114    /// Creates an aggregation level with explicit bounds.
115    #[must_use]
116    pub const fn new(kind: AggregateKind, lower: Bound, upper: Bound) -> Self {
117        Self {
118            kind,
119            lower,
120            upper,
121            unique: false,
122            optional_elements: false,
123        }
124    }
125
126    /// Marks the elements `UNIQUE`.
127    #[must_use]
128    pub const fn unique(mut self) -> Self {
129        self.unique = true;
130        self
131    }
132
133    /// Marks an `ARRAY`'s elements `OPTIONAL`.
134    #[must_use]
135    pub const fn optional_elements(mut self) -> Self {
136        self.optional_elements = true;
137        self
138    }
139}
140
141/// One explicit positional attribute.
142#[derive(Debug, Clone, PartialEq, Eq)]
143#[non_exhaustive]
144pub struct Attribute {
145    /// Declared attribute name.
146    pub name: String,
147    /// Declared scalar type, or the innermost element type of an aggregate.
148    pub type_name: String,
149    /// Whether `OPTIONAL` was present.
150    pub optional: bool,
151    /// Whether a `LIST`, `SET`, `ARRAY`, or `BAG` wrapper was present. Equal
152    /// to `!aggregation.is_empty()`.
153    pub aggregate: bool,
154    /// Aggregation levels, outermost first: `LIST [2:3] OF UNIQUE LIST [1:?]
155    /// OF IfcLengthMeasure` has two, the first `unique`. Empty for a scalar.
156    pub aggregation: Vec<Aggregation>,
157}
158
159impl Attribute {
160    /// Creates a required scalar attribute.
161    #[must_use]
162    pub fn new(name: impl Into<String>, type_name: impl Into<String>) -> Self {
163        Self {
164            name: name.into(),
165            type_name: type_name.into(),
166            optional: false,
167            aggregate: false,
168            aggregation: Vec::new(),
169        }
170    }
171
172    /// Marks the attribute as optional.
173    #[must_use]
174    pub const fn optional(mut self) -> Self {
175        self.optional = true;
176        self
177    }
178
179    /// Marks the attribute as an aggregate without recording its shape.
180    ///
181    /// Prefer [`Self::with_aggregation`], which records the kind and bounds.
182    #[must_use]
183    pub const fn aggregate(mut self) -> Self {
184        self.aggregate = true;
185        self
186    }
187
188    /// Wraps the type in one more aggregation level, inside any already
189    /// added: call it outermost first.
190    #[must_use]
191    pub fn with_aggregation(mut self, aggregation: Aggregation) -> Self {
192        self.aggregation.push(aggregation);
193        self.aggregate = true;
194        self
195    }
196}
197
198/// One `INVERSE` attribute: `Name : SET [0:1] OF Entity FOR Attribute;`.
199#[derive(Debug, Clone, PartialEq, Eq)]
200#[non_exhaustive]
201pub struct InverseAttribute {
202    /// Declared name, unqualified.
203    pub name: String,
204    /// The supertype named when a subtype redeclares an inherited inverse
205    /// (`SELF\X.Name : ...`); `None` for a new inverse attribute.
206    pub redeclares: Option<String>,
207    /// The entity whose attribute points back at this one.
208    pub entity: String,
209    /// The attribute named after `FOR`, as written (it may be qualified as
210    /// `Entity.Attribute`).
211    pub for_attribute: String,
212    /// `SET` or `BAG` with its bounds; `None` for a single-valued inverse.
213    pub aggregation: Option<Aggregation>,
214}
215
216impl InverseAttribute {
217    /// Creates a single-valued inverse attribute.
218    #[must_use]
219    pub fn new(
220        name: impl Into<String>,
221        entity: impl Into<String>,
222        for_attribute: impl Into<String>,
223    ) -> Self {
224        Self {
225            name: name.into(),
226            redeclares: None,
227            entity: entity.into(),
228            for_attribute: for_attribute.into(),
229            aggregation: None,
230        }
231    }
232
233    /// Makes the inverse an aggregate (`SET` or `BAG`).
234    #[must_use]
235    pub fn with_aggregation(mut self, aggregation: Aggregation) -> Self {
236        self.aggregation = Some(aggregation);
237        self
238    }
239}
240
241/// One `UNIQUE` rule: the named attributes are unique, jointly, across all
242/// instances of the entity.
243#[derive(Debug, Clone, PartialEq, Eq)]
244#[non_exhaustive]
245pub struct UniqueRule {
246    /// Rule label, e.g. `UR1`; `None` when the rule is unlabelled.
247    pub label: Option<String>,
248    /// Attribute names as written; qualified names (`SELF\X.Y`) are kept.
249    pub attributes: Vec<String>,
250}
251
252impl UniqueRule {
253    /// Creates a labelled rule over `attributes`.
254    #[must_use]
255    pub fn new(label: impl Into<String>, attributes: Vec<String>) -> Self {
256        Self {
257            label: Some(label.into()),
258            attributes,
259        }
260    }
261}
262
263/// One `WHERE` rule: a named constraint an instance must satisfy.
264///
265/// EXPRESS states these as `Label : expression;`. The expression is kept as
266/// written rather than parsed: it is a full EXPRESS expression language
267/// (TYPEOF, SIZEOF, QUERY, arithmetic), and evaluating it is a separate
268/// concern from recording that the constraint exists and what it says.
269///
270/// Capturing them lets a consumer prove a claim like "no rule constrains
271/// this attribute" instead of asserting it from prose.
272#[derive(Debug, Clone, PartialEq, Eq)]
273#[non_exhaustive]
274pub struct WhereRule {
275    /// Rule label as declared, e.g. `CurveIs3D`.
276    pub label: String,
277    /// Constraint expression as written, whitespace-normalised.
278    pub expression: String,
279}
280
281impl WhereRule {
282    /// Creates a rule.
283    #[must_use]
284    pub fn new(label: impl Into<String>, expression: impl Into<String>) -> Self {
285        Self {
286            label: label.into(),
287            expression: expression.into(),
288        }
289    }
290}
291
292/// One explicit redeclaration of an inherited attribute: `SELF\X.a : T;`.
293#[derive(Debug, Clone, PartialEq, Eq)]
294#[non_exhaustive]
295pub struct Redeclaration {
296    /// The supertype named in the qualifier (`X`), as written.
297    pub supertype: String,
298    /// The inherited attribute (`a`), unqualified.
299    pub name: String,
300    /// The narrowed type token, extracted like [`Attribute::type_name`].
301    pub type_name: String,
302}
303
304impl Redeclaration {
305    /// Creates a redeclaration of the inherited `supertype.name`.
306    #[must_use]
307    pub fn new(
308        supertype: impl Into<String>,
309        name: impl Into<String>,
310        type_name: impl Into<String>,
311    ) -> Self {
312        Self {
313            supertype: supertype.into(),
314            name: name.into(),
315            type_name: type_name.into(),
316        }
317    }
318}
319
320/// One structural entity declaration.
321#[derive(Debug, Clone, PartialEq, Eq)]
322#[non_exhaustive]
323pub struct EntityDef {
324    /// Declared entity name.
325    pub name: String,
326    /// Direct supertypes in `SUBTYPE OF` order; empty for a root entity.
327    ///
328    /// EXPRESS allows several (`SUBTYPE OF (a, b)`), and the order is
329    /// load-bearing: ISO 10303-21:2016 §12.2.5.2 lays inherited attributes
330    /// out supertype by supertype in exactly this order. IFC never uses more
331    /// than one; the AP schemas do (AP242: 248 entities).
332    pub supertypes: Vec<String>,
333    /// Whether the declaration includes `ABSTRACT`.
334    pub abstract_: bool,
335    /// Explicit attributes declared by this entity, excluding derived and
336    /// inverse declarations.
337    pub attributes: Vec<Attribute>,
338    /// Names of attributes this entity declares in its `DERIVE` block.
339    ///
340    /// A subtype may redeclare an inherited explicit attribute as derived:
341    ///
342    /// ```text
343    /// DERIVE
344    ///   SELF\IfcGeometricRepresentationContext.Precision : IfcReal
345    ///       := NVL(ParentContext.Precision, 1.E-5);
346    /// ```
347    ///
348    /// The redeclaration keeps the attribute's inherited *position* but
349    /// removes it from what an instance may state: Part 21 writes such a slot
350    /// as `*`, not as a value and not as `$`. A consumer that does not know an
351    /// attribute is derived cannot tell those apart, so this list is the
352    /// minimum needed to write a conforming file.
353    ///
354    /// Names are stored unqualified — the `SELF\Entity.` prefix is stripped —
355    /// because that is how they match the inherited attribute they redeclare.
356    /// Entries are in declaration order. Derived attributes that are *new*
357    /// rather than redeclarations appear here too; they occupy no positional
358    /// slot, so consumers resolving slots should match against inherited
359    /// attribute names rather than assuming every entry is positional.
360    pub derived: Vec<String>,
361    /// Explicit redeclarations of inherited attributes (`SELF\X.a : T;`).
362    ///
363    /// A redeclaration narrows the type of an inherited attribute; it is not
364    /// a new attribute. ISO 10303-21:2016 §12.2.8: it has "no effect on the
365    /// encoding" and "shall not be considered an attribute of the subtype for
366    /// encoding purposes". It is therefore kept out of [`Self::attributes`],
367    /// which would otherwise grow a positional slot no record contains.
368    pub redeclared: Vec<Redeclaration>,
369    /// `WHERE` rules declared by this entity, in declaration order.
370    ///
371    /// Only this entity's own rules: EXPRESS does not merge a subtype's
372    /// rules with its supertype's, and a consumer checking an instance must
373    /// walk the supertype chain itself.
374    pub where_rules: Vec<WhereRule>,
375    /// `INVERSE` attributes declared by this entity, in declaration order.
376    pub inverses: Vec<InverseAttribute>,
377    /// `UNIQUE` rules declared by this entity, in declaration order.
378    pub unique_rules: Vec<UniqueRule>,
379}
380
381impl EntityDef {
382    /// Creates an empty concrete entity declaration.
383    #[must_use]
384    pub fn new(name: impl Into<String>) -> Self {
385        Self {
386            name: name.into(),
387            supertypes: Vec::new(),
388            abstract_: false,
389            attributes: Vec::new(),
390            derived: Vec::new(),
391            redeclared: Vec::new(),
392            where_rules: Vec::new(),
393            inverses: Vec::new(),
394            unique_rules: Vec::new(),
395        }
396    }
397
398    /// Marks the entity `ABSTRACT`.
399    #[must_use]
400    pub const fn abstract_(mut self) -> Self {
401        self.abstract_ = true;
402        self
403    }
404
405    /// Appends a `WHERE` rule.
406    #[must_use]
407    pub fn with_where_rule(mut self, rule: WhereRule) -> Self {
408        self.where_rules.push(rule);
409        self
410    }
411
412    /// Appends an `INVERSE` attribute.
413    #[must_use]
414    pub fn with_inverse(mut self, inverse: InverseAttribute) -> Self {
415        self.inverses.push(inverse);
416        self
417    }
418
419    /// Appends a `UNIQUE` rule.
420    #[must_use]
421    pub fn with_unique_rule(mut self, rule: UniqueRule) -> Self {
422        self.unique_rules.push(rule);
423        self
424    }
425
426    /// Appends a direct supertype, after any already declared.
427    ///
428    /// Call once per supertype in `SUBTYPE OF` order.
429    #[must_use]
430    pub fn with_supertype(mut self, supertype: impl Into<String>) -> Self {
431        self.supertypes.push(supertype.into());
432        self
433    }
434
435    /// The first direct supertype, if any.
436    ///
437    /// Sufficient for single-inheritance schemas such as IFC. Code that must
438    /// be correct for any EXPRESS schema reads [`Self::supertypes`].
439    #[must_use]
440    pub fn supertype(&self) -> Option<&str> {
441        self.supertypes.first().map(String::as_str)
442    }
443
444    /// Records an explicit redeclaration of an inherited attribute.
445    #[must_use]
446    pub fn with_redeclared(mut self, redeclaration: Redeclaration) -> Self {
447        self.redeclared.push(redeclaration);
448        self
449    }
450
451    /// Appends an explicit attribute in declaration order.
452    #[must_use]
453    pub fn with_attribute(mut self, attribute: Attribute) -> Self {
454        self.attributes.push(attribute);
455        self
456    }
457
458    /// Declares an attribute name as derived, as a `DERIVE` block would.
459    #[must_use]
460    pub fn with_derived(mut self, name: impl Into<String>) -> Self {
461        self.derived.push(name.into());
462        self
463    }
464
465    /// Whether `name` is declared derived by this entity.
466    ///
467    /// Comparison is ASCII case-insensitive: EXPRESS identifiers are
468    /// case-sensitive in principle, but schema text and Part 21 keywords
469    /// disagree on case often enough that matching exactly is a foot-gun.
470    #[must_use]
471    pub fn is_derived(&self, name: &str) -> bool {
472        self.derived
473            .iter()
474            .any(|declared| declared.eq_ignore_ascii_case(name))
475    }
476
477    /// Whether this entity explicitly redeclares the inherited `name`.
478    ///
479    /// Case-insensitive, like [`Self::is_derived`].
480    #[must_use]
481    pub fn is_redeclared(&self, name: &str) -> bool {
482        self.redeclared
483            .iter()
484            .any(|declared| declared.name.eq_ignore_ascii_case(name))
485    }
486}
487
488/// Structural shape of a `TYPE` declaration.
489#[derive(Debug, Clone, PartialEq, Eq)]
490#[non_exhaustive]
491pub enum TypeKind {
492    /// Alias or other right-hand-side syntax retained as text.
493    Defined(String),
494    /// `ENUMERATION OF` member names.
495    Enumeration(Vec<String>),
496    /// `SELECT` member type names.
497    Select(Vec<String>),
498}
499
500/// One `TYPE` declaration.
501#[derive(Debug, Clone, PartialEq, Eq)]
502#[non_exhaustive]
503pub struct TypeDef {
504    /// Declared type name.
505    pub name: String,
506    /// Structurally recognized declaration kind.
507    pub kind: TypeKind,
508}
509
510impl TypeDef {
511    /// Creates a type declaration.
512    #[must_use]
513    pub fn new(name: impl Into<String>, kind: TypeKind) -> Self {
514        Self {
515            name: name.into(),
516            kind,
517        }
518    }
519
520    /// Returns whether this declaration aliases another type.
521    #[must_use]
522    pub const fn is_defined(&self) -> bool {
523        matches!(self.kind, TypeKind::Defined(_))
524    }
525}
526
527/// Extracts the supported structural subset from EXPRESS source.
528///
529/// Unsupported declarations and executable expressions are skipped. This
530/// function is intentionally tolerant and returns the declarations it can
531/// identify rather than claiming full language validation.
532#[must_use]
533pub fn parse(source: &str) -> ParsedSchema {
534    let cleaned = strip_comments(source);
535    let upper = ascii_uppercase(&cleaned);
536    let name = schema_name(&cleaned, &upper).unwrap_or_default();
537    let entities = blocks(&cleaned, &upper, "ENTITY", "END_ENTITY")
538        .filter_map(parse_entity)
539        .collect();
540    let types = blocks(&cleaned, &upper, "TYPE", "END_TYPE")
541        .filter_map(parse_type)
542        .collect();
543    ParsedSchema {
544        name,
545        entities,
546        types,
547    }
548}
549
550fn strip_comments(source: &str) -> String {
551    let bytes = source.as_bytes();
552    let mut output = bytes.to_vec();
553    let mut position = 0;
554    let mut quoted = false;
555    while position < bytes.len() {
556        if bytes[position] == b'\'' {
557            if quoted && bytes.get(position + 1) == Some(&b'\'') {
558                position += 2;
559                continue;
560            }
561            quoted = !quoted;
562            position += 1;
563            continue;
564        }
565        if !quoted && bytes[position..].starts_with(b"(*") {
566            let start = position;
567            position += 2;
568            while position < bytes.len() && !bytes[position..].starts_with(b"*)") {
569                position += 1;
570            }
571            position = (position + 2).min(bytes.len());
572            blank_non_newlines(&mut output[start..position]);
573            continue;
574        }
575        if !quoted && bytes[position..].starts_with(b"--") {
576            let start = position;
577            position += 2;
578            while position < bytes.len() && bytes[position] != b'\n' {
579                position += 1;
580            }
581            blank_non_newlines(&mut output[start..position]);
582            continue;
583        }
584        position += 1;
585    }
586    String::from_utf8(output).expect("input was valid UTF-8")
587}
588
589fn blank_non_newlines(bytes: &mut [u8]) {
590    for byte in bytes {
591        if *byte != b'\n' && *byte != b'\r' {
592            *byte = b' ';
593        }
594    }
595}
596
597fn ascii_uppercase(source: &str) -> String {
598    let mut bytes = source.as_bytes().to_vec();
599    bytes.make_ascii_uppercase();
600    String::from_utf8(bytes).expect("ASCII case conversion preserves UTF-8")
601}
602
603fn schema_name(source: &str, upper: &str) -> Option<String> {
604    let start = find_keyword(upper, "SCHEMA", 0)? + "SCHEMA".len();
605    let end = source[start..].find(';')? + start;
606    source[start..end]
607        .split_whitespace()
608        .next()
609        .map(ToOwned::to_owned)
610}
611
612fn blocks<'a>(
613    source: &'a str,
614    upper: &'a str,
615    start_keyword: &'static str,
616    end_keyword: &'static str,
617) -> impl Iterator<Item = &'a str> {
618    let mut cursor = 0;
619    std::iter::from_fn(move || {
620        let start = find_keyword(upper, start_keyword, cursor)?;
621        let end_start = find_keyword(upper, end_keyword, start + start_keyword.len())?;
622        let semicolon = source[end_start..]
623            .find(';')
624            .map_or(source.len(), |offset| end_start + offset + 1);
625        cursor = semicolon;
626        Some(&source[start..semicolon])
627    })
628}
629
630fn find_keyword(haystack: &str, needle: &str, from: usize) -> Option<usize> {
631    let bytes = haystack.as_bytes();
632    let mut cursor = from;
633    while let Some(relative) = haystack[cursor..].find(needle) {
634        let position = cursor + relative;
635        let before = position.checked_sub(1).and_then(|index| bytes.get(index));
636        let after = bytes.get(position + needle.len());
637        if before.is_none_or(|byte| !is_identifier_byte(*byte))
638            && after.is_none_or(|byte| !is_identifier_byte(*byte))
639        {
640            return Some(position);
641        }
642        cursor = position + needle.len();
643    }
644    None
645}
646
647/// Find a block keyword (`DERIVE`, `INVERSE`, `UNIQUE`, `WHERE`) where it
648/// actually opens a block, rather than where it merely appears as a word.
649///
650/// EXPRESS reuses these words inside declarations: `LIST [1:?] OF UNIQUE
651/// IfcRepresentationMap` on `IfcTypeProduct` contains `UNIQUE` in the middle of
652/// an attribute, and treating that as the start of a UNIQUE block truncates the
653/// entity's attribute list. Both `IfcTypeProduct.RepresentationMaps` and `.Tag`
654/// were lost that way, which silently made every product type unauthorable.
655///
656/// A block keyword only opens a block at statement level: the first token after
657/// the previous statement's `;` (or after the entity header). Anything else is
658/// part of a declaration.
659fn find_block_keyword(source: &str, upper: &str, needle: &str, from: usize) -> Option<usize> {
660    let mut cursor = from;
661    while let Some(position) = find_keyword(upper, needle, cursor) {
662        let preceding = source[from..position]
663            .rfind(';')
664            .map_or(&source[from..position], |offset| {
665                &source[from + offset + 1..position]
666            });
667        if preceding.trim().is_empty() {
668            return Some(position);
669        }
670        cursor = position + needle.len();
671    }
672    None
673}
674
675fn is_identifier_byte(byte: u8) -> bool {
676    byte.is_ascii_alphanumeric() || byte == b'_'
677}
678
679fn parse_entity(block: &str) -> Option<EntityDef> {
680    let upper = ascii_uppercase(block);
681    let header_end = block.find(';')?;
682    let header = &block[..header_end];
683    let header_upper = &upper[..header_end];
684    let entity_position = find_keyword(header_upper, "ENTITY", 0)? + "ENTITY".len();
685    let name = header[entity_position..]
686        .split_whitespace()
687        .next()?
688        .trim_matches(|character: char| !character.is_alphanumeric() && character != '_')
689        .to_owned();
690    let supertypes = clause_names(header, header_upper, "SUBTYPE OF");
691    let abstract_ = find_keyword(header_upper, "ABSTRACT", 0).is_some();
692
693    let body_end = ["DERIVE", "INVERSE", "UNIQUE", "WHERE", "END_ENTITY"]
694        .into_iter()
695        .filter_map(|keyword| find_block_keyword(block, &upper, keyword, header_end + 1))
696        .min()
697        .unwrap_or(block.len());
698    let mut attributes = Vec::new();
699    let mut redeclared = Vec::new();
700    for statement in block[header_end + 1..body_end].split(';') {
701        if let Some(redeclaration) = parse_redeclaration(statement) {
702            redeclared.push(redeclaration);
703        } else if let Some(attribute) = parse_attribute(statement) {
704            attributes.push(attribute);
705        }
706    }
707    let derived = parse_derive_block(block, &upper, header_end + 1);
708    let where_rules = parse_where_block(block, &upper, header_end + 1);
709    let inverses = block_statements(block, &upper, "INVERSE", header_end + 1)
710        .filter_map(parse_inverse)
711        .collect();
712    let unique_rules = block_statements(block, &upper, "UNIQUE", header_end + 1)
713        .filter_map(parse_unique_rule)
714        .collect();
715
716    Some(EntityDef {
717        name,
718        supertypes,
719        abstract_,
720        attributes,
721        derived,
722        redeclared,
723        where_rules,
724        inverses,
725        unique_rules,
726    })
727}
728
729/// The `;`-separated statements of one entity block (`INVERSE`, `UNIQUE`),
730/// from its statement-level keyword to the next block keyword or
731/// `END_ENTITY`. Blocks appear in the order DERIVE, INVERSE, UNIQUE, WHERE.
732fn block_statements<'a>(
733    block: &'a str,
734    upper: &str,
735    keyword: &'static str,
736    from: usize,
737) -> impl Iterator<Item = &'a str> {
738    let range = find_block_keyword(block, upper, keyword, from).and_then(|start| {
739        let start = start + keyword.len();
740        let end = ["INVERSE", "UNIQUE", "WHERE", "END_ENTITY"]
741            .into_iter()
742            .filter(|next| *next != keyword)
743            .filter_map(|next| find_block_keyword(block, upper, next, start))
744            .min()
745            .unwrap_or(block.len());
746        (start < end).then_some(start..end)
747    });
748    range
749        .map_or("", |range| &block[range])
750        .split(';')
751        .filter(|statement| !statement.trim().is_empty())
752}
753
754/// Parse `Name : [SET|BAG [l:u] OF] Entity FOR Attribute`.
755fn parse_inverse(statement: &str) -> Option<InverseAttribute> {
756    let (target, declaration) = statement.split_once(':')?;
757    let target = target.trim();
758    let (redeclares, name) = match target
759        .get(..5)
760        .filter(|prefix| prefix.eq_ignore_ascii_case("SELF\\"))
761    {
762        Some(_) => {
763            let (supertype, name) = target[5..].split_once('.')?;
764            (Some(supertype.trim().to_owned()), name.trim())
765        }
766        None => (None, target),
767    };
768    if name.is_empty() || !name.bytes().all(is_identifier_byte) {
769        return None;
770    }
771    let upper = ascii_uppercase(declaration);
772    let for_position = find_keyword(&upper, "FOR", 0)?;
773    let for_attribute = declaration[for_position + "FOR".len()..].trim();
774    let (aggregation, element) = aggregation_levels(&declaration[..for_position]);
775    let entity = element.split_whitespace().next()?;
776    if for_attribute.is_empty() || aggregation.len() > 1 {
777        return None;
778    }
779    Some(InverseAttribute {
780        name: name.to_owned(),
781        redeclares,
782        entity: entity.to_owned(),
783        for_attribute: for_attribute.split_whitespace().collect(),
784        aggregation: aggregation.into_iter().next(),
785    })
786}
787
788/// Parse `[Label :] a, b, ...` from a `UNIQUE` block.
789fn parse_unique_rule(statement: &str) -> Option<UniqueRule> {
790    // A qualified attribute (`SELF\X.Y`) contains no `:`, so a `:` can only
791    // end the label.
792    let (label, attributes) = match statement.split_once(':') {
793        Some((label, attributes)) => {
794            let label = label.trim();
795            if label.is_empty() || !label.bytes().all(is_identifier_byte) {
796                return None;
797            }
798            (Some(label.to_owned()), attributes)
799        }
800        None => (None, statement),
801    };
802    let attributes: Vec<String> = attributes
803        .split(',')
804        .map(|attribute| attribute.split_whitespace().collect::<String>())
805        .filter(|attribute| !attribute.is_empty())
806        .collect();
807    (!attributes.is_empty()).then_some(UniqueRule { label, attributes })
808}
809
810/// Split the aggregation levels off a type expression, outermost first,
811/// returning them and the remaining element type text.
812///
813/// `LIST [2:3] OF UNIQUE LIST [1:?] OF IfcLengthMeasure` yields two levels
814/// and `IfcLengthMeasure`. Bounds are optional for `LIST`, `SET` and `BAG`
815/// (then `[0:?]`); `ARRAY` requires them but is not rejected without.
816fn aggregation_levels(declaration: &str) -> (Vec<Aggregation>, &str) {
817    let mut levels = Vec::new();
818    let mut rest = declaration.trim_start();
819    loop {
820        let word_end = rest
821            .find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
822            .unwrap_or(rest.len());
823        let Some(kind) = AggregateKind::from_keyword(&rest[..word_end].to_ascii_uppercase()) else {
824            return (levels, rest);
825        };
826        let mut after = rest[word_end..].trim_start();
827        let (lower, upper) = if let Some(inner) = after.strip_prefix('[') {
828            let Some(close) = inner.find(']') else {
829                return (levels, rest);
830            };
831            let bounds = &inner[..close];
832            after = inner[close + 1..].trim_start();
833            match bounds.split_once(':') {
834                Some((lower, upper)) => (Bound::parse(lower), Bound::parse(upper)),
835                None => (Bound::parse(bounds), Bound::parse(bounds)),
836            }
837        } else {
838            (Bound::Integer(0), Bound::Unbounded)
839        };
840        let Some(of) = after
841            .get(..2)
842            .filter(|word| word.eq_ignore_ascii_case("OF"))
843            .map(|_| after[2..].trim_start())
844        else {
845            return (levels, rest);
846        };
847        let mut level = Aggregation::new(kind, lower, upper);
848        rest = of;
849        for (keyword, mark) in [
850            (
851                "OPTIONAL",
852                Aggregation::optional_elements as fn(Aggregation) -> Aggregation,
853            ),
854            ("UNIQUE", Aggregation::unique),
855        ] {
856            if rest
857                .get(..keyword.len())
858                .is_some_and(|word| word.eq_ignore_ascii_case(keyword))
859                && !rest[keyword.len()..]
860                    .starts_with(|c: char| c.is_ascii_alphanumeric() || c == '_')
861            {
862                level = mark(level);
863                rest = rest[keyword.len()..].trim_start();
864            }
865        }
866        levels.push(level);
867    }
868}
869
870/// Collect the `WHERE` rules declared by one entity.
871///
872/// The block runs from a statement-level `WHERE` to `END_ENTITY`. Each rule is
873/// `Label : expression;`. `find_block_keyword` is required rather than a plain
874/// keyword search: `WHERE` also appears inside QUERY expressions
875/// (`QUERY(t <* Types | WHERE ...)`), and treating one of those as the block
876/// start would drop every rule declared before it.
877///
878/// Splitting rules on `;` is safe because EXPRESS expressions contain no
879/// semicolons; a rule's expression may still span lines, so whitespace is
880/// normalised to keep the stored text comparable.
881/// Parse one `Label : expression` statement from a `WHERE` block.
882///
883/// The label ends at the first `:`. A rule expression may itself contain `:`
884/// (`a <= b : c` does not occur, but qualified enum references like
885/// `IfcEnum.VALUE` and ranges do), so only the first is a separator.
886fn parse_where_rule(statement: &str) -> Option<WhereRule> {
887    let (label, expression) = statement.split_once(':')?;
888    let label = label.trim();
889    if label.is_empty() || !label.bytes().all(is_identifier_byte) {
890        return None;
891    }
892    let expression = expression.split_whitespace().collect::<Vec<_>>().join(" ");
893    if expression.is_empty() {
894        return None;
895    }
896    Some(WhereRule {
897        label: label.to_owned(),
898        expression,
899    })
900}
901
902fn parse_where_block(block: &str, upper: &str, from: usize) -> Vec<WhereRule> {
903    let Some(start) = find_block_keyword(block, upper, "WHERE", from) else {
904        return Vec::new();
905    };
906    let start = start + "WHERE".len();
907    let end = find_keyword(upper, "END_ENTITY", start).unwrap_or(block.len());
908    if end <= start {
909        return Vec::new();
910    }
911    block[start..end]
912        .split(';')
913        .filter_map(parse_where_rule)
914        .collect()
915}
916
917/// Collect the attribute names declared in an entity's `DERIVE` block.
918///
919/// The block runs from `DERIVE` to whichever of `INVERSE`/`UNIQUE`/`WHERE`/
920/// `END_ENTITY` comes first. Each statement looks like
921/// `SELF\Super.Name : Type := expression;` for a redeclaration, or
922/// `Name : Type := expression;` for a new derived attribute.
923///
924/// Splitting on `;` is safe here because the initialiser expressions in a
925/// DERIVE block are EXPRESS expressions, which do not contain semicolons.
926fn parse_derive_block(block: &str, upper: &str, from: usize) -> Vec<String> {
927    let Some(start) = find_keyword(upper, "DERIVE", from) else {
928        return Vec::new();
929    };
930    let start = start + "DERIVE".len();
931    let end = ["INVERSE", "UNIQUE", "WHERE", "END_ENTITY"]
932        .into_iter()
933        .filter_map(|keyword| find_keyword(upper, keyword, start))
934        .min()
935        .unwrap_or(block.len());
936    if end <= start {
937        return Vec::new();
938    }
939
940    block[start..end]
941        .split(';')
942        .filter_map(derived_attribute_name)
943        .collect()
944}
945
946/// Extract the attribute name from one `DERIVE` statement.
947///
948/// `SELF\IfcGeometricRepresentationContext.Precision : IfcReal := ...` yields
949/// `Precision`: the qualifying `SELF\Entity.` prefix names the supertype the
950/// attribute is inherited from, not the attribute.
951fn derived_attribute_name(statement: &str) -> Option<String> {
952    let (target, _) = statement.split_once(':')?;
953    let target = target.trim();
954    // A redeclaration qualifies the name with the declaring supertype; the
955    // attribute itself is the final dotted segment.
956    let name = target.rsplit('.').next()?.trim();
957    let name = name.rsplit('\\').next()?.trim();
958    if name.is_empty() || !name.bytes().all(is_identifier_byte) {
959        return None;
960    }
961    Some(name.to_owned())
962}
963
964/// Every name in a `CLAUSE (a, b, ...)` list, in written order.
965fn clause_names(header: &str, upper: &str, clause: &str) -> Vec<String> {
966    let Some(position) = find_keyword(upper, clause, 0).map(|p| p + clause.len()) else {
967        return Vec::new();
968    };
969    let Some(open) = header[position..].find('(').map(|o| position + o + 1) else {
970        return Vec::new();
971    };
972    let Some(close) = header[open..].find(')').map(|o| open + o) else {
973        return Vec::new();
974    };
975    header[open..close]
976        .split(',')
977        .map(str::trim)
978        .filter(|name| !name.is_empty())
979        .map(ToOwned::to_owned)
980        .collect()
981}
982
983/// Parse `SELF\X.a : T` as an explicit redeclaration, or `None` for an
984/// ordinary attribute statement.
985fn parse_redeclaration(statement: &str) -> Option<Redeclaration> {
986    let (target, _) = statement.split_once(':')?;
987    let target = target.trim();
988    let qualified = target
989        .get(..5)
990        .filter(|prefix| prefix.eq_ignore_ascii_case("SELF\\"))
991        .map(|_| &target[5..])?;
992    let (supertype, name) = qualified.split_once('.')?;
993    let (supertype, name) = (supertype.trim(), name.trim());
994    if supertype.is_empty()
995        || name.is_empty()
996        || !supertype.bytes().all(is_identifier_byte)
997        || !name.bytes().all(is_identifier_byte)
998    {
999        return None;
1000    }
1001    // Reuse the attribute type extraction on a synthetic plain statement.
1002    let type_name =
1003        parse_attribute(&format!("{name}{}", &statement[statement.find(':')?..]))?.type_name;
1004    Some(Redeclaration {
1005        supertype: supertype.to_owned(),
1006        name: name.to_owned(),
1007        type_name,
1008    })
1009}
1010
1011fn parse_attribute(statement: &str) -> Option<Attribute> {
1012    let (name, declaration) = statement.split_once(':')?;
1013    let name = name.trim();
1014    if name.is_empty() {
1015        return None;
1016    }
1017    let mut declaration = declaration.trim();
1018    let optional = declaration
1019        .get(.."OPTIONAL".len())
1020        .is_some_and(|word| word.eq_ignore_ascii_case("OPTIONAL"))
1021        && !declaration["OPTIONAL".len()..]
1022            .starts_with(|c: char| c.is_ascii_alphanumeric() || c == '_');
1023    if optional {
1024        declaration = declaration["OPTIONAL".len()..].trim_start();
1025    }
1026    let (aggregation, element) = aggregation_levels(declaration);
1027    let type_name = element
1028        .split_whitespace()
1029        .next()?
1030        .trim_matches(|character: char| matches!(character, '(' | ')' | ';'))
1031        .to_owned();
1032    Some(Attribute {
1033        name: name.to_owned(),
1034        type_name,
1035        optional,
1036        aggregate: !aggregation.is_empty(),
1037        aggregation,
1038    })
1039}
1040
1041fn parse_type(block: &str) -> Option<TypeDef> {
1042    let upper = ascii_uppercase(block);
1043    let statement_end = block.find(';')?;
1044    let statement = &block[..statement_end];
1045    let statement_upper = &upper[..statement_end];
1046    let type_position = find_keyword(statement_upper, "TYPE", 0)? + "TYPE".len();
1047    let equals = statement[type_position..].find('=')? + type_position;
1048    let name = statement[type_position..equals].trim().to_owned();
1049    let right = statement[equals + 1..].trim();
1050    let right_upper = ascii_uppercase(right);
1051    let kind = if let Some(position) = find_keyword(&right_upper, "ENUMERATION", 0) {
1052        TypeKind::Enumeration(parenthesized_names(right, position + "ENUMERATION".len()))
1053    } else if let Some(position) = find_keyword(&right_upper, "SELECT", 0) {
1054        TypeKind::Select(parenthesized_names(right, position + "SELECT".len()))
1055    } else {
1056        TypeKind::Defined(right.to_owned())
1057    };
1058    Some(TypeDef { name, kind })
1059}
1060
1061fn parenthesized_names(source: &str, from: usize) -> Vec<String> {
1062    let Some(open) = source[from..].find('(').map(|offset| from + offset + 1) else {
1063        return Vec::new();
1064    };
1065    let close = source[open..]
1066        .find(')')
1067        .map_or(source.len(), |offset| open + offset);
1068    source[open..close]
1069        .split(',')
1070        .map(str::trim)
1071        .filter(|name| !name.is_empty())
1072        .map(ToOwned::to_owned)
1073        .collect()
1074}