Compare commits

...

1 Commits

Author SHA1 Message Date
Petru Conduraru 0279a5ab7c express: include referencing SELECT types in typeof() (#3187)
Per ISO 10303-11 12.4.3, TYPEOF() must return every type a value conforms
to, including any SELECT data type that references it (transitively) as a
member. The previous implementation only walked entity supertypes and
defined-type chains, so it never reported SELECT types. This silently
broke every WHERE rule that tests SELECT membership through typeof: for
example IfcFillAreaStyle.MaxOneColour and ConsistentHatchStyleDef test
`'ifc4.ifccolour' in typeof(style)`, so an IfcFillAreaStyle with two
IfcColourRgb entries was never rejected.

typeof() now builds a per-schema (cached) reverse map from a type name to
the SELECT types that reference it as a direct member, then closes the
base type set over that map so selects-of-selects are also included. The
canonical implementation lives in the rule_compiler.py preamble and is
copied verbatim into every generated rules/*.py, so the source of truth
and the generated files are updated in lockstep.

Verified: typeof(IfcColourRgb) now yields ifccolour, ifccolourorfactor,
and ifcfillstyleselect; an IfcFillAreaStyle with two colours now fails
MaxOneColour. All 138 express rule fixtures still pass (75 pass-cases at 0
errors, 63 fail-cases still flagged), no regressions.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 08:06:49 +03:00
13 changed files with 316 additions and 39 deletions
@@ -939,10 +939,27 @@ def typeof(inst):
# If V evaluates to indeterminate (?), an empty set is returned.
return express_set([])
schema_name = inst.is_a(True).split('.')[0].lower()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
# Per ISO 10303-11 12.4.3, TYPEOF() also includes every select data type
# that references (transitively) any of the value's types as a member of
# its select list. Cache, per schema, the reverse map type name -> select
# types directly referencing it.
select_parents = getattr(typeof, '_select_parents', None)
if select_parents is None:
select_parents = typeof._select_parents = {}
schema_selects = select_parents.get(schema_name)
if schema_selects is None:
schema_selects = {}
for decl in schema.declarations():
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.select_type):
sel_name = decl.name().lower()
for member in decl.select_list():
schema_selects.setdefault(member.name().lower(), []).append(sel_name)
select_parents[schema_name] = schema_selects
def inner():
decl = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(inst.is_a())
decl = schema.declaration_by_name(inst.is_a())
while decl:
yield '.'.join((schema_name, decl.name().lower()))
yield decl.name().lower()
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.entity):
decl = decl.supertype()
else:
@@ -951,7 +968,15 @@ def typeof(inst):
decl = decl.declared_type()
if not isinstance(decl, ifcopenshell.ifcopenshell_wrapper.type_declaration):
break
return express_set(inner())
names = set(inner())
queue = list(names)
while queue:
name = queue.pop()
for sel_name in schema_selects.get(name, ()):
if sel_name not in names:
names.add(sel_name)
queue.append(sel_name)
return express_set('.'.join((schema_name, name)) for name in names)
class indeterminate_type:
def __bool__(self):
@@ -96,11 +96,24 @@ def typeof(inst):
if not inst:
return express_set([])
schema_name = inst.is_a(True).split('.')[0].lower()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
select_parents = getattr(typeof, '_select_parents', None)
if select_parents is None:
select_parents = typeof._select_parents = {}
schema_selects = select_parents.get(schema_name)
if schema_selects is None:
schema_selects = {}
for decl in schema.declarations():
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.select_type):
sel_name = decl.name().lower()
for member in decl.select_list():
schema_selects.setdefault(member.name().lower(), []).append(sel_name)
select_parents[schema_name] = schema_selects
def inner():
decl = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(inst.is_a())
decl = schema.declaration_by_name(inst.is_a())
while decl:
yield '.'.join((schema_name, decl.name().lower()))
yield decl.name().lower()
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.entity):
decl = decl.supertype()
else:
@@ -109,7 +122,15 @@ def typeof(inst):
decl = decl.declared_type()
if not isinstance(decl, ifcopenshell.ifcopenshell_wrapper.type_declaration):
break
return express_set(inner())
names = set(inner())
queue = list(names)
while queue:
name = queue.pop()
for sel_name in schema_selects.get(name, ()):
if sel_name not in names:
names.add(sel_name)
queue.append(sel_name)
return express_set('.'.join((schema_name, name)) for name in names)
class indeterminate_type:
@@ -96,11 +96,24 @@ def typeof(inst):
if not inst:
return express_set([])
schema_name = inst.is_a(True).split('.')[0].lower()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
select_parents = getattr(typeof, '_select_parents', None)
if select_parents is None:
select_parents = typeof._select_parents = {}
schema_selects = select_parents.get(schema_name)
if schema_selects is None:
schema_selects = {}
for decl in schema.declarations():
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.select_type):
sel_name = decl.name().lower()
for member in decl.select_list():
schema_selects.setdefault(member.name().lower(), []).append(sel_name)
select_parents[schema_name] = schema_selects
def inner():
decl = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(inst.is_a())
decl = schema.declaration_by_name(inst.is_a())
while decl:
yield '.'.join((schema_name, decl.name().lower()))
yield decl.name().lower()
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.entity):
decl = decl.supertype()
else:
@@ -109,7 +122,15 @@ def typeof(inst):
decl = decl.declared_type()
if not isinstance(decl, ifcopenshell.ifcopenshell_wrapper.type_declaration):
break
return express_set(inner())
names = set(inner())
queue = list(names)
while queue:
name = queue.pop()
for sel_name in schema_selects.get(name, ()):
if sel_name not in names:
names.add(sel_name)
queue.append(sel_name)
return express_set('.'.join((schema_name, name)) for name in names)
class indeterminate_type:
@@ -96,11 +96,24 @@ def typeof(inst):
if not inst:
return express_set([])
schema_name = inst.is_a(True).split('.')[0].lower()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
select_parents = getattr(typeof, '_select_parents', None)
if select_parents is None:
select_parents = typeof._select_parents = {}
schema_selects = select_parents.get(schema_name)
if schema_selects is None:
schema_selects = {}
for decl in schema.declarations():
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.select_type):
sel_name = decl.name().lower()
for member in decl.select_list():
schema_selects.setdefault(member.name().lower(), []).append(sel_name)
select_parents[schema_name] = schema_selects
def inner():
decl = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(inst.is_a())
decl = schema.declaration_by_name(inst.is_a())
while decl:
yield '.'.join((schema_name, decl.name().lower()))
yield decl.name().lower()
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.entity):
decl = decl.supertype()
else:
@@ -109,7 +122,15 @@ def typeof(inst):
decl = decl.declared_type()
if not isinstance(decl, ifcopenshell.ifcopenshell_wrapper.type_declaration):
break
return express_set(inner())
names = set(inner())
queue = list(names)
while queue:
name = queue.pop()
for sel_name in schema_selects.get(name, ()):
if sel_name not in names:
names.add(sel_name)
queue.append(sel_name)
return express_set('.'.join((schema_name, name)) for name in names)
class indeterminate_type:
@@ -96,11 +96,24 @@ def typeof(inst):
if not inst:
return express_set([])
schema_name = inst.is_a(True).split('.')[0].lower()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
select_parents = getattr(typeof, '_select_parents', None)
if select_parents is None:
select_parents = typeof._select_parents = {}
schema_selects = select_parents.get(schema_name)
if schema_selects is None:
schema_selects = {}
for decl in schema.declarations():
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.select_type):
sel_name = decl.name().lower()
for member in decl.select_list():
schema_selects.setdefault(member.name().lower(), []).append(sel_name)
select_parents[schema_name] = schema_selects
def inner():
decl = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(inst.is_a())
decl = schema.declaration_by_name(inst.is_a())
while decl:
yield '.'.join((schema_name, decl.name().lower()))
yield decl.name().lower()
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.entity):
decl = decl.supertype()
else:
@@ -109,7 +122,15 @@ def typeof(inst):
decl = decl.declared_type()
if not isinstance(decl, ifcopenshell.ifcopenshell_wrapper.type_declaration):
break
return express_set(inner())
names = set(inner())
queue = list(names)
while queue:
name = queue.pop()
for sel_name in schema_selects.get(name, ()):
if sel_name not in names:
names.add(sel_name)
queue.append(sel_name)
return express_set('.'.join((schema_name, name)) for name in names)
class indeterminate_type:
@@ -96,11 +96,24 @@ def typeof(inst):
if not inst:
return express_set([])
schema_name = inst.is_a(True).split('.')[0].lower()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
select_parents = getattr(typeof, '_select_parents', None)
if select_parents is None:
select_parents = typeof._select_parents = {}
schema_selects = select_parents.get(schema_name)
if schema_selects is None:
schema_selects = {}
for decl in schema.declarations():
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.select_type):
sel_name = decl.name().lower()
for member in decl.select_list():
schema_selects.setdefault(member.name().lower(), []).append(sel_name)
select_parents[schema_name] = schema_selects
def inner():
decl = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(inst.is_a())
decl = schema.declaration_by_name(inst.is_a())
while decl:
yield '.'.join((schema_name, decl.name().lower()))
yield decl.name().lower()
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.entity):
decl = decl.supertype()
else:
@@ -109,7 +122,15 @@ def typeof(inst):
decl = decl.declared_type()
if not isinstance(decl, ifcopenshell.ifcopenshell_wrapper.type_declaration):
break
return express_set(inner())
names = set(inner())
queue = list(names)
while queue:
name = queue.pop()
for sel_name in schema_selects.get(name, ()):
if sel_name not in names:
names.add(sel_name)
queue.append(sel_name)
return express_set('.'.join((schema_name, name)) for name in names)
class indeterminate_type:
@@ -96,11 +96,24 @@ def typeof(inst):
if not inst:
return express_set([])
schema_name = inst.is_a(True).split('.')[0].lower()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
select_parents = getattr(typeof, '_select_parents', None)
if select_parents is None:
select_parents = typeof._select_parents = {}
schema_selects = select_parents.get(schema_name)
if schema_selects is None:
schema_selects = {}
for decl in schema.declarations():
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.select_type):
sel_name = decl.name().lower()
for member in decl.select_list():
schema_selects.setdefault(member.name().lower(), []).append(sel_name)
select_parents[schema_name] = schema_selects
def inner():
decl = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(inst.is_a())
decl = schema.declaration_by_name(inst.is_a())
while decl:
yield '.'.join((schema_name, decl.name().lower()))
yield decl.name().lower()
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.entity):
decl = decl.supertype()
else:
@@ -109,7 +122,15 @@ def typeof(inst):
decl = decl.declared_type()
if not isinstance(decl, ifcopenshell.ifcopenshell_wrapper.type_declaration):
break
return express_set(inner())
names = set(inner())
queue = list(names)
while queue:
name = queue.pop()
for sel_name in schema_selects.get(name, ()):
if sel_name not in names:
names.add(sel_name)
queue.append(sel_name)
return express_set('.'.join((schema_name, name)) for name in names)
class indeterminate_type:
@@ -96,11 +96,24 @@ def typeof(inst):
if not inst:
return express_set([])
schema_name = inst.is_a(True).split('.')[0].lower()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
select_parents = getattr(typeof, '_select_parents', None)
if select_parents is None:
select_parents = typeof._select_parents = {}
schema_selects = select_parents.get(schema_name)
if schema_selects is None:
schema_selects = {}
for decl in schema.declarations():
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.select_type):
sel_name = decl.name().lower()
for member in decl.select_list():
schema_selects.setdefault(member.name().lower(), []).append(sel_name)
select_parents[schema_name] = schema_selects
def inner():
decl = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(inst.is_a())
decl = schema.declaration_by_name(inst.is_a())
while decl:
yield '.'.join((schema_name, decl.name().lower()))
yield decl.name().lower()
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.entity):
decl = decl.supertype()
else:
@@ -109,7 +122,15 @@ def typeof(inst):
decl = decl.declared_type()
if not isinstance(decl, ifcopenshell.ifcopenshell_wrapper.type_declaration):
break
return express_set(inner())
names = set(inner())
queue = list(names)
while queue:
name = queue.pop()
for sel_name in schema_selects.get(name, ()):
if sel_name not in names:
names.add(sel_name)
queue.append(sel_name)
return express_set('.'.join((schema_name, name)) for name in names)
class indeterminate_type:
@@ -96,11 +96,24 @@ def typeof(inst):
if not inst:
return express_set([])
schema_name = inst.is_a(True).split('.')[0].lower()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
select_parents = getattr(typeof, '_select_parents', None)
if select_parents is None:
select_parents = typeof._select_parents = {}
schema_selects = select_parents.get(schema_name)
if schema_selects is None:
schema_selects = {}
for decl in schema.declarations():
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.select_type):
sel_name = decl.name().lower()
for member in decl.select_list():
schema_selects.setdefault(member.name().lower(), []).append(sel_name)
select_parents[schema_name] = schema_selects
def inner():
decl = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(inst.is_a())
decl = schema.declaration_by_name(inst.is_a())
while decl:
yield '.'.join((schema_name, decl.name().lower()))
yield decl.name().lower()
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.entity):
decl = decl.supertype()
else:
@@ -109,7 +122,15 @@ def typeof(inst):
decl = decl.declared_type()
if not isinstance(decl, ifcopenshell.ifcopenshell_wrapper.type_declaration):
break
return express_set(inner())
names = set(inner())
queue = list(names)
while queue:
name = queue.pop()
for sel_name in schema_selects.get(name, ()):
if sel_name not in names:
names.add(sel_name)
queue.append(sel_name)
return express_set('.'.join((schema_name, name)) for name in names)
class indeterminate_type:
@@ -96,11 +96,24 @@ def typeof(inst):
if not inst:
return express_set([])
schema_name = inst.is_a(True).split('.')[0].lower()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
select_parents = getattr(typeof, '_select_parents', None)
if select_parents is None:
select_parents = typeof._select_parents = {}
schema_selects = select_parents.get(schema_name)
if schema_selects is None:
schema_selects = {}
for decl in schema.declarations():
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.select_type):
sel_name = decl.name().lower()
for member in decl.select_list():
schema_selects.setdefault(member.name().lower(), []).append(sel_name)
select_parents[schema_name] = schema_selects
def inner():
decl = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(inst.is_a())
decl = schema.declaration_by_name(inst.is_a())
while decl:
yield '.'.join((schema_name, decl.name().lower()))
yield decl.name().lower()
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.entity):
decl = decl.supertype()
else:
@@ -109,7 +122,15 @@ def typeof(inst):
decl = decl.declared_type()
if not isinstance(decl, ifcopenshell.ifcopenshell_wrapper.type_declaration):
break
return express_set(inner())
names = set(inner())
queue = list(names)
while queue:
name = queue.pop()
for sel_name in schema_selects.get(name, ()):
if sel_name not in names:
names.add(sel_name)
queue.append(sel_name)
return express_set('.'.join((schema_name, name)) for name in names)
class indeterminate_type:
@@ -96,11 +96,24 @@ def typeof(inst):
if not inst:
return express_set([])
schema_name = inst.is_a(True).split('.')[0].lower()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
select_parents = getattr(typeof, '_select_parents', None)
if select_parents is None:
select_parents = typeof._select_parents = {}
schema_selects = select_parents.get(schema_name)
if schema_selects is None:
schema_selects = {}
for decl in schema.declarations():
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.select_type):
sel_name = decl.name().lower()
for member in decl.select_list():
schema_selects.setdefault(member.name().lower(), []).append(sel_name)
select_parents[schema_name] = schema_selects
def inner():
decl = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(inst.is_a())
decl = schema.declaration_by_name(inst.is_a())
while decl:
yield '.'.join((schema_name, decl.name().lower()))
yield decl.name().lower()
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.entity):
decl = decl.supertype()
else:
@@ -109,7 +122,15 @@ def typeof(inst):
decl = decl.declared_type()
if not isinstance(decl, ifcopenshell.ifcopenshell_wrapper.type_declaration):
break
return express_set(inner())
names = set(inner())
queue = list(names)
while queue:
name = queue.pop()
for sel_name in schema_selects.get(name, ()):
if sel_name not in names:
names.add(sel_name)
queue.append(sel_name)
return express_set('.'.join((schema_name, name)) for name in names)
class indeterminate_type:
@@ -96,11 +96,24 @@ def typeof(inst):
if not inst:
return express_set([])
schema_name = inst.is_a(True).split('.')[0].lower()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
select_parents = getattr(typeof, '_select_parents', None)
if select_parents is None:
select_parents = typeof._select_parents = {}
schema_selects = select_parents.get(schema_name)
if schema_selects is None:
schema_selects = {}
for decl in schema.declarations():
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.select_type):
sel_name = decl.name().lower()
for member in decl.select_list():
schema_selects.setdefault(member.name().lower(), []).append(sel_name)
select_parents[schema_name] = schema_selects
def inner():
decl = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(inst.is_a())
decl = schema.declaration_by_name(inst.is_a())
while decl:
yield '.'.join((schema_name, decl.name().lower()))
yield decl.name().lower()
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.entity):
decl = decl.supertype()
else:
@@ -109,7 +122,15 @@ def typeof(inst):
decl = decl.declared_type()
if not isinstance(decl, ifcopenshell.ifcopenshell_wrapper.type_declaration):
break
return express_set(inner())
names = set(inner())
queue = list(names)
while queue:
name = queue.pop()
for sel_name in schema_selects.get(name, ()):
if sel_name not in names:
names.add(sel_name)
queue.append(sel_name)
return express_set('.'.join((schema_name, name)) for name in names)
class indeterminate_type:
@@ -96,11 +96,24 @@ def typeof(inst):
if not inst:
return express_set([])
schema_name = inst.is_a(True).split('.')[0].lower()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
select_parents = getattr(typeof, '_select_parents', None)
if select_parents is None:
select_parents = typeof._select_parents = {}
schema_selects = select_parents.get(schema_name)
if schema_selects is None:
schema_selects = {}
for decl in schema.declarations():
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.select_type):
sel_name = decl.name().lower()
for member in decl.select_list():
schema_selects.setdefault(member.name().lower(), []).append(sel_name)
select_parents[schema_name] = schema_selects
def inner():
decl = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(inst.is_a())
decl = schema.declaration_by_name(inst.is_a())
while decl:
yield '.'.join((schema_name, decl.name().lower()))
yield decl.name().lower()
if isinstance(decl, ifcopenshell.ifcopenshell_wrapper.entity):
decl = decl.supertype()
else:
@@ -109,7 +122,15 @@ def typeof(inst):
decl = decl.declared_type()
if not isinstance(decl, ifcopenshell.ifcopenshell_wrapper.type_declaration):
break
return express_set(inner())
names = set(inner())
queue = list(names)
while queue:
name = queue.pop()
for sel_name in schema_selects.get(name, ()):
if sel_name not in names:
names.add(sel_name)
queue.append(sel_name)
return express_set('.'.join((schema_name, name)) for name in names)
class indeterminate_type: