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