diff mbox series

[v3,2/3] qapi: Do not generate empty enum

Message ID 20230315112811.22355-3-philmd@linaro.org
State New
Headers show
Series qapi: Simplify enum generation | expand

Commit Message

Philippe Mathieu-Daudé March 15, 2023, 11:28 a.m. UTC
Per the C++ standard, empty enum are ill-formed. Do not generate
them in order to avoid:

  In file included from qga/qga-qapi-emit-events.c:14:
  qga/qga-qapi-emit-events.h:20:1: error: empty enum is invalid
     20 | } qga_QAPIEvent;
        | ^

Reported-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Philippe Mathieu-Daudé <philmd@linaro.org>
---
 docs/devel/qapi-code-gen.rst | 6 +++---
 scripts/qapi/events.py       | 2 ++
 scripts/qapi/schema.py       | 5 ++++-
 scripts/qapi/types.py        | 2 ++
 4 files changed, 11 insertions(+), 4 deletions(-)

Comments

Richard Henderson March 15, 2023, 3:02 p.m. UTC | #1
On 3/15/23 04:28, Philippe Mathieu-Daudé wrote:
> Per the C++ standard, empty enum are ill-formed. Do not generate
> them in order to avoid:
> 
>    In file included from qga/qga-qapi-emit-events.c:14:
>    qga/qga-qapi-emit-events.h:20:1: error: empty enum is invalid
>       20 | } qga_QAPIEvent;
>          | ^
> 
> Reported-by: Markus Armbruster<armbru@redhat.com>
> Signed-off-by: Philippe Mathieu-Daudé<philmd@linaro.org>
> ---
>   docs/devel/qapi-code-gen.rst | 6 +++---
>   scripts/qapi/events.py       | 2 ++
>   scripts/qapi/schema.py       | 5 ++++-
>   scripts/qapi/types.py        | 2 ++
>   4 files changed, 11 insertions(+), 4 deletions(-)

Reviewed-by: Richard Henderson <richard.henderson@linaro.org>

r~
Markus Armbruster March 16, 2023, 12:31 p.m. UTC | #2
Philippe Mathieu-Daudé <philmd@linaro.org> writes:

> Per the C++ standard, empty enum are ill-formed. Do not generate
> them in order to avoid:
>
>   In file included from qga/qga-qapi-emit-events.c:14:
>   qga/qga-qapi-emit-events.h:20:1: error: empty enum is invalid
>      20 | } qga_QAPIEvent;
>         | ^
>
> Reported-by: Markus Armbruster <armbru@redhat.com>
> Signed-off-by: Philippe Mathieu-Daudé <philmd@linaro.org>

Two failures in "make check-qapi-schema" (which is run by "make check"):

1. Positive test case qapi-schema-test

    --- /work/armbru/qemu/bld-x86/../tests/qapi-schema/qapi-schema-test.out
    +++ 
    @@ -19,7 +19,6 @@
         member enum2: EnumOne optional=True
         member enum3: EnumOne optional=False
         member enum4: EnumOne optional=True
    -enum MyEnum
     object Empty1
     object Empty2
         base Empty1

   You forgot to update expected test output.  No big deal.

2. Negative test case union-empty

    --- /work/armbru/qemu/bld-x86/../tests/qapi-schema/union-empty.err
    +++ 
    @@ -1,2 +1,2 @@
    -union-empty.json: In union 'Union':
    -union-empty.json:4: union has no branches
    +union-empty.json: In struct 'Base':
    +union-empty.json:3: member 'type' uses unknown type 'Empty'
    stderr:
    qapi-schema-test FAIL
    union-empty FAIL

   The error message regresses.

   I can see two ways to fix this:

   (A) You can't just drop empty enumeration types on the floor.  To not
       generate code for them, you need to skip them wherever we
       generate code for enumeration types.

   (B) Outlaw empty enumeration types.

I recommend to give (B) a try, it's likely simpler.
Daniel P. Berrangé March 16, 2023, 1:54 p.m. UTC | #3
On Thu, Mar 16, 2023 at 01:31:04PM +0100, Markus Armbruster wrote:
> Philippe Mathieu-Daudé <philmd@linaro.org> writes:
> 
> > Per the C++ standard, empty enum are ill-formed. Do not generate
> > them in order to avoid:
> >
> >   In file included from qga/qga-qapi-emit-events.c:14:
> >   qga/qga-qapi-emit-events.h:20:1: error: empty enum is invalid
> >      20 | } qga_QAPIEvent;
> >         | ^
> >
> > Reported-by: Markus Armbruster <armbru@redhat.com>
> > Signed-off-by: Philippe Mathieu-Daudé <philmd@linaro.org>
> 
> Two failures in "make check-qapi-schema" (which is run by "make check"):
> 
> 1. Positive test case qapi-schema-test
> 
>     --- /work/armbru/qemu/bld-x86/../tests/qapi-schema/qapi-schema-test.out
>     +++ 
>     @@ -19,7 +19,6 @@
>          member enum2: EnumOne optional=True
>          member enum3: EnumOne optional=False
>          member enum4: EnumOne optional=True
>     -enum MyEnum
>      object Empty1
>      object Empty2
>          base Empty1
> 
>    You forgot to update expected test output.  No big deal.
> 
> 2. Negative test case union-empty
> 
>     --- /work/armbru/qemu/bld-x86/../tests/qapi-schema/union-empty.err
>     +++ 
>     @@ -1,2 +1,2 @@
>     -union-empty.json: In union 'Union':
>     -union-empty.json:4: union has no branches
>     +union-empty.json: In struct 'Base':
>     +union-empty.json:3: member 'type' uses unknown type 'Empty'
>     stderr:
>     qapi-schema-test FAIL
>     union-empty FAIL
> 
>    The error message regresses.
> 
>    I can see two ways to fix this:
> 
>    (A) You can't just drop empty enumeration types on the floor.  To not
>        generate code for them, you need to skip them wherever we
>        generate code for enumeration types.
> 
>    (B) Outlaw empty enumeration types.
> 
> I recommend to give (B) a try, it's likely simpler.

Possible trap-door with (B), if we have any enums where *every*
member is conditionalized on a CONFIG_XXX rule, there might be
certain build scenarios where an enum suddenly becomes empty.

With regards,
Daniel
Juan Quintela March 16, 2023, 2:39 p.m. UTC | #4
Daniel P. Berrangé <berrange@redhat.com> wrote:
> On Thu, Mar 16, 2023 at 01:31:04PM +0100, Markus Armbruster wrote:
>> Philippe Mathieu-Daudé <philmd@linaro.org> writes:
>> 
>> > Per the C++ standard, empty enum are ill-formed. Do not generate
>> > them in order to avoid:
>> >
>> >   In file included from qga/qga-qapi-emit-events.c:14:
>> >   qga/qga-qapi-emit-events.h:20:1: error: empty enum is invalid
>> >      20 | } qga_QAPIEvent;
>> >         | ^
>> >
>> > Reported-by: Markus Armbruster <armbru@redhat.com>
>> > Signed-off-by: Philippe Mathieu-Daudé <philmd@linaro.org>
>> 
>> Two failures in "make check-qapi-schema" (which is run by "make check"):
>> 
>> 1. Positive test case qapi-schema-test
>> 
>>     --- /work/armbru/qemu/bld-x86/../tests/qapi-schema/qapi-schema-test.out
>>     +++ 
>>     @@ -19,7 +19,6 @@
>>          member enum2: EnumOne optional=True
>>          member enum3: EnumOne optional=False
>>          member enum4: EnumOne optional=True
>>     -enum MyEnum
>>      object Empty1
>>      object Empty2
>>          base Empty1
>> 
>>    You forgot to update expected test output.  No big deal.
>> 
>> 2. Negative test case union-empty
>> 
>>     --- /work/armbru/qemu/bld-x86/../tests/qapi-schema/union-empty.err
>>     +++ 
>>     @@ -1,2 +1,2 @@
>>     -union-empty.json: In union 'Union':
>>     -union-empty.json:4: union has no branches
>>     +union-empty.json: In struct 'Base':
>>     +union-empty.json:3: member 'type' uses unknown type 'Empty'
>>     stderr:
>>     qapi-schema-test FAIL
>>     union-empty FAIL
>> 
>>    The error message regresses.
>> 
>>    I can see two ways to fix this:
>> 
>>    (A) You can't just drop empty enumeration types on the floor.  To not
>>        generate code for them, you need to skip them wherever we
>>        generate code for enumeration types.
>> 
>>    (B) Outlaw empty enumeration types.
>> 
>> I recommend to give (B) a try, it's likely simpler.
>
> Possible trap-door with (B), if we have any enums where *every*
> member is conditionalized on a CONFIG_XXX rule, there might be
> certain build scenarios where an enum suddenly becomes empty.

Do we have an example for this?
Because it looks really weird.  I would expect that the "container" unit
of that enumeration is #ifdef out of compilation somehow.

Later, Juan.
Daniel P. Berrangé March 16, 2023, 2:42 p.m. UTC | #5
On Thu, Mar 16, 2023 at 03:39:59PM +0100, Juan Quintela wrote:
> Daniel P. Berrangé <berrange@redhat.com> wrote:
> > On Thu, Mar 16, 2023 at 01:31:04PM +0100, Markus Armbruster wrote:
> >> Philippe Mathieu-Daudé <philmd@linaro.org> writes:
> >> 
> >> > Per the C++ standard, empty enum are ill-formed. Do not generate
> >> > them in order to avoid:
> >> >
> >> >   In file included from qga/qga-qapi-emit-events.c:14:
> >> >   qga/qga-qapi-emit-events.h:20:1: error: empty enum is invalid
> >> >      20 | } qga_QAPIEvent;
> >> >         | ^
> >> >
> >> > Reported-by: Markus Armbruster <armbru@redhat.com>
> >> > Signed-off-by: Philippe Mathieu-Daudé <philmd@linaro.org>
> >> 
> >> Two failures in "make check-qapi-schema" (which is run by "make check"):
> >> 
> >> 1. Positive test case qapi-schema-test
> >> 
> >>     --- /work/armbru/qemu/bld-x86/../tests/qapi-schema/qapi-schema-test.out
> >>     +++ 
> >>     @@ -19,7 +19,6 @@
> >>          member enum2: EnumOne optional=True
> >>          member enum3: EnumOne optional=False
> >>          member enum4: EnumOne optional=True
> >>     -enum MyEnum
> >>      object Empty1
> >>      object Empty2
> >>          base Empty1
> >> 
> >>    You forgot to update expected test output.  No big deal.
> >> 
> >> 2. Negative test case union-empty
> >> 
> >>     --- /work/armbru/qemu/bld-x86/../tests/qapi-schema/union-empty.err
> >>     +++ 
> >>     @@ -1,2 +1,2 @@
> >>     -union-empty.json: In union 'Union':
> >>     -union-empty.json:4: union has no branches
> >>     +union-empty.json: In struct 'Base':
> >>     +union-empty.json:3: member 'type' uses unknown type 'Empty'
> >>     stderr:
> >>     qapi-schema-test FAIL
> >>     union-empty FAIL
> >> 
> >>    The error message regresses.
> >> 
> >>    I can see two ways to fix this:
> >> 
> >>    (A) You can't just drop empty enumeration types on the floor.  To not
> >>        generate code for them, you need to skip them wherever we
> >>        generate code for enumeration types.
> >> 
> >>    (B) Outlaw empty enumeration types.
> >> 
> >> I recommend to give (B) a try, it's likely simpler.
> >
> > Possible trap-door with (B), if we have any enums where *every*
> > member is conditionalized on a CONFIG_XXX rule, there might be
> > certain build scenarios where an enum suddenly becomes empty.
> 
> Do we have an example for this?
> Because it looks really weird.  I would expect that the "container" unit
> of that enumeration is #ifdef out of compilation somehow.

I'm not sure if such an example physically exists. I know the  audio
code gets close, with all but 2 options conditional:

{ 'enum': 'AudiodevDriver',
  'data': [ 'none',
            { 'name': 'alsa', 'if': 'CONFIG_AUDIO_ALSA' },
            { 'name': 'coreaudio', 'if': 'CONFIG_AUDIO_COREAUDIO' },
            { 'name': 'dbus', 'if': 'CONFIG_DBUS_DISPLAY' },
            { 'name': 'dsound', 'if': 'CONFIG_AUDIO_DSOUND' },
            { 'name': 'jack', 'if': 'CONFIG_AUDIO_JACK' },
            { 'name': 'oss', 'if': 'CONFIG_AUDIO_OSS' },
            { 'name': 'pa', 'if': 'CONFIG_AUDIO_PA' },
            { 'name': 'sdl', 'if': 'CONFIG_AUDIO_SDL' },
            { 'name': 'sndio', 'if': 'CONFIG_AUDIO_SNDIO' },
            { 'name': 'spice', 'if': 'CONFIG_SPICE' },
            'wav' ] }

Just wanted to warn that we shouldn't assume empty enums can't
exist, because it would be quite easy to add 2 extra conditionals
to this audio example, and the enum wouldn't appear empty at a
glance, but none the less could be empty in some compile scenarios

With regards,
Daniel
Markus Armbruster March 16, 2023, 2:57 p.m. UTC | #6
Daniel P. Berrangé <berrange@redhat.com> writes:

> On Thu, Mar 16, 2023 at 01:31:04PM +0100, Markus Armbruster wrote:
>> Philippe Mathieu-Daudé <philmd@linaro.org> writes:
>> 
>> > Per the C++ standard, empty enum are ill-formed. Do not generate

The C standard.  The C++ standard doesn't apply here :)

>> > them in order to avoid:
>> >
>> >   In file included from qga/qga-qapi-emit-events.c:14:
>> >   qga/qga-qapi-emit-events.h:20:1: error: empty enum is invalid
>> >      20 | } qga_QAPIEvent;
>> >         | ^
>> >
>> > Reported-by: Markus Armbruster <armbru@redhat.com>
>> > Signed-off-by: Philippe Mathieu-Daudé <philmd@linaro.org>
>> 
>> Two failures in "make check-qapi-schema" (which is run by "make check"):
>> 
>> 1. Positive test case qapi-schema-test
>> 
>>     --- /work/armbru/qemu/bld-x86/../tests/qapi-schema/qapi-schema-test.out
>>     +++ 
>>     @@ -19,7 +19,6 @@
>>          member enum2: EnumOne optional=True
>>          member enum3: EnumOne optional=False
>>          member enum4: EnumOne optional=True
>>     -enum MyEnum
>>      object Empty1
>>      object Empty2
>>          base Empty1
>> 
>>    You forgot to update expected test output.  No big deal.
>> 
>> 2. Negative test case union-empty
>> 
>>     --- /work/armbru/qemu/bld-x86/../tests/qapi-schema/union-empty.err
>>     +++ 
>>     @@ -1,2 +1,2 @@
>>     -union-empty.json: In union 'Union':
>>     -union-empty.json:4: union has no branches
>>     +union-empty.json: In struct 'Base':
>>     +union-empty.json:3: member 'type' uses unknown type 'Empty'
>>     stderr:
>>     qapi-schema-test FAIL
>>     union-empty FAIL
>> 
>>    The error message regresses.
>> 
>>    I can see two ways to fix this:
>> 
>>    (A) You can't just drop empty enumeration types on the floor.  To not
>>        generate code for them, you need to skip them wherever we
>>        generate code for enumeration types.
>> 
>>    (B) Outlaw empty enumeration types.
>> 
>> I recommend to give (B) a try, it's likely simpler.
>
> Possible trap-door with (B), if we have any enums where *every*
> member is conditionalized on a CONFIG_XXX rule, there might be
> certain build scenarios where an enum suddenly becomes empty.

True.  Scratch the idea.

Trap-door also applies to (A): we can still end up with empty enums.

(C) Always emit a dummy member.  This is actually what we do now:

    typedef enum OnOffAuto {
        ON_OFF_AUTO_AUTO = 1,
        ON_OFF_AUTO_ON = 2,
        ON_OFF_AUTO_OFF = 3,
        ON_OFF_AUTO__MAX,               <--- the dummy
    } OnOffAuto;

But the next patch changes it to

    typedef enum OnOffAuto {
        ON_OFF_AUTO_AUTO,
        ON_OFF_AUTO_ON,
        ON_OFF_AUTO_OFF,
    #define ON_OFF_AUTO__MAX 3
    } OnOffAuto;

Two problems, actually.

One, we lose the dummy.  We could add one back like

    typedef enum OnOffAuto {
        ON_OFF_AUTO__DUMMY = 0,
        ON_OFF_AUTO_AUTO = 0,
        ON_OFF_AUTO_ON,
        ON_OFF_AUTO_OFF,
    #define ON_OFF_AUTO__MAX 3
    } OnOffAuto;

But all of this falls apart with conditional members!

Example 1 (taken from qapi/block-core.json):

    { 'enum': 'BlockdevAioOptions',
      'data': [ 'threads', 'native',
                { 'name': 'io_uring', 'if': 'CONFIG_LINUX_IO_URING' } ] }

Generates now:

    typedef enum BlockdevAioOptions {
        BLOCKDEV_AIO_OPTIONS_THREADS,
        BLOCKDEV_AIO_OPTIONS_NATIVE,
    #if defined(CONFIG_LINUX_IO_URING)
        BLOCKDEV_AIO_OPTIONS_IO_URING,
    #endif /* defined(CONFIG_LINUX_IO_URING) */
        BLOCKDEV_AIO_OPTIONS__MAX,
    } BlockdevAioOptions;

BLOCKDEV_AIO_OPTIONS__MAX is 3 if defined(CONFIG_LINUX_IO_URING), else
2.

After the next patch:

    typedef enum BlockdevAioOptions {
        BLOCKDEV_AIO_OPTIONS_THREADS,
        BLOCKDEV_AIO_OPTIONS_NATIVE,
    #if defined(CONFIG_LINUX_IO_URING)
        BLOCKDEV_AIO_OPTIONS_IO_URING,
    #endif /* defined(CONFIG_LINUX_IO_URING) */
    #define BLOCKDEV_AIO_OPTIONS__MAX 3
    } BlockdevAioOptions;

Now it's always 3.

Example 2 (same with members reordered):

    { 'enum': 'BlockdevAioOptions',
      'data': [ { 'name': 'io_uring', 'if': 'CONFIG_LINUX_IO_URING' },
                'threads', 'native' ] }

Same problem for __MAX, additional problem for __DUMMY:

    typedef enum BlockdevAioOptions {
        BLOCKDEV_AIO_OPTIONS__DUMMY = 0,
    #if defined(CONFIG_LINUX_IO_URING)
        BLOCKDEV_AIO_OPTIONS_IO_URING = 0,
    #endif /* defined(CONFIG_LINUX_IO_URING) */
        BLOCKDEV_AIO_OPTIONS_THREADS,
        BLOCKDEV_AIO_OPTIONS_NATIVE,
    #define BLOCKDEV_AIO_OPTIONS__MAX 3
    } BlockdevAioOptions;

If CONFIG_LINUX_IO_URING is off, the enum starts at 1 instead of 0.

Arrays indexed by the enum start with a hole.  Code using them is
probably not prepared for holes.

*Sigh*
Philippe Mathieu-Daudé March 21, 2023, 2:31 p.m. UTC | #7
On 16/3/23 15:57, Markus Armbruster wrote:
> Daniel P. Berrangé <berrange@redhat.com> writes:
> 
>> On Thu, Mar 16, 2023 at 01:31:04PM +0100, Markus Armbruster wrote:
>>> Philippe Mathieu-Daudé <philmd@linaro.org> writes:
>>>
>>>> Per the C++ standard, empty enum are ill-formed. Do not generate
> 
> The C standard.  The C++ standard doesn't apply here :)

I can't find how empty enums are considered by the C standard...

> But all of this falls apart with conditional members!
> 
> Example 1 (taken from qapi/block-core.json):
> 
>      { 'enum': 'BlockdevAioOptions',
>        'data': [ 'threads', 'native',
>                  { 'name': 'io_uring', 'if': 'CONFIG_LINUX_IO_URING' } ] }
> 
> Generates now:
> 
>      typedef enum BlockdevAioOptions {
>          BLOCKDEV_AIO_OPTIONS_THREADS,
>          BLOCKDEV_AIO_OPTIONS_NATIVE,
>      #if defined(CONFIG_LINUX_IO_URING)
>          BLOCKDEV_AIO_OPTIONS_IO_URING,
>      #endif /* defined(CONFIG_LINUX_IO_URING) */
>          BLOCKDEV_AIO_OPTIONS__MAX,
>      } BlockdevAioOptions;
> 
> BLOCKDEV_AIO_OPTIONS__MAX is 3 if defined(CONFIG_LINUX_IO_URING), else
> 2.
> 
> After the next patch:
> 
>      typedef enum BlockdevAioOptions {
>          BLOCKDEV_AIO_OPTIONS_THREADS,
>          BLOCKDEV_AIO_OPTIONS_NATIVE,
>      #if defined(CONFIG_LINUX_IO_URING)
>          BLOCKDEV_AIO_OPTIONS_IO_URING,
>      #endif /* defined(CONFIG_LINUX_IO_URING) */
>      #define BLOCKDEV_AIO_OPTIONS__MAX 3
>      } BlockdevAioOptions;
> 
> Now it's always 3.

Good catch... Using:

-- >8 --
diff --git a/scripts/qapi/types.py b/scripts/qapi/types.py
@@ -110,8 +110,11 @@ def gen_enum(name: str,

      ret += mcgen('''
  } %(c_name)s;
+_Static_assert(%(c_last_enum)s == %(c_length)s - 1, "%(c_name)s");
  ''',
-                 c_name=c_name(name))
+                 c_name=c_name(name),
+                 c_last_enum=c_enum_const(name, members[-1].name, prefix),
+                 c_length=len(members))

      ret += mcgen('''
---

I get:

./qapi/qapi-types-block-core.h:355:1: error: static_assert failed due to 
requirement 'BLOCKDEV_AIO_OPTIONS_NATIVE == 3 - 1' "BlockdevAioOptions"
_Static_assert(BLOCKDEV_AIO_OPTIONS_IO_URING == 3 - 1, 
"BlockdevAioOptions");
^              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
./qapi/qapi-types-block-core.h:430:1: error: static_assert failed due to 
requirement 'BLOCKDEV_DRIVER_VVFAT == 47 - 1' "BlockdevDriver"
_Static_assert(BLOCKDEV_DRIVER_VVFAT == 47 - 1, "BlockdevDriver");
^              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
./qapi/qapi-types-char.h:110:1: error: static_assert failed due to 
requirement 'CHARDEV_BACKEND_KIND_MEMORY == 22 - 1' "ChardevBackendKind"
_Static_assert(CHARDEV_BACKEND_KIND_MEMORY == 22 - 1, "ChardevBackendKind");
^              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
...
Philippe Mathieu-Daudé March 21, 2023, 2:48 p.m. UTC | #8
On 16/3/23 15:57, Markus Armbruster wrote:
> Daniel P. Berrangé <berrange@redhat.com> writes:


> But all of this falls apart with conditional members!
> 
> Example 1 (taken from qapi/block-core.json):
> 
>      { 'enum': 'BlockdevAioOptions',
>        'data': [ 'threads', 'native',
>                  { 'name': 'io_uring', 'if': 'CONFIG_LINUX_IO_URING' } ] }
> 
> Generates now:
> 
>      typedef enum BlockdevAioOptions {
>          BLOCKDEV_AIO_OPTIONS_THREADS,
>          BLOCKDEV_AIO_OPTIONS_NATIVE,
>      #if defined(CONFIG_LINUX_IO_URING)
>          BLOCKDEV_AIO_OPTIONS_IO_URING,
>      #endif /* defined(CONFIG_LINUX_IO_URING) */
>          BLOCKDEV_AIO_OPTIONS__MAX,
>      } BlockdevAioOptions;
> 
> BLOCKDEV_AIO_OPTIONS__MAX is 3 if defined(CONFIG_LINUX_IO_URING), else
> 2.
> 
> After the next patch:
> 
>      typedef enum BlockdevAioOptions {
>          BLOCKDEV_AIO_OPTIONS_THREADS,
>          BLOCKDEV_AIO_OPTIONS_NATIVE,
>      #if defined(CONFIG_LINUX_IO_URING)
>          BLOCKDEV_AIO_OPTIONS_IO_URING,
>      #endif /* defined(CONFIG_LINUX_IO_URING) */
>      #define BLOCKDEV_AIO_OPTIONS__MAX 3
>      } BlockdevAioOptions;
> 
> Now it's always 3.
> 
> Example 2 (same with members reordered):
> 
>      { 'enum': 'BlockdevAioOptions',
>        'data': [ { 'name': 'io_uring', 'if': 'CONFIG_LINUX_IO_URING' },
>                  'threads', 'native' ] }
> 
> Same problem for __MAX, additional problem for __DUMMY:
> 
>      typedef enum BlockdevAioOptions {
>          BLOCKDEV_AIO_OPTIONS__DUMMY = 0,
>      #if defined(CONFIG_LINUX_IO_URING)
>          BLOCKDEV_AIO_OPTIONS_IO_URING = 0,
>      #endif /* defined(CONFIG_LINUX_IO_URING) */
>          BLOCKDEV_AIO_OPTIONS_THREADS,
>          BLOCKDEV_AIO_OPTIONS_NATIVE,
>      #define BLOCKDEV_AIO_OPTIONS__MAX 3
>      } BlockdevAioOptions;
> 
> If CONFIG_LINUX_IO_URING is off, the enum starts at 1 instead of 0.
> 
> Arrays indexed by the enum start with a hole.  Code using them is
> probably not prepared for holes.

Can we meet half-way only generating the MAX definitions for
unconditional enums, keeping the conditional ones as is?

-- >8 --
diff --git a/scripts/qapi/types.py b/scripts/qapi/types.py
@@ -88,16 +88,18 @@ def gen_enum(name: str,
               members: List[QAPISchemaEnumMember],
               prefix: Optional[str] = None) -> str:
      assert members
-    # append automatically generated _MAX value
-    enum_members = members + [QAPISchemaEnumMember('_MAX', None)]
-
      ret = mcgen('''

  typedef enum %(c_name)s {
  ''',
                  c_name=c_name(name))

-    for memb in enum_members:
+    has_cond = any(memb.ifcond.is_present() for memb in members)
+    if has_cond:
+        # append automatically generated _MAX value
+        members += [QAPISchemaEnumMember('_MAX', None)]
+
+    for memb in members:
          ret += memb.ifcond.gen_if()
          ret += mcgen('''
      %(c_enum)s,
@@ -105,6 +107,13 @@ def gen_enum(name: str,
                       c_enum=c_enum_const(name, memb.name, prefix))
          ret += memb.ifcond.gen_endif()

+    if not has_cond:
+        ret += mcgen('''
+#define %(c_name)s %(c_length)s
+''',
+                     c_name=c_enum_const(name, '_MAX', prefix),
+                     c_length=len(members))
+
      ret += mcgen('''
  } %(c_name)s;
  ''',
---
Daniel P. Berrangé March 21, 2023, 3:19 p.m. UTC | #9
On Tue, Mar 21, 2023 at 03:31:56PM +0100, Philippe Mathieu-Daudé wrote:
> On 16/3/23 15:57, Markus Armbruster wrote:
> > Daniel P. Berrangé <berrange@redhat.com> writes:
> > 
> > > On Thu, Mar 16, 2023 at 01:31:04PM +0100, Markus Armbruster wrote:
> > > > Philippe Mathieu-Daudé <philmd@linaro.org> writes:
> > > > 
> > > > > Per the C++ standard, empty enum are ill-formed. Do not generate
> > 
> > The C standard.  The C++ standard doesn't apply here :)
> 
> I can't find how empty enums are considered by the C standard...

The C standard doesn't really matter either.

What we actually care about is whether GCC and CLang consider the
empty enums to be permissible or not. or to put it another way...
if it compiles, ship it :-)

With regards,
Daniel
Markus Armbruster March 21, 2023, 7 p.m. UTC | #10
Philippe Mathieu-Daudé <philmd@linaro.org> writes:

> Can we meet half-way only generating the MAX definitions for
> unconditional enums, keeping the conditional ones as is?
>
> -- >8 --
> diff --git a/scripts/qapi/types.py b/scripts/qapi/types.py
> @@ -88,16 +88,18 @@ def gen_enum(name: str,
>               members: List[QAPISchemaEnumMember],
>               prefix: Optional[str] = None) -> str:
>      assert members
> -    # append automatically generated _MAX value
> -    enum_members = members + [QAPISchemaEnumMember('_MAX', None)]
> -
>      ret = mcgen('''
>
>  typedef enum %(c_name)s {
>  ''',
>                  c_name=c_name(name))
>
> -    for memb in enum_members:
> +    has_cond = any(memb.ifcond.is_present() for memb in members)
> +    if has_cond:
> +        # append automatically generated _MAX value
> +        members += [QAPISchemaEnumMember('_MAX', None)]
> +
> +    for memb in members:
>          ret += memb.ifcond.gen_if()
>          ret += mcgen('''
>      %(c_enum)s,
> @@ -105,6 +107,13 @@ def gen_enum(name: str,
>                       c_enum=c_enum_const(name, memb.name, prefix))
>          ret += memb.ifcond.gen_endif()
>
> +    if not has_cond:
> +        ret += mcgen('''
> +#define %(c_name)s %(c_length)s
> +''',
> +                     c_name=c_enum_const(name, '_MAX', prefix),
> +                     c_length=len(members))
> +
>      ret += mcgen('''
>  } %(c_name)s;
>  ''',
> ---

I doubt the benefit "we need a silly case FOO__MAX only sometimes" is
worth the special case.

We could generate something like

    #if [last_member's condition]
    #define FOO__MAX (FOO_last_member + 1)
    #elif [second_to_last_member's condition]
    #define FOO__MAX (FOO_second_to_last_member + 1)
    ...
    #else
    #define FOO__MAX (FOO_last_unconditional_member + 1)
    #endif

but whether that is worth the additional complexity seems doubtful, too.
Eric Blake March 21, 2023, 9:43 p.m. UTC | #11
On Tue, Mar 21, 2023 at 03:19:28PM +0000, Daniel P. Berrangé wrote:
> On Tue, Mar 21, 2023 at 03:31:56PM +0100, Philippe Mathieu-Daudé wrote:
> > On 16/3/23 15:57, Markus Armbruster wrote:
> > > Daniel P. Berrangé <berrange@redhat.com> writes:
> > > 
> > > > On Thu, Mar 16, 2023 at 01:31:04PM +0100, Markus Armbruster wrote:
> > > > > Philippe Mathieu-Daudé <philmd@linaro.org> writes:
> > > > > 
> > > > > > Per the C++ standard, empty enum are ill-formed. Do not generate
> > > 
> > > The C standard.  The C++ standard doesn't apply here :)
> > 
> > I can't find how empty enums are considered by the C standard...
> 
> The C standard doesn't really matter either.
> 
> What we actually care about is whether GCC and CLang consider the
> empty enums to be permissible or not. or to put it another way...
> if it compiles, ship it :-)

But it doesn't compile:

$ cat foo.c
typedef enum Blah {
} Blah;
int main(void) {
  Blah b = 0;
}
$ gcc -o foo -Wall foo.c
foo.c:2:1: error: empty enum is invalid
    2 | } Blah;
      | ^
foo.c: In function ‘main’:
foo.c:4:5: error: unknown type name ‘Blah’; use ‘enum’ keyword to refer to the type
    4 |     Blah b = 0;
      |     ^~~~
      |     enum 
foo.c:4:10: warning: unused variable ‘b’ [-Wunused-variable]
    4 |     Blah b = 0;
      |          ^

So we _do_ need to avoid creating an enum with all members optional in
the configuration where all options are disabled, if we want that
configuration to compile.  Or require that all QAPI enums have at
least one non-optional member.
Markus Armbruster March 22, 2023, 5:45 a.m. UTC | #12
Eric Blake <eblake@redhat.com> writes:

> On Tue, Mar 21, 2023 at 03:19:28PM +0000, Daniel P. Berrangé wrote:
>> On Tue, Mar 21, 2023 at 03:31:56PM +0100, Philippe Mathieu-Daudé wrote:
>> > On 16/3/23 15:57, Markus Armbruster wrote:
>> > > Daniel P. Berrangé <berrange@redhat.com> writes:
>> > > 
>> > > > On Thu, Mar 16, 2023 at 01:31:04PM +0100, Markus Armbruster wrote:
>> > > > > Philippe Mathieu-Daudé <philmd@linaro.org> writes:
>> > > > > 
>> > > > > > Per the C++ standard, empty enum are ill-formed. Do not generate
>> > > 
>> > > The C standard.  The C++ standard doesn't apply here :)
>> > 
>> > I can't find how empty enums are considered by the C standard...

C99 §6.7.2.2 Enumeration specifiers

               enum-specifier:
                       enum identifier-opt { enumerator-list }
                       enum identifier-opt { enumerator-list , }
                       enum identifier

               enumerator-list:
                       enumerator
                       enumerator-list , enumerator

               enumerator:
                       enumeration-constant
                       enumeration-constant = constant-expr

Empty enum is a syntax error.

>> The C standard doesn't really matter either.
>> 
>> What we actually care about is whether GCC and CLang consider the
>> empty enums to be permissible or not. or to put it another way...
>> if it compiles, ship it :-)
>
> But it doesn't compile:
>
> $ cat foo.c
> typedef enum Blah {
> } Blah;
> int main(void) {
>   Blah b = 0;
> }
> $ gcc -o foo -Wall foo.c
> foo.c:2:1: error: empty enum is invalid
>     2 | } Blah;
>       | ^

Gcc opts for an error message more useful than "identifier expected".

> foo.c: In function ‘main’:
> foo.c:4:5: error: unknown type name ‘Blah’; use ‘enum’ keyword to refer to the type
>     4 |     Blah b = 0;
>       |     ^~~~
>       |     enum 
> foo.c:4:10: warning: unused variable ‘b’ [-Wunused-variable]
>     4 |     Blah b = 0;
>       |          ^
>
> So we _do_ need to avoid creating an enum with all members optional in
> the configuration where all options are disabled, if we want that
> configuration to compile.  Or require that all QAPI enums have at
> least one non-optional member.

There is just one way to avoid creating such an enum: make sure the QAPI
enum has members in all configurations we care for.

The issue at hand is whether catching empty enums in qapi-gen already is
practical.  Desirable, because qapi-gen errors are friendlier than C
compiler errors in generated code.

Note "practical": qapi-gen makes an effort to catch errors before the C
compiler catches them.  But catching all of them is impractical.

Having qapi-gen catch empty enums sure is practical for truly empty
enums.  But I doubt an enum without any members is a mistake people make
much.

It isn't for enums with only conditional members: the configuration that
makes them all vanish may or may not actually matter, or even exist, and
qapi-gen can't tell.  The C compiler can tell, but only for the
configuration being compiled.

Requiring at least one non-optional member is a restriction: enums with
only conditional members are now rejected even when the configuration
that makes them all vanish does not actually matter.

Is shielding the user from C compiler errors about empty enums worth the
restriction?
diff mbox series

Patch

diff --git a/docs/devel/qapi-code-gen.rst b/docs/devel/qapi-code-gen.rst
index 23e7f2fb1c..d684c7c24d 100644
--- a/docs/devel/qapi-code-gen.rst
+++ b/docs/devel/qapi-code-gen.rst
@@ -206,6 +206,9 @@  Syntax::
 
 Member 'enum' names the enum type.
 
+Empty enumeration (no member) does not generate anything (not even
+constant PREFIX__MAX).
+
 Each member of the 'data' array defines a value of the enumeration
 type.  The form STRING is shorthand for :code:`{ 'name': STRING }`.  The
 'name' values must be be distinct.
@@ -214,9 +217,6 @@  Example::
 
  { 'enum': 'MyEnum', 'data': [ 'value1', 'value2', 'value3' ] }
 
-Nothing prevents an empty enumeration, although it is probably not
-useful.
-
 On the wire, an enumeration type's value is represented by its
 (string) name.  In C, it's represented by an enumeration constant.
 These are of the form PREFIX_NAME, where PREFIX is derived from the
diff --git a/scripts/qapi/events.py b/scripts/qapi/events.py
index 3cf01e96b6..48f4ca9613 100644
--- a/scripts/qapi/events.py
+++ b/scripts/qapi/events.py
@@ -207,6 +207,8 @@  def _begin_user_module(self, name: str) -> None:
 
     def visit_end(self) -> None:
         self._add_module('./emit', ' * QAPI Events emission')
+        if not self._event_enum_members:
+            return
         self._genc.preamble_add(mcgen('''
 #include "qemu/osdep.h"
 #include "%(prefix)sqapi-emit-events.h"
diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 207e4d71f3..28045dbe93 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -309,6 +309,7 @@  class QAPISchemaEnumType(QAPISchemaType):
 
     def __init__(self, name, info, doc, ifcond, features, members, prefix):
         super().__init__(name, info, doc, ifcond, features)
+        assert members
         for m in members:
             assert isinstance(m, QAPISchemaEnumMember)
             m.set_defined_in(name)
@@ -1047,8 +1048,10 @@  def _make_implicit_object_type(self, name, info, ifcond, role, members):
         return name
 
     def _def_enum_type(self, expr: QAPIExpression):
-        name = expr['enum']
         data = expr['data']
+        if not data:
+            return
+        name = expr['enum']
         prefix = expr.get('prefix')
         ifcond = QAPISchemaIfCond(expr.get('if'))
         info = expr.info
diff --git a/scripts/qapi/types.py b/scripts/qapi/types.py
index c39d054d2c..7a7be7315f 100644
--- a/scripts/qapi/types.py
+++ b/scripts/qapi/types.py
@@ -42,6 +42,7 @@ 
 def gen_enum_lookup(name: str,
                     members: List[QAPISchemaEnumMember],
                     prefix: Optional[str] = None) -> str:
+    assert members
     max_index = c_enum_const(name, '_MAX', prefix)
     feats = ''
     ret = mcgen('''
@@ -86,6 +87,7 @@  def gen_enum_lookup(name: str,
 def gen_enum(name: str,
              members: List[QAPISchemaEnumMember],
              prefix: Optional[str] = None) -> str:
+    assert members
     # append automatically generated _MAX value
     enum_members = members + [QAPISchemaEnumMember('_MAX', None)]