Skip to main content

ifc_properties/
exact.rs

1//! Exact, fail-closed IFC2X3/IFC4/IFC4X3 property resolution.
2//!
3//! Unlike the permissive views, this traversal rejects any incomplete or
4//! malformed assignment data before it can claim an exact absence. Every
5//! structural fact comes from the table of the one release the header
6//! declares; nothing is aliased across releases.
7//!
8//! # Exact versus permissive
9//!
10//! The permissive views (`property_set`, `quantity_sets`, `query`) are for
11//! interactive inspection. A rule engine, validator or checker uses this
12//! exact API and must never map one of its errors to "absent": an error
13//! means the file could not prove the answer, which is not the same as the
14//! property being missing.
15//!
16//! Exact values keep their declared IFC value type and explicit unit
17//! identity. A downstream adapter that cannot project a value category or
18//! unit losslessly into its own model must reject it rather than coerce it.
19
20mod assignment;
21mod complex;
22mod composite;
23mod enumerate;
24mod material;
25mod measure;
26mod predefined;
27mod quantity;
28mod refs;
29mod release;
30mod set;
31mod unit;
32mod value;
33mod values;
34
35use std::{fmt, sync::Arc};
36
37use ifc_model::{EntityId, Model};
38use ifc_schema::SchemaVersion;
39
40use assignment::assigned_sets;
41pub use enumerate::{
42    exact_properties, exact_properties_where, exact_property_sets_where, ExactPropertyEntry,
43    ExactPropertySetEntry,
44};
45pub use material::{
46    exact_material_properties_where, exact_material_property, exact_material_property_sets_where,
47};
48pub use predefined::{exact_predefined_sets, ExactPredefinedSet};
49use release::validate_model;
50use set::find_property;
51pub use unit::{exact_unit, ExactUnit, ExactUnitError};
52pub use values::{
53    ExactBoundedValue, ExactComplexMember, ExactComplexValue, ExactEntityRef, ExactEnumeratedValue,
54    ExactEnumeration, ExactReferenceValue, ExactTableRow, ExactTableValue, ExactTypedValue,
55};
56
57/// Provenance of an exact result.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59#[non_exhaustive]
60pub enum ExactSource {
61    /// Resolved from a property set assigned directly to the occurrence
62    /// via `IfcRelDefinesByProperties`.
63    Occurrence,
64    /// Resolved from a set in `HasPropertySets` of the `IfcTypeObject` with
65    /// this entity id: inherited through the occurrence's type, or, when a
66    /// type object is queried, that object's own set (#193), so the id is
67    /// then the queried object's.
68    Type(EntityId),
69    /// Resolved from a material property set of the material definition
70    /// with this entity id (#218): an `IfcMaterialProperties`, or in IFC2X3
71    /// one of its subtypes, whose `Material` is that definition.
72    Material(EntityId),
73}
74
75/// Exact IFC logical value without collapsing unknown into a boolean.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum ExactLogical {
78    /// `IfcLogical` value `.F.`.
79    False,
80    /// `IfcLogical` value `.U.` — genuinely undetermined, not absent.
81    Unknown,
82    /// `IfcLogical` value `.T.`.
83    True,
84}
85
86/// Values accepted by the exact resolver.
87///
88/// An `IfcPropertySingleValue` or a simple quantity resolves to one of the
89/// scalar variants. The other `IfcSimpleProperty` kinds resolve to a
90/// composite variant whose scalars carry their own declared types
91/// ([`ExactTypedValue`]), and a complex property or quantity to
92/// [`ExactValue::Complex`]; for those, [`ExactProperty::value_type`] is
93/// `None`.
94#[derive(Debug, Clone, PartialEq)]
95#[non_exhaustive]
96pub enum ExactValue {
97    /// The property carried no value (`$`), distinct from an absent property.
98    Null,
99    /// An `IFCBOOLEAN` payload.
100    Bool(bool),
101    /// An `IFCLOGICAL` payload, keeping the tri-state distinction.
102    Logical(ExactLogical),
103    /// An `IFCBINARY` payload, stored as its literal encoded text.
104    Binary(Arc<str>),
105    /// An `IFCINTEGER` payload.
106    Integer(i64),
107    /// An `IFCREAL` (or compatible measure) payload; always finite.
108    Real(f64),
109    /// An `IFCTEXT`/`IFCLABEL`/`IFCIDENTIFIER`-family string payload.
110    Text(Arc<str>),
111    /// An `IfcPropertyEnumeratedValue`: its selected values and its
112    /// reference enumeration.
113    Enumerated(ExactEnumeratedValue),
114    /// An `IfcPropertyListValue`: its `ListValues` in file order, all of
115    /// one declared type; empty only when the attribute is `$` (IFC4 and
116    /// IFC4X3). The shared `Unit` is [`ExactProperty::unit_id`].
117    List(Vec<ExactTypedValue>),
118    /// An `IfcPropertyBoundedValue`: lower and upper bound and set point.
119    Bounded(Box<ExactBoundedValue>),
120    /// An `IfcPropertyTableValue`: its rows, expression, units and
121    /// interpolation.
122    Table(ExactTableValue),
123    /// An `IfcPropertyReferenceValue`: its usage name and target entity.
124    Reference(ExactReferenceValue),
125    /// An enumeration constant held by a predefined set's attribute, as
126    /// written without its dots, e.g. `SWINGING` for
127    /// `IfcDoorPanelProperties.PanelOperation`; always a member of the
128    /// declared enumeration in the bound release.
129    Enum(Arc<str>),
130    /// An entity held by a predefined set's attribute, e.g. the
131    /// `IfcShapeAspect` of `ShapeAspectStyle`; checked, not followed.
132    Entity(ExactEntityRef),
133    /// An `IfcComplexProperty` or `IfcPhysicalComplexQuantity` (#208): its
134    /// members, each resolved as a set member is. The complex is present
135    /// but is no value of any type, so [`ExactProperty::value_type`] and
136    /// [`ExactProperty::unit_id`] are `None`.
137    Complex(ExactComplexValue),
138}
139
140/// A uniquely resolved property with IFC identity and provenance.
141#[derive(Debug, Clone, PartialEq)]
142pub struct ExactProperty {
143    /// Whether the value came from the occurrence or from a type object's
144    /// `HasPropertySets` (inherited, or the queried type object's own).
145    pub source: ExactSource,
146    /// The owning `IfcPropertySet.Name` or `IfcElementQuantity.Name`; for a
147    /// predefined set its `Name`, or its entity name in the release's
148    /// spelling (`IfcDoorLiningProperties`) when it states none.
149    pub property_set: Arc<str>,
150    /// Entity id of the `IfcPropertySet`, `IfcElementQuantity` or
151    /// predefined set.
152    pub set_id: EntityId,
153    /// Entity id of the `IfcProperty` (single, enumerated, list, bounded,
154    /// table or reference value, or complex), or of the
155    /// `IfcPhysicalQuantity` (e.g. `IfcQuantityLength`, or a complex
156    /// quantity). For an attribute of a predefined set, which is no entity
157    /// of its own, the set's id.
158    pub property_id: EntityId,
159    /// Declared IFC value type (for example `IFCINTEGER` or `IFCLENGTHMEASURE`).
160    ///
161    /// `None` for a single value whose `NominalValue` is `$`, for a
162    /// composite value, whose scalars carry their own types, and for a
163    /// complex property or quantity, which has no value type at all.
164    ///
165    /// For a quantity it is the declared type of its value attribute in the
166    /// bound release (`LengthValue : IfcLengthMeasure` gives
167    /// `IFCLENGTHMEASURE`), since a quantity stores a bare number. For a
168    /// predefined set's attribute it is the attribute's declared type
169    /// (`LiningDepth : IfcPositiveLengthMeasure` gives
170    /// `IFCPOSITIVELENGTHMEASURE`), also when the value is `$`, except that a
171    /// select-typed attribute reports the member type the file wrote.
172    pub value_type: Option<Arc<str>>,
173    /// The explicit unit that applies to every value, if stated:
174    /// `IfcPropertySingleValue.Unit`, `IfcPropertyListValue.Unit`,
175    /// `IfcPropertyBoundedValue.Unit`, the `Unit` of an enumerated value's
176    /// `IfcPropertyEnumeration`, or `IfcPhysicalSimpleQuantity.Unit`.
177    /// A table's two units are in [`ExactTableValue`]; a reference value has
178    /// none.
179    pub unit_id: Option<EntityId>,
180    /// The resolved `NominalValue`, composite value, or the quantity's value.
181    pub value: ExactValue,
182}
183
184/// Exact lookup result.
185#[derive(Debug, Clone, PartialEq)]
186#[non_exhaustive]
187pub enum ExactResolution {
188    /// The property was found exactly once across occurrence and inherited sets.
189    Present(ExactProperty),
190    /// No occurrence or inherited property set, quantity set or predefined
191    /// set carried a matching property, quantity or attribute; this is a
192    /// proven absence, not a lookup failure.
193    ///
194    /// A predefined set's attribute that is `$` is not absent: it is
195    /// `Present` with [`ExactValue::Null`].
196    Absent,
197}
198
199/// Why a property cannot be resolved exactly.
200#[derive(Debug, Clone, PartialEq, Eq)]
201#[non_exhaustive]
202pub enum ExactPropertyError {
203    /// The model carries STEP-level diagnostics, so exactness cannot be
204    /// guaranteed; count of diagnostics is reported for context.
205    IncompleteModel {
206        /// Number of diagnostics recorded against the model.
207        diagnostics: usize,
208    },
209    /// The model header declares no `FILE_SCHEMA`.
210    MissingSchema,
211    /// The model header declares more than one schema.
212    MultipleSchemas {
213        /// Number of schemas declared in the header.
214        schemas: usize,
215    },
216    /// The model header declares a schema other than IFC2X3, IFC4 or
217    /// IFC4X3 (`IFC4X3_ADD2`).
218    UnsupportedSchema {
219        /// The declared schema token.
220        schema: String,
221    },
222    /// An entity reference points at an id absent from the model.
223    MissingReference {
224        /// The entity holding the reference.
225        from: EntityId,
226        /// The entity id that could not be resolved.
227        to: EntityId,
228    },
229    /// An aggregate attribute (list/set) was `$`, empty when required, or
230    /// not encoded as a list at all.
231    MalformedAggregate {
232        /// The entity whose attribute was malformed.
233        entity: EntityId,
234        /// Name of the offending attribute.
235        attribute: &'static str,
236    },
237    /// The occurrence is assigned to more than one `IfcTypeObject` via
238    /// `IfcRelDefinesByType`, which IFC forbids.
239    MultipleTypeAssignments {
240        /// The occurrence with conflicting type assignments.
241        object: EntityId,
242        /// The first `IfcTypeObject` found.
243        first: EntityId,
244        /// The second, conflicting `IfcTypeObject` found.
245        second: EntityId,
246    },
247    /// `IfcRelDefinesByProperties.RelatedObjects` names an object that is
248    /// not a non-type `IfcObjectDefinition`. A type object there is refused
249    /// even when it is the queried object: its sets belong in its
250    /// `HasPropertySets` (IFC4 and IFC4X3 `NoRelatedTypeObject`; IFC2X3
251    /// admits only `IfcObject`).
252    InvalidOccurrenceTarget {
253        /// The `IfcRelDefinesByProperties` relationship.
254        relationship: EntityId,
255        /// The invalid related object.
256        object: EntityId,
257    },
258    /// `IfcRelDefinesByType.RelatedObjects` names an object that is not an
259    /// `IfcObject`.
260    InvalidTypeTarget {
261        /// The `IfcRelDefinesByType` relationship.
262        relationship: EntityId,
263        /// The invalid related object.
264        object: EntityId,
265    },
266    /// The queried entity can carry no property sets in the declared
267    /// release: it is neither an object `IfcRelDefinesByProperties` may
268    /// relate (`IfcObject` in IFC2X3, a non-type `IfcObjectDefinition` in
269    /// IFC4 and IFC4X3) nor an `IfcTypeObject` (accepted since #193). For
270    /// a material query (#218): the entity is no material definition the
271    /// release's `IfcMaterialProperties.Material` accepts.
272    InvalidQueryObject {
273        /// The rejected query object.
274        object: EntityId,
275        /// The object's actual IFC type name.
276        type_name: Arc<str>,
277    },
278    /// The same entity id appears more than once in an aggregate attribute
279    /// that must have unique members.
280    DuplicateAggregateMember {
281        /// The entity holding the aggregate.
282        entity: EntityId,
283        /// Name of the offending attribute.
284        attribute: &'static str,
285        /// The entity id that appeared more than once.
286        member: EntityId,
287    },
288    /// Two property sets with the same name matched the query for the same
289    /// source (occurrence or type), making the result ambiguous.
290    DuplicateMatchingSets {
291        /// Whether the ambiguity arose among occurrence or type sets.
292        source: ExactSource,
293        /// The first matching property set.
294        first: EntityId,
295        /// The second, conflicting matching property set.
296        second: EntityId,
297    },
298    /// Two properties with the same name matched within the same set.
299    DuplicateMatchingProperties {
300        /// The `IfcPropertySet` containing the ambiguous properties.
301        set: EntityId,
302        /// The first matching property.
303        first: EntityId,
304        /// The second, conflicting matching property.
305        second: EntityId,
306    },
307    /// A `Name` attribute expected to be a string was `$`, a reference, or
308    /// otherwise not text.
309    MalformedName {
310        /// The entity whose name attribute was malformed.
311        entity: EntityId,
312        /// Name of the offending attribute (normally `"Name"`).
313        attribute: &'static str,
314    },
315    /// A `RelatingPropertyDefinition` reference resolves to an entity that
316    /// is not an `IfcPropertySetDefinition`; or a predefined property set
317    /// holds the requested name in an attribute this resolver cannot read
318    /// exactly (an aggregate, such as
319    /// `IfcReinforcementDefinitionProperties.ReinforcementSectionDefinitions`);
320    /// or a predefined set that states no `Name`, and so cannot be ruled out
321    /// by a set name, has an attribute of the requested name (#66).
322    UnsupportedDefinition {
323        /// The rejected entity.
324        entity: EntityId,
325        /// The entity's actual IFC type name.
326        type_name: Arc<str>,
327    },
328    /// A member of `IfcPropertySet.HasProperties` or
329    /// `IfcComplexProperty.HasProperties` is not an `IfcProperty`, or a
330    /// member of `IfcElementQuantity.Quantities` or
331    /// `IfcPhysicalComplexQuantity.HasQuantities` is not an
332    /// `IfcPhysicalQuantity`.
333    UnsupportedProperty {
334        /// The rejected entity.
335        entity: EntityId,
336        /// The entity's actual IFC type name.
337        type_name: Arc<str>,
338    },
339    /// A value the release requires was `$`: a quantity's value, or a
340    /// required value of another kind (IFC2X3
341    /// `IfcPropertyReferenceValue.PropertyReference`).
342    MissingValueSlot {
343        /// The property with the missing value.
344        property: EntityId,
345    },
346    /// An entity's attribute count does not match what the declared
347    /// release's schema declares for its type (a malformed or truncated STEP record).
348    MalformedEntitySlots {
349        /// The malformed entity.
350        entity: EntityId,
351        /// The entity's IFC type name.
352        type_name: Arc<str>,
353        /// Attribute count the schema declares for this type.
354        expected: usize,
355        /// Attribute count actually present on the entity.
356        actual: usize,
357    },
358    /// `IfcPropertySingleValue.Unit` references an entity that is not a
359    /// member of the `IfcUnit` select.
360    UnsupportedUnit {
361        /// The property with the invalid unit reference.
362        property: EntityId,
363    },
364    /// `IfcPropertySingleValue.NominalValue` carries a typed value whose
365    /// declared type is not accepted by `IFCVALUE`, or whose payload does
366    /// not match its declared type.
367    UnsupportedValue {
368        /// The property with the invalid value.
369        property: EntityId,
370    },
371    /// `IfcPropertySingleValue.NominalValue` is an `IFCREAL` that is NaN or
372    /// infinite, which IFC does not permit.
373    NonFiniteReal {
374        /// The property with the non-finite real value.
375        property: EntityId,
376    },
377    /// A traversed record, typed value, or select member names a construct
378    /// that the release declared in `FILE_SCHEMA` does not define, for
379    /// example an `IfcDoorType` or an `IfcPropertySetDefinitionSet` in an
380    /// IFC2X3 file. The file mixes releases, so nothing it says about the
381    /// property is trusted.
382    NotInSchema {
383        /// The entity holding or being the foreign construct.
384        entity: EntityId,
385        /// The construct's name as written in the file.
386        name: Arc<str>,
387        /// The release the header declares.
388        schema: SchemaVersion,
389    },
390    /// Values that the property's release constrains contradict one of
391    /// those constraints, so no one reading of them is exact: list members
392    /// or bounds of different types, table columns of unequal length, a
393    /// selected value missing from the referenced enumeration, or a repeated
394    /// member of a `LIST OF UNIQUE`.
395    InconsistentValues {
396        /// The property or `IfcPropertyEnumeration` holding the values.
397        entity: EntityId,
398        /// The release's label of the violated WHERE rule (for example
399        /// `WR31`, or `SameUnitUpperLower` in IFC4), or `<Attribute> UNIQUE`
400        /// for a repeated member of a unique list.
401        rule: &'static str,
402    },
403    /// The entity name given to [`exact_predefined_sets`] is not a
404    /// predefined property set in the declared release: not declared there
405    /// at all, or an `IfcPropertySet`, a quantity set, or one of their
406    /// supertypes.
407    NotAPredefinedSet {
408        /// The entity name as requested.
409        name: Arc<str>,
410        /// The release the header declares.
411        schema: SchemaVersion,
412    },
413    /// A proper subtype of `IfcRelDefinesByProperties` or
414    /// `IfcRelDefinesByType` relates the queried object, such as IFC2X3
415    /// `IfcRelOverridesProperties`. Its semantics change which value applies,
416    /// and the exact resolver does not interpret them, so it refuses rather
417    /// than answer as if the relationship were absent.
418    UnsupportedRelationship {
419        /// The relationship instance.
420        relationship: EntityId,
421        /// Its IFC type name.
422        type_name: Arc<str>,
423    },
424    /// A complex property or quantity reaches itself again through its
425    /// members (#208). The schema forbids only a direct self-member; a
426    /// longer cycle has no finite resolution either.
427    ComplexCycle {
428        /// The complex whose member list closes the cycle.
429        complex: EntityId,
430        /// The member already being resolved higher up the path.
431        member: EntityId,
432    },
433    /// A complex property or quantity nested deeper than the resolver
434    /// follows below one set member (#208).
435    ComplexTooDeep {
436        /// The complex whose members would exceed the depth.
437        complex: EntityId,
438        /// The nesting depth followed.
439        limit: usize,
440    },
441    /// Resolving one set member followed more nested member references
442    /// than its budget (#208). Members may be shared between complexes,
443    /// so a small file can expand into an enormous tree.
444    ComplexBudgetExceeded {
445        /// The complex whose member exceeded the budget.
446        complex: EntityId,
447        /// The nested member references followed.
448        limit: usize,
449    },
450}
451impl fmt::Display for ExactPropertyError {
452    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453        write!(f, "exact IFC property resolution failed: {self:?}")
454    }
455}
456
457/// The IFC release a model's properties are resolved against.
458///
459/// This is the release `exact_property` binds to: the single `FILE_SCHEMA`
460/// token, if it names a release the exact resolver supports (IFC2X3 TC1,
461/// IFC4 ADD2 TC1 or IFC4X3 ADD2, the bundled tables). A consumer binds its vocabulary per release with this answer
462/// instead of re-parsing the header. It fails exactly as `exact_property`
463/// fails at model level: diagnostics, no schema, several schemas, or an
464/// unsupported one.
465///
466/// # Errors
467///
468/// [`ExactPropertyError::IncompleteModel`], [`ExactPropertyError::MissingSchema`],
469/// [`ExactPropertyError::MultipleSchemas`], or
470/// [`ExactPropertyError::UnsupportedSchema`].
471pub fn exact_schema(model: &Model) -> Result<SchemaVersion, ExactPropertyError> {
472    validate_model(model).map(|release| release.version)
473}
474
475impl std::error::Error for ExactPropertyError {}
476
477/// Resolve a property or quantity by exact set and property name.
478///
479/// Single, enumerated, list, bounded, table and reference values resolve,
480/// the last five as composite [`ExactValue`]s. An `IfcComplexProperty` or
481/// `IfcPhysicalComplexQuantity` resolves as [`ExactValue::Complex`]: present,
482/// with no value type (#208).
483///
484/// A predefined property set (`IfcDoorLiningProperties` and the like) is
485/// searched too: its members are the attributes its entity declares, by
486/// schema name (`LiningDepth`), and its set name is its `Name`, or its
487/// entity name (`IfcDoorLiningProperties`) when it states none. A door with
488/// one `IfcDoorPanelProperties` per leaf is ambiguous here; list such sets
489/// with [`exact_predefined_sets`].
490///
491/// The model is resolved against the single release its `FILE_SCHEMA`
492/// declares, IFC2X3, IFC4 or IFC4X3 (see [`exact_schema`]); every domain,
493/// select and slot count is that release's. With `set_name == None`, all
494/// assigned sets are searched. Occurrence values override matching inherited
495/// values at property level. To enumerate every property instead of naming
496/// one, use [`exact_properties`].
497///
498/// `object` may also be an `IfcTypeObject` of the release (an `IfcWallType`,
499/// an IFC2X3 `IfcDoorStyle`, ...). Its own `HasPropertySets` are then
500/// searched, with the validation an occurrence's inherited type sets get,
501/// and a result carries [`ExactSource::Type`] with `object`'s id. `$` states
502/// no sets, a proven absence; an empty list is refused as for an inherited
503/// type. A type object named in an `IfcRelDefinesByProperties` is refused
504/// ([`ExactPropertyError::InvalidOccurrenceTarget`]), never ignored.
505///
506/// Quantity sets are searched like property sets, as buildingSMART IDS
507/// treats a quantity as a property: `Qto_WallBaseQuantities.Length` resolves
508/// to the `IfcQuantityLength` of that name. A property set and a quantity set
509/// of the same matching name on one source are ambiguous and refused.
510/// WHERE rules such as `LengthValue >= 0` are not evaluated; the value is
511/// the file's.
512///
513/// # Errors
514///
515/// Any [`ExactPropertyError`]: the answer is refused rather than guessed
516/// whenever the evidence is incomplete, ambiguous, or foreign to the
517/// declared release.
518pub fn exact_property(
519    model: &Model,
520    object: EntityId,
521    set_name: Option<&str>,
522    property_name: &str,
523) -> Result<ExactResolution, ExactPropertyError> {
524    let release = validate_model(model)?;
525    let assigned = assigned_sets(model, release, object)?;
526    let occurrence = find_property(
527        model,
528        release,
529        &assigned.occurrence_sets,
530        ExactSource::Occurrence,
531        set_name,
532        property_name,
533    )?;
534    let inherited = match &assigned.type_sets {
535        Some((type_id, sets)) => find_property(
536            model,
537            release,
538            sets,
539            ExactSource::Type(*type_id),
540            set_name,
541            property_name,
542        )?,
543        None => None,
544    };
545    Ok(occurrence
546        .or(inherited)
547        .map_or(ExactResolution::Absent, ExactResolution::Present))
548}