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

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