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