OILS / opy / _regtest / src / opy / opy_main.py View on Github | oilshell.org

372 lines, 240 significant
1#!/usr/bin/env python
2"""
3opy_main.py
4"""
5from __future__ import print_function
6
7import cStringIO
8import hashlib
9import optparse
10import os
11import sys
12import marshal
13import logging
14
15# Like oil.py, set PYTHONPATH internally? So symlinks work?
16# Actually '.' is implicitly in PYTHONPATH, so we don't need it.
17# If we were in bin/oil.py, then we would need this.
18#this_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
19#sys.path.append(os.path.join(this_dir))
20
21from .pgen2 import driver, pgen, grammar
22from .pgen2 import token, tokenize
23from . import pytree
24
25from .compiler2 import transformer
26from .compiler2 import pycodegen
27from .compiler2 import opcode
28
29from .misc import inspect_pyc
30
31# Disabled for now because byterun imports 'six', and that breaks the build.
32#from .byterun import execfile
33
34from .util_opy import log
35
36from core import args
37from core import util
38
39
40# From lib2to3/pygram.py. This takes the place of the 'symbol' module.
41# compiler/transformer module needs this.
42
43class Symbols(object):
44
45 def __init__(self, gr):
46 """Initializer.
47
48 Creates an attribute for each grammar symbol (nonterminal),
49 whose value is the symbol's type (an int >= 256).
50 """
51 for name, symbol in gr.symbol2number.items():
52 setattr(self, name, symbol)
53 #log('%s -> %d' % (name, symbol))
54 # For transformer to use
55 self.number2symbol = gr.number2symbol
56 #assert 0
57
58
59def HostStdlibNames():
60 import symbol
61 import token
62 names = {}
63 for k, v in symbol.sym_name.items():
64 names[k] = v
65 for k, v in token.tok_name.items():
66 names[k] = v
67 return names
68
69
70def WriteGrammar(grammar_path, pickle_path):
71 log("Generating grammar tables from %s", grammar_path)
72 g = pgen.generate_grammar(grammar_path)
73 log("Writing grammar tables to %s", pickle_path)
74 try:
75 # calls pickle.dump on self.__dict__ after making it deterministic
76 g.dump(pickle_path)
77 except OSError as e:
78 log("Writing failed: %s", e)
79
80
81# Emulate the interface that Transformer expects from parsermodule.c.
82class Pgen2PythonParser(object):
83 def __init__(self, driver, start_symbol):
84 self.driver = driver
85 self.start_symbol = start_symbol
86
87 def suite(self, text):
88 f = cStringIO.StringIO(text)
89 tokens = tokenize.generate_tokens(f.readline)
90 tree = self.driver.parse_tokens(tokens, start_symbol=self.start_symbol)
91 return tree
92
93
94def CountTupleTree(tu):
95 """Count the nodes in a tuple parse tree."""
96 if isinstance(tu, tuple):
97 s = 0
98 for entry in tu:
99 s += CountTupleTree(entry)
100 return s
101 elif isinstance(tu, int):
102 return 1
103 elif isinstance(tu, str):
104 return 1
105 else:
106 raise AssertionError(tu)
107
108
109class TupleTreePrinter(object):
110 def __init__(self, names):
111 self._names = names
112
113 def Print(self, tu, f=sys.stdout, indent=0):
114 ind = ' ' * indent
115 f.write(ind)
116 if isinstance(tu, tuple):
117 f.write(self._names[tu[0]])
118 f.write('\n')
119 for entry in tu[1:]:
120 self.Print(entry, f, indent=indent+1)
121 elif isinstance(tu, int):
122 f.write(str(tu))
123 f.write('\n')
124 elif isinstance(tu, str):
125 f.write(str(tu))
126 f.write('\n')
127 else:
128 raise AssertionError(tu)
129
130
131def Options():
132 """Returns an option parser instance."""
133 p = optparse.OptionParser()
134
135 # NOTE: default command is None because empty string is valid.
136 p.add_option(
137 '-c', dest='command', default=None,
138 help='Python command to run')
139 return p
140
141
142# Made by the Makefile.
143PICKLE_REL_PATH = '_build/opy/py27.grammar.pickle'
144
145def OpyCommandMain(argv):
146 """Dispatch to the right action."""
147
148 # TODO: Use core/args.
149 opts, argv = Options().parse_args(argv)
150
151 try:
152 action = argv[0]
153 except IndexError:
154 raise args.UsageError('opy: Missing required subcommand.')
155
156 if action in ('parse', 'compile'):
157 loader = util.GetResourceLoader()
158 f = loader.open(PICKLE_REL_PATH)
159 gr = grammar.Grammar()
160 gr.load(f)
161 f.close()
162
163 # In Python 2 code, always use from __future__ import print_function.
164 try:
165 del gr.keywords["print"]
166 except KeyError:
167 pass
168
169 FILE_INPUT = gr.symbol2number['file_input']
170
171 symbols = Symbols(gr)
172 pytree.Init(symbols) # for type_repr() pretty printing
173 transformer.Init(symbols) # for _names and other dicts
174 else:
175 # e.g. pgen2 doesn't use any of these. Maybe we should make a different
176 # tool.
177 gr = None
178 FILE_INPUT = None
179 symbols = None
180
181 #do_glue = False
182 do_glue = True
183
184 if do_glue: # Make it a flag
185 # Emulating parser.st structures from parsermodule.c.
186 # They have a totuple() method, which outputs tuples like this.
187 def py2st(gr, raw_node):
188 type, value, context, children = raw_node
189 # See pytree.Leaf
190 if context:
191 _, (lineno, column) = context
192 else:
193 lineno = 0 # default in Leaf
194 column = 0
195
196 if children:
197 return (type,) + tuple(children)
198 else:
199 return (type, value, lineno, column)
200 convert = py2st
201 else:
202 convert = pytree.convert
203
204 dr = driver.Driver(gr, convert=convert)
205
206 if action == 'pgen2':
207 grammar_path = argv[1]
208 pickle_path = argv[2]
209 WriteGrammar(grammar_path, pickle_path)
210
211 elif action == 'stdlib-parse':
212 # This is what the compiler/ package was written against.
213 import parser
214
215 py_path = argv[1]
216 with open(py_path) as f:
217 st = parser.suite(f.read())
218
219 tree = st.totuple()
220
221 n = CountTupleTree(tree)
222 log('COUNT %d', n)
223 printer = TupleTreePrinter(HostStdlibNames())
224 printer.Print(tree)
225
226 elif action == 'parse':
227 py_path = argv[1]
228 with open(py_path) as f:
229 tokens = tokenize.generate_tokens(f.readline)
230 tree = dr.parse_tokens(tokens, start_symbol=FILE_INPUT)
231
232 if isinstance(tree, tuple):
233 n = CountTupleTree(tree)
234 log('COUNT %d', n)
235
236 printer = TupleTreePrinter(transformer._names)
237 printer.Print(tree)
238 else:
239 tree.PrettyPrint(sys.stdout)
240 log('\tChildren: %d' % len(tree.children), file=sys.stderr)
241
242 elif action == 'compile':
243 # 'opy compile' is pgen2 + compiler2
244
245 py_path = argv[1]
246 out_path = argv[2]
247
248 if do_glue:
249 py_parser = Pgen2PythonParser(dr, FILE_INPUT)
250 printer = TupleTreePrinter(transformer._names)
251 tr = transformer.Pgen2Transformer(py_parser, printer)
252 else:
253 tr = transformer.Transformer()
254
255 with open(py_path) as f:
256 contents = f.read()
257 co = pycodegen.compile(contents, py_path, 'exec', transformer=tr)
258 log("Code length: %d", len(co.co_code))
259
260 # Write the .pyc file
261 with open(out_path, 'wb') as out_f:
262 h = pycodegen.getPycHeader(py_path)
263 out_f.write(h)
264 marshal.dump(co, out_f)
265
266 elif action == 'dis':
267 pyc_path = argv[1]
268 try:
269 report_path = argv[2]
270 report_f = open(report_path, 'w')
271 except IndexError:
272 report_f = sys.stdout
273
274 with open(pyc_path, 'rb') as f:
275 # TODO: Make this a flag.
276 #v = inspect_pyc.Visitor(dis_bytecode=False)
277 v = inspect_pyc.Visitor()
278 v.Visit(f)
279
280 v.Report(report_f)
281
282 elif action == 'dis-md5':
283 pyc_paths = argv[1:]
284 if not pyc_paths:
285 raise args.UsageError('dis-md5: At least one .pyc path is required.')
286
287 for path in pyc_paths:
288 h = hashlib.md5()
289 with open(path) as f:
290 magic = f.read(4)
291 h.update(magic)
292 ignored_timestamp = f.read(4)
293 while True:
294 b = f.read(64 * 1024)
295 if not b:
296 break
297 h.update(b)
298 print('%6d %s %s' % (os.path.getsize(path), h.hexdigest(), path))
299
300 # NOTE: Unused
301 elif action == 'old-compile':
302 py_path = argv[1]
303 out_path = argv[2]
304
305 if do_glue:
306 py_parser = Pgen2PythonParser(dr, FILE_INPUT)
307 printer = TupleTreePrinter(transformer._names)
308 tr = transformer.Pgen2Transformer(py_parser, printer)
309 else:
310 tr = transformer.Transformer()
311
312 f = open(py_path)
313 contents = f.read()
314 co = pycodegen.compile(contents, py_path, 'exec', transformer=tr)
315 log("Code length: %d", len(co.co_code))
316
317 # Write the .pyc file
318 with open(out_path, 'wb') as out_f:
319 h = pycodegen.getPycHeader(py_path)
320 out_f.write(h)
321 marshal.dump(co, out_f)
322
323 # TODO: Not used
324 elif action == 'compile2':
325 in_path = argv[1]
326 out_path = argv[2]
327
328 from compiler2 import pycodegen as pycodegen2
329 from misc import stdlib_compile
330
331 stdlib_compile.compileAndWrite(in_path, out_path, pycodegen2.compile)
332
333 elif action == 'run':
334 # TODO: Add an option like -v in __main__
335
336 #level = logging.DEBUG if args.verbose else logging.WARNING
337 #logging.basicConfig(level=level)
338 #logging.basicConfig(level=logging.DEBUG)
339
340 # Compile and run, without writing pyc file
341 py_path = argv[1]
342 opy_argv = argv[1:]
343
344 if py_path.endswith('.py'):
345 py_parser = Pgen2PythonParser(dr, FILE_INPUT)
346 printer = TupleTreePrinter(transformer._names)
347 tr = transformer.Pgen2Transformer(py_parser, printer)
348 with open(py_path) as f:
349 contents = f.read()
350 co = pycodegen.compile(contents, py_path, 'exec', transformer=tr)
351 #execfile.run_code_object(co, opy_argv)
352
353 elif py_path.endswith('.pyc') or py_path.endswith('.opyc'):
354 with open(py_path) as f:
355 f.seek(8) # past header. TODO: validate it!
356 co = marshal.load(f)
357 #execfile.run_code_object(co, opy_argv)
358
359 else:
360 raise args.UsageError('Invalid path %r' % py_path)
361
362 else:
363 raise args.UsageError('Invalid action %r' % action)
364
365 # Examples of nodes Leaf(type, value):
366 # Leaf(1, 'def')
367 # Leaf(4, '\n')
368 # Leaf(8, ')')
369 # Oh are these just tokens?
370 # yes.
371
372 # Node(prefix, children)