]> git.proxmox.com Git - mirror_qemu.git/blame - scripts/qapi/introspect.py
qapi/introspect.py: create a typed 'Annotated' data strutcure
[mirror_qemu.git] / scripts / qapi / introspect.py
CommitLineData
5ddeec83
MA
1"""
2QAPI introspection generator
3
4Copyright (C) 2015-2018 Red Hat, Inc.
5
6Authors:
7 Markus Armbruster <armbru@redhat.com>
8
9This work is licensed under the terms of the GNU GPL, version 2.
10See the COPYING file in the top-level directory.
11"""
39a18158 12
9db27346
JS
13from typing import (
14 Any,
15 Dict,
4f7f97a7
JS
16 Generic,
17 Iterable,
9db27346
JS
18 List,
19 Optional,
4f7f97a7
JS
20 Tuple,
21 TypeVar,
9db27346
JS
22 Union,
23)
5f50cede 24
5af8263d
JS
25from .common import (
26 c_name,
27 gen_endif,
28 gen_if,
29 mcgen,
30)
7137a960 31from .gen import QAPISchemaMonolithicCVisitor
67fea575
JS
32from .schema import (
33 QAPISchemaArrayType,
34 QAPISchemaBuiltinType,
35 QAPISchemaType,
36)
39a18158
MA
37
38
9db27346
JS
39# This module constructs a tree data structure that is used to
40# generate the introspection information for QEMU. It is shaped
41# like a JSON value.
42#
43# A complexity over JSON is that our values may or may not be annotated.
44#
45# Un-annotated values may be:
46# Scalar: str, bool, None.
47# Non-scalar: List, Dict
48# _value = Union[str, bool, None, Dict[str, JSONValue], List[JSONValue]]
49#
50# With optional annotations, the type of all values is:
51# JSONValue = Union[_Value, Annotated[_Value]]
52#
53# Sadly, mypy does not support recursive types; so the _Stub alias is used to
54# mark the imprecision in the type model where we'd otherwise use JSONValue.
55_Stub = Any
56_Scalar = Union[str, bool, None]
57_NonScalar = Union[Dict[str, _Stub], List[_Stub]]
58_Value = Union[_Scalar, _NonScalar]
4f7f97a7 59JSONValue = Union[_Value, 'Annotated[_Value]']
9db27346
JS
60
61
4f7f97a7
JS
62_ValueT = TypeVar('_ValueT', bound=_Value)
63
64
65class Annotated(Generic[_ValueT]):
66 """
67 Annotated generally contains a SchemaInfo-like type (as a dict),
68 But it also used to wrap comments/ifconds around scalar leaf values,
69 for the benefit of features and enums.
70 """
71 # TODO: Remove after Python 3.7 adds @dataclass:
72 # pylint: disable=too-few-public-methods
73 def __init__(self, value: _ValueT, ifcond: Iterable[str],
74 comment: Optional[str] = None):
75 self.value = value
76 self.comment: Optional[str] = comment
77 self.ifcond: Tuple[str, ...] = tuple(ifcond)
24cfd6ad
MA
78
79
05556960 80def _tree_to_qlit(obj, level=0, dict_value=False):
7d0f982b
MAL
81
82 def indent(level):
83 return level * 4 * ' '
84
4f7f97a7 85 if isinstance(obj, Annotated):
05556960
JS
86 # NB: _tree_to_qlit is called recursively on the values of a
87 # key:value pair; those values can't be decorated with
88 # comments or conditionals.
89 msg = "dict values cannot have attached comments or if-conditionals."
90 assert not dict_value, msg
91
8c643361 92 ret = ''
4f7f97a7
JS
93 if obj.comment:
94 ret += indent(level) + '/* %s */\n' % obj.comment
95 if obj.ifcond:
96 ret += gen_if(obj.ifcond)
97 ret += _tree_to_qlit(obj.value, level)
98 if obj.ifcond:
99 ret += '\n' + gen_endif(obj.ifcond)
d626b6c1
MAL
100 return ret
101
7d0f982b 102 ret = ''
05556960 103 if not dict_value:
7d0f982b 104 ret += indent(level)
39a18158 105 if obj is None:
7d0f982b 106 ret += 'QLIT_QNULL'
39a18158 107 elif isinstance(obj, str):
7d0f982b 108 ret += 'QLIT_QSTR(' + to_c_string(obj) + ')'
39a18158 109 elif isinstance(obj, list):
2e8a843d 110 elts = [_tree_to_qlit(elt, level + 1).strip('\n')
39a18158 111 for elt in obj]
7d0f982b
MAL
112 elts.append(indent(level + 1) + "{}")
113 ret += 'QLIT_QLIST(((QLitObject[]) {\n'
40bb1376 114 ret += '\n'.join(elts) + '\n'
7d0f982b 115 ret += indent(level) + '}))'
39a18158 116 elif isinstance(obj, dict):
7d0f982b
MAL
117 elts = []
118 for key, value in sorted(obj.items()):
119 elts.append(indent(level + 1) + '{ %s, %s }' %
2e8a843d
MA
120 (to_c_string(key),
121 _tree_to_qlit(value, level + 1, True)))
7d0f982b
MAL
122 elts.append(indent(level + 1) + '{}')
123 ret += 'QLIT_QDICT(((QLitDictEntry[]) {\n'
124 ret += ',\n'.join(elts) + '\n'
125 ret += indent(level) + '}))'
876c6751
PX
126 elif isinstance(obj, bool):
127 ret += 'QLIT_QBOOL(%s)' % ('true' if obj else 'false')
39a18158
MA
128 else:
129 assert False # not implemented
40bb1376
MAL
130 if level > 0:
131 ret += ','
39a18158
MA
132 return ret
133
134
135def to_c_string(string):
136 return '"' + string.replace('\\', r'\\').replace('"', r'\"') + '"'
137
138
71b3f045
MA
139class QAPISchemaGenIntrospectVisitor(QAPISchemaMonolithicCVisitor):
140
93b564c4 141 def __init__(self, prefix, unmask):
2cae67bc
MA
142 super().__init__(
143 prefix, 'qapi-introspect',
71b3f045 144 ' * QAPI/QMP schema introspection', __doc__)
1a9a507b 145 self._unmask = unmask
39a18158 146 self._schema = None
2e8a843d 147 self._trees = []
39a18158 148 self._used_types = []
1a9a507b 149 self._name_map = {}
71b3f045
MA
150 self._genc.add(mcgen('''
151#include "qemu/osdep.h"
eb815e24 152#include "%(prefix)sqapi-introspect.h"
71b3f045
MA
153
154''',
155 prefix=prefix))
156
157 def visit_begin(self, schema):
158 self._schema = schema
39a18158
MA
159
160 def visit_end(self):
161 # visit the types that are actually used
162 for typ in self._used_types:
163 typ.visit(self)
39a18158 164 # generate C
7d0f982b 165 name = c_name(self._prefix, protect=False) + 'qmp_schema_qlit'
71b3f045 166 self._genh.add(mcgen('''
7d0f982b
MAL
167#include "qapi/qmp/qlit.h"
168
169extern const QLitObject %(c_name)s;
39a18158 170''',
71b3f045 171 c_name=c_name(name)))
71b3f045 172 self._genc.add(mcgen('''
7d0f982b 173const QLitObject %(c_name)s = %(c_string)s;
39a18158 174''',
71b3f045 175 c_name=c_name(name),
2e8a843d 176 c_string=_tree_to_qlit(self._trees)))
39a18158 177 self._schema = None
2e8a843d 178 self._trees = []
71b3f045
MA
179 self._used_types = []
180 self._name_map = {}
1a9a507b 181
25a0d9c9
EB
182 def visit_needed(self, entity):
183 # Ignore types on first pass; visit_end() will pick up used types
184 return not isinstance(entity, QAPISchemaType)
185
1a9a507b
MA
186 def _name(self, name):
187 if self._unmask:
188 return name
189 if name not in self._name_map:
190 self._name_map[name] = '%d' % len(self._name_map)
191 return self._name_map[name]
39a18158
MA
192
193 def _use_type(self, typ):
6b67bcac
JS
194 assert self._schema is not None
195
39a18158
MA
196 # Map the various integer types to plain int
197 if typ.json_type() == 'int':
198 typ = self._schema.lookup_type('int')
199 elif (isinstance(typ, QAPISchemaArrayType) and
200 typ.element_type.json_type() == 'int'):
201 typ = self._schema.lookup_type('intList')
202 # Add type to work queue if new
203 if typ not in self._used_types:
204 self._used_types.append(typ)
1a9a507b 205 # Clients should examine commands and events, not types. Hide
1aa806cc
EB
206 # type names as integers to reduce the temptation. Also, it
207 # saves a few characters on the wire.
1a9a507b
MA
208 if isinstance(typ, QAPISchemaBuiltinType):
209 return typ.name
ce5fcb47
EB
210 if isinstance(typ, QAPISchemaArrayType):
211 return '[' + self._use_type(typ.element_type) + ']'
1a9a507b 212 return self._name(typ.name)
39a18158 213
84bece7d
JS
214 @staticmethod
215 def _gen_features(features):
4f7f97a7 216 return [Annotated(f.name, f.ifcond) for f in features]
84bece7d 217
2e8a843d 218 def _gen_tree(self, name, mtype, obj, ifcond, features):
5f50cede 219 comment: Optional[str] = None
ce5fcb47 220 if mtype not in ('command', 'event', 'builtin', 'array'):
8c643361
EB
221 if not self._unmask:
222 # Output a comment to make it easy to map masked names
223 # back to the source when reading the generated output.
5f50cede 224 comment = f'"{self._name(name)}" = {name}'
1a9a507b 225 name = self._name(name)
39a18158
MA
226 obj['name'] = name
227 obj['meta-type'] = mtype
84bece7d
JS
228 if features:
229 obj['features'] = self._gen_features(features)
4f7f97a7 230 self._trees.append(Annotated(obj, ifcond, comment))
39a18158
MA
231
232 def _gen_member(self, member):
24cfd6ad 233 obj = {'name': member.name, 'type': self._use_type(member.type)}
39a18158 234 if member.optional:
24cfd6ad 235 obj['default'] = None
84bece7d
JS
236 if member.features:
237 obj['features'] = self._gen_features(member.features)
4f7f97a7 238 return Annotated(obj, member.ifcond)
39a18158
MA
239
240 def _gen_variants(self, tag_name, variants):
241 return {'tag': tag_name,
242 'variants': [self._gen_variant(v) for v in variants]}
243
244 def _gen_variant(self, variant):
24cfd6ad 245 obj = {'case': variant.name, 'type': self._use_type(variant.type)}
4f7f97a7 246 return Annotated(obj, variant.ifcond)
39a18158
MA
247
248 def visit_builtin_type(self, name, info, json_type):
2e8a843d 249 self._gen_tree(name, 'builtin', {'json-type': json_type}, [], None)
39a18158 250
013b4efc 251 def visit_enum_type(self, name, info, ifcond, features, members, prefix):
4f7f97a7
JS
252 self._gen_tree(
253 name, 'enum',
254 {'values': [Annotated(m.name, m.ifcond) for m in members]},
255 ifcond, features
256 )
39a18158 257
fbf09a2f 258 def visit_array_type(self, name, info, ifcond, element_type):
ce5fcb47 259 element = self._use_type(element_type)
2e8a843d 260 self._gen_tree('[' + element + ']', 'array', {'element-type': element},
013b4efc 261 ifcond, None)
39a18158 262
7b3bc9e2
MA
263 def visit_object_type_flat(self, name, info, ifcond, features,
264 members, variants):
39a18158
MA
265 obj = {'members': [self._gen_member(m) for m in members]}
266 if variants:
267 obj.update(self._gen_variants(variants.tag_member.name,
268 variants.variants))
6a8c0b51 269
2e8a843d 270 self._gen_tree(name, 'object', obj, ifcond, features)
39a18158 271
013b4efc 272 def visit_alternate_type(self, name, info, ifcond, features, variants):
4f7f97a7
JS
273 self._gen_tree(
274 name, 'alternate',
275 {'members': [Annotated({'type': self._use_type(m.type)},
276 m.ifcond)
277 for m in variants.variants]},
278 ifcond, features
279 )
39a18158 280
7b3bc9e2
MA
281 def visit_command(self, name, info, ifcond, features,
282 arg_type, ret_type, gen, success_response, boxed,
04f22362 283 allow_oob, allow_preconfig, coroutine):
6b67bcac
JS
284 assert self._schema is not None
285
39a18158
MA
286 arg_type = arg_type or self._schema.the_empty_object_type
287 ret_type = ret_type or self._schema.the_empty_object_type
25b1ef31 288 obj = {'arg-type': self._use_type(arg_type),
1aa806cc 289 'ret-type': self._use_type(ret_type)}
25b1ef31
MA
290 if allow_oob:
291 obj['allow-oob'] = allow_oob
2e8a843d 292 self._gen_tree(name, 'command', obj, ifcond, features)
23394b4c 293
013b4efc 294 def visit_event(self, name, info, ifcond, features, arg_type, boxed):
6b67bcac 295 assert self._schema is not None
39a18158 296 arg_type = arg_type or self._schema.the_empty_object_type
2e8a843d 297 self._gen_tree(name, 'event', {'arg-type': self._use_type(arg_type)},
013b4efc 298 ifcond, features)
39a18158 299
1a9a507b 300
fb0bc835 301def gen_introspect(schema, output_dir, prefix, opt_unmask):
26df4e7f
MA
302 vis = QAPISchemaGenIntrospectVisitor(prefix, opt_unmask)
303 schema.visit(vis)
71b3f045 304 vis.write(output_dir)