Lichen

Annotated lplc

1028:e67ebeaf5626
5 months ago Paul Boddie Merged changes from the default branch. trailing-data
paul@1023 1
#!/usr/bin/env python2
paul@0 2
paul@562 3
"""
paul@562 4
Lichen Python-like compiler tool.
paul@562 5
paul@1023 6
Copyright (C) 2016-2018, 2021, 2024 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@916 154
-j          Number of processes to be used when compiling
paul@612 155
-o          Indicate the output executable name
paul@612 156
-W          Show warnings on the topics indicated
paul@558 157
paul@558 158
Currently, the following warnings are supported:
paul@558 159
paul@562 160
all         Show all possible warnings
paul@562 161
paul@562 162
args        Show invocations where a callable may be involved that cannot accept
paul@562 163
            the arguments provided
paul@562 164
paul@651 165
Control over program organisation can be exercised using the following options
paul@651 166
with each requiring an input filename providing a particular form of
paul@651 167
information:
paul@651 168
paul@651 169
--attr-codes        Attribute codes identifying named object attributes
paul@651 170
--attr-locations    Attribute locations in objects
paul@651 171
--param-codes       Parameter codes identifying named parameters
paul@651 172
--param-locations   Parameter locations in signatures
paul@651 173
paul@698 174
A filename can immediately follow such an option, separated from the option by
paul@698 175
an equals sign, or it can appear as the next argument after the option
paul@698 176
(separated by a space).
paul@698 177
paul@562 178
The following informational options can be specified to produce output instead
paul@562 179
of compiling a program:
paul@558 180
paul@562 181
--help      Show a summary of the command syntax and options
paul@567 182
-h          Equivalent to --help
paul@567 183
-?          Equivalent to --help
paul@562 184
--version   Show version information for this tool
paul@567 185
-V          Equivalent to --version
paul@558 186
""" % basename
paul@558 187
        sys.exit(1)
paul@558 188
paul@562 189
    # Show the version information if requested.
paul@562 190
paul@567 191
    elif "--version" in args or "-V" in args:
paul@562 192
        print >>sys.stderr, """\
paul@562 193
lplc %s
paul@916 194
Copyright (C) 2006-2018, 2021 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@916 209
    make_processes = []
paul@442 210
    make_verbose = True
paul@651 211
    outputs = []
paul@651 212
    paramnames = []
paul@651 213
    paramlocations = []
paul@525 214
    reset = False
paul@612 215
    reset_all = False
paul@562 216
    timings = True
paul@474 217
    traceback = False
paul@525 218
    verbose = False
paul@558 219
    warnings = []
paul@442 220
paul@646 221
    unrecognised = []
paul@442 222
    filenames = []
paul@442 223
paul@442 224
    # Obtain program filenames by default.
paul@442 225
paul@442 226
    l = filenames
paul@558 227
    needed = None
paul@442 228
paul@442 229
    for arg in args:
paul@698 230
        if arg.startswith("--attr-codes"): l, needed = start_arg_list(attrnames, arg, 1)
paul@698 231
        elif arg.startswith("--attr-locations"): l, needed = start_arg_list(attrlocations, arg, 1)
paul@651 232
        elif arg in ("-c", "--compile"): make = False
paul@612 233
        elif arg in ("-E", "--no-env"): ignore_env = True
paul@612 234
        elif arg in ("-g", "--debug"): debug = True
paul@614 235
        elif arg in ("-G", "--gc-sections"): gc_sections = True
paul@916 236
        elif arg.startswith("-j"): l, needed = start_arg_list(make_processes, arg, 1)
paul@651 237
        # "P" handled below.
paul@698 238
        elif arg.startswith("--param-codes"): l, needed = start_arg_list(paramnames, arg, 1)
paul@698 239
        elif arg.startswith("--param-locations"): l, needed = start_arg_list(paramlocations, arg, 1)
paul@612 240
        elif arg in ("-q", "--quiet"): make_verbose = False
paul@612 241
        elif arg in ("-r", "--reset"): reset = True
paul@612 242
        elif arg in ("-R", "--reset-all"): reset_all = True
paul@612 243
        elif arg in ("-t", "--no-timing"): timings = False
paul@612 244
        elif arg in ("-tb", "--traceback"): traceback = True
paul@698 245
        elif arg.startswith("-o"): l, needed = start_arg_list(outputs, arg, 1)
paul@648 246
        elif arg in ("-v", "--verbose"): verbose = True
paul@698 247
        elif arg.startswith("-W"): l, needed = start_arg_list(warnings, arg, 1)
paul@646 248
        elif arg.startswith("-"): unrecognised.append(arg)
paul@442 249
        else:
paul@442 250
            l.append(arg)
paul@558 251
            if needed:
paul@558 252
                needed -= 1
paul@442 253
paul@558 254
        if needed == 0:
paul@558 255
            l = filenames
paul@442 256
paul@646 257
    # Report unrecognised options.
paul@646 258
paul@646 259
    if unrecognised:
paul@646 260
        print >>sys.stderr, "The following options were not recognised: %s" % ", ".join(unrecognised)
paul@646 261
        sys.exit(1)
paul@646 262
paul@567 263
    # Add extra components to the module search path from the environment.
paul@567 264
paul@567 265
    if not ignore_env:
paul@567 266
        extra = environ.get("LICHENPATH")
paul@567 267
        if extra:
paul@567 268
            libdirs = extra.split(":") + libdirs
paul@567 269
paul@567 270
    # Show the module search path if requested.
paul@567 271
paul@612 272
    if "-P" in args or "--show-path" in args:
paul@567 273
        for libdir in libdirs:
paul@567 274
            print libdir
paul@567 275
        sys.exit(0)
paul@567 276
paul@442 277
    # Obtain the program filename.
paul@442 278
paul@558 279
    if len(filenames) != 1:
paul@558 280
        print >>sys.stderr, "One main program file must be specified."
paul@442 281
        sys.exit(1)
paul@442 282
paul@442 283
    filename = abspath(filenames[0])
paul@562 284
paul@562 285
    if not isfile(filename):
paul@562 286
        print >>sys.stderr, "Filename %s is not a valid input." % filenames[0]
paul@562 287
        sys.exit(1)
paul@562 288
paul@0 289
    path.append(split(filename)[0])
paul@0 290
paul@442 291
    # Obtain the output filename.
paul@442 292
paul@558 293
    if outputs and not make:
paul@558 294
        print >>sys.stderr, "Output specified but building disabled."
paul@558 295
paul@449 296
    output = outputs and outputs[0] or "_main"
paul@442 297
paul@442 298
    # Define the output data directories.
paul@442 299
paul@613 300
    datadir = "%s%s%s" % (output, extsep, "lplc") # _main.lplc by default
paul@442 301
    cache_dir = join(datadir, "_cache")
paul@442 302
    deduced_dir = join(datadir, "_deduced")
paul@442 303
    output_dir = join(datadir, "_output")
paul@442 304
    generated_dir = join(datadir, "_generated")
paul@0 305
paul@612 306
    # Perform any full reset of the working data.
paul@612 307
paul@612 308
    if reset_all:
paul@612 309
        remove_all(datadir)
paul@612 310
        
paul@0 311
    # Load the program.
paul@0 312
paul@0 313
    try:
paul@562 314
        if timings: now = time()
paul@0 315
paul@558 316
        i = importer.Importer(path, cache_dir, verbose, warnings)
paul@0 317
        m = i.initialise(filename, reset)
paul@41 318
        success = i.finalise()
paul@0 319
paul@562 320
        if timings: now = stopwatch("Inspection", now)
paul@0 321
paul@41 322
        # Check for success, indicating missing references otherwise.
paul@41 323
paul@41 324
        if not success:
paul@41 325
            show_missing(i.missing)
paul@275 326
            sys.exit(1)
paul@41 327
paul@442 328
        d = deducer.Deducer(i, deduced_dir)
paul@44 329
        d.to_output()
paul@44 330
paul@562 331
        if timings: now = stopwatch("Deduction", now)
paul@44 332
paul@651 333
        o = optimiser.Optimiser(i, d, output_dir,
paul@651 334
                                getvalue(attrnames, 0), getvalue(attrlocations, 0),
paul@651 335
                                getvalue(paramnames, 0), getvalue(paramlocations, 0))
paul@92 336
        o.to_output()
paul@92 337
paul@651 338
        if timings: now = stopwatch("Optimisation", now)
paul@651 339
paul@643 340
        # Detect structure or signature changes demanding a reset of the
paul@643 341
        # generated sources.
paul@643 342
paul@643 343
        reset = reset or o.need_reset()
paul@643 344
paul@442 345
        g = generator.Generator(i, o, generated_dir)
paul@651 346
        g.to_output(reset, debug, gc_sections)
paul@126 347
paul@562 348
        if timings: now = stopwatch("Generation", now)
paul@126 349
paul@442 350
        t = translator.Translator(i, d, o, generated_dir)
paul@633 351
        t.to_output(reset, debug, gc_sections)
paul@113 352
paul@562 353
        if timings: now = stopwatch("Translation", now)
paul@442 354
paul@442 355
        # Compile the program unless otherwise indicated.
paul@442 356
paul@442 357
        if make:
paul@916 358
            processes = make_processes and ["-j"] + make_processes or []
paul@916 359
            make_clean_cmd = ["make", "-C", generated_dir] + processes + ["clean"]
paul@442 360
            make_cmd = make_clean_cmd[:-1]
paul@442 361
paul@609 362
            retval = call(make_cmd, make_verbose)
paul@442 363
paul@442 364
            if not retval:
paul@562 365
                if timings: stopwatch("Compilation", now)
paul@442 366
            else:
paul@442 367
                sys.exit(retval)
paul@442 368
paul@442 369
            # Move the executable into the current directory.
paul@442 370
paul@442 371
            rename(join(generated_dir, "main"), output)
paul@113 372
paul@0 373
    # Report any errors.
paul@0 374
paul@445 375
    except error.SyntaxError, exc:
paul@445 376
        show_syntax_error(exc)
paul@474 377
        if traceback:
paul@445 378
            raise
paul@445 379
        sys.exit(1)
paul@445 380
paul@0 381
    except ProcessingError, exc:
paul@0 382
        print exc
paul@474 383
        if traceback:
paul@0 384
            raise
paul@275 385
        sys.exit(1)
paul@0 386
paul@0 387
    else:
paul@275 388
        sys.exit(0)
paul@0 389
paul@0 390
# vim: tabstop=4 expandtab shiftwidth=4