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

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