1 #!/usr/bin/env python2 2 3 """ 4 Lichen Python-like compiler tool. 5 6 Copyright (C) 2016-2018, 2021, 2024 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 -j Number of processes to be used when compiling 155 -o Indicate the output executable name 156 -W Show warnings on the topics indicated 157 158 Currently, the following warnings are supported: 159 160 all Show all possible warnings 161 162 args Show invocations where a callable may be involved that cannot accept 163 the arguments provided 164 165 Control over program organisation can be exercised using the following options 166 with each requiring an input filename providing a particular form of 167 information: 168 169 --attr-codes Attribute codes identifying named object attributes 170 --attr-locations Attribute locations in objects 171 --param-codes Parameter codes identifying named parameters 172 --param-locations Parameter locations in signatures 173 174 A filename can immediately follow such an option, separated from the option by 175 an equals sign, or it can appear as the next argument after the option 176 (separated by a space). 177 178 The following informational options can be specified to produce output instead 179 of compiling a program: 180 181 --help Show a summary of the command syntax and options 182 -h Equivalent to --help 183 -? Equivalent to --help 184 --version Show version information for this tool 185 -V Equivalent to --version 186 """ % basename 187 sys.exit(1) 188 189 # Show the version information if requested. 190 191 elif "--version" in args or "-V" in args: 192 print >>sys.stderr, """\ 193 lplc %s 194 Copyright (C) 2006-2018, 2021 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_processes = [] 210 make_verbose = True 211 outputs = [] 212 paramnames = [] 213 paramlocations = [] 214 reset = False 215 reset_all = False 216 timings = True 217 traceback = False 218 verbose = False 219 warnings = [] 220 221 unrecognised = [] 222 filenames = [] 223 224 # Obtain program filenames by default. 225 226 l = filenames 227 needed = None 228 229 for arg in args: 230 if arg.startswith("--attr-codes"): l, needed = start_arg_list(attrnames, arg, 1) 231 elif arg.startswith("--attr-locations"): l, needed = start_arg_list(attrlocations, arg, 1) 232 elif arg in ("-c", "--compile"): make = False 233 elif arg in ("-E", "--no-env"): ignore_env = True 234 elif arg in ("-g", "--debug"): debug = True 235 elif arg in ("-G", "--gc-sections"): gc_sections = True 236 elif arg.startswith("-j"): l, needed = start_arg_list(make_processes, arg, 1) 237 # "P" handled below. 238 elif arg.startswith("--param-codes"): l, needed = start_arg_list(paramnames, arg, 1) 239 elif arg.startswith("--param-locations"): l, needed = start_arg_list(paramlocations, arg, 1) 240 elif arg in ("-q", "--quiet"): make_verbose = False 241 elif arg in ("-r", "--reset"): reset = True 242 elif arg in ("-R", "--reset-all"): reset_all = True 243 elif arg in ("-t", "--no-timing"): timings = False 244 elif arg in ("-tb", "--traceback"): traceback = True 245 elif arg.startswith("-o"): l, needed = start_arg_list(outputs, arg, 1) 246 elif arg in ("-v", "--verbose"): verbose = True 247 elif arg.startswith("-W"): l, needed = start_arg_list(warnings, arg, 1) 248 elif arg.startswith("-"): unrecognised.append(arg) 249 else: 250 l.append(arg) 251 if needed: 252 needed -= 1 253 254 if needed == 0: 255 l = filenames 256 257 # Report unrecognised options. 258 259 if unrecognised: 260 print >>sys.stderr, "The following options were not recognised: %s" % ", ".join(unrecognised) 261 sys.exit(1) 262 263 # Add extra components to the module search path from the environment. 264 265 if not ignore_env: 266 extra = environ.get("LICHENPATH") 267 if extra: 268 libdirs = extra.split(":") + libdirs 269 270 # Show the module search path if requested. 271 272 if "-P" in args or "--show-path" in args: 273 for libdir in libdirs: 274 print libdir 275 sys.exit(0) 276 277 # Obtain the program filename. 278 279 if len(filenames) != 1: 280 print >>sys.stderr, "One main program file must be specified." 281 sys.exit(1) 282 283 filename = abspath(filenames[0]) 284 285 if not isfile(filename): 286 print >>sys.stderr, "Filename %s is not a valid input." % filenames[0] 287 sys.exit(1) 288 289 path.append(split(filename)[0]) 290 291 # Obtain the output filename. 292 293 if outputs and not make: 294 print >>sys.stderr, "Output specified but building disabled." 295 296 output = outputs and outputs[0] or "_main" 297 298 # Define the output data directories. 299 300 datadir = "%s%s%s" % (output, extsep, "lplc") # _main.lplc by default 301 cache_dir = join(datadir, "_cache") 302 deduced_dir = join(datadir, "_deduced") 303 output_dir = join(datadir, "_output") 304 generated_dir = join(datadir, "_generated") 305 306 # Perform any full reset of the working data. 307 308 if reset_all: 309 remove_all(datadir) 310 311 # Load the program. 312 313 try: 314 if timings: now = time() 315 316 i = importer.Importer(path, cache_dir, verbose, warnings) 317 m = i.initialise(filename, reset) 318 success = i.finalise() 319 320 if timings: now = stopwatch("Inspection", now) 321 322 # Check for success, indicating missing references otherwise. 323 324 if not success: 325 show_missing(i.missing) 326 sys.exit(1) 327 328 d = deducer.Deducer(i, deduced_dir) 329 d.to_output() 330 331 if timings: now = stopwatch("Deduction", now) 332 333 o = optimiser.Optimiser(i, d, output_dir, 334 getvalue(attrnames, 0), getvalue(attrlocations, 0), 335 getvalue(paramnames, 0), getvalue(paramlocations, 0)) 336 o.to_output() 337 338 if timings: now = stopwatch("Optimisation", now) 339 340 # Detect structure or signature changes demanding a reset of the 341 # generated sources. 342 343 reset = reset or o.need_reset() 344 345 g = generator.Generator(i, o, generated_dir) 346 g.to_output(reset, debug, gc_sections) 347 348 if timings: now = stopwatch("Generation", now) 349 350 t = translator.Translator(i, d, o, generated_dir) 351 t.to_output(reset, debug, gc_sections) 352 353 if timings: now = stopwatch("Translation", now) 354 355 # Compile the program unless otherwise indicated. 356 357 if make: 358 processes = make_processes and ["-j"] + make_processes or [] 359 make_clean_cmd = ["make", "-C", generated_dir] + processes + ["clean"] 360 make_cmd = make_clean_cmd[:-1] 361 362 retval = call(make_cmd, make_verbose) 363 364 if not retval: 365 if timings: stopwatch("Compilation", now) 366 else: 367 sys.exit(retval) 368 369 # Move the executable into the current directory. 370 371 rename(join(generated_dir, "main"), output) 372 373 # Report any errors. 374 375 except error.SyntaxError, exc: 376 show_syntax_error(exc) 377 if traceback: 378 raise 379 sys.exit(1) 380 381 except ProcessingError, exc: 382 print exc 383 if traceback: 384 raise 385 sys.exit(1) 386 387 else: 388 sys.exit(0) 389 390 # vim: tabstop=4 expandtab shiftwidth=4