dsl.py 23.2 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 load_if_file, is_python_code, compile_python_object, \
41
42
43
    re
from typing import Any, cast, List, Tuple, Union, Iterator, Iterable, Optional, \
    Callable, Generator
eckhart's avatar
eckhart committed
44

45

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


57
58
59
60
61
62
63
64
65
SECTION_MARKER = """\n
#######################################################################
#
# {marker}
#
#######################################################################
\n"""

RX_SECTION_MARKER = re.compile(SECTION_MARKER.format(marker=r'.*?SECTION.*?'))
66
RX_WHITESPACE = re.compile(r'\s*')
67
68

SYMBOLS_SECTION = "SYMBOLS SECTION - Can be edited. Changes will be preserved."
69
PREPROCESSOR_SECTION = "PREPROCESSOR SECTION - Can be edited. Changes will be preserved."
70
71
72
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
73
END_SECTIONS_MARKER = "END OF DHPARSER-SECTIONS"
74

Eckhart Arnold's avatar
Eckhart Arnold committed
75
DHPARSER_MAIN = '''
76
def compile_src(source, log_dir=''):
77
78
    """Compiles ``source`` and returns (result, errors, ast).
    """
di68kap's avatar
di68kap committed
79
    with logging(log_dir):
80
        compiler = get_compiler()
eckhart's avatar
eckhart committed
81
82
83
84
        result_tuple = compile_source(source, get_preprocessor(),
                                      get_grammar(),
                                      get_transformer(), compiler)
    return result_tuple
Eckhart Arnold's avatar
Eckhart Arnold committed
85

86
87

if __name__ == "__main__":
88
89
90
91
92
93
94
95
96
97
98
99
100
    # recompile grammar if needed
    grammar_path = os.path.abspath(__file__).replace('Compiler.py', '.ebnf')
    if os.path.exists(grammar_path):
        if not recompile_grammar(grammar_path, force=False,
                                  notify=lambda:print('recompiling ' + grammar_path)):
            error_file = os.path.basename(__file__).replace('Compiler.py', '_ebnf_ERRORS.txt')
            with open(error_file, encoding="utf-8") as f:
                print(f.read())
            sys.exit(1)
    else:
        print('Could not check whether grammar requires recompiling, '
              'because grammar was not found at: ' + grammar_path)

101
    if len(sys.argv) > 1:
102
        # compile file
di68kap's avatar
di68kap committed
103
104
105
        file_name, log_dir = sys.argv[1], ''
        if file_name in ['-d', '--debug'] and len(sys.argv) > 2:
            file_name, log_dir = sys.argv[2], 'LOGS'
106
        result, errors, ast = compile_src(file_name, log_dir)
107
        if errors:
di68kap's avatar
di68kap committed
108
109
            cwd = os.getcwd()
            rel_path = file_name[len(cwd):] if file_name.startswith(cwd) else file_name
110
            for error in errors:
di68kap's avatar
di68kap committed
111
                print(rel_path + ':' + str(error))
112
            sys.exit(1)
113
        else:
114
            print(result.as_xml() if isinstance(result, Node) else result)
115
    else:
116
        print("Usage: {NAME}Compiler.py [FILENAME]")
117
118
'''

119

120
121
122
123
class DSLException(Exception):
    """
    Base class for DSL-exceptions.
    """
124
    def __init__(self, errors: Union[List[Error], Generator[Error, None, None]]):
Eckhart Arnold's avatar
Eckhart Arnold committed
125
        assert isinstance(errors, Iterator) or isinstance(errors, list) \
126
            or isinstance(errors, tuple)
127
        self.errors = list(errors)
128
129

    def __str__(self):
130
131
132
133
134
        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)
135
136


137
class DefinitionError(DSLException):
138
139
    """
    Raised when (already) the grammar of a domain specific language (DSL)
140
    contains errors. Usually, these are repackaged parse.GrammarError(s).
141
    """
142
143
    def __init__(self, errors, grammar_src):
        super().__init__(errors)
144
145
146
        self.grammar_src = grammar_src


147
class CompilationError(DSLException):
148
149
    """
    Raised when a string or file in a domain specific language (DSL)
150
151
    contains errors. These can also contain definition errors that
    have been caught early.
152
    """
153
154
    def __init__(self, errors, dsl_text, dsl_grammar, AST, result):
        super().__init__(errors)
155
156
157
        self.dsl_text = dsl_text
        self.dsl_grammar = dsl_grammar
        self.AST = AST
158
        self.result = result
159
160


161
162
163
164
165
def error_str(messages: Iterable[Error]) -> str:
    """
    Returns all true errors (i.e. not just warnings) from the
    `messages` as a concatenated multiline string.
    """
166
    return '\n\n'.join(str(m) for m in messages if is_error(m.code))
167
168


169
def grammar_instance(grammar_representation) -> Tuple[Grammar, str]:
170
171
    """
    Returns a grammar object and the source code of the grammar, from
172
    the given `grammar`-data which can be either a file name, ebnf-code,
173
    python-code, a Grammar-derived grammar class or an instance of
174
175
    such a class (i.e. a grammar object already).
    """
176
    if isinstance(grammar_representation, str):
177
        # read grammar
178
        grammar_src = load_if_file(grammar_representation)
179
        if is_python_code(grammar_src):
eckhart's avatar
eckhart committed
180
            parser_py, messages = grammar_src, []  # type: str, List[Error]
181
        else:
Eckhart Arnold's avatar
Eckhart Arnold committed
182
            with logging(False):
183
                result, messages, _ = compile_source(
eckhart's avatar
eckhart committed
184
                    grammar_src, None,
Eckhart Arnold's avatar
Eckhart Arnold committed
185
                    get_ebnf_grammar(), get_ebnf_transformer(), get_ebnf_compiler())
186
                parser_py = cast(str, result)
187
        if has_errors(messages):
188
            raise DefinitionError(only_errors(messages), grammar_src)
eckhart's avatar
eckhart committed
189
        parser_root = compile_python_object(DHPARSER_IMPORTS + parser_py, r'\w+Grammar$')()
190
191
192
    else:
        # assume that dsl_grammar is a ParserHQ-object or Grammar class
        grammar_src = ''
193
        if isinstance(grammar_representation, Grammar):
194
            parser_root = grammar_representation
195
        else:
Eckhart Arnold's avatar
Eckhart Arnold committed
196
            # assume ``grammar_representation`` is a grammar class and get the root object
197
            parser_root = grammar_representation()
198
199
200
    return parser_root, grammar_src


201
def compileDSL(text_or_file: str,
202
               preprocessor: Optional[PreprocessorFunc],
203
               dsl_grammar: Union[str, Grammar],
204
               ast_transformation: TransformationFunc,
205
               compiler: Compiler) -> Any:
206
207
    """
    Compiles a text in a domain specific language (DSL) with an
208
209
    EBNF-specified grammar. Returns the compiled text or raises a
    compilation error.
eckhart's avatar
eckhart committed
210

211
    Raises:
212
        CompilationError if any errors occurred during compilation
213
214
    """
    assert isinstance(text_or_file, str)
215
    assert isinstance(compiler, Compiler)
Eckhart Arnold's avatar
Eckhart Arnold committed
216

217
    parser, grammar_src = grammar_instance(dsl_grammar)
218
    result, messages, AST = compile_source(text_or_file, preprocessor, parser,
219
                                           ast_transformation, compiler)
220
    if has_errors(messages):
Eckhart Arnold's avatar
Eckhart Arnold committed
221
        src = load_if_file(text_or_file)
Eckhart Arnold's avatar
Eckhart Arnold committed
222
        raise CompilationError(only_errors(messages), src, grammar_src, AST, result)
223
224
225
    return result


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

Eckhart Arnold's avatar
Eckhart Arnold committed
232
233
234
235
    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
236
            code.
Eckhart Arnold's avatar
Eckhart Arnold committed
237
238
239
    Returns:
        An instance of class ``ebnf.EBNFCompiler``
    Raises:
eckhart's avatar
eckhart committed
240
        CompilationError if any errors occurred during compilation
Eckhart Arnold's avatar
Eckhart Arnold committed
241
242
    """
    grammar = get_ebnf_grammar()
243
    compiler = get_ebnf_compiler(branding, ebnf_src)
244
245
    transformer = get_ebnf_transformer()
    compileDSL(ebnf_src, nil_preprocessor, grammar, transformer, compiler)
Eckhart Arnold's avatar
Eckhart Arnold committed
246
247
248
    return compiler


249
def compileEBNF(ebnf_src: str, branding="DSL") -> str:
250
251
    """
    Compiles an EBNF source file and returns the source code of a
252
    compiler suite with skeletons for preprocessor, transformer and
Eckhart Arnold's avatar
Eckhart Arnold committed
253
    compiler.
254
255
256
257

    Args:
        ebnf_src(str):  Either the file name of an EBNF grammar or
            the EBNF grammar itself as a string.
258
        branding (str):  Branding name for the compiler suite source
eckhart's avatar
eckhart committed
259
            code.
260
    Returns:
261
        The complete compiler suite skeleton as Python source code.
262
    Raises:
eckhart's avatar
eckhart committed
263
        CompilationError if any errors occurred during compilation
264
    """
Eckhart Arnold's avatar
Eckhart Arnold committed
265
    compiler = raw_compileEBNF(ebnf_src, branding)
266
267
    src = ["#/usr/bin/python\n",
           SECTION_MARKER.format(marker=SYMBOLS_SECTION), DHPARSER_IMPORTS,
268
           SECTION_MARKER.format(marker=PREPROCESSOR_SECTION), compiler.gen_preprocessor_skeleton(),
Eckhart Arnold's avatar
Eckhart Arnold committed
269
           SECTION_MARKER.format(marker=PARSER_SECTION), compiler.result,
270
271
272
           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)]
273
274
275
    return '\n'.join(src)


276
def grammar_provider(ebnf_src: str, branding="DSL") -> Grammar:
277
    """
278
    Compiles an EBNF grammar and returns a grammar-parser provider
279
280
281
282
283
284
    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
285
286
            suite source code.

287
    Returns:
288
        A provider function for a grammar object for texts in the
289
290
        language defined by ``ebnf_src``.
    """
291
    grammar_src = compileDSL(ebnf_src, nil_preprocessor, get_ebnf_grammar(),
eckhart's avatar
eckhart committed
292
                             get_ebnf_transformer(), get_ebnf_compiler(branding, ebnf_src))
eckhart's avatar
eckhart committed
293
294
295
    grammar_factory = compile_python_object(DHPARSER_IMPORTS + grammar_src, r'get_(?:\w+_)?grammar$')
    grammar_factory.python_src__ = grammar_src
    return grammar_factory
296
297


298
def load_compiler_suite(compiler_suite: str) -> \
299
300
        Tuple[PreprocessorFactoryFunc, ParserFactoryFunc,
              TransformerFactoryFunc, CompilerFactoryFunc]:
301
    """
302
    Extracts a compiler suite from file or string `compiler_suite`
303
    and returns it as a tuple (preprocessor, parser, ast, compiler).
eckhart's avatar
eckhart committed
304

Eckhart Arnold's avatar
Eckhart Arnold committed
305
    Returns:
306
307
        4-tuple (preprocessor function, parser class,
                 ast transformer function, compiler class)
308
309
310
311
    """
    global RX_SECTION_MARKER
    assert isinstance(compiler_suite, str)
    source = load_if_file(compiler_suite)
312
    imports = DHPARSER_IMPORTS
313
314
    if is_python_code(compiler_suite):
        try:
eckhart's avatar
eckhart committed
315
            _, imports, preprocessor_py, parser_py, ast_py, compiler_py, _ = \
316
                RX_SECTION_MARKER.split(source)
eckhart's avatar
eckhart committed
317
        except ValueError:
318
319
            raise AssertionError('File "' + compiler_suite + '" seems to be corrupted. '
                                 'Please delete or repair file manually.')
320
        # TODO: Compile in one step and pick parts from namespace later ?
eckhart's avatar
eckhart committed
321
322
323
324
        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$')
325
    else:
326
327
        # Assume source is an ebnf grammar.
        # Is there really any reasonable application case for this?
Eckhart Arnold's avatar
Eckhart Arnold committed
328
        with logging(False):
eckhart's avatar
eckhart committed
329
            compiler_py, messages, n = compile_source(source, None, get_ebnf_grammar(),
eckhart's avatar
eckhart committed
330
331
                                                      get_ebnf_transformer(),
                                                      get_ebnf_compiler(compiler_suite, source))
332
        if has_errors(messages):
333
            raise DefinitionError(only_errors(messages), source)
334
        preprocessor = get_ebnf_preprocessor
335
        parser = get_ebnf_grammar
336
        ast = get_ebnf_transformer
eckhart's avatar
eckhart committed
337
    compiler = compile_python_object(imports + compiler_py, r'get_(?:\w+_)?compiler$')
338

339
    return preprocessor, parser, ast, compiler
340
341


342
def is_outdated(compiler_suite: str, grammar_source: str) -> bool:
343
344
    """
    Returns ``True``  if the ``compile_suite`` needs to be updated.
eckhart's avatar
eckhart committed
345
346
347

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

352
    Args:
353
354
355
356
357
358
359
360
361
        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
362
        n1, grammar, n2, n3 = load_compiler_suite(compiler_suite)
363
        return grammar_changed(grammar(), grammar_source)
364
365
366
367
    except ValueError:
        return True


368
def run_compiler(text_or_file: str, compiler_suite: str) -> Any:
369
370
371
372
    """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
373
            the source code directly. (Which is determined by
374
375
376
377
378
            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
379

380
    Returns:
eckhart's avatar
eckhart committed
381
        The result of the compilation, the form and type of which
382
        depends entirely on the compiler.
eckhart's avatar
eckhart committed
383

384
385
386
    Raises:
        CompilerError
    """
387
388
    preprocessor, parser, ast, compiler = load_compiler_suite(compiler_suite)
    return compileDSL(text_or_file, preprocessor(), parser(), ast(), compiler())
389
390


391
def compile_on_disk(source_file: str, compiler_suite="", extension=".xml") -> Iterable[Error]:
392
393
    """
    Compiles the a source file with a given compiler and writes the
394
395
    result to a file.

Eckhart Arnold's avatar
Eckhart Arnold committed
396
397
398
    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
399
    skeletons for a preprocessor, AST transformation table, and compiler.
Eckhart Arnold's avatar
Eckhart Arnold committed
400
401
    If the Python script already exists only the parser name in the
    script will be updated. (For this to work, the different names
402
    need to be delimited section marker blocks.). `compile_on_disk()`
Eckhart Arnold's avatar
Eckhart Arnold committed
403
404
    returns a list of error messages or an empty list if no errors
    occurred.
405

406
407
408
409
    Parameters:
        source_file(str):  The file name of the source text to be
            compiled.
        compiler_suite(str):  The file name of the compiler suite
410
            (usually ending with 'Compiler.py'), with which the source
411
412
413
414
415
416
417
            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
418

419
    Returns:
420
        A (potentially empty) list of error or warning messages.
Eckhart Arnold's avatar
Eckhart Arnold committed
421
    """
422
    filepath = os.path.normpath(source_file)
eckhart's avatar
eckhart committed
423
    f = None  # Optional[TextIO]
424
425
    with open(source_file, encoding="utf-8") as f:
        source = f.read()
426
    rootname = os.path.splitext(filepath)[0]
427
    compiler_name = os.path.basename(rootname)
428
    if compiler_suite:
429
        sfactory, pfactory, tfactory, cfactory = load_compiler_suite(compiler_suite)
eckhart's avatar
eckhart committed
430
        compiler1 = cfactory()
431
    else:
432
        sfactory = get_ebnf_preprocessor
433
434
        pfactory = get_ebnf_grammar
        tfactory = get_ebnf_transformer
435
        cfactory = get_ebnf_compiler
eckhart's avatar
eckhart committed
436
437
        compiler1 = cfactory()
        compiler1.set_grammar_name(compiler_name, source_file)
438
    result, messages, _ = compile_source(source, sfactory(), pfactory(), tfactory(), compiler1)
eckhart's avatar
eckhart committed
439

440
441
    if has_errors(messages):
        return messages
442

443
444
445
    elif cfactory == get_ebnf_compiler:
        # trans == get_ebnf_transformer or trans == EBNFTransformer:
        # either an EBNF- or no compiler suite given
446
        ebnf_compiler = cast(EBNFCompiler, compiler1)
447
        global SECTION_MARKER, RX_SECTION_MARKER, PREPROCESSOR_SECTION, PARSER_SECTION, \
Eckhart Arnold's avatar
Eckhart Arnold committed
448
            AST_SECTION, COMPILER_SECTION, END_SECTIONS_MARKER, RX_WHITESPACE, \
449
            DHPARSER_MAIN
450
        f = None
451
        try:
452
            f = open(rootname + 'Compiler.py', 'r', encoding="utf-8")
453
            source = f.read()
454
            sections = RX_SECTION_MARKER.split(source)
455
            intro, imports, preprocessor, _, ast, compiler, outro = sections
456
457
458
            ast_trans_table = compile_python_object(DHPARSER_IMPORTS + ast,
                                                    r'(?:\w+_)?AST_transformation_table$')
            messages.extend(ebnf_compiler.verify_transformation_table(ast_trans_table))
459
            # TODO: Verify compiler
460
461
462
        except (PermissionError, FileNotFoundError, IOError):
            intro, imports, preprocessor, _, ast, compiler, outro = '', '', '', '', '', '', ''
        except ValueError:
463
            name = '"' + rootname + 'Compiler.py"'
eckhart's avatar
eckhart committed
464
465
            raise ValueError('Could not identify all required sections in ' + name
                             + '. Please delete or repair ' + name + ' manually!')
466
        finally:
467
468
469
            if f:
                f.close()
                f = None
470

471
        if RX_WHITESPACE.fullmatch(intro):
472
            intro = '#!/usr/bin/python3'
473
        if RX_WHITESPACE.fullmatch(outro):
Eckhart Arnold's avatar
Eckhart Arnold committed
474
            outro = DHPARSER_MAIN.format(NAME=compiler_name)
475
        if RX_WHITESPACE.fullmatch(imports):
476
            imports = DHParser.ebnf.DHPARSER_IMPORTS
477
478
        if RX_WHITESPACE.fullmatch(preprocessor):
            preprocessor = ebnf_compiler.gen_preprocessor_skeleton()
479
        if RX_WHITESPACE.fullmatch(ast):
480
            ast = ebnf_compiler.gen_transformer_skeleton()
481
        if RX_WHITESPACE.fullmatch(compiler):
482
            compiler = ebnf_compiler.gen_compiler_skeleton()
483

eckhart's avatar
eckhart committed
484
        compilerscript = rootname + 'Compiler.py'
485
        try:
eckhart's avatar
eckhart committed
486
            f = open(compilerscript, 'w', encoding="utf-8")
487
488
            f.write(intro)
            f.write(SECTION_MARKER.format(marker=SYMBOLS_SECTION))
489
            f.write(imports)
490
491
            f.write(SECTION_MARKER.format(marker=PREPROCESSOR_SECTION))
            f.write(preprocessor)
492
            f.write(SECTION_MARKER.format(marker=PARSER_SECTION))
eckhart's avatar
eckhart committed
493
            f.write(cast(str, result))
494
495
496
497
498
499
500
            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
501
            print('# Could not write file "' + compilerscript + '" because of: '
502
503
504
                  + "\n# ".join(str(error).split('\n)')))
            print(result)
        finally:
505
506
            if f:
                f.close()
507

eckhart's avatar
eckhart committed
508
509
510
511
512
        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)

513
    else:
514
        f = None
515
516
517
        try:
            f = open(rootname + extension, 'w', encoding="utf-8")
            if isinstance(result, Node):
518
519
520
521
                if extension.lower() == '.xml':
                    f.write(result.as_xml())
                else:
                    f.write(result.as_sxpr())
eckhart's avatar
eckhart committed
522
            elif isinstance(result, str):
523
                f.write(result)
eckhart's avatar
eckhart committed
524
525
            else:
                raise AssertionError('Illegal result type: ' + str(type(result)))
526
527
528
529
530
        except (PermissionError, FileNotFoundError, IOError) as error:
            print('# Could not write file "' + rootname + '.py" because of: '
                  + "\n# ".join(str(error).split('\n)')))
            print(result)
        finally:
531
532
            if f:
                f.close()
533

534
    return messages
535
536


537
def recompile_grammar(ebnf_filename, force=False,
eckhart's avatar
eckhart committed
538
                      notify: Callable = lambda: None) -> bool:
539
    """
540
    Re-compiles an EBNF-grammar if necessary, that is, if either no
541
542
543
544
    corresponding 'XXXXCompiler.py'-file exists or if that file is
    outdated.

    Parameters:
545
546
547
        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.
548
549
        force(bool):  If False (default), the grammar will only be
            recompiled if it has been changed.
550
551
552
        notify(Callable):  'notify' is a function without parameters that
            is called when recompilation actually takes place. This can
            be used to inform the user.
553
554
555
556
557
558
559
560
    """
    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

561
    base, _ = os.path.splitext(ebnf_filename)
562
563
    compiler_name = base + 'Compiler.py'
    error_file_name = base + '_ebnf_ERRORS.txt'
564
    messages = []  # type: Iterable[Error]
565
566
    if (not os.path.exists(compiler_name) or force or
            grammar_changed(compiler_name, ebnf_filename)):
567
        notify()
568
569
        messages = compile_on_disk(ebnf_filename)
        if messages:
570
            # print("Errors while compiling: " + ebnf_filename + '!')
Eckhart Arnold's avatar
Eckhart Arnold committed
571
            with open(error_file_name, 'w', encoding="utf-8") as f:
572
                for e in messages:
Eckhart Arnold's avatar
Eckhart Arnold committed
573
                    f.write(str(e))
574
                    f.write('\n')
575
576
            if has_errors(messages):
                return False
577

578
    if not messages and os.path.exists(error_file_name):
579
580
        os.remove(error_file_name)
    return True