24.09., 9:00 - 11:00: Due to updates GitLab will be unavailable for some minutes between 09:00 and 11:00.

dsl.py 22.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# dsl.py - Support for domain specific notations for DHParser
#
# Copyright 2016  by Eckhart Arnold (arnold@badw.de)
#                 Bavarian Academy of Sciences an Humanities (badw.de)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.  See the License for the specific language governing
# permissions and limitations under the License.
17 18


19
"""
20
Module ``dsl`` contains various functions to support the
21 22 23
compilation of domain specific languages based on an EBNF-grammar.
"""

24

25
import os
eckhart's avatar
eckhart committed
26 27
import platform
import stat
28

29
import DHParser.ebnf
eckhart's avatar
eckhart committed
30
from DHParser.compile import Compiler, compile_source
31
from DHParser.ebnf import EBNFCompiler, grammar_changed, DHPARSER_IMPORTS, \
32 33
    get_ebnf_preprocessor, get_ebnf_grammar, get_ebnf_transformer, get_ebnf_compiler, \
    PreprocessorFactoryFunc, ParserFactoryFunc, TransformerFactoryFunc, CompilerFactoryFunc
34
from DHParser.error import Error, is_error, has_errors, only_errors
35
from DHParser.log import logging
36
from DHParser.parse import Grammar
37
from DHParser.preprocess import nil_preprocessor, PreprocessorFunc
38
from DHParser.syntaxtree import Node
eckhart's avatar
eckhart committed
39
from DHParser.transform import TransformationFunc
40
from DHParser.toolkit import DHPARSER_DIR, load_if_file, is_python_code, compile_python_object, re
41 42
from typing import Any, cast, List, Tuple, Union, Iterator, Iterable, Optional, \
    Callable, Generator
eckhart's avatar
eckhart committed
43

44

45
__all__ = ('DefinitionError',
Eckhart Arnold's avatar
Eckhart Arnold committed
46 47 48
           'CompilationError',
           'load_compiler_suite',
           'compileDSL',
Eckhart Arnold's avatar
Eckhart Arnold committed
49
           'raw_compileEBNF',
50
           'compileEBNF',
51
           'grammar_provider',
52 53
           'compile_on_disk',
           'recompile_grammar')
Eckhart Arnold's avatar
Eckhart Arnold committed
54 55


56 57 58 59 60 61 62 63 64
def read_template(template_name: str) -> str:
    """
    Reads a script-template from a template file named `template_name`
    in the template-directory and returns it as a string.
    """
    with open(os.path.join(DHPARSER_DIR, 'templates', template_name), 'r') as f:
        return f.read()


65 66 67 68 69 70 71 72 73
SECTION_MARKER = """\n
#######################################################################
#
# {marker}
#
#######################################################################
\n"""

RX_SECTION_MARKER = re.compile(SECTION_MARKER.format(marker=r'.*?SECTION.*?'))
74
RX_WHITESPACE = re.compile(r'\s*')
75 76

SYMBOLS_SECTION = "SYMBOLS SECTION - Can be edited. Changes will be preserved."
77
PREPROCESSOR_SECTION = "PREPROCESSOR SECTION - Can be edited. Changes will be preserved."
78 79 80
PARSER_SECTION = "PARSER SECTION - Don't edit! CHANGES WILL BE OVERWRITTEN!"
AST_SECTION = "AST SECTION - Can be edited. Changes will be preserved."
COMPILER_SECTION = "COMPILER SECTION - Can be edited. Changes will be preserved."
di68kap's avatar
di68kap committed
81
END_SECTIONS_MARKER = "END OF DHPARSER-SECTIONS"
82

83
DHPARSER_MAIN = read_template('DSLCompiler.pyi')
84

85

86 87 88 89
class DSLException(Exception):
    """
    Base class for DSL-exceptions.
    """
90
    def __init__(self, errors: Union[List[Error], Generator[Error, None, None]]):
Eckhart Arnold's avatar
Eckhart Arnold committed
91
        assert isinstance(errors, Iterator) or isinstance(errors, list) \
92
            or isinstance(errors, tuple)
93
        self.errors = list(errors)
94 95

    def __str__(self):
96 97 98 99 100
        if len(self.errors) == 1:
            return str(self.errors[0])
        return '\n' + '\n'.join(("%i. " % (i + 1) + str(err))
                                for i, err in enumerate(self.errors))
        # return '\n'.join(str(err) for err in self.errors)
101 102


103
class DefinitionError(DSLException):
104 105
    """
    Raised when (already) the grammar of a domain specific language (DSL)
106
    contains errors. Usually, these are repackaged parse.GrammarError(s).
107
    """
108 109
    def __init__(self, errors, grammar_src):
        super().__init__(errors)
110 111 112
        self.grammar_src = grammar_src


113
class CompilationError(DSLException):
114 115
    """
    Raised when a string or file in a domain specific language (DSL)
116 117
    contains errors. These can also contain definition errors that
    have been caught early.
118
    """
119 120
    def __init__(self, errors, dsl_text, dsl_grammar, AST, result):
        super().__init__(errors)
121 122 123
        self.dsl_text = dsl_text
        self.dsl_grammar = dsl_grammar
        self.AST = AST
124
        self.result = result
125 126


127 128 129 130 131
def error_str(messages: Iterable[Error]) -> str:
    """
    Returns all true errors (i.e. not just warnings) from the
    `messages` as a concatenated multiline string.
    """
132
    return '\n\n'.join(str(m) for m in messages if is_error(m.code))
133 134


135
def grammar_instance(grammar_representation) -> Tuple[Grammar, str]:
136 137
    """
    Returns a grammar object and the source code of the grammar, from
138
    the given `grammar`-data which can be either a file name, ebnf-code,
139
    python-code, a Grammar-derived grammar class or an instance of
140 141
    such a class (i.e. a grammar object already).
    """
142
    if isinstance(grammar_representation, str):
143
        # read grammar
144
        grammar_src = load_if_file(grammar_representation)
145
        if is_python_code(grammar_src):
eckhart's avatar
eckhart committed
146
            parser_py, messages = grammar_src, []  # type: str, List[Error]
147
        else:
Eckhart Arnold's avatar
Eckhart Arnold committed
148
            with logging(False):
149
                result, messages, _ = compile_source(
eckhart's avatar
eckhart committed
150
                    grammar_src, None,
Eckhart Arnold's avatar
Eckhart Arnold committed
151
                    get_ebnf_grammar(), get_ebnf_transformer(), get_ebnf_compiler())
152
                parser_py = cast(str, result)
153
        if has_errors(messages):
154
            raise DefinitionError(only_errors(messages), grammar_src)
eckhart's avatar
eckhart committed
155
        parser_root = compile_python_object(DHPARSER_IMPORTS + parser_py, r'\w+Grammar$')()
156 157 158
    else:
        # assume that dsl_grammar is a ParserHQ-object or Grammar class
        grammar_src = ''
159
        if isinstance(grammar_representation, Grammar):
160
            parser_root = grammar_representation
161
        else:
Eckhart Arnold's avatar
Eckhart Arnold committed
162
            # assume ``grammar_representation`` is a grammar class and get the root object
163
            parser_root = grammar_representation()
164 165 166
    return parser_root, grammar_src


167
def compileDSL(text_or_file: str,
168
               preprocessor: Optional[PreprocessorFunc],
169
               dsl_grammar: Union[str, Grammar],
170
               ast_transformation: TransformationFunc,
171
               compiler: Compiler) -> Any:
172 173
    """
    Compiles a text in a domain specific language (DSL) with an
174 175
    EBNF-specified grammar. Returns the compiled text or raises a
    compilation error.
eckhart's avatar
eckhart committed
176

177
    Raises:
178
        CompilationError if any errors occurred during compilation
179 180
    """
    assert isinstance(text_or_file, str)
181
    assert isinstance(compiler, Compiler)
Eckhart Arnold's avatar
Eckhart Arnold committed
182

183
    parser, grammar_src = grammar_instance(dsl_grammar)
184
    result, messages, AST = compile_source(text_or_file, preprocessor, parser,
185
                                           ast_transformation, compiler)
186
    if has_errors(messages):
Eckhart Arnold's avatar
Eckhart Arnold committed
187
        src = load_if_file(text_or_file)
Eckhart Arnold's avatar
Eckhart Arnold committed
188
        raise CompilationError(only_errors(messages), src, grammar_src, AST, result)
189 190 191
    return result


192
def raw_compileEBNF(ebnf_src: str, branding="DSL") -> EBNFCompiler:
193 194
    """
    Compiles an EBNF grammar file and returns the compiler object
Eckhart Arnold's avatar
Eckhart Arnold committed
195
    that was used and which can now be queried for the result as well
196
    as skeleton code for preprocessor, transformer and compiler objects.
eckhart's avatar
eckhart committed
197

Eckhart Arnold's avatar
Eckhart Arnold committed
198 199 200 201
    Args:
        ebnf_src(str):  Either the file name of an EBNF grammar or
            the EBNF grammar itself as a string.
        branding (str):  Branding name for the compiler suite source
eckhart's avatar
eckhart committed
202
            code.
Eckhart Arnold's avatar
Eckhart Arnold committed
203 204 205
    Returns:
        An instance of class ``ebnf.EBNFCompiler``
    Raises:
eckhart's avatar
eckhart committed
206
        CompilationError if any errors occurred during compilation
Eckhart Arnold's avatar
Eckhart Arnold committed
207 208
    """
    grammar = get_ebnf_grammar()
209
    compiler = get_ebnf_compiler(branding, ebnf_src)
210 211
    transformer = get_ebnf_transformer()
    compileDSL(ebnf_src, nil_preprocessor, grammar, transformer, compiler)
Eckhart Arnold's avatar
Eckhart Arnold committed
212 213 214
    return compiler


215
def compileEBNF(ebnf_src: str, branding="DSL") -> str:
216 217
    """
    Compiles an EBNF source file and returns the source code of a
218
    compiler suite with skeletons for preprocessor, transformer and
Eckhart Arnold's avatar
Eckhart Arnold committed
219
    compiler.
220 221 222 223

    Args:
        ebnf_src(str):  Either the file name of an EBNF grammar or
            the EBNF grammar itself as a string.
224
        branding (str):  Branding name for the compiler suite source
eckhart's avatar
eckhart committed
225
            code.
226
    Returns:
227
        The complete compiler suite skeleton as Python source code.
228
    Raises:
eckhart's avatar
eckhart committed
229
        CompilationError if any errors occurred during compilation
230
    """
Eckhart Arnold's avatar
Eckhart Arnold committed
231
    compiler = raw_compileEBNF(ebnf_src, branding)
232 233
    src = ["#/usr/bin/python\n",
           SECTION_MARKER.format(marker=SYMBOLS_SECTION), DHPARSER_IMPORTS,
234
           SECTION_MARKER.format(marker=PREPROCESSOR_SECTION), compiler.gen_preprocessor_skeleton(),
Eckhart Arnold's avatar
Eckhart Arnold committed
235
           SECTION_MARKER.format(marker=PARSER_SECTION), compiler.result,
236 237 238
           SECTION_MARKER.format(marker=AST_SECTION), compiler.gen_transformer_skeleton(),
           SECTION_MARKER.format(marker=COMPILER_SECTION), compiler.gen_compiler_skeleton(),
           SECTION_MARKER.format(marker=SYMBOLS_SECTION), DHPARSER_MAIN.format(NAME=branding)]
239 240 241
    return '\n'.join(src)


242
def grammar_provider(ebnf_src: str, branding="DSL") -> Grammar:
243
    """
244
    Compiles an EBNF grammar and returns a grammar-parser provider
245 246 247 248 249 250
    function for that grammar.

    Args:
        ebnf_src(str):  Either the file name of an EBNF grammar or
            the EBNF grammar itself as a string.
        branding (str or bool):  Branding name for the compiler
eckhart's avatar
eckhart committed
251 252
            suite source code.

253
    Returns:
254
        A provider function for a grammar object for texts in the
255 256
        language defined by ``ebnf_src``.
    """
257
    grammar_src = compileDSL(ebnf_src, nil_preprocessor, get_ebnf_grammar(),
eckhart's avatar
eckhart committed
258
                             get_ebnf_transformer(), get_ebnf_compiler(branding, ebnf_src))
eckhart's avatar
eckhart committed
259 260 261
    grammar_factory = compile_python_object(DHPARSER_IMPORTS + grammar_src, r'get_(?:\w+_)?grammar$')
    grammar_factory.python_src__ = grammar_src
    return grammar_factory
262 263


264
def load_compiler_suite(compiler_suite: str) -> \
265 266
        Tuple[PreprocessorFactoryFunc, ParserFactoryFunc,
              TransformerFactoryFunc, CompilerFactoryFunc]:
267
    """
268
    Extracts a compiler suite from file or string `compiler_suite`
269
    and returns it as a tuple (preprocessor, parser, ast, compiler).
eckhart's avatar
eckhart committed
270

Eckhart Arnold's avatar
Eckhart Arnold committed
271
    Returns:
272 273
        4-tuple (preprocessor function, parser class,
                 ast transformer function, compiler class)
274 275 276 277
    """
    global RX_SECTION_MARKER
    assert isinstance(compiler_suite, str)
    source = load_if_file(compiler_suite)
278
    imports = DHPARSER_IMPORTS
279 280
    if is_python_code(compiler_suite):
        try:
eckhart's avatar
eckhart committed
281
            _, imports, preprocessor_py, parser_py, ast_py, compiler_py, _ = \
282
                RX_SECTION_MARKER.split(source)
eckhart's avatar
eckhart committed
283
        except ValueError:
284 285
            raise AssertionError('File "' + compiler_suite + '" seems to be corrupted. '
                                 'Please delete or repair file manually.')
286
        # TODO: Compile in one step and pick parts from namespace later ?
eckhart's avatar
eckhart committed
287 288 289 290
        preprocessor = compile_python_object(imports + preprocessor_py,
                                             r'get_(?:\w+_)?preprocessor$')
        parser = compile_python_object(imports + parser_py, r'get_(?:\w+_)?grammar$')
        ast = compile_python_object(imports + ast_py, r'get_(?:\w+_)?transformer$')
291
    else:
292 293
        # Assume source is an ebnf grammar.
        # Is there really any reasonable application case for this?
Eckhart Arnold's avatar
Eckhart Arnold committed
294
        with logging(False):
eckhart's avatar
eckhart committed
295
            compiler_py, messages, n = compile_source(source, None, get_ebnf_grammar(),
eckhart's avatar
eckhart committed
296 297
                                                      get_ebnf_transformer(),
                                                      get_ebnf_compiler(compiler_suite, source))
298
        if has_errors(messages):
299
            raise DefinitionError(only_errors(messages), source)
300
        preprocessor = get_ebnf_preprocessor
301
        parser = get_ebnf_grammar
302
        ast = get_ebnf_transformer
eckhart's avatar
eckhart committed
303
    compiler = compile_python_object(imports + compiler_py, r'get_(?:\w+_)?compiler$')
304

305
    return preprocessor, parser, ast, compiler
306 307


308
def is_outdated(compiler_suite: str, grammar_source: str) -> bool:
309 310
    """
    Returns ``True``  if the ``compile_suite`` needs to be updated.
eckhart's avatar
eckhart committed
311 312 313

    An update is needed, if either the grammar in the compieler suite
    does not reflect the latest changes of ``grammar_source`` or if
314 315
    sections from the compiler suite have diligently been overwritten
    with whitespace order to trigger their recreation. Note: Do not
eckhart's avatar
eckhart committed
316
    delete or overwrite the section marker itself.
317

318
    Args:
319 320 321 322 323 324 325 326 327
        compiler_suite:  the parser class representing the grammar
            or the file name of a compiler suite containing the grammar
        grammar_source:  File name or string representation of the
            EBNF code of the grammar

    Returns (bool):
        True, if ``compiler_suite`` seems to be out of date.
    """
    try:
eckhart's avatar
eckhart committed
328
        n1, grammar, n2, n3 = load_compiler_suite(compiler_suite)
329
        return grammar_changed(grammar(), grammar_source)
330 331 332 333
    except ValueError:
        return True


334
def run_compiler(text_or_file: str, compiler_suite: str) -> Any:
335 336 337 338
    """Compiles a source with a given compiler suite.

    Args:
        text_or_file (str):  Either the file name of the source code or
eckhart's avatar
eckhart committed
339
            the source code directly. (Which is determined by
340 341 342 343 344
            heuristics. If ``text_or_file`` contains at least on
            linefeed then it is always assumed to be a source text and
            not a file name.)
        compiler_suite(str):  File name of the compiler suite to be
            used.
eckhart's avatar
eckhart committed
345

346
    Returns:
eckhart's avatar
eckhart committed
347
        The result of the compilation, the form and type of which
348
        depends entirely on the compiler.
eckhart's avatar
eckhart committed
349

350 351 352
    Raises:
        CompilerError
    """
353 354
    preprocessor, parser, ast, compiler = load_compiler_suite(compiler_suite)
    return compileDSL(text_or_file, preprocessor(), parser(), ast(), compiler())
355 356


357
def compile_on_disk(source_file: str, compiler_suite="", extension=".xml") -> Iterable[Error]:
358 359
    """
    Compiles the a source file with a given compiler and writes the
360 361
    result to a file.

Eckhart Arnold's avatar
Eckhart Arnold committed
362 363 364
    If no ``compiler_suite`` is given it is assumed that the source
    file is an EBNF grammar. In this case the result will be a Python
    script containing a parser for that grammar as well as the
365
    skeletons for a preprocessor, AST transformation table, and compiler.
Eckhart Arnold's avatar
Eckhart Arnold committed
366 367
    If the Python script already exists only the parser name in the
    script will be updated. (For this to work, the different names
368
    need to be delimited section marker blocks.). `compile_on_disk()`
Eckhart Arnold's avatar
Eckhart Arnold committed
369 370
    returns a list of error messages or an empty list if no errors
    occurred.
371

372 373 374 375
    Parameters:
        source_file(str):  The file name of the source text to be
            compiled.
        compiler_suite(str):  The file name of the compiler suite
376
            (usually ending with 'Compiler.py'), with which the source
377 378 379 380 381 382 383
            file shall be compiled. If this is left empty, the source
            file is assumed to be an EBNF-Grammar that will be compiled
            with the internal EBNF-Compiler.
        extension(str):  The result of the compilation (if successful)
            is written to a file with the same name but a different
            extension than the source file. This parameter sets the
            extension.
eckhart's avatar
eckhart committed
384

385
    Returns:
386
        A (potentially empty) list of error or warning messages.
Eckhart Arnold's avatar
Eckhart Arnold committed
387
    """
388
    filepath = os.path.normpath(source_file)
eckhart's avatar
eckhart committed
389
    f = None  # Optional[TextIO]
390 391
    with open(source_file, encoding="utf-8") as f:
        source = f.read()
392
    rootname = os.path.splitext(filepath)[0]
393
    compiler_name = os.path.basename(rootname)
394
    if compiler_suite:
395
        sfactory, pfactory, tfactory, cfactory = load_compiler_suite(compiler_suite)
eckhart's avatar
eckhart committed
396
        compiler1 = cfactory()
397
    else:
398
        sfactory = get_ebnf_preprocessor
399 400
        pfactory = get_ebnf_grammar
        tfactory = get_ebnf_transformer
401
        cfactory = get_ebnf_compiler
eckhart's avatar
eckhart committed
402 403
        compiler1 = cfactory()
        compiler1.set_grammar_name(compiler_name, source_file)
404
    result, messages, _ = compile_source(source, sfactory(), pfactory(), tfactory(), compiler1)
eckhart's avatar
eckhart committed
405

406 407
    if has_errors(messages):
        return messages
408

409 410 411
    elif cfactory == get_ebnf_compiler:
        # trans == get_ebnf_transformer or trans == EBNFTransformer:
        # either an EBNF- or no compiler suite given
412
        ebnf_compiler = cast(EBNFCompiler, compiler1)
413
        global SECTION_MARKER, RX_SECTION_MARKER, PREPROCESSOR_SECTION, PARSER_SECTION, \
Eckhart Arnold's avatar
Eckhart Arnold committed
414
            AST_SECTION, COMPILER_SECTION, END_SECTIONS_MARKER, RX_WHITESPACE, \
415
            DHPARSER_MAIN
416
        f = None
417
        try:
418
            f = open(rootname + 'Compiler.py', 'r', encoding="utf-8")
419
            source = f.read()
420
            sections = RX_SECTION_MARKER.split(source)
421
            intro, imports, preprocessor, _, ast, compiler, outro = sections
422 423 424
            ast_trans_table = compile_python_object(DHPARSER_IMPORTS + ast,
                                                    r'(?:\w+_)?AST_transformation_table$')
            messages.extend(ebnf_compiler.verify_transformation_table(ast_trans_table))
425
            # TODO: Verify compiler
426 427 428
        except (PermissionError, FileNotFoundError, IOError):
            intro, imports, preprocessor, _, ast, compiler, outro = '', '', '', '', '', '', ''
        except ValueError:
429
            name = '"' + rootname + 'Compiler.py"'
eckhart's avatar
eckhart committed
430 431
            raise ValueError('Could not identify all required sections in ' + name
                             + '. Please delete or repair ' + name + ' manually!')
432
        finally:
433 434 435
            if f:
                f.close()
                f = None
436

437
        if RX_WHITESPACE.fullmatch(intro):
438
            intro = '#!/usr/bin/python3'
439
        if RX_WHITESPACE.fullmatch(outro):
Eckhart Arnold's avatar
Eckhart Arnold committed
440
            outro = DHPARSER_MAIN.format(NAME=compiler_name)
441
        if RX_WHITESPACE.fullmatch(imports):
442
            imports = DHParser.ebnf.DHPARSER_IMPORTS
443 444
        if RX_WHITESPACE.fullmatch(preprocessor):
            preprocessor = ebnf_compiler.gen_preprocessor_skeleton()
445
        if RX_WHITESPACE.fullmatch(ast):
446
            ast = ebnf_compiler.gen_transformer_skeleton()
447
        if RX_WHITESPACE.fullmatch(compiler):
448
            compiler = ebnf_compiler.gen_compiler_skeleton()
449

eckhart's avatar
eckhart committed
450
        compilerscript = rootname + 'Compiler.py'
451
        try:
eckhart's avatar
eckhart committed
452
            f = open(compilerscript, 'w', encoding="utf-8")
453 454
            f.write(intro)
            f.write(SECTION_MARKER.format(marker=SYMBOLS_SECTION))
455
            f.write(imports)
456 457
            f.write(SECTION_MARKER.format(marker=PREPROCESSOR_SECTION))
            f.write(preprocessor)
458
            f.write(SECTION_MARKER.format(marker=PARSER_SECTION))
eckhart's avatar
eckhart committed
459
            f.write(cast(str, result))
460 461 462 463 464 465 466
            f.write(SECTION_MARKER.format(marker=AST_SECTION))
            f.write(ast)
            f.write(SECTION_MARKER.format(marker=COMPILER_SECTION))
            f.write(compiler)
            f.write(SECTION_MARKER.format(marker=END_SECTIONS_MARKER))
            f.write(outro)
        except (PermissionError, FileNotFoundError, IOError) as error:
eckhart's avatar
eckhart committed
467
            print('# Could not write file "' + compilerscript + '" because of: '
468 469 470
                  + "\n# ".join(str(error).split('\n)')))
            print(result)
        finally:
471 472
            if f:
                f.close()
473

eckhart's avatar
eckhart committed
474 475 476 477 478
        if platform.system() != "Windows":
            # set file permissions so that the compilerscript can be executed
            st = os.stat(compilerscript)
            os.chmod(compilerscript, st.st_mode | stat.S_IEXEC)

479
    else:
480
        f = None
481 482 483
        try:
            f = open(rootname + extension, 'w', encoding="utf-8")
            if isinstance(result, Node):
484 485 486 487
                if extension.lower() == '.xml':
                    f.write(result.as_xml())
                else:
                    f.write(result.as_sxpr())
eckhart's avatar
eckhart committed
488
            elif isinstance(result, str):
489
                f.write(result)
eckhart's avatar
eckhart committed
490 491
            else:
                raise AssertionError('Illegal result type: ' + str(type(result)))
492 493 494 495 496
        except (PermissionError, FileNotFoundError, IOError) as error:
            print('# Could not write file "' + rootname + '.py" because of: '
                  + "\n# ".join(str(error).split('\n)')))
            print(result)
        finally:
497 498
            if f:
                f.close()
499

500
    return messages
501 502


503
def recompile_grammar(ebnf_filename, force=False,
eckhart's avatar
eckhart committed
504
                      notify: Callable = lambda: None) -> bool:
505
    """
506
    Re-compiles an EBNF-grammar if necessary, that is, if either no
507 508 509 510
    corresponding 'XXXXCompiler.py'-file exists or if that file is
    outdated.

    Parameters:
511 512 513
        ebnf_filename(str):  The filename of the ebnf-source of the grammar.
            In case this is a directory and not a file, all files within
            this directory ending with .ebnf will be compiled.
514 515
        force(bool):  If False (default), the grammar will only be
            recompiled if it has been changed.
516 517 518
        notify(Callable):  'notify' is a function without parameters that
            is called when recompilation actually takes place. This can
            be used to inform the user.
519 520 521 522 523
    Returns:
        bool:  True, if recompilation of grammar has been successful or did
            not take place, because the Grammar hasn't changed since the last
            compilation. False, if the recompilation of the grammar has been
            attempted but failed.
524 525 526 527 528 529 530 531
    """
    if os.path.isdir(ebnf_filename):
        success = True
        for entry in os.listdir(ebnf_filename):
            if entry.lower().endswith('.ebnf') and os.path.isfile(entry):
                success = success and recompile_grammar(entry, force)
        return success

532
    base, _ = os.path.splitext(ebnf_filename)
533 534
    compiler_name = base + 'Compiler.py'
    error_file_name = base + '_ebnf_ERRORS.txt'
535
    messages = []  # type: Iterable[Error]
536 537
    if (not os.path.exists(compiler_name) or force or
            grammar_changed(compiler_name, ebnf_filename)):
538
        notify()
539 540
        messages = compile_on_disk(ebnf_filename)
        if messages:
541
            # print("Errors while compiling: " + ebnf_filename + '!')
Eckhart Arnold's avatar
Eckhart Arnold committed
542
            with open(error_file_name, 'w', encoding="utf-8") as f:
543
                for e in messages:
Eckhart Arnold's avatar
Eckhart Arnold committed
544
                    f.write(str(e))
545
                    f.write('\n')
546 547
            if has_errors(messages):
                return False
548

549
    if not messages and os.path.exists(error_file_name):
550 551
        os.remove(error_file_name)
    return True