2022-01-19 12:18:33 +11:00
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
2014-02-17 22:13:55 +00:00
2020-09-08 14:40:27 +02:00
import os
2016-04-09 13:29:35 +02:00
import operator
import itertools
2014-02-17 22:13:55 +00:00
from pyparsing import *
2020-11-01 20:08:27 +07:00
try :
from functools import reduce
except :
pass
2016-04-09 13:29:35 +02:00
2014-02-17 22:13:55 +00:00
class Expression :
def __init__ ( self , contents ) :
self . contents = contents [ 0 ]
2020-11-01 20:08:27 +07:00
2014-02-17 22:13:55 +00:00
def __repr__ ( self ) :
2020-11-01 20:08:27 +07:00
if self . op is None :
return repr ( self . contents )
c = [ isinstance ( c , str ) and c or str ( c ) for c in self . contents ]
if " %s " in self . op :
return self . op % ( " " . join ( c ) )
else :
return " ( %s ) " % ( " %s " % self . op ) . join ( c )
2014-02-17 22:13:55 +00:00
def __iter__ ( self ) :
return self . contents . __iter__ ( )
2020-11-01 20:08:27 +07:00
2014-02-17 22:13:55 +00:00
class Union ( Expression ) :
op = " | "
2020-11-01 20:08:27 +07:00
2014-02-17 22:13:55 +00:00
class Concat ( Expression ) :
op = " + "
2020-11-01 20:08:27 +07:00
2014-02-17 22:13:55 +00:00
class Optional ( Expression ) :
op = " Optional( %s ) "
2020-11-01 20:08:27 +07:00
2014-02-17 22:13:55 +00:00
class Repeated ( Expression ) :
op = " ZeroOrMore( %s ) "
2020-11-01 20:08:27 +07:00
2014-02-17 22:13:55 +00:00
class Term ( Expression ) :
op = None
2020-11-01 20:08:27 +07:00
2014-02-17 22:13:55 +00:00
class Keyword :
def __init__ ( self , contents ) :
self . contents = contents [ 0 ]
2020-11-01 20:08:27 +07:00
2014-02-17 22:13:55 +00:00
def __repr__ ( self ) :
return self . contents
2020-11-01 20:08:27 +07:00
2014-02-17 22:13:55 +00:00
class Terminal :
def __init__ ( self , contents ) :
self . contents = contents [ 0 ]
s = self . contents
2020-11-01 20:08:27 +07:00
self . is_keyword = len ( s ) > = 4 and s [ 0 : : len ( s ) - 1 ] == ' " " ' and all ( c in alphanums + " _ " for c in s [ 1 : - 1 ] )
2016-04-09 13:29:35 +02:00
def __repr__ ( self ) :
ty = " CaselessKeyword " if self . is_keyword else " CaselessLiteral "
return " %s ( %s ) " % ( ty , self . contents )
2014-02-17 22:13:55 +00:00
LPAREN = Suppress ( " ( " )
RPAREN = Suppress ( " ) " )
LBRACK = Suppress ( " [ " )
RBRACK = Suppress ( " ] " )
LBRACE = Suppress ( " { " )
RBRACE = Suppress ( " } " )
EQUALS = Suppress ( " = " )
2020-11-01 20:08:27 +07:00
VBAR = Suppress ( " | " )
2014-02-17 22:13:55 +00:00
PERIOD = Suppress ( " . " )
2020-11-01 20:08:27 +07:00
HASH = Suppress ( " # " )
2014-02-17 22:13:55 +00:00
2020-11-01 20:08:27 +07:00
identifier = Word ( alphanums + " _ " )
2026-07-14 16:45:40 +05:00
keyword = Word ( alphanums + " _ " ) . set_parse_action ( Keyword )
2014-02-17 22:13:55 +00:00
expression = Forward ( )
2026-07-14 16:45:40 +05:00
optional = Group ( LBRACK + expression + RBRACK ) . set_parse_action ( Optional )
repeated = Group ( LBRACE + expression + RBRACE ) . set_parse_action ( Repeated )
terminal = quotedString . set_parse_action ( Terminal )
term = ( keyword | terminal | optional | repeated | ( LPAREN + expression + RPAREN ) ) . set_parse_action ( Term )
concat = Group ( term + OneOrMore ( term ) ) . set_parse_action ( Concat )
2020-11-01 20:08:27 +07:00
factor = concat | term
2026-07-14 16:45:40 +05:00
union = Group ( factor + OneOrMore ( VBAR + factor ) ) . set_parse_action ( Union )
2020-11-01 20:08:27 +07:00
rule = identifier + EQUALS + expression + PERIOD
2014-02-17 22:13:55 +00:00
expression << ( union | factor )
grammar = OneOrMore ( Group ( rule ) )
grammar . ignore ( HASH + restOfLine )
2026-07-14 16:45:40 +05:00
express = grammar . parse_file ( os . path . join ( os . path . dirname ( __file__ ) , " express.bnf " ) )
2014-02-17 22:13:55 +00:00
2020-11-01 20:08:27 +07:00
def find_bytype ( expr , ty , li = None ) :
if li is None :
li = [ ]
2014-02-17 22:13:55 +00:00
if isinstance ( expr , Term ) :
expr = expr . contents
2016-04-09 13:29:35 +02:00
if isinstance ( expr , ty ) :
li . append ( expr )
return set ( li )
2014-02-17 22:13:55 +00:00
elif isinstance ( expr , Expression ) :
for term in expr :
2016-04-09 13:29:35 +02:00
find_bytype ( term , ty , li )
2014-02-17 22:13:55 +00:00
return set ( li )
2020-11-01 20:08:27 +07:00
2014-02-17 22:13:55 +00:00
actions = {
2020-11-01 20:08:27 +07:00
" type_decl " : " TypeDeclaration " ,
" entity_decl " : " EntityDeclaration " ,
" enumeration_type " : " EnumerationType " ,
" aggregation_types " : " AggregationType " ,
" general_aggregation_types " : " AggregationType " ,
" select_type " : " SelectType " ,
" binary_type " : " BinaryType " ,
" subtype_declaration " : " SubTypeExpression " ,
" supertype_constraint " : " SuperTypeExpression " ,
" derive_clause " : " AttributeList " ,
" inverse_clause " : " AttributeList " ,
" inverse_attr " : " InverseAttribute " ,
" bound_spec " : " BoundSpecification " ,
" explicit_attr " : " ExplicitAttribute " ,
" width_spec " : " WidthSpec " ,
" string_type " : " StringType " ,
" named_types " : " NamedType " ,
" simple_types " : " SimpleType " ,
2022-12-26 11:29:32 +01:00
" function_decl " : " FunctionDeclaration " ,
" rule_decl " : " RuleDeclaration " ,
2014-02-17 22:13:55 +00:00
}
to_emit = set ( id for id , expr in express )
emitted = set ( )
to_combine = set ( [ " simple_id " ] )
2026-06-25 13:35:35 +02:00
to_original_text = set ( [ " simple_string_literal " ] )
2014-02-17 22:13:55 +00:00
statements = [ ]
2020-11-01 20:08:27 +07:00
terminals = reduce ( lambda x , y : x | y , ( find_bytype ( e , Terminal ) for id , e in express ) )
keywords = list ( filter ( operator . attrgetter ( " is_keyword " ) , terminals ) )
2026-07-14 17:04:30 +05:00
# terminals is identity-ordered (no __eq__/__hash__), so sort for determinism
keywords . sort ( key = lambda x : repr ( x ) )
2016-04-09 13:29:35 +02:00
negated_keywords = map ( lambda s : " ~ %s " % s , keywords )
2022-07-10 21:51:25 +02:00
no_action = { " letter " , " digit " , " digits " , " real_literal " , " integer_literal " , " string_literal " , " simple_string_literal " , " letter " , " not_quote " , " not_paren_star_quote_special " }
2014-02-17 22:13:55 +00:00
while True :
emitted_in_loop = set ( )
for id , expr in express :
2016-04-09 13:29:35 +02:00
kws = map ( repr , find_bytype ( expr , Keyword ) )
2014-02-17 22:13:55 +00:00
found = [ k in emitted for k in kws ]
if id in to_emit and all ( found ) :
emitted_in_loop . add ( id )
emitted . add ( id )
stmt = " ( %s ) " % expr
if id in to_combine :
2026-07-14 16:45:40 +05:00
stmt = " + " . join ( itertools . chain ( negated_keywords , ( " original_text_for(Combine %s ) " % stmt , ) ) )
2026-06-25 13:35:35 +02:00
elif id in to_original_text :
2026-06-26 15:13:22 +02:00
# We use lower() because it better matches the previous default of the CaselessLiterals for individual lexemes and express dictates case-insensitive comparisons anyway
2026-07-14 16:45:40 +05:00
stmt = " (original_text_for %s ).add_parse_action(token_map(str.lower)) " % stmt
2020-08-29 10:23:59 +02:00
if id not in no_action and not isinstance ( expr . contents , Keyword ) and not id in to_combine :
node_type = " ListNode " if " ZeroOrMore " in stmt else " Node "
2020-11-01 20:08:27 +07:00
action = actions . get ( id , ' lambda s, loc, t: %s (s, loc, t, rule= " %s " ) ' % ( node_type , id ) )
2026-07-14 16:45:40 +05:00
stmt = " %s .set_parse_action( %s ) " % ( stmt , action )
2020-11-01 20:08:27 +07:00
statements . append ( ' %s = %s ( " %s " ) ' % ( id , stmt , id ) )
2014-02-17 22:13:55 +00:00
to_emit - = emitted_in_loop
2020-11-01 20:08:27 +07:00
if not emitted_in_loop :
break
2014-02-17 22:13:55 +00:00
2026-07-14 17:04:30 +05:00
for id in sorted ( to_emit ) :
2020-11-01 20:08:27 +07:00
statements . append ( ' %s = Forward()( " %s " ) ' % ( id , id ) )
2014-02-17 22:13:55 +00:00
2026-07-14 17:04:30 +05:00
for id in sorted ( to_emit ) :
2014-02-17 22:13:55 +00:00
expr = [ e for k , e in express if k == id ] [ 0 ]
stmt = " ( %s ) " % expr
if id in to_combine :
stmt = " Suppress %s " % stmt
2026-06-25 13:35:35 +02:00
elif id in to_original_text :
2026-07-14 16:45:40 +05:00
stmt = " (original_text_for %s ).add_parse_action(token_map(str.lower)) " % stmt
2020-08-29 10:23:59 +02:00
if id not in no_action and not isinstance ( expr . contents , Keyword ) :
2022-12-26 11:29:32 +01:00
children = list ( map ( operator . attrgetter ( ' contents ' ) , reduce ( lambda x , y : x | y , ( find_bytype ( e , Keyword ) for e in [ expr ] ) ) ) )
has_duplicates = len ( children ) > len ( set ( children ) )
node_type = " ListNode " if ( " ZeroOrMore " in stmt or has_duplicates ) else " Node "
2026-07-14 16:45:40 +05:00
action = " .set_parse_action( %s ) " % (
2020-11-01 20:08:27 +07:00
actions [ id ] if id in actions else ' lambda s, loc, t: %s (s, loc, t, rule= " %s " ) ' % ( node_type , id )
)
2020-08-29 10:23:59 +02:00
stmt = " ( %s ) %s " % ( stmt , action )
2014-02-17 22:13:55 +00:00
statements . append ( " %s << %s " % ( id , stmt ) )
2022-07-10 21:51:25 +02:00
if __name__ == " __main__ " :
2026-07-14 18:24:22 +05:00
print ( r """
# This file is generated by ifcopenshell.express.bootstrap
2020-08-29 10:23:59 +02:00
2024-06-24 11:20:04 +05:00
from __future__ import annotations
2020-08-29 10:23:59 +02:00
import os
2015-08-13 14:26:14 +02:00
import sys
import pickle
2020-08-29 10:23:59 +02:00
import schema
import mapping
2014-02-17 22:13:55 +00:00
2020-08-29 10:23:59 +02:00
from pyparsing import *
from nodes import *
2024-06-24 11:20:04 +05:00
def parse(fn: str) -> mapping.Mapping:
2020-08-29 10:23:59 +02:00
cache_file = fn + " .cache.dat "
2020-09-08 14:40:27 +02:00
if os.path.exists(cache_file) and os.path.getmtime(cache_file) >= os.path.getmtime(fn):
2020-08-29 10:23:59 +02:00
with open(cache_file, " rb " ) as f:
m = pickle.load(f)
2026-07-14 16:42:04 +05:00
else:
2020-08-29 10:23:59 +02:00
%s
syntax.ignore( " -- " + restOfLine)
syntax.ignore(Regex(r " \ ((?: \ *(?:[^*]* \ *+)+? \ )) " ))
2026-07-14 16:45:40 +05:00
ast = syntax.parse_file(fn)
2020-08-29 10:23:59 +02:00
s = schema.Schema(ast)
m = mapping.Mapping(s)
with open(cache_file, " wb " ) as f:
pickle.dump(m, f, protocol=0)
return m
2026-07-14 16:42:04 +05:00
2020-08-29 10:23:59 +02:00
if __name__ == " __main__ " :
m = parse(sys.argv[1])
import importlib
for output in sys.argv[2:]:
mdl = importlib.import_module(output)
mdl.Generator(m).emit()
sys.stdout.write(m.schema.name)
2026-06-15 09:56:36 +02:00
"""
% ( " \n " . join ( statements ) )
2022-07-10 21:51:25 +02:00
)