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