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