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}