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