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

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