OILS / mycpp / mycpp_main.py View on Github | oils.pub

291 lines, 184 significant
1#!/usr/bin/env python3
2from __future__ import print_function
3"""
4mycpp_main.py - Translate a subset of Python to C++, using MyPy's typed AST.
5"""
6
7import optparse
8import os
9import sys
10import time
11
12START_TIME = time.time() # measure before imports
13
14# MyPy deps
15from mypy.build import build as mypy_build
16from mypy.main import process_options
17
18from mycpp.util import log
19from mycpp import translate
20
21from typing import (List, Optional, Tuple, Any, Iterator, TYPE_CHECKING)
22
23if TYPE_CHECKING:
24 from mypy.nodes import MypyFile
25 from mypy.modulefinder import BuildSource
26 from mypy.build import BuildResult
27
28
29def Options() -> optparse.OptionParser:
30 """Returns an option parser instance."""
31
32 p = optparse.OptionParser()
33 p.add_option('-v',
34 '--verbose',
35 dest='verbose',
36 action='store_true',
37 default=False,
38 help='Show details about translation')
39
40 p.add_option('--cc-out',
41 dest='cc_out',
42 default=None,
43 help='.cc file to write to')
44
45 p.add_option('--to-header',
46 dest='to_header',
47 action='append',
48 default=[],
49 help='Export this module to a header, e.g. frontend.args')
50
51 p.add_option('--header-out',
52 dest='header_out',
53 default=None,
54 help='Write this header')
55
56 p.add_option('--facts-out-dir',
57 dest='facts_out_dir',
58 default=None,
59 help='Dump control flow graphs for testing')
60
61 p.add_option('--preamble-path',
62 dest='preamble_path',
63 default='',
64 help='#include this path in the generated .cc file')
65
66 p.add_option(
67 '--stack-roots-warn',
68 dest='stack_roots_warn',
69 default=None,
70 type='int',
71 help='Emit warnings about functions with too many stack roots')
72
73 p.add_option('--minimize-stack-roots',
74 dest='minimize_stack_roots',
75 action='store_true',
76 default=False,
77 help='Try to minimize the number of GC stack roots.')
78
79 p.add_option('--no-minimize-stack-roots',
80 dest='no_minimize_stack_roots',
81 action='store_true',
82 default=False,
83 help='Do NOT minimize the number of GC stack roots.')
84
85 return p
86
87
88# Copied from mypyc/build.py
89def get_mypy_config(
90 paths: List[str],
91 mypy_options: Optional[List[str]]) -> Tuple[List['BuildSource'], Any]:
92 """Construct mypy BuildSources and Options from file and options lists"""
93 # It is kind of silly to do this but oh well
94 mypy_options = mypy_options or []
95 mypy_options.append('--')
96 mypy_options.extend(paths)
97
98 sources, options = process_options(mypy_options)
99
100 options.show_traceback = True
101 # Needed to get types for all AST nodes
102 options.export_types = True
103 # TODO: Support incremental checking
104 options.incremental = False
105 # 10/2019: FIX for MyPy 0.730. Not sure why I need this but I do.
106 options.preserve_asts = True
107
108 # 1/2023: Workaround for conditional import in osh/builtin_comp.py
109 # Same as devtools/types.sh
110 options.warn_unused_ignores = False
111
112 for source in sources:
113 options.per_module_options.setdefault(source.module,
114 {})['mypyc'] = True
115
116 return sources, options
117
118
119_FIRST = ('asdl.runtime', 'core.vm')
120
121# should be LAST because they use base classes
122_LAST = ('builtin.bracket_osh', 'builtin.completion_osh', 'core.shell')
123
124
125def ModulesToCompile(result: 'BuildResult',
126 mod_names: List[str]) -> Iterator[Tuple[str, 'MypyFile']]:
127 # HACK TO PUT asdl/runtime FIRST.
128 #
129 # Another fix is to hoist those to the declaration phase? Not sure if that
130 # makes sense.
131
132 # FIRST files. Somehow the MyPy builder reorders the modules.
133 for name, module in result.files.items():
134 if name in _FIRST:
135 yield name, module
136
137 for name, module in result.files.items():
138 # Only translate files that were mentioned on the command line
139 suffix = name.split('.')[-1]
140 if suffix not in mod_names:
141 continue
142
143 if name in _FIRST: # We already did these
144 continue
145
146 if name in _LAST: # We'll do these later
147 continue
148
149 yield name, module
150
151 # LAST files
152 for name, module in result.files.items():
153 if name in _LAST:
154 yield name, module
155
156
157def _DedupeHack(
158 to_compile: List[Tuple[str,
159 'MypyFile']]) -> List[Tuple[str, 'MypyFile']]:
160 # Filtering step
161 filtered = []
162 seen = set()
163 for name, module in to_compile:
164 # HACK: Why do I get oil.asdl.tdop in addition to asdl.tdop?
165 if name.startswith('oil.'):
166 name = name[4:]
167
168 # ditto with testpkg.module1
169 if name.startswith('mycpp.'):
170 name = name[6:]
171
172 if name not in seen: # remove dupe
173 filtered.append((name, module))
174 seen.add(name)
175 return filtered
176
177
178def main(argv: List[str]) -> int:
179 timer = translate.Timer(START_TIME)
180
181 # Hack:
182 mypy_options = [
183 '--py2',
184 '--strict',
185 '--no-implicit-optional',
186 '--no-strict-optional',
187 # for consistency?
188 '--follow-imports=silent',
189 #'--verbose',
190 ]
191
192 o = Options()
193 opts, argv = o.parse_args(argv)
194 paths = argv[1:] # e.g. asdl/typed_arith_parse.py
195
196 timer.Section('mycpp: LOADING %s', ' '.join(paths))
197
198 #log('\tmycpp: MYPYPATH = %r', os.getenv('MYPYPATH'))
199
200 if 0:
201 print(opts)
202 print(paths)
203 return
204
205 # e.g. asdl/typed_arith_parse.py -> 'typed_arith_parse'
206 mod_names = [os.path.basename(p) for p in paths]
207 mod_names = [os.path.splitext(name)[0] for name in mod_names]
208
209 # Ditto
210 to_header = opts.to_header
211
212 #log('to_header %s', to_header)
213
214 sources, options = get_mypy_config(paths, mypy_options)
215 if 0:
216 for source in sources:
217 log('source %s', source)
218 log('')
219 #log('options %s', options)
220
221 #
222 # Type checking, which builds a Dict[Expression, Type] (12+ seconds)
223 #
224 result = mypy_build(sources=sources, options=options)
225
226 if result.errors:
227 log('')
228 log('-' * 80)
229 for e in result.errors:
230 log(e)
231 log('-' * 80)
232 log('')
233 return 1
234
235 # no-op
236 if 0:
237 for name in result.graph:
238 log('result %s %s', name, result.graph[name])
239 log('')
240
241 to_compile = list(ModulesToCompile(result, mod_names))
242 to_compile = _DedupeHack(to_compile)
243
244 if 0:
245 for name, module in to_compile:
246 log('to_compile %s', name)
247 log('')
248 #import pickle
249 # can't pickle but now I see deserialize() nodes and stuff
250 #s = pickle.dumps(module)
251 #log('%d pickle', len(s))
252
253 if opts.cc_out:
254 f = open(opts.cc_out, 'w')
255 else:
256 f = sys.stdout
257
258 header_f = None
259 if opts.header_out:
260 header_f = open(opts.header_out, 'w') # Not closed
261
262 f.write("""\
263// This file is GENERATED by mycpp, from Python source code
264
265""")
266
267 # This logic works around the Python optparse API
268 minimize_stack_roots = True
269 if opts.minimize_stack_roots:
270 minimize_stack_roots = True
271 if opts.no_minimize_stack_roots:
272 minimize_stack_roots = False
273
274 return translate.Run(timer,
275 f,
276 header_f,
277 result.types,
278 to_header,
279 to_compile,
280 preamble_path=opts.preamble_path,
281 stack_roots_warn=opts.stack_roots_warn,
282 minimize_stack_roots=minimize_stack_roots,
283 facts_out_dir=opts.facts_out_dir)
284
285
286if __name__ == '__main__':
287 try:
288 sys.exit(main(sys.argv))
289 except RuntimeError as e:
290 print('FATAL: %s' % e, file=sys.stderr)
291 sys.exit(1)