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

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