Skip to content

express parser

Maarten Vroegindeweij edited this page Feb 18, 2026 · 1 revision

@ifc-factory/express-parser

Layer 0 — Zero dependencies. Parses EXPRESS schema (.exp) files into an Abstract Syntax Tree (AST).

What is EXPRESS?

EXPRESS is a data modeling language defined in ISO 10303-11. It is used to define the IFC schema. An EXPRESS file (.exp) describes:

  • Entities with attributes, inheritance, and constraints
  • Types (enumerations, selects, aliases, aggregations)
  • Functions and Rules for data validation

Example EXPRESS snippet:

SCHEMA IFC4X3;

TYPE IfcLabel = STRING(255);
END_TYPE;

TYPE IfcWallTypeEnum = ENUMERATION OF
  (MOVABLE, PARAPET, PARTITIONING, PLUMBINGWALL, SHEAR,
   SOLIDWALL, STANDARD, POLYGONAL, ELEMENTEDWALL,
   RETAININGWALL, WAVEWALL, USERDEFINED, NOTDEFINED);
END_TYPE;

ENTITY IfcRoot
 ABSTRACT SUPERTYPE OF (ONEOF(IfcObjectDefinition, IfcPropertyDefinition, IfcRelationship));
  GlobalId : IfcGloballyUniqueId;
  OwnerHistory : OPTIONAL IfcOwnerHistory;
  Name : OPTIONAL IfcLabel;
  Description : OPTIONAL IfcText;
 UNIQUE
  UR1 : GlobalId;
END_ENTITY;

ENTITY IfcWall
 SUBTYPE OF (IfcBuildingElement);
  PredefinedType : OPTIONAL IfcWallTypeEnum;
 WHERE
  CorrectPredefinedType : NOT(EXISTS(PredefinedType)) OR
    (PredefinedType <> IfcWallTypeEnum.USERDEFINED) OR
    ((PredefinedType = IfcWallTypeEnum.USERDEFINED) AND EXISTS(SELF\IfcObject.ObjectType));
END_ENTITY;

END_SCHEMA;

Installation

pnpm add @ifc-factory/express-parser

Usage

Parse a Schema

import { parseExpress } from '@ifc-factory/express-parser';
import { readFileSync } from 'node:fs';

const source = readFileSync('IFC4X3_ADD2.exp', 'utf-8');
const schema = parseExpress(source);

console.log(`Schema: ${schema.name}`);
console.log(`Declarations: ${schema.declarations.length}`);

Analyze Entities

for (const decl of schema.declarations) {
  if (decl.kind === 'entity') {
    console.log(`Entity: ${decl.name}`);
    console.log(`  Abstract: ${decl.abstract}`);
    console.log(`  Extends: ${decl.subtypeOf ?? '(none)'}`);
    console.log(`  Attributes: ${decl.attributes.length}`);

    for (const attr of decl.attributes) {
      const opt = attr.optional ? 'OPTIONAL ' : '';
      console.log(`    ${attr.name}: ${opt}${formatType(attr.type)}`);
    }

    if (decl.inverseAttributes.length > 0) {
      console.log(`  Inverse attributes: ${decl.inverseAttributes.length}`);
    }
    if (decl.whereRules.length > 0) {
      console.log(`  WHERE rules: ${decl.whereRules.length}`);
    }
  }
}

Analyze Types

for (const decl of schema.declarations) {
  if (decl.kind !== 'type') continue;

  const ut = decl.underlyingType;
  switch (ut.kind) {
    case 'enumeration':
      console.log(`Enum ${decl.name}: ${ut.values.join(', ')}`);
      break;
    case 'select':
      console.log(`Select ${decl.name}: ${ut.types.join(' | ')}`);
      break;
    case 'named':
      console.log(`Alias ${decl.name} = ${ut.name}`);
      break;
    case 'aggregation':
      console.log(`${decl.name} = ${ut.aggregationType} OF ${formatType(ut.elementType)}`);
      break;
    case 'simple':
      console.log(`${decl.name} = ${ut.type}`);
      break;
  }
}

Tokenize Only

import { tokenize } from '@ifc-factory/express-parser';

const tokens = tokenize(source);
for (const token of tokens) {
  console.log(`${token.type}: ${token.value} (line ${token.line})`);
}

API

parseExpress(source: string): SchemaNode

Parses a complete EXPRESS schema and returns the AST. Throws ParseError on syntax errors.

tokenize(source: string): Token[]

Low-level tokenizer. Returns an array of tokens. Useful for debugging or building custom parsers.

AST Types

SchemaNode

interface SchemaNode {
  name: string;               // Schema name (e.g., "IFC4X3_DEV_923b0514")
  declarations: Declaration[];  // All declarations in order
}

Declaration (union type)

type Declaration =
  | TypeDeclaration
  | EntityDeclaration
  | FunctionDeclaration
  | RuleDeclaration;

TypeDeclaration

interface TypeDeclaration {
  kind: 'type';
  name: string;                      // e.g., "IfcLabel"
  underlyingType: UnderlyingType;    // What this type resolves to
  whereRules: WhereRule[];           // Optional constraints
}

UnderlyingType (union)

type UnderlyingType =
  | { kind: 'enumeration'; values: string[] }
  | { kind: 'select'; types: string[] }
  | { kind: 'aggregation'; aggregationType: 'SET' | 'LIST' | 'BAG' | 'ARRAY'; elementType: AttributeType; bounds?: { lower: number; upper: number } }
  | { kind: 'named'; name: string }         // Reference to another type
  | { kind: 'simple'; type: string };        // INTEGER, REAL, STRING, etc.

EntityDeclaration

interface EntityDeclaration {
  kind: 'entity';
  name: string;                          // e.g., "IfcWall"
  abstract: boolean;                     // Is this entity abstract?
  supertypeConstraint?: SupertypeConstraint;  // SUPERTYPE OF (ONEOF(...))
  subtypeOf?: string;                    // Parent entity name
  attributes: ExplicitAttribute[];       // Own explicit attributes
  deriveAttributes: DeriveAttribute[];   // DERIVE attributes
  inverseAttributes: InverseAttribute[]; // INVERSE attributes
  whereRules: WhereRule[];               // WHERE rules
  uniqueRules: UniqueRule[];             // UNIQUE rules
}

ExplicitAttribute

interface ExplicitAttribute {
  name: string;          // e.g., "GlobalId"
  optional: boolean;     // OPTIONAL keyword present?
  type: AttributeType;   // The attribute's type
}

InverseAttribute

interface InverseAttribute {
  name: string;           // e.g., "IsDecomposedBy"
  type: AttributeType;    // e.g., SET OF IfcRelAggregates
  forEntity: string;      // The related entity type
  forAttribute: string;   // The related attribute name
}

DeriveAttribute

interface DeriveAttribute {
  name: string;           // e.g., "Dim"
  type: AttributeType;
  expression: string;     // Raw EXPRESS expression string
}

FunctionDeclaration / RuleDeclaration

interface FunctionDeclaration {
  kind: 'function';
  name: string;
  body: string;           // Raw body text (not deeply parsed)
}

interface RuleDeclaration {
  kind: 'rule';
  name: string;
  body: string;           // Raw body text (not deeply parsed)
}

Lexer Details

The lexer (tokenizer) handles:

  • 90+ EXPRESS keywords (SCHEMA, ENTITY, TYPE, END_ENTITY, etc.) — case-insensitive
  • Identifiers — alphanumeric + underscore
  • String literals — single-quoted ('text')
  • Integers123, -42
  • Real numbers3.14, 1.0E-6
  • Operators:=, <>, <=, >=, <, >, =, :, ;, ,, (, ), ., \, |, [, ], {, }
  • Nested comments(* ... *) (can be nested)
  • Line comments-- ...

Parser Details

The parser is a recursive descent parser:

  • No external parser generator (no PEG, ANTLR, etc.)
  • FUNCTION and RULE bodies are captured as raw strings (not deeply parsed) — this is sufficient for code generation
  • Error handling includes source location (line, column) for debugging
  • Handles EXPRESS-specific constructs like ABSTRACT SUPERTYPE OF (ONEOF(...)) and SUBTYPE OF (...)

Error Handling

import { parseExpress, ParseError } from '@ifc-factory/express-parser';

try {
  const schema = parseExpress(source);
} catch (error) {
  if (error instanceof ParseError) {
    console.log(`Parse error at line ${error.location.line}, column ${error.location.column}`);
    console.log(error.message);
  }
}

See Also

  • codegen — Uses the EXPRESS AST to generate TypeScript
  • schema — The output of code generation
  • IFC Concepts — Background on IFC and EXPRESS

Clone this wiki locally