OILS / mycpp / mycpp_main.py View on Github | oilshell.org

413 lines, 249 significant
1#!/usr/bin/env python3
2"""
3mycpp_main.py - Translate a subset of Python to C++, using MyPy's typed AST.
4"""
5from __future__ import print_function
6
7import optparse
8import os
9import sys
10
11from typing import List, Optional, Tuple
12
13from mypy.build import build as mypy_build
14from mypy.build import BuildSource
15from mypy.main import process_options
16
17from mycpp import ir_pass
18from mycpp import const_pass
19from mycpp import cppgen_pass
20from mycpp import debug_pass
21from mycpp import control_flow_pass
22from mycpp import pass_state
23from mycpp.util import log
24
25
26def Options():
27 """Returns an option parser instance."""
28
29 p = optparse.OptionParser()
30 p.add_option('-v',
31 '--verbose',
32 dest='verbose',
33 action='store_true',
34 default=False,
35 help='Show details about translation')
36
37 p.add_option('--cc-out',
38 dest='cc_out',
39 default=None,
40 help='.cc file to write to')
41
42 p.add_option('--to-header',
43 dest='to_header',
44 action='append',
45 default=[],
46 help='Export this module to a header, e.g. frontend.args')
47
48 p.add_option('--header-out',
49 dest='header_out',
50 default=None,
51 help='Write this header')
52
53 p.add_option(
54 '--stack-roots-warn',
55 dest='stack_roots_warn',
56 default=None,
57 type='int',
58 help='Emit warnings about functions with too many stack roots')
59
60 p.add_option(
61 '--minimize-stack-roots',
62 dest='minimize_stack_roots',
63 default=False,
64 help='Try to minimize the number of GC stack roots.')
65
66 return p
67
68
69# Copied from mypyc/build.py
70def get_mypy_config(
71 paths: List[str], mypy_options: Optional[List[str]]
72) -> Tuple[List[BuildSource], Options]:
73 """Construct mypy BuildSources and Options from file and options lists"""
74 # It is kind of silly to do this but oh well
75 mypy_options = mypy_options or []
76 mypy_options.append('--')
77 mypy_options.extend(paths)
78
79 sources, options = process_options(mypy_options)
80
81 options.show_traceback = True
82 # Needed to get types for all AST nodes
83 options.export_types = True
84 # TODO: Support incremental checking
85 options.incremental = False
86 # 10/2019: FIX for MyPy 0.730. Not sure why I need this but I do.
87 options.preserve_asts = True
88
89 # 1/2023: Workaround for conditional import in osh/builtin_comp.py
90 # Same as devtools/types.sh
91 options.warn_unused_ignores = False
92
93 for source in sources:
94 options.per_module_options.setdefault(source.module,
95 {})['mypyc'] = True
96
97 return sources, options
98
99
100_FIRST = ('asdl.runtime', 'core.vm')
101
102# should be LAST because they use base classes
103_LAST = ('builtin.bracket_osh', 'builtin.completion_osh', 'core.shell')
104
105
106def ModulesToCompile(result, mod_names):
107 # HACK TO PUT asdl/runtime FIRST.
108 #
109 # Another fix is to hoist those to the declaration phase? Not sure if that
110 # makes sense.
111
112 # FIRST files. Somehow the MyPy builder reorders the modules.
113 for name, module in result.files.items():
114 if name in _FIRST:
115 yield name, module
116
117 for name, module in result.files.items():
118 # Only translate files that were mentioned on the command line
119 suffix = name.split('.')[-1]
120 if suffix not in mod_names:
121 continue
122
123 if name in _FIRST: # We already did these
124 continue
125
126 if name in _LAST: # We'll do these later
127 continue
128
129 yield name, module
130
131 # LAST files
132 for name, module in result.files.items():
133 if name in _LAST:
134 yield name, module
135
136
137def main(argv):
138 # TODO: Put these in the shell script
139 mypy_options = [
140 '--py2',
141 '--strict',
142 '--no-implicit-optional',
143 '--no-strict-optional',
144 # for consistency?
145 '--follow-imports=silent',
146 #'--verbose',
147 ]
148
149 o = Options()
150 opts, argv = o.parse_args(argv)
151
152 paths = argv[1:] # e.g. asdl/typed_arith_parse.py
153
154 log('\tmycpp: LOADING %s', ' '.join(paths))
155 #log('\tmycpp: MYPYPATH = %r', os.getenv('MYPYPATH'))
156
157 if 0:
158 print(opts)
159 print(paths)
160 return
161
162 # e.g. asdl/typed_arith_parse.py -> 'typed_arith_parse'
163 mod_names = [os.path.basename(p) for p in paths]
164 mod_names = [os.path.splitext(name)[0] for name in mod_names]
165
166 # Ditto
167 to_header = opts.to_header
168 #if to_header:
169 if 0:
170 to_header = [os.path.basename(p) for p in to_header]
171 to_header = [os.path.splitext(name)[0] for name in to_header]
172
173 #log('to_header %s', to_header)
174
175 sources, options = get_mypy_config(paths, mypy_options)
176 if 0:
177 for source in sources:
178 log('source %s', source)
179 log('')
180 #log('options %s', options)
181
182 #result = emitmodule.parse_and_typecheck(sources, options)
183 import time
184 start_time = time.time()
185 result = mypy_build(sources=sources, options=options)
186 #log('elapsed 1: %f', time.time() - start_time)
187
188 if result.errors:
189 log('')
190 log('-' * 80)
191 for e in result.errors:
192 log(e)
193 log('-' * 80)
194 log('')
195 return 1
196
197 # Important functions in mypyc/build.py:
198 #
199 # generate_c (251 lines)
200 # parse_and_typecheck
201 # compile_modules_to_c
202
203 # mypyc/emitmodule.py (487 lines)
204 # def compile_modules_to_c(result: BuildResult, module_names: List[str],
205 # class ModuleGenerator:
206 # # This generates a whole bunch of textual code!
207
208 # literals, modules, errors = genops.build_ir(file_nodes, result.graph,
209 # result.types)
210
211 # TODO: Debug what comes out of here.
212 #build.dump_graph(result.graph)
213 #return
214
215 # no-op
216 if 0:
217 for name in result.graph:
218 log('result %s %s', name, result.graph[name])
219 log('')
220
221 # GLOBAL Constant pass over all modules. We want to collect duplicate
222 # strings together. And have globally unique IDs str0, str1, ... strN.
223 const_lookup = {} # Dict {StrExpr node => string name}
224 const_code = []
225 pass1 = const_pass.Collect(result.types, const_lookup, const_code)
226
227 to_compile = list(ModulesToCompile(result, mod_names))
228
229 # HACK: Why do I get oil.asdl.tdop in addition to asdl.tdop?
230 #names = set(name for name, _ in to_compile)
231
232 filtered = []
233 seen = set()
234 for name, module in to_compile:
235 if name.startswith('oil.'):
236 name = name[4:]
237
238 # ditto with testpkg.module1
239 if name.startswith('mycpp.'):
240 name = name[6:]
241
242 if name not in seen: # remove dupe
243 filtered.append((name, module))
244 seen.add(name)
245
246 to_compile = filtered
247
248 #import pickle
249 if 0:
250 for name, module in to_compile:
251 log('to_compile %s', name)
252 log('')
253
254 # can't pickle but now I see deserialize() nodes and stuff
255 #s = pickle.dumps(module)
256 #log('%d pickle', len(s))
257
258 # Print the tree for debugging
259 if 0:
260 for name, module in to_compile:
261 builder = debug_pass.Print(result.types)
262 builder.visit_mypy_file(module)
263 return
264
265 if opts.cc_out:
266 f = open(opts.cc_out, 'w')
267 else:
268 f = sys.stdout
269
270 f.write("""\
271// BEGIN mycpp output
272
273#include "mycpp/runtime.h"
274
275""")
276
277 # Convert the mypy AST into our own IR.
278 dot_exprs = {} # module name -> {expr node -> access type}
279 log('\tmycpp pass: IR')
280 for _, module in to_compile:
281 p = ir_pass.Build(result.types)
282 p.visit_mypy_file(module)
283 dot_exprs[module.path] = p.dot_exprs
284
285 # Collect constants and then emit code.
286 log('\tmycpp pass: CONST')
287 for name, module in to_compile:
288 pass1.visit_mypy_file(module)
289
290 # Instead of top-level code, should we generate a function and call it from
291 # main?
292 for line in const_code:
293 f.write('%s\n' % line)
294 f.write('\n')
295
296 # Note: doesn't take into account module names!
297 virtual = pass_state.Virtual()
298
299 if opts.header_out:
300 header_f = open(opts.header_out, 'w') # Not closed
301
302 log('\tmycpp pass: FORWARD DECL')
303
304 # Forward declarations first.
305 # class Foo; class Bar;
306 for name, module in to_compile:
307 #log('forward decl name %s', name)
308 if name in to_header:
309 out_f = header_f
310 else:
311 out_f = f
312 p2 = cppgen_pass.Generate(result.types,
313 const_lookup,
314 out_f,
315 virtual=virtual,
316 forward_decl=True,
317 dot_exprs=dot_exprs[module.path])
318
319 p2.visit_mypy_file(module)
320 MaybeExitWithErrors(p2)
321
322 # After seeing class and method names in the first pass, figure out which
323 # ones are virtual. We use this info in the second pass.
324 virtual.Calculate()
325 if 0:
326 log('virtuals %s', virtual.virtuals)
327 log('has_vtable %s', virtual.has_vtable)
328
329 local_vars = {} # FuncDef node -> (name, c_type) list
330 ctx_member_vars = {
331 } # Dict[ClassDef node for ctx_Foo, Dict[member_name: str, Type]]
332
333 log('\tmycpp pass: PROTOTYPES')
334
335 # First generate ALL C++ declarations / "headers".
336 # class Foo { void method(); }; class Bar { void method(); };
337 for name, module in to_compile:
338 #log('decl name %s', name)
339 if name in to_header:
340 out_f = header_f
341 else:
342 out_f = f
343 p3 = cppgen_pass.Generate(result.types,
344 const_lookup,
345 out_f,
346 local_vars=local_vars,
347 ctx_member_vars=ctx_member_vars,
348 virtual=virtual,
349 decl=True,
350 dot_exprs=dot_exprs[module.path])
351
352 p3.visit_mypy_file(module)
353 MaybeExitWithErrors(p3)
354
355 if 0:
356 log('\tctx_member_vars')
357 from pprint import pformat
358 print(pformat(ctx_member_vars), file=sys.stderr)
359
360 log('\tmycpp pass: CONTROL FLOW')
361
362 cfgs = {} # fully qualified function name -> control flow graph
363 for name, module in to_compile:
364 cfg_pass = control_flow_pass.Build(result.types, virtual, local_vars,
365 dot_exprs[module.path])
366 cfg_pass.visit_mypy_file(module)
367 cfgs.update(cfg_pass.cfgs)
368
369 log('\tmycpp pass: DATAFLOW')
370 stack_roots = None
371 if opts.minimize_stack_roots:
372 stack_roots = pass_state.ComputeMinimalStackRoots(cfgs)
373 else:
374 pass_state.DumpControlFlowGraphs(cfgs)
375
376 log('\tmycpp pass: IMPL')
377
378 # Now the definitions / implementations.
379 # void Foo:method() { ... }
380 # void Bar:method() { ... }
381 for name, module in to_compile:
382 p4 = cppgen_pass.Generate(result.types,
383 const_lookup,
384 f,
385 local_vars=local_vars,
386 ctx_member_vars=ctx_member_vars,
387 stack_roots_warn=opts.stack_roots_warn,
388 dot_exprs=dot_exprs[module.path],
389 stack_roots=stack_roots)
390 p4.visit_mypy_file(module)
391 MaybeExitWithErrors(p4)
392
393 return 0 # success
394
395
396def MaybeExitWithErrors(p):
397 # Check for errors we collected
398 num_errors = len(p.errors_keep_going)
399 if num_errors != 0:
400 log('')
401 log('%s: %d translation errors (after type checking)', sys.argv[0],
402 num_errors)
403
404 # A little hack to tell the test-invalid-examples harness how many errors we had
405 sys.exit(min(num_errors, 255))
406
407
408if __name__ == '__main__':
409 try:
410 sys.exit(main(sys.argv))
411 except RuntimeError as e:
412 print('FATAL: %s' % e, file=sys.stderr)
413 sys.exit(1)