Lichen

Annotated lplc

1027:dd0745ab8b8a
5 months ago Paul Boddie Reordered GCC arguments to prevent linking failures. Someone decided to change the GCC invocation or linking semantics at some point, meaning that libraries specified "too early" in the argument list no longer provide the symbols required by the program objects, whereas specifying them at the end of the argument list allows those symbols to be found and obtained.
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