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