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 |