1 #!/usr/bin/env python 2 3 """ 4 Lichen Python-like compiler tool. 5 6 Copyright (C) 2016, 2017 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 for filename in listdir(dirname): 103 pathname = join(dirname, filename) 104 if isdir(pathname): 105 remove_all(pathname) 106 else: 107 remove(pathname) 108 109 # Main program. 110 111 if __name__ == "__main__": 112 basename = split(sys.argv[0])[-1] 113 args = sys.argv[1:] 114 path = libdirs 115 116 # Show help text if requested or if no arguments are given. 117 118 if "--help" in args or "-h" in args or "-?" in args or not args: 119 print >>sys.stderr, """\ 120 Usage: %s [ <options> ] <filename> 121 122 Compile the program whose principal file is given in place of <filename>. 123 The following options may be specified: 124 125 -c Only partially compile the program; do not build or link it 126 --compile Equivalent to -c 127 -E Ignore environment variables affecting the module search path 128 --no-env Equivalent to -E 129 -g Generate debugging information for the built executable 130 --debug Equivalent to -g 131 -G Remove superfluous sections of the built executable 132 --gc-sections Equivalent to -G 133 -P Show the module search path 134 --show-path Equivalent to -P 135 -q Silence messages produced when building an executable 136 --quiet Equivalent to -q 137 -r Reset (discard) cached information; inspect the whole program again 138 --reset Equivalent to -r 139 -R Reset (discard) all program details including translated code 140 --reset-all Equivalent to -R 141 -t Silence timing messages 142 --no-timing Equivalent to -t 143 -tb Provide a traceback for any internal errors (development only) 144 --traceback Equivalent to -tb 145 -v Report compiler activities in a verbose fashion (development only) 146 --verbose Equivalent to -v 147 148 Some options may be followed by values, either immediately after the option 149 (without any space between) or in the arguments that follow them: 150 151 -o Indicate the output executable name 152 -W Show warnings on the topics indicated 153 154 Currently, the following warnings are supported: 155 156 all Show all possible warnings 157 158 args Show invocations where a callable may be involved that cannot accept 159 the arguments provided 160 161 Control over program organisation can be exercised using the following options 162 with each requiring an input filename providing a particular form of 163 information: 164 165 --attr-codes Attribute codes identifying named object attributes 166 --attr-locations Attribute locations in objects 167 --param-codes Parameter codes identifying named parameters 168 --param-locations Parameter locations in signatures 169 170 A filename can immediately follow such an option, separated from the option by 171 an equals sign, or it can appear as the next argument after the option 172 (separated by a space). 173 174 The following informational options can be specified to produce output instead 175 of compiling a program: 176 177 --help Show a summary of the command syntax and options 178 -h Equivalent to --help 179 -? Equivalent to --help 180 --version Show version information for this tool 181 -V Equivalent to --version 182 """ % basename 183 sys.exit(1) 184 185 # Show the version information if requested. 186 187 elif "--version" in args or "-V" in args: 188 print >>sys.stderr, """\ 189 lplc %s 190 Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 191 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk> 192 This program is free software; you may redistribute it under the terms of 193 the GNU General Public License version 3 or (at your option) a later version. 194 This program has absolutely no warranty. 195 """ % VERSION 196 sys.exit(1) 197 198 # Determine the options and arguments. 199 200 attrnames = [] 201 attrlocations = [] 202 debug = False 203 gc_sections = False 204 ignore_env = False 205 make = True 206 make_verbose = True 207 outputs = [] 208 paramnames = [] 209 paramlocations = [] 210 reset = False 211 reset_all = False 212 timings = True 213 traceback = False 214 verbose = False 215 warnings = [] 216 217 unrecognised = [] 218 filenames = [] 219 220 # Obtain program filenames by default. 221 222 l = filenames 223 needed = None 224 225 for arg in args: 226 if arg.startswith("--attr-codes"): l, needed = start_arg_list(attrnames, arg, 1) 227 elif arg.startswith("--attr-locations"): l, needed = start_arg_list(attrlocations, arg, 1) 228 elif arg in ("-c", "--compile"): make = False 229 elif arg in ("-E", "--no-env"): ignore_env = True 230 elif arg in ("-g", "--debug"): debug = True 231 elif arg in ("-G", "--gc-sections"): gc_sections = True 232 # "P" handled below. 233 elif arg.startswith("--param-codes"): l, needed = start_arg_list(paramnames, arg, 1) 234 elif arg.startswith("--param-locations"): l, needed = start_arg_list(paramlocations, arg, 1) 235 elif arg in ("-q", "--quiet"): make_verbose = False 236 elif arg in ("-r", "--reset"): reset = True 237 elif arg in ("-R", "--reset-all"): reset_all = True 238 elif arg in ("-t", "--no-timing"): timings = False 239 elif arg in ("-tb", "--traceback"): traceback = True 240 elif arg.startswith("-o"): l, needed = start_arg_list(outputs, arg, 1) 241 elif arg in ("-v", "--verbose"): verbose = True 242 elif arg.startswith("-W"): l, needed = start_arg_list(warnings, arg, 1) 243 elif arg.startswith("-"): unrecognised.append(arg) 244 else: 245 l.append(arg) 246 if needed: 247 needed -= 1 248 249 if needed == 0: 250 l = filenames 251 252 # Report unrecognised options. 253 254 if unrecognised: 255 print >>sys.stderr, "The following options were not recognised: %s" % ", ".join(unrecognised) 256 sys.exit(1) 257 258 # Add extra components to the module search path from the environment. 259 260 if not ignore_env: 261 extra = environ.get("LICHENPATH") 262 if extra: 263 libdirs = extra.split(":") + libdirs 264 265 # Show the module search path if requested. 266 267 if "-P" in args or "--show-path" in args: 268 for libdir in libdirs: 269 print libdir 270 sys.exit(0) 271 272 # Obtain the program filename. 273 274 if len(filenames) != 1: 275 print >>sys.stderr, "One main program file must be specified." 276 sys.exit(1) 277 278 filename = abspath(filenames[0]) 279 280 if not isfile(filename): 281 print >>sys.stderr, "Filename %s is not a valid input." % filenames[0] 282 sys.exit(1) 283 284 path.append(split(filename)[0]) 285 286 # Obtain the output filename. 287 288 if outputs and not make: 289 print >>sys.stderr, "Output specified but building disabled." 290 291 output = outputs and outputs[0] or "_main" 292 293 # Define the output data directories. 294 295 datadir = "%s%s%s" % (output, extsep, "lplc") # _main.lplc by default 296 cache_dir = join(datadir, "_cache") 297 deduced_dir = join(datadir, "_deduced") 298 output_dir = join(datadir, "_output") 299 generated_dir = join(datadir, "_generated") 300 301 # Perform any full reset of the working data. 302 303 if reset_all: 304 remove_all(datadir) 305 306 # Load the program. 307 308 try: 309 if timings: now = time() 310 311 i = importer.Importer(path, cache_dir, verbose, warnings) 312 m = i.initialise(filename, reset) 313 success = i.finalise() 314 315 if timings: now = stopwatch("Inspection", now) 316 317 # Check for success, indicating missing references otherwise. 318 319 if not success: 320 show_missing(i.missing) 321 sys.exit(1) 322 323 d = deducer.Deducer(i, deduced_dir) 324 d.to_output() 325 326 if timings: now = stopwatch("Deduction", now) 327 328 o = optimiser.Optimiser(i, d, output_dir, 329 getvalue(attrnames, 0), getvalue(attrlocations, 0), 330 getvalue(paramnames, 0), getvalue(paramlocations, 0)) 331 o.to_output() 332 333 if timings: now = stopwatch("Optimisation", now) 334 335 # Detect structure or signature changes demanding a reset of the 336 # generated sources. 337 338 reset = reset or o.need_reset() 339 340 g = generator.Generator(i, o, generated_dir) 341 g.to_output(reset, debug, gc_sections) 342 343 if timings: now = stopwatch("Generation", now) 344 345 t = translator.Translator(i, d, o, generated_dir) 346 t.to_output(reset, debug, gc_sections) 347 348 if timings: now = stopwatch("Translation", now) 349 350 # Compile the program unless otherwise indicated. 351 352 if make: 353 make_clean_cmd = ["make", "-C", generated_dir, "clean"] 354 make_cmd = make_clean_cmd[:-1] 355 356 retval = call(make_cmd, make_verbose) 357 358 if not retval: 359 if timings: stopwatch("Compilation", now) 360 else: 361 sys.exit(retval) 362 363 # Move the executable into the current directory. 364 365 rename(join(generated_dir, "main"), output) 366 367 # Report any errors. 368 369 except error.SyntaxError, exc: 370 show_syntax_error(exc) 371 if traceback: 372 raise 373 sys.exit(1) 374 375 except ProcessingError, exc: 376 print exc 377 if traceback: 378 raise 379 sys.exit(1) 380 381 else: 382 sys.exit(0) 383 384 # vim: tabstop=4 expandtab shiftwidth=4