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