From: Markus Armbruster <armbru@redhat.com>
To: John Snow <jsnow@redhat.com>
Cc: qemu-devel@nongnu.org, Peter Maydell <peter.maydell@linaro.org>,
Michael Roth <michael.roth@amd.com>
Subject: Re: [PATCH 13/19] qapi/schema: fix typing for QAPISchemaVariants.tag_member
Date: Wed, 17 Jan 2024 09:19:04 +0100 [thread overview]
Message-ID: <87le8onzif.fsf@pond.sub.org> (raw)
In-Reply-To: <CAFn=p-ZsQnwMtDEN70UdTz75bN6FgzxPbM0yNicOoULpPtV97A@mail.gmail.com> (John Snow's message of "Wed, 10 Jan 2024 03:35:51 -0500")
John Snow <jsnow@redhat.com> writes:
> On Wed, Jan 10, 2024 at 2:53 AM Markus Armbruster <armbru@redhat.com> wrote:
>>
>> John Snow <jsnow@redhat.com> writes:
>>
>> > On Wed, Nov 22, 2023 at 11:02 AM John Snow <jsnow@redhat.com> wrote:
>> >>
>> >> On Wed, Nov 22, 2023 at 9:05 AM Markus Armbruster <armbru@redhat.com> wrote:
>> >> >
>> >> > John Snow <jsnow@redhat.com> writes:
>> >> >
>> >> > > There are two related changes here:
>> >> > >
>> >> > > (1) We need to perform type narrowing for resolving the type of
>> >> > > tag_member during check(), and
>> >> > >
>> >> > > (2) tag_member is a delayed initialization field, but we can hide it
>> >> > > behind a property that raises an Exception if it's called too
>> >> > > early. This simplifies the typing in quite a few places and avoids
>> >> > > needing to assert that the "tag_member is not None" at a dozen
>> >> > > callsites, which can be confusing and suggest the wrong thing to a
>> >> > > drive-by contributor.
>> >> > >
>> >> > > Signed-off-by: John Snow <jsnow@redhat.com>
>> >> >
>> >> > Without looking closely: review of PATCH 10 applies, doesn't it?
>> >> >
>> >>
>> >> Yep!
>> >
>> > Hm, actually, maybe not quite as cleanly.
>> >
>> > The problem is we *are* initializing that field immediately with
>> > whatever we were passed in during __init__, which means the field is
>> > indeed Optional. Later, during check(), we happen to eliminate that
>> > usage of None.
>>
>> You're right.
>>
>> QAPISchemaVariants.__init__() takes @tag_name and @tag_member. Exactly
>> one of them must be None. When creating a union's QAPISchemaVariants,
>> it's tag_member, and when creating an alternate's, it's tag_name.
>>
>> Why?
>>
>> A union's tag is an ordinary member selected by name via
>> 'discriminator': TAG_NAME. We can't resolve the name at this time,
>> because it may be buried arbitrarily deep in the base type chain.
>>
>> An alternate's tag is an implicitly created "member" of type 'QType'.
>> "Member" in scare-quotes, because is special: it exists in C, but not on
>> the wire, and not in introspection.
>>
>> Historical note: simple unions also had an implictly created tag member,
>> and its type was the implicit enum type enumerating the branches.
>>
>> So _def_union_type() passes TAG_NAME to .__init__(), and
>> _def_alternate_type() creates and passes the implicit tag member.
>> Hardly elegant, but it works.
>>
>> > To remove the use of the @property trick here, we could:
>> >
>> > ... declare the field, then only initialize it if we were passed a
>> > non-None value. But then check() would need to rely on something like
>> > hasattr to check if it was set or not, which is maybe an unfortunate
>> > code smell.
>> > So I think you'd still wind up needing a ._tag_member field which is
>> > Optional and always gets set during __init__, then setting a proper
>> > .tag_member field during check().
>> >
>> > Or I could just leave this one as-is. Or something else. I think the
>> > dirt has to get swept somewhere, because we don't *always* have enough
>> > information to fully initialize it at __init__ time, it's a
>> > conditional delayed initialization, unlike the others which are
>> > unconditionally delayed.
>>
>> Yes.
>>
>> Here's a possible "something else":
>>
>> 1. Drop parameter .__init__() parameter @tag_member, and leave
>> .tag_member unset there.
>>
>> 2. Set .tag_member in .check(): if .tag_name, look up that member (no
>> change). Else, it's an alternate; create the alternate's implicit tag
>> member.
>>
>> Drawback: before, we create AST in just one place, namely
>> QAPISchema._def_exprs(). Now we also create some in .check().
>
> I suppose I don't have a concrete argument against this beyond "It
> doesn't seem prettier than using the @property getter."
>
>>
>> Here's another "something else":
>>
>> 1. Fuse parameters .__init__() @tag_member and @tag_name. The type
>> becomes Union. Store for .check().
>>
>> 2. Set .tag_member in .check(): if we stored a name, look up that
>> member, else we must have stored an implicit member, so use that.
>>
>> 3. We check "is this a union?" like if self._tag_name. Needs
>> adjustment.
>>
>> Feels a bit awkward to me.
>
> Yeah, a little. Mechanically simple, though, I think.
>
>> We can also do nothing, as you said. We don't *have* to express
>> ".check() resolves unresolved tag member" in the type system. We can
>> just live with .tag_member remaining Optional.
>
> This is the only option I'm sure I don't want - it's misleading to
> users of the API for the purposes of new generators using a fully
> realized schema object. I think it's important to remove Optional[]
> where possible to avoid the question "When will this be set to None?"
> if the answer is just "For your purposes, never."
>
> It's an implementation detail of object initialization leaking out.
>
> (Also, I just counted and leaving the field as Optional adds 22 new
> type errors; that's a lot of callsites to bandage with new conditions.
> nah.)
>
> The *other* way to not do anything is to just leave the @property
> solution in O:-)
>
>>
>> Differently awkward, I guess.
>>
>> Thoughts?
>
> Partial to the getter, unless option 1 or 2 leads to simplification of
> the check() code,
Put a pin into that.
> which I haven't really experimented with. If that's
> something you'd really rather avoid, I might ask for you to decide on
> your preferred alternative - I don't have strong feelings between 'em.
All the solutions so far give me slight "there has to be a better way"
vibes. Staring at QAPISchemaVariants, I realized: more than half of the
actual code are under "if union / else" conditionals. So I tried the
more object-oriented solution: classes instead of conditionals. Diff
appended.
Shedding the conditionals does lead to slightly simple .check(), so
maybe you like it.
Once this is done, we can narrow .tag_member's type from
Optional[QAPISchemaObjectTypeMember] to QAPISchemaObjectTypeMember the
exact same way as QAPISchemaObjectTypeMember.type: replace
self.tag_member = None by self.tag_member: QAPISchemaObjectTypeMember.
Admittedly a bit more churn that your solution, but the result has
slightly less code, and feels slitghly cleaner to me. Thoughts?
> (Not helpful, oops. Thanks for your feedback and review, though.
> You've successfully talked me down to a much smaller series over time
> :p)
diff --git a/scripts/qapi/introspect.py b/scripts/qapi/introspect.py
index c38df61a6d..e5cea8004e 100644
--- a/scripts/qapi/introspect.py
+++ b/scripts/qapi/introspect.py
@@ -26,6 +26,7 @@
from .gen import QAPISchemaMonolithicCVisitor
from .schema import (
QAPISchema,
+ QAPISchemaAlternatives,
QAPISchemaArrayType,
QAPISchemaBuiltinType,
QAPISchemaEntity,
@@ -343,12 +344,12 @@ def visit_object_type_flat(self, name: str, info: Optional[QAPISourceInfo],
def visit_alternate_type(self, name: str, info: Optional[QAPISourceInfo],
ifcond: QAPISchemaIfCond,
features: List[QAPISchemaFeature],
- variants: QAPISchemaVariants) -> None:
+ alternatives: QAPISchemaAlternatives) -> None:
self._gen_tree(
name, 'alternate',
{'members': [Annotated({'type': self._use_type(m.type)},
m.ifcond)
- for m in variants.variants]},
+ for m in alternatives.variants]},
ifcond, features
)
diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 0d9a70ab4c..949ee6bfd4 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -563,8 +563,7 @@ class QAPISchemaAlternateType(QAPISchemaType):
def __init__(self, name, info, doc, ifcond, features, variants):
super().__init__(name, info, doc, ifcond, features)
- assert isinstance(variants, QAPISchemaVariants)
- assert variants.tag_member
+ assert isinstance(variants, QAPISchemaAlternatives)
variants.set_defined_in(name)
variants.tag_member.set_defined_in(self.name)
self.variants = variants
@@ -625,19 +624,12 @@ def visit(self, visitor):
self.name, self.info, self.ifcond, self.features, self.variants)
-class QAPISchemaVariants:
- def __init__(self, tag_name, info, tag_member, variants):
- # Unions pass tag_name but not tag_member.
- # Alternates pass tag_member but not tag_name.
- # After check(), tag_member is always set.
- assert bool(tag_member) != bool(tag_name)
- assert (isinstance(tag_name, str) or
- isinstance(tag_member, QAPISchemaObjectTypeMember))
+class QAPISchemaVariantsBase:
+ def __init__(self, info, variants):
for v in variants:
assert isinstance(v, QAPISchemaVariant)
- self._tag_name = tag_name
self.info = info
- self.tag_member = tag_member
+ self.tag_member = None
self.variants = variants
def set_defined_in(self, name):
@@ -645,48 +637,6 @@ def set_defined_in(self, name):
v.set_defined_in(name)
def check(self, schema, seen):
- if self._tag_name: # union
- self.tag_member = seen.get(c_name(self._tag_name))
- base = "'base'"
- # Pointing to the base type when not implicit would be
- # nice, but we don't know it here
- if not self.tag_member or self._tag_name != self.tag_member.name:
- raise QAPISemError(
- self.info,
- "discriminator '%s' is not a member of %s"
- % (self._tag_name, base))
- # Here we do:
- base_type = schema.resolve_type(self.tag_member.defined_in)
- if not base_type.is_implicit():
- base = "base type '%s'" % self.tag_member.defined_in
- if not isinstance(self.tag_member.type, QAPISchemaEnumType):
- raise QAPISemError(
- self.info,
- "discriminator member '%s' of %s must be of enum type"
- % (self._tag_name, base))
- if self.tag_member.optional:
- raise QAPISemError(
- self.info,
- "discriminator member '%s' of %s must not be optional"
- % (self._tag_name, base))
- if self.tag_member.ifcond.is_present():
- raise QAPISemError(
- self.info,
- "discriminator member '%s' of %s must not be conditional"
- % (self._tag_name, base))
- else: # alternate
- assert isinstance(self.tag_member.type, QAPISchemaEnumType)
- assert not self.tag_member.optional
- assert not self.tag_member.ifcond.is_present()
- if self._tag_name: # union
- # branches that are not explicitly covered get an empty type
- cases = {v.name for v in self.variants}
- for m in self.tag_member.type.members:
- if m.name not in cases:
- v = QAPISchemaVariant(m.name, self.info,
- 'q_empty', m.ifcond)
- v.set_defined_in(self.tag_member.defined_in)
- self.variants.append(v)
if not self.variants:
raise QAPISemError(self.info, "union has no branches")
for v in self.variants:
@@ -713,6 +663,65 @@ def check_clash(self, info, seen):
v.type.check_clash(info, dict(seen))
+class QAPISchemaVariants(QAPISchemaVariantsBase):
+ def __init__(self, info, variants, tag_name):
+ assert isinstance(tag_name, str)
+ super().__init__(info, variants)
+ self._tag_name = tag_name
+
+ def check(self, schema, seen):
+ self.tag_member = seen.get(c_name(self._tag_name))
+ base = "'base'"
+ # Pointing to the base type when not implicit would be
+ # nice, but we don't know it here
+ if not self.tag_member or self._tag_name != self.tag_member.name:
+ raise QAPISemError(
+ self.info,
+ "discriminator '%s' is not a member of %s"
+ % (self._tag_name, base))
+ # Here we do:
+ base_type = schema.resolve_type(self.tag_member.defined_in)
+ if not base_type.is_implicit():
+ base = "base type '%s'" % self.tag_member.defined_in
+ if not isinstance(self.tag_member.type, QAPISchemaEnumType):
+ raise QAPISemError(
+ self.info,
+ "discriminator member '%s' of %s must be of enum type"
+ % (self._tag_name, base))
+ if self.tag_member.optional:
+ raise QAPISemError(
+ self.info,
+ "discriminator member '%s' of %s must not be optional"
+ % (self._tag_name, base))
+ if self.tag_member.ifcond.is_present():
+ raise QAPISemError(
+ self.info,
+ "discriminator member '%s' of %s must not be conditional"
+ % (self._tag_name, base))
+ # branches that are not explicitly covered get an empty type
+ cases = {v.name for v in self.variants}
+ for m in self.tag_member.type.members:
+ if m.name not in cases:
+ v = QAPISchemaVariant(m.name, self.info,
+ 'q_empty', m.ifcond)
+ v.set_defined_in(self.tag_member.defined_in)
+ self.variants.append(v)
+ super().check(schema, seen)
+
+
+class QAPISchemaAlternatives(QAPISchemaVariantsBase):
+ def __init__(self, info, variants, tag_member):
+ assert isinstance(tag_member, QAPISchemaObjectTypeMember)
+ super().__init__(info, variants)
+ self.tag_member = tag_member
+
+ def check(self, schema, seen):
+ assert isinstance(self.tag_member.type, QAPISchemaEnumType)
+ assert not self.tag_member.optional
+ assert not self.tag_member.ifcond.is_present()
+ super().check(schema, seen)
+
+
class QAPISchemaMember:
""" Represents object members, enum members and features """
role = 'member'
@@ -1184,7 +1193,7 @@ def _def_union_type(self, expr: QAPIExpression):
QAPISchemaObjectType(name, info, expr.doc, ifcond, features,
base, members,
QAPISchemaVariants(
- tag_name, info, None, variants)))
+ info, variants, tag_name)))
def _def_alternate_type(self, expr: QAPIExpression):
name = expr['alternate']
@@ -1202,7 +1211,7 @@ def _def_alternate_type(self, expr: QAPIExpression):
self._def_definition(
QAPISchemaAlternateType(
name, info, expr.doc, ifcond, features,
- QAPISchemaVariants(None, info, tag_member, variants)))
+ QAPISchemaAlternatives(info, variants, tag_member)))
def _def_command(self, expr: QAPIExpression):
name = expr['command']
diff --git a/scripts/qapi/types.py b/scripts/qapi/types.py
index c39d054d2c..05da30b855 100644
--- a/scripts/qapi/types.py
+++ b/scripts/qapi/types.py
@@ -23,6 +23,7 @@
)
from .schema import (
QAPISchema,
+ QAPISchemaAlternatives,
QAPISchemaEnumMember,
QAPISchemaFeature,
QAPISchemaIfCond,
@@ -369,11 +370,11 @@ def visit_alternate_type(self,
info: Optional[QAPISourceInfo],
ifcond: QAPISchemaIfCond,
features: List[QAPISchemaFeature],
- variants: QAPISchemaVariants) -> None:
+ alternatives: QAPISchemaAlternatives) -> None:
with ifcontext(ifcond, self._genh):
self._genh.preamble_add(gen_fwd_object_or_array(name))
self._genh.add(gen_object(name, ifcond, None,
- [variants.tag_member], variants))
+ [alternatives.tag_member], alternatives))
with ifcontext(ifcond, self._genh, self._genc):
self._gen_type_cleanup(name)
diff --git a/scripts/qapi/visit.py b/scripts/qapi/visit.py
index c56ea4d724..725bfcef50 100644
--- a/scripts/qapi/visit.py
+++ b/scripts/qapi/visit.py
@@ -28,6 +28,7 @@
)
from .schema import (
QAPISchema,
+ QAPISchemaAlternatives,
QAPISchemaEnumMember,
QAPISchemaEnumType,
QAPISchemaFeature,
@@ -222,7 +223,8 @@ def gen_visit_enum(name: str) -> str:
c_name=c_name(name))
-def gen_visit_alternate(name: str, variants: QAPISchemaVariants) -> str:
+def gen_visit_alternate(name: str,
+ alternatives: QAPISchemaAlternatives) -> str:
ret = mcgen('''
bool visit_type_%(c_name)s(Visitor *v, const char *name,
@@ -244,7 +246,7 @@ def gen_visit_alternate(name: str, variants: QAPISchemaVariants) -> str:
''',
c_name=c_name(name))
- for var in variants.variants:
+ for var in alternatives.variants:
ret += var.ifcond.gen_if()
ret += mcgen('''
case %(case)s:
@@ -414,10 +416,10 @@ def visit_alternate_type(self,
info: Optional[QAPISourceInfo],
ifcond: QAPISchemaIfCond,
features: List[QAPISchemaFeature],
- variants: QAPISchemaVariants) -> None:
+ alternatives: QAPISchemaAlternatives) -> None:
with ifcontext(ifcond, self._genh, self._genc):
self._genh.add(gen_visit_decl(name))
- self._genc.add(gen_visit_alternate(name, variants))
+ self._genc.add(gen_visit_alternate(name, alternatives))
def gen_visit(schema: QAPISchema,
next prev parent reply other threads:[~2024-01-17 8:19 UTC|newest]
Thread overview: 76+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-11-16 1:43 [PATCH 00/19] qapi: statically type schema.py John Snow
2023-11-16 1:43 ` [PATCH 01/19] qapi/schema: fix QAPISchemaEntity.__repr__() John Snow
2023-11-16 7:01 ` Philippe Mathieu-Daudé
2023-11-16 1:43 ` [PATCH 02/19] qapi/schema: add pylint suppressions John Snow
2023-11-21 12:23 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 03/19] qapi/schema: name QAPISchemaInclude entities John Snow
2023-11-21 13:33 ` Markus Armbruster
2023-11-21 16:22 ` John Snow
2023-11-22 9:37 ` Markus Armbruster
2023-12-13 0:45 ` John Snow
2023-11-16 1:43 ` [PATCH 04/19] qapi/schema: declare type for QAPISchemaObjectTypeMember.type John Snow
2023-11-16 1:43 ` [PATCH 05/19] qapi/schema: make c_type() and json_type() abstract methods John Snow
2023-11-16 7:03 ` Philippe Mathieu-Daudé
2023-11-21 13:36 ` Markus Armbruster
2023-11-21 13:43 ` Daniel P. Berrangé
2023-11-21 16:28 ` John Snow
2023-11-21 16:34 ` Daniel P. Berrangé
2023-11-22 9:50 ` Markus Armbruster
2023-11-22 9:54 ` Daniel P. Berrangé
2023-11-16 1:43 ` [PATCH 06/19] qapi/schema: adjust type narrowing for mypy's benefit John Snow
2023-11-16 7:04 ` Philippe Mathieu-Daudé
2023-11-21 14:09 ` Markus Armbruster
2023-11-21 16:36 ` John Snow
2023-11-22 12:00 ` Markus Armbruster
2023-11-22 18:12 ` John Snow
2023-11-23 11:00 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 07/19] qapi/introspect: assert schema.lookup_type did not fail John Snow
2023-11-21 14:17 ` Markus Armbruster
2023-11-21 16:41 ` John Snow
2023-11-22 9:52 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 08/19] qapi/schema: add static typing and assertions to lookup_type() John Snow
2023-11-21 14:21 ` Markus Armbruster
2023-11-21 16:46 ` John Snow
2023-11-22 12:09 ` Markus Armbruster
2023-11-22 15:55 ` John Snow
2023-11-23 11:04 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 09/19] qapi/schema: assert info is present when necessary John Snow
2023-11-16 7:05 ` Philippe Mathieu-Daudé
2023-11-16 1:43 ` [PATCH 10/19] qapi/schema: make QAPISchemaArrayType.element_type non-Optional John Snow
2023-11-21 14:27 ` Markus Armbruster
2023-11-21 16:51 ` John Snow
2023-11-16 1:43 ` [PATCH 11/19] qapi/schema: fix QAPISchemaArrayType.check's call to resolve_type John Snow
2023-11-22 12:59 ` Markus Armbruster
2023-11-22 15:58 ` John Snow
2023-11-23 13:03 ` Markus Armbruster
2024-01-10 19:33 ` John Snow
2024-01-11 9:33 ` Markus Armbruster
2024-01-11 22:24 ` John Snow
2023-11-16 1:43 ` [PATCH 12/19] qapi/schema: split "checked" field into "checking" and "checked" John Snow
2023-11-22 14:02 ` Markus Armbruster
2024-01-10 20:21 ` John Snow
2024-01-11 9:24 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 13/19] qapi/schema: fix typing for QAPISchemaVariants.tag_member John Snow
2023-11-22 14:05 ` Markus Armbruster
2023-11-22 16:02 ` John Snow
2024-01-10 1:47 ` John Snow
2024-01-10 7:52 ` Markus Armbruster
2024-01-10 8:35 ` John Snow
2024-01-17 8:19 ` Markus Armbruster [this message]
2024-01-17 10:32 ` Markus Armbruster
2024-01-17 10:53 ` Markus Armbruster
2024-02-01 20:54 ` John Snow
2023-11-16 1:43 ` [PATCH 14/19] qapi/schema: assert QAPISchemaVariants are QAPISchemaObjectType John Snow
2023-11-23 13:51 ` Markus Armbruster
2024-01-10 0:42 ` John Snow
2023-11-16 1:43 ` [PATCH 15/19] qapi/parser: demote QAPIExpression to Dict[str, Any] John Snow
2023-11-23 14:12 ` Markus Armbruster
2024-01-10 0:14 ` John Snow
2024-01-10 7:58 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 16/19] qapi/schema: add type hints John Snow
2023-11-24 15:02 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 17/19] qapi/schema: turn on mypy strictness John Snow
2023-11-16 1:43 ` [PATCH 18/19] qapi/schema: remove unnecessary asserts John Snow
2023-11-28 9:22 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 19/19] qapi/schema: refactor entity lookup helpers John Snow
2023-11-28 12:06 ` Markus Armbruster
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=87le8onzif.fsf@pond.sub.org \
--to=armbru@redhat.com \
--cc=jsnow@redhat.com \
--cc=michael.roth@amd.com \
--cc=peter.maydell@linaro.org \
--cc=qemu-devel@nongnu.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).