Lichen

Annotated lplc

913:85cb541354aa
2021-06-16 Paul Boddie Silence warnings about incomplete string copying despite zero-initialised memory being used.
paul@0 1
#!/usr/bin/env python
paul@0 2
paul@562 3
"""
paul@562 4
Lichen Python-like compiler tool.
paul@562 5
paul@827 6
Copyright (C) 2016, 2017, 2018 Paul Boddie <paul@boddie.org.uk>
paul@562 7
paul@562 8
This program is free software; you can redistribute it and/or modify it under
paul@562 9
the terms of the GNU General Public License as published by the Free Software
paul@562 10
Foundation; either version 3 of the License, or (at your option) any later
paul@562 11
version.
paul@562 12
paul@562 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@562 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@562 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@562 16
details.
paul@562 17
paul@562 18
You should have received a copy of the GNU General Public License along with
paul@562 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@562 20
"""
paul@562 21
paul@562 22
VERSION = "0.1"
paul@562 23
paul@0 24
from errors import *
paul@612 25
from os import environ, listdir, remove, rename
paul@613 26
from os.path import abspath, exists, extsep, isdir, isfile, join, split
paul@445 27
from pyparser import error
paul@442 28
from subprocess import Popen, PIPE
paul@0 29
from time import time
paul@126 30
import importer, deducer, optimiser, generator, translator
paul@0 31
import sys
paul@0 32
paul@0 33
libdirs = [
paul@0 34
    join(split(__file__)[0], "lib"),
paul@567 35
    split(__file__)[0],
paul@0 36
    "/usr/share/lichen/lib"
paul@0 37
    ]
paul@0 38
paul@0 39
def load_module(filename, module_name):
paul@0 40
    for libdir in libdirs:
paul@0 41
        path = join(libdir, filename)
paul@0 42
        if exists(path):
paul@0 43
            return i.load_from_file(path, module_name)
paul@0 44
    return None
paul@0 45
paul@41 46
def show_missing(missing):
paul@41 47
    missing = list(missing)
paul@41 48
    missing.sort()
paul@41 49
    for module_name, name in missing:
paul@41 50
        print >>sys.stderr, "Module %s references an unknown object: %s" % (module_name, name)
paul@41 51
paul@445 52
def show_syntax_error(exc):
paul@445 53
    print >>sys.stderr, "Syntax error at column %d on line %d in file %s:" % (exc.offset, exc.lineno, exc.filename)
paul@445 54
    print >>sys.stderr
paul@445 55
    print >>sys.stderr, exc.text.rstrip()
paul@445 56
    print >>sys.stderr, " " * exc.offset + "^"
paul@445 57
paul@0 58
def stopwatch(activity, now):
paul@0 59
    print >>sys.stderr, "%s took %.2f seconds" % (activity, time() - now)
paul@0 60
    return time()
paul@0 61
paul@442 62
def call(tokens, verbose=False):
paul@442 63
    out = not verbose and PIPE or None
paul@442 64
    cmd = Popen(tokens, stdout=out, stderr=out)
paul@442 65
    stdout, stderr = cmd.communicate()
paul@442 66
    return cmd.wait()
paul@442 67
paul@698 68
def start_arg_list(l, arg, needed):
paul@558 69
paul@558 70
    """
paul@698 71
    Add to 'l' any value given as part of 'arg'. The 'needed' number of values
paul@698 72
    is provided in case no value is found.
paul@558 73
paul@558 74
    Return 'l' and 'needed' decremented by 1 together in a tuple.
paul@558 75
    """
paul@558 76
paul@698 77
    if arg.startswith("--"):
paul@698 78
        try:
paul@698 79
            prefix_length = arg.index("=") + 1
paul@698 80
        except ValueError:
paul@698 81
            prefix_length = len(arg)
paul@698 82
    else:
paul@698 83
        prefix_length = 2
paul@698 84
paul@698 85
    s = arg[prefix_length:].strip()
paul@558 86
    if s:
paul@558 87
        l.append(s)
paul@558 88
        return l, needed - 1
paul@558 89
    else:
paul@558 90
        return l, needed
paul@558 91
paul@651 92
def getvalue(l, i):
paul@651 93
    if l and len(l) > i:
paul@651 94
        return l[i]
paul@651 95
    else:
paul@651 96
        return None
paul@651 97
paul@612 98
def remove_all(dirname):
paul@612 99
paul@612 100
    "Remove 'dirname' and its contents."
paul@612 101
paul@827 102
    if not isdir(dirname):
paul@827 103
        return
paul@827 104
paul@612 105
    for filename in listdir(dirname):
paul@612 106
        pathname = join(dirname, filename)
paul@612 107
        if isdir(pathname):
paul@612 108
            remove_all(pathname)
paul@612 109
        else:
paul@612 110
            remove(pathname)
paul@612 111
paul@0 112
# Main program.
paul@0 113
paul@0 114
if __name__ == "__main__":
paul@558 115
    basename = split(sys.argv[0])[-1]
paul@442 116
    args = sys.argv[1:]
paul@525 117
    path = libdirs
paul@0 118
paul@562 119
    # Show help text if requested or if no arguments are given.
paul@562 120
paul@567 121
    if "--help" in args or "-h" in args or "-?" in args or not args:
paul@558 122
        print >>sys.stderr, """\
paul@558 123
Usage: %s [ <options> ] <filename>
paul@558 124
paul@558 125
Compile the program whose principal file is given in place of <filename>.
paul@558 126
The following options may be specified:
paul@558 127
paul@612 128
-c          Only partially compile the program; do not build or link it
paul@612 129
--compile   Equivalent to -c
paul@612 130
-E          Ignore environment variables affecting the module search path
paul@612 131
--no-env    Equivalent to -E
paul@612 132
-g          Generate debugging information for the built executable
paul@612 133
--debug     Equivalent to -g
paul@614 134
-G          Remove superfluous sections of the built executable
paul@614 135
--gc-sections Equivalent to -G
paul@612 136
-P          Show the module search path
paul@612 137
--show-path Equivalent to -P
paul@612 138
-q          Silence messages produced when building an executable
paul@612 139
--quiet     Equivalent to -q
paul@612 140
-r          Reset (discard) cached information; inspect the whole program again
paul@612 141
--reset     Equivalent to -r
paul@612 142
-R          Reset (discard) all program details including translated code
paul@612 143
--reset-all Equivalent to -R
paul@612 144
-t          Silence timing messages
paul@612 145
--no-timing Equivalent to -t
paul@612 146
-tb         Provide a traceback for any internal errors (development only)
paul@612 147
--traceback Equivalent to -tb
paul@612 148
-v          Report compiler activities in a verbose fashion (development only)
paul@612 149
--verbose   Equivalent to -v
paul@558 150
paul@558 151
Some options may be followed by values, either immediately after the option
paul@558 152
(without any space between) or in the arguments that follow them:
paul@558 153
paul@612 154
-o          Indicate the output executable name
paul@612 155
-W          Show warnings on the topics indicated
paul@558 156
paul@558 157
Currently, the following warnings are supported:
paul@558 158
paul@562 159
all         Show all possible warnings
paul@562 160
paul@562 161
args        Show invocations where a callable may be involved that cannot accept
paul@562 162
            the arguments provided
paul@562 163
paul@651 164
Control over program organisation can be exercised using the following options
paul@651 165
with each requiring an input filename providing a particular form of
paul@651 166
information:
paul@651 167
paul@651 168
--attr-codes        Attribute codes identifying named object attributes
paul@651 169
--attr-locations    Attribute locations in objects
paul@651 170
--param-codes       Parameter codes identifying named parameters
paul@651 171
--param-locations   Parameter locations in signatures
paul@651 172
paul@698 173
A filename can immediately follow such an option, separated from the option by
paul@698 174
an equals sign, or it can appear as the next argument after the option
paul@698 175
(separated by a space).
paul@698 176
paul@562 177
The following informational options can be specified to produce output instead
paul@562 178
of compiling a program:
paul@558 179
paul@562 180
--help      Show a summary of the command syntax and options
paul@567 181
-h          Equivalent to --help
paul@567 182
-?          Equivalent to --help
paul@562 183
--version   Show version information for this tool
paul@567 184
-V          Equivalent to --version
paul@558 185
""" % basename
paul@558 186
        sys.exit(1)
paul@558 187
paul@562 188
    # Show the version information if requested.
paul@562 189
paul@567 190
    elif "--version" in args or "-V" in args:
paul@562 191
        print >>sys.stderr, """\
paul@562 192
lplc %s
paul@562 193
Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013,
paul@562 194
              2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>
paul@562 195
This program is free software; you may redistribute it under the terms of
paul@562 196
the GNU General Public License version 3 or (at your option) a later version.
paul@562 197
This program has absolutely no warranty.
paul@562 198
""" % VERSION
paul@562 199
        sys.exit(1)
paul@562 200
paul@442 201
    # Determine the options and arguments.
paul@442 202
paul@651 203
    attrnames = []
paul@651 204
    attrlocations = []
paul@442 205
    debug = False
paul@614 206
    gc_sections = False
paul@567 207
    ignore_env = False
paul@442 208
    make = True
paul@442 209
    make_verbose = True
paul@651 210
    outputs = []
paul@651 211
    paramnames = []
paul@651 212
    paramlocations = []
paul@525 213
    reset = False
paul@612 214
    reset_all = False
paul@562 215
    timings = True
paul@474 216
    traceback = False
paul@525 217
    verbose = False
paul@558 218
    warnings = []
paul@442 219
paul@646 220
    unrecognised = []
paul@442 221
    filenames = []
paul@442 222
paul@442 223
    # Obtain program filenames by default.
paul@442 224
paul@442 225
    l = filenames
paul@558 226
    needed = None
paul@442 227
paul@442 228
    for arg in args:
paul@698 229
        if arg.startswith("--attr-codes"): l, needed = start_arg_list(attrnames, arg, 1)
paul@698 230
        elif arg.startswith("--attr-locations"): l, needed = start_arg_list(attrlocations, arg, 1)
paul@651 231
        elif arg in ("-c", "--compile"): make = False
paul@612 232
        elif arg in ("-E", "--no-env"): ignore_env = True
paul@612 233
        elif arg in ("-g", "--debug"): debug = True
paul@614 234
        elif arg in ("-G", "--gc-sections"): gc_sections = True
paul@651 235
        # "P" handled below.
paul@698 236
        elif arg.startswith("--param-codes"): l, needed = start_arg_list(paramnames, arg, 1)
paul@698 237
        elif arg.startswith("--param-locations"): l, needed = start_arg_list(paramlocations, arg, 1)
paul@612 238
        elif arg in ("-q", "--quiet"): make_verbose = False
paul@612 239
        elif arg in ("-r", "--reset"): reset = True
paul@612 240
        elif arg in ("-R", "--reset-all"): reset_all = True
paul@612 241
        elif arg in ("-t", "--no-timing"): timings = False
paul@612 242
        elif arg in ("-tb", "--traceback"): traceback = True
paul@698 243
        elif arg.startswith("-o"): l, needed = start_arg_list(outputs, arg, 1)
paul@648 244
        elif arg in ("-v", "--verbose"): verbose = True
paul@698 245
        elif arg.startswith("-W"): l, needed = start_arg_list(warnings, arg, 1)
paul@646 246
        elif arg.startswith("-"): unrecognised.append(arg)
paul@442 247
        else:
paul@442 248
            l.append(arg)
paul@558 249
            if needed:
paul@558 250
                needed -= 1
paul@442 251
paul@558 252
        if needed == 0:
paul@558 253
            l = filenames
paul@442 254
paul@646 255
    # Report unrecognised options.
paul@646 256
paul@646 257
    if unrecognised:
paul@646 258
        print >>sys.stderr, "The following options were not recognised: %s" % ", ".join(unrecognised)
paul@646 259
        sys.exit(1)
paul@646 260
paul@567 261
    # Add extra components to the module search path from the environment.
paul@567 262
paul@567 263
    if not ignore_env:
paul@567 264
        extra = environ.get("LICHENPATH")
paul@567 265
        if extra:
paul@567 266
            libdirs = extra.split(":") + libdirs
paul@567 267
paul@567 268
    # Show the module search path if requested.
paul@567 269
paul@612 270
    if "-P" in args or "--show-path" in args:
paul@567 271
        for libdir in libdirs:
paul@567 272
            print libdir
paul@567 273
        sys.exit(0)
paul@567 274
paul@442 275
    # Obtain the program filename.
paul@442 276
paul@558 277
    if len(filenames) != 1:
paul@558 278
        print >>sys.stderr, "One main program file must be specified."
paul@442 279
        sys.exit(1)
paul@442 280
paul@442 281
    filename = abspath(filenames[0])
paul@562 282
paul@562 283
    if not isfile(filename):
paul@562 284
        print >>sys.stderr, "Filename %s is not a valid input." % filenames[0]
paul@562 285
        sys.exit(1)
paul@562 286
paul@0 287
    path.append(split(filename)[0])
paul@0 288
paul@442 289
    # Obtain the output filename.
paul@442 290
paul@558 291
    if outputs and not make:
paul@558 292
        print >>sys.stderr, "Output specified but building disabled."
paul@558 293
paul@449 294
    output = outputs and outputs[0] or "_main"
paul@442 295
paul@442 296
    # Define the output data directories.
paul@442 297
paul@613 298
    datadir = "%s%s%s" % (output, extsep, "lplc") # _main.lplc by default
paul@442 299
    cache_dir = join(datadir, "_cache")
paul@442 300
    deduced_dir = join(datadir, "_deduced")
paul@442 301
    output_dir = join(datadir, "_output")
paul@442 302
    generated_dir = join(datadir, "_generated")
paul@0 303
paul@612 304
    # Perform any full reset of the working data.
paul@612 305
paul@612 306
    if reset_all:
paul@612 307
        remove_all(datadir)
paul@612 308
        
paul@0 309
    # Load the program.
paul@0 310
paul@0 311
    try:
paul@562 312
        if timings: now = time()
paul@0 313
paul@558 314
        i = importer.Importer(path, cache_dir, verbose, warnings)
paul@0 315
        m = i.initialise(filename, reset)
paul@41 316
        success = i.finalise()
paul@0 317
paul@562 318
        if timings: now = stopwatch("Inspection", now)
paul@0 319
paul@41 320
        # Check for success, indicating missing references otherwise.
paul@41 321
paul@41 322
        if not success:
paul@41 323
            show_missing(i.missing)
paul@275 324
            sys.exit(1)
paul@41 325
paul@442 326
        d = deducer.Deducer(i, deduced_dir)
paul@44 327
        d.to_output()
paul@44 328
paul@562 329
        if timings: now = stopwatch("Deduction", now)
paul@44 330
paul@651 331
        o = optimiser.Optimiser(i, d, output_dir,
paul@651 332
                                getvalue(attrnames, 0), getvalue(attrlocations, 0),
paul@651 333
                                getvalue(paramnames, 0), getvalue(paramlocations, 0))
paul@92 334
        o.to_output()
paul@92 335
paul@651 336
        if timings: now = stopwatch("Optimisation", now)
paul@651 337
paul@643 338
        # Detect structure or signature changes demanding a reset of the
paul@643 339
        # generated sources.
paul@643 340
paul@643 341
        reset = reset or o.need_reset()
paul@643 342
paul@442 343
        g = generator.Generator(i, o, generated_dir)
paul@651 344
        g.to_output(reset, debug, gc_sections)
paul@126 345
paul@562 346
        if timings: now = stopwatch("Generation", now)
paul@126 347
paul@442 348
        t = translator.Translator(i, d, o, generated_dir)
paul@633 349
        t.to_output(reset, debug, gc_sections)
paul@113 350
paul@562 351
        if timings: now = stopwatch("Translation", now)
paul@442 352
paul@442 353
        # Compile the program unless otherwise indicated.
paul@442 354
paul@442 355
        if make:
paul@442 356
            make_clean_cmd = ["make", "-C", generated_dir, "clean"]
paul@442 357
            make_cmd = make_clean_cmd[:-1]
paul@442 358
paul@609 359
            retval = call(make_cmd, make_verbose)
paul@442 360
paul@442 361
            if not retval:
paul@562 362
                if timings: stopwatch("Compilation", now)
paul@442 363
            else:
paul@442 364
                sys.exit(retval)
paul@442 365
paul@442 366
            # Move the executable into the current directory.
paul@442 367
paul@442 368
            rename(join(generated_dir, "main"), output)
paul@113 369
paul@0 370
    # Report any errors.
paul@0 371
paul@445 372
    except error.SyntaxError, exc:
paul@445 373
        show_syntax_error(exc)
paul@474 374
        if traceback:
paul@445 375
            raise
paul@445 376
        sys.exit(1)
paul@445 377
paul@0 378
    except ProcessingError, exc:
paul@0 379
        print exc
paul@474 380
        if traceback:
paul@0 381
            raise
paul@275 382
        sys.exit(1)
paul@0 383
paul@0 384
    else:
paul@275 385
        sys.exit(0)
paul@0 386
paul@0 387
# vim: tabstop=4 expandtab shiftwidth=4