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, the names of derived attributes, defined types, enumerations,
5//! and selects. It deliberately does **not** implement full EXPRESS semantics:
6//! expressions, rules, functions, procedures, constants, uniqueness
7//! constraints, inverse relationships, and complete type checking remain
8//! opaque. Derived attributes are reported by *name only* — their initialiser
9//! expressions are not evaluated. Consumers needing language validation must
10//! use a complete EXPRESS implementation.
11
12/// A structurally parsed schema.
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct ParsedSchema {
15    /// Declared schema name, or an empty string when absent.
16    pub name: String,
17    /// Entity declarations in source order.
18    pub entities: Vec<EntityDef>,
19    /// Type declarations in source order.
20    pub types: Vec<TypeDef>,
21}
22
23/// One explicit positional attribute.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Attribute {
26    /// Declared attribute name.
27    pub name: String,
28    /// Declared scalar or element type token.
29    pub type_name: String,
30    /// Whether `OPTIONAL` was present.
31    pub optional: bool,
32    /// Whether a `LIST`, `SET`, `ARRAY`, or `BAG` wrapper was present.
33    pub aggregate: bool,
34}
35
36impl Attribute {
37    /// Creates a required scalar attribute.
38    #[must_use]
39    pub fn new(name: impl Into<String>, type_name: impl Into<String>) -> Self {
40        Self {
41            name: name.into(),
42            type_name: type_name.into(),
43            optional: false,
44            aggregate: false,
45        }
46    }
47
48    /// Marks the attribute as optional.
49    #[must_use]
50    pub const fn optional(mut self) -> Self {
51        self.optional = true;
52        self
53    }
54
55    /// Marks the attribute as an aggregate.
56    #[must_use]
57    pub const fn aggregate(mut self) -> Self {
58        self.aggregate = true;
59        self
60    }
61}
62
63/// One structural entity declaration.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct EntityDef {
66    /// Declared entity name.
67    pub name: String,
68    /// First direct supertype, when a `SUBTYPE OF` clause is present.
69    pub supertype: Option<String>,
70    /// Whether the declaration includes `ABSTRACT`.
71    pub abstract_: bool,
72    /// Explicit attributes declared by this entity, excluding derived and
73    /// inverse declarations.
74    pub attributes: Vec<Attribute>,
75    /// Names of attributes this entity declares in its `DERIVE` block.
76    ///
77    /// A subtype may redeclare an inherited explicit attribute as derived:
78    ///
79    /// ```text
80    /// DERIVE
81    ///   SELF\IfcGeometricRepresentationContext.Precision : IfcReal
82    ///       := NVL(ParentContext.Precision, 1.E-5);
83    /// ```
84    ///
85    /// The redeclaration keeps the attribute's inherited *position* but
86    /// removes it from what an instance may state: Part 21 writes such a slot
87    /// as `*`, not as a value and not as `$`. A consumer that does not know an
88    /// attribute is derived cannot tell those apart, so this list is the
89    /// minimum needed to write a conforming file.
90    ///
91    /// Names are stored unqualified — the `SELF\Entity.` prefix is stripped —
92    /// because that is how they match the inherited attribute they redeclare.
93    /// Entries are in declaration order. Derived attributes that are *new*
94    /// rather than redeclarations appear here too; they occupy no positional
95    /// slot, so consumers resolving slots should match against inherited
96    /// attribute names rather than assuming every entry is positional.
97    pub derived: Vec<String>,
98}
99
100impl EntityDef {
101    /// Creates an empty concrete entity declaration.
102    #[must_use]
103    pub fn new(name: impl Into<String>) -> Self {
104        Self {
105            name: name.into(),
106            supertype: None,
107            abstract_: false,
108            attributes: Vec::new(),
109            derived: Vec::new(),
110        }
111    }
112
113    /// Sets the direct supertype.
114    #[must_use]
115    pub fn with_supertype(mut self, supertype: impl Into<String>) -> Self {
116        self.supertype = Some(supertype.into());
117        self
118    }
119
120    /// Appends an explicit attribute in declaration order.
121    #[must_use]
122    pub fn with_attribute(mut self, attribute: Attribute) -> Self {
123        self.attributes.push(attribute);
124        self
125    }
126
127    /// Declares an attribute name as derived, as a `DERIVE` block would.
128    #[must_use]
129    pub fn with_derived(mut self, name: impl Into<String>) -> Self {
130        self.derived.push(name.into());
131        self
132    }
133
134    /// Whether `name` is declared derived by this entity.
135    ///
136    /// Comparison is ASCII case-insensitive: EXPRESS identifiers are
137    /// case-sensitive in principle, but schema text and Part 21 keywords
138    /// disagree on case often enough that matching exactly is a foot-gun.
139    #[must_use]
140    pub fn is_derived(&self, name: &str) -> bool {
141        self.derived
142            .iter()
143            .any(|declared| declared.eq_ignore_ascii_case(name))
144    }
145}
146
147/// Structural shape of a `TYPE` declaration.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub enum TypeKind {
150    /// Alias or other right-hand-side syntax retained as text.
151    Defined(String),
152    /// `ENUMERATION OF` member names.
153    Enumeration(Vec<String>),
154    /// `SELECT` member type names.
155    Select(Vec<String>),
156}
157
158/// One `TYPE` declaration.
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct TypeDef {
161    /// Declared type name.
162    pub name: String,
163    /// Structurally recognized declaration kind.
164    pub kind: TypeKind,
165}
166
167impl TypeDef {
168    /// Returns whether this declaration aliases another type.
169    #[must_use]
170    pub const fn is_defined(&self) -> bool {
171        matches!(self.kind, TypeKind::Defined(_))
172    }
173}
174
175/// Extracts the supported structural subset from EXPRESS source.
176///
177/// Unsupported declarations and executable expressions are skipped. This
178/// function is intentionally tolerant and returns the declarations it can
179/// identify rather than claiming full language validation.
180#[must_use]
181pub fn parse(source: &str) -> ParsedSchema {
182    let cleaned = strip_comments(source);
183    let upper = ascii_uppercase(&cleaned);
184    let name = schema_name(&cleaned, &upper).unwrap_or_default();
185    let entities = blocks(&cleaned, &upper, "ENTITY", "END_ENTITY")
186        .filter_map(parse_entity)
187        .collect();
188    let types = blocks(&cleaned, &upper, "TYPE", "END_TYPE")
189        .filter_map(parse_type)
190        .collect();
191    ParsedSchema {
192        name,
193        entities,
194        types,
195    }
196}
197
198fn strip_comments(source: &str) -> String {
199    let bytes = source.as_bytes();
200    let mut output = bytes.to_vec();
201    let mut position = 0;
202    let mut quoted = false;
203    while position < bytes.len() {
204        if bytes[position] == b'\'' {
205            if quoted && bytes.get(position + 1) == Some(&b'\'') {
206                position += 2;
207                continue;
208            }
209            quoted = !quoted;
210            position += 1;
211            continue;
212        }
213        if !quoted && bytes[position..].starts_with(b"(*") {
214            let start = position;
215            position += 2;
216            while position < bytes.len() && !bytes[position..].starts_with(b"*)") {
217                position += 1;
218            }
219            position = (position + 2).min(bytes.len());
220            blank_non_newlines(&mut output[start..position]);
221            continue;
222        }
223        if !quoted && bytes[position..].starts_with(b"--") {
224            let start = position;
225            position += 2;
226            while position < bytes.len() && bytes[position] != b'\n' {
227                position += 1;
228            }
229            blank_non_newlines(&mut output[start..position]);
230            continue;
231        }
232        position += 1;
233    }
234    String::from_utf8(output).expect("input was valid UTF-8")
235}
236
237fn blank_non_newlines(bytes: &mut [u8]) {
238    for byte in bytes {
239        if *byte != b'\n' && *byte != b'\r' {
240            *byte = b' ';
241        }
242    }
243}
244
245fn ascii_uppercase(source: &str) -> String {
246    let mut bytes = source.as_bytes().to_vec();
247    bytes.make_ascii_uppercase();
248    String::from_utf8(bytes).expect("ASCII case conversion preserves UTF-8")
249}
250
251fn schema_name(source: &str, upper: &str) -> Option<String> {
252    let start = find_keyword(upper, "SCHEMA", 0)? + "SCHEMA".len();
253    let end = source[start..].find(';')? + start;
254    source[start..end]
255        .split_whitespace()
256        .next()
257        .map(ToOwned::to_owned)
258}
259
260fn blocks<'a>(
261    source: &'a str,
262    upper: &'a str,
263    start_keyword: &'static str,
264    end_keyword: &'static str,
265) -> impl Iterator<Item = &'a str> {
266    let mut cursor = 0;
267    std::iter::from_fn(move || {
268        let start = find_keyword(upper, start_keyword, cursor)?;
269        let end_start = find_keyword(upper, end_keyword, start + start_keyword.len())?;
270        let semicolon = source[end_start..]
271            .find(';')
272            .map_or(source.len(), |offset| end_start + offset + 1);
273        cursor = semicolon;
274        Some(&source[start..semicolon])
275    })
276}
277
278fn find_keyword(haystack: &str, needle: &str, from: usize) -> Option<usize> {
279    let bytes = haystack.as_bytes();
280    let mut cursor = from;
281    while let Some(relative) = haystack[cursor..].find(needle) {
282        let position = cursor + relative;
283        let before = position.checked_sub(1).and_then(|index| bytes.get(index));
284        let after = bytes.get(position + needle.len());
285        if before.is_none_or(|byte| !is_identifier_byte(*byte))
286            && after.is_none_or(|byte| !is_identifier_byte(*byte))
287        {
288            return Some(position);
289        }
290        cursor = position + needle.len();
291    }
292    None
293}
294
295/// Find a block keyword (`DERIVE`, `INVERSE`, `UNIQUE`, `WHERE`) where it
296/// actually opens a block, rather than where it merely appears as a word.
297///
298/// EXPRESS reuses these words inside declarations: `LIST [1:?] OF UNIQUE
299/// IfcRepresentationMap` on `IfcTypeProduct` contains `UNIQUE` in the middle of
300/// an attribute, and treating that as the start of a UNIQUE block truncates the
301/// entity's attribute list. Both `IfcTypeProduct.RepresentationMaps` and `.Tag`
302/// were lost that way, which silently made every product type unauthorable.
303///
304/// A block keyword only opens a block at statement level: the first token after
305/// the previous statement's `;` (or after the entity header). Anything else is
306/// part of a declaration.
307fn find_block_keyword(source: &str, upper: &str, needle: &str, from: usize) -> Option<usize> {
308    let mut cursor = from;
309    while let Some(position) = find_keyword(upper, needle, cursor) {
310        let preceding = source[from..position]
311            .rfind(';')
312            .map_or(&source[from..position], |offset| {
313                &source[from + offset + 1..position]
314            });
315        if preceding.trim().is_empty() {
316            return Some(position);
317        }
318        cursor = position + needle.len();
319    }
320    None
321}
322
323fn is_identifier_byte(byte: u8) -> bool {
324    byte.is_ascii_alphanumeric() || byte == b'_'
325}
326
327fn parse_entity(block: &str) -> Option<EntityDef> {
328    let upper = ascii_uppercase(block);
329    let header_end = block.find(';')?;
330    let header = &block[..header_end];
331    let header_upper = &upper[..header_end];
332    let entity_position = find_keyword(header_upper, "ENTITY", 0)? + "ENTITY".len();
333    let name = header[entity_position..]
334        .split_whitespace()
335        .next()?
336        .trim_matches(|character: char| !character.is_alphanumeric() && character != '_')
337        .to_owned();
338    let supertype = clause_name(header, header_upper, "SUBTYPE OF");
339    let abstract_ = find_keyword(header_upper, "ABSTRACT", 0).is_some();
340
341    let body_end = ["DERIVE", "INVERSE", "UNIQUE", "WHERE", "END_ENTITY"]
342        .into_iter()
343        .filter_map(|keyword| find_block_keyword(block, &upper, keyword, header_end + 1))
344        .min()
345        .unwrap_or(block.len());
346    let attributes = block[header_end + 1..body_end]
347        .split(';')
348        .filter_map(parse_attribute)
349        .collect();
350    let derived = parse_derive_block(block, &upper, header_end + 1);
351
352    Some(EntityDef {
353        name,
354        supertype,
355        abstract_,
356        attributes,
357        derived,
358    })
359}
360
361/// Collect the attribute names declared in an entity's `DERIVE` block.
362///
363/// The block runs from `DERIVE` to whichever of `INVERSE`/`UNIQUE`/`WHERE`/
364/// `END_ENTITY` comes first. Each statement looks like
365/// `SELF\Super.Name : Type := expression;` for a redeclaration, or
366/// `Name : Type := expression;` for a new derived attribute.
367///
368/// Splitting on `;` is safe here because the initialiser expressions in a
369/// DERIVE block are EXPRESS expressions, which do not contain semicolons.
370fn parse_derive_block(block: &str, upper: &str, from: usize) -> Vec<String> {
371    let Some(start) = find_keyword(upper, "DERIVE", from) else {
372        return Vec::new();
373    };
374    let start = start + "DERIVE".len();
375    let end = ["INVERSE", "UNIQUE", "WHERE", "END_ENTITY"]
376        .into_iter()
377        .filter_map(|keyword| find_keyword(upper, keyword, start))
378        .min()
379        .unwrap_or(block.len());
380    if end <= start {
381        return Vec::new();
382    }
383
384    block[start..end]
385        .split(';')
386        .filter_map(derived_attribute_name)
387        .collect()
388}
389
390/// Extract the attribute name from one `DERIVE` statement.
391///
392/// `SELF\IfcGeometricRepresentationContext.Precision : IfcReal := ...` yields
393/// `Precision`: the qualifying `SELF\Entity.` prefix names the supertype the
394/// attribute is inherited from, not the attribute.
395fn derived_attribute_name(statement: &str) -> Option<String> {
396    let (target, _) = statement.split_once(':')?;
397    let target = target.trim();
398    // A redeclaration qualifies the name with the declaring supertype; the
399    // attribute itself is the final dotted segment.
400    let name = target.rsplit('.').next()?.trim();
401    let name = name.rsplit('\\').next()?.trim();
402    if name.is_empty() || !name.bytes().all(is_identifier_byte) {
403        return None;
404    }
405    Some(name.to_owned())
406}
407
408fn clause_name(header: &str, upper: &str, clause: &str) -> Option<String> {
409    let position = find_keyword(upper, clause, 0)? + clause.len();
410    let open = header[position..].find('(')? + position + 1;
411    let close = header[open..].find(')')? + open;
412    header[open..close]
413        .split(',')
414        .next()
415        .map(str::trim)
416        .filter(|name| !name.is_empty())
417        .map(ToOwned::to_owned)
418}
419
420fn parse_attribute(statement: &str) -> Option<Attribute> {
421    let (name, declaration) = statement.split_once(':')?;
422    let name = name.trim();
423    if name.is_empty() {
424        return None;
425    }
426    let declaration = declaration.trim();
427    let upper = ascii_uppercase(declaration);
428    let optional = find_keyword(&upper, "OPTIONAL", 0).is_some();
429    let aggregate = ["LIST", "SET", "ARRAY", "BAG"]
430        .into_iter()
431        .any(|keyword| find_keyword(&upper, keyword, 0).is_some());
432    let scalar = if aggregate {
433        find_keyword(&upper, "OF", 0)
434            .map_or(declaration, |position| declaration[position + 2..].trim())
435    } else if optional {
436        find_keyword(&upper, "OPTIONAL", 0).map_or(declaration, |position| {
437            declaration[position + "OPTIONAL".len()..].trim()
438        })
439    } else {
440        declaration
441    };
442    let type_name = scalar
443        .trim_start_matches(|character: char| character.is_ascii_whitespace())
444        .strip_prefix("UNIQUE ")
445        .unwrap_or(scalar)
446        .split_whitespace()
447        .next()?
448        .trim_matches(|character: char| matches!(character, '(' | ')' | ';'))
449        .to_owned();
450    Some(Attribute {
451        name: name.to_owned(),
452        type_name,
453        optional,
454        aggregate,
455    })
456}
457
458fn parse_type(block: &str) -> Option<TypeDef> {
459    let upper = ascii_uppercase(block);
460    let statement_end = block.find(';')?;
461    let statement = &block[..statement_end];
462    let statement_upper = &upper[..statement_end];
463    let type_position = find_keyword(statement_upper, "TYPE", 0)? + "TYPE".len();
464    let equals = statement[type_position..].find('=')? + type_position;
465    let name = statement[type_position..equals].trim().to_owned();
466    let right = statement[equals + 1..].trim();
467    let right_upper = ascii_uppercase(right);
468    let kind = if let Some(position) = find_keyword(&right_upper, "ENUMERATION", 0) {
469        TypeKind::Enumeration(parenthesized_names(right, position + "ENUMERATION".len()))
470    } else if let Some(position) = find_keyword(&right_upper, "SELECT", 0) {
471        TypeKind::Select(parenthesized_names(right, position + "SELECT".len()))
472    } else {
473        TypeKind::Defined(right.to_owned())
474    };
475    Some(TypeDef { name, kind })
476}
477
478fn parenthesized_names(source: &str, from: usize) -> Vec<String> {
479    let Some(open) = source[from..].find('(').map(|offset| from + offset + 1) else {
480        return Vec::new();
481    };
482    let close = source[open..]
483        .find(')')
484        .map_or(source.len(), |offset| open + offset);
485    source[open..close]
486        .split(',')
487        .map(str::trim)
488        .filter(|name| !name.is_empty())
489        .map(ToOwned::to_owned)
490        .collect()
491}