Subject suggests you're just polishing error messages here.  In fact,
you're fixing the generator to detect errors.  Suggest something like

    qapi: Tighten checking of unions

Eric Blake <ebl...@redhat.com> writes:

> Previous commits demonstrated that the generator had several
> flaws with less-than-perfect unions:
> - make the use of a base without discriminator a hard error,
> since the previous patch removed all remaining uses of it
> - a simple union that listed the same branch twice (or two variant
> names that map to the same C enumerator, including the implicit
> MAX sentinel) ended up generating invalid C code
> - checking 'if discriminator' prior to 'if discriminator == {}'
> leads to dead code in python, and ended up processing anonymous
> unions as if they were simple unions
> - an anonymous union that listed two branches with the same qtype
> ended up generating invalid C code
> - the generator crashed on anonymous union attempts to use an
> array type
> - the generator was silently ignoring a base type for anonymous
> unions
> - the generator allowed unknown types or nested anonymous unions
> as a branch in an anonymous union
>
> Signed-off-by: Eric Blake <ebl...@redhat.com>
> ---
[...]
> diff --git a/scripts/qapi-types.py b/scripts/qapi-types.py
> index f6fb930..2390887 100644
> --- a/scripts/qapi-types.py
> +++ b/scripts/qapi-types.py
> @@ -181,17 +181,8 @@ const int %(name)s_qtypes[QTYPE_MAX] = {
>      name=name)
>
>      for key in members:
> -        qapi_type = members[key]
> -        if builtin_types.has_key(qapi_type):
> -            qtype = builtin_types[qapi_type]
> -        elif find_struct(qapi_type):
> -            qtype = "QTYPE_QDICT"
> -        elif find_union(qapi_type):
> -            qtype = "QTYPE_QDICT"
> -        elif find_enum(qapi_type):
> -            qtype = "QTYPE_QSTRING"
> -        else:
> -            assert False, "Invalid anonymous union member"
> +        qtype = find_anonymous_member_qtype(members[key])
> +        assert qtype, "Invalid anonymous union member"
>
>          ret += mcgen('''
>      [ %(qtype)s ] = %(abbrev)s_KIND_%(enum)s,
> diff --git a/scripts/qapi.py b/scripts/qapi.py
> index 3ce8c33..fc7b7f1 100644
> --- a/scripts/qapi.py
> +++ b/scripts/qapi.py
> @@ -224,6 +224,23 @@ def find_base_fields(base):
>          return None
>      return base_struct_define['data']
>
> +# Return the qtype of an anonymous union branch, or None on error.
> +def find_anonymous_member_qtype(qapi_type):
> +    if builtin_types.has_key(qapi_type):
> +        return builtin_types[qapi_type]
> +    elif find_struct(qapi_type):
> +        return "QTYPE_QDICT"
> +    elif find_enum(qapi_type):
> +        return "QTYPE_QSTRING"
> +    else:
> +        union = find_union(qapi_type)
> +        if union:
> +            discriminator = union.get('discriminator')
> +            if discriminator == {}:
> +                return None
> +            return "QTYPE_QDICT"
> +    return None
> +
>  # Return the discriminator enum define if discriminator is specified as an
>  # enum type, otherwise return None.
>  def discriminator_find_enum_define(expr):
> @@ -258,24 +275,28 @@ def check_union(expr, expr_info):
>      base = expr.get('base')
>      discriminator = expr.get('discriminator')
>      members = expr['data']
> +    values = { 'MAX': '(automatic)' }
> +    types_seen = {}
>
> -    # If the object has a member 'base', its value must name a complex type.
> -    if base:
> -        base_fields = find_base_fields(base)
> -        if not base_fields:
> -            raise QAPIExprError(expr_info,
> -                                "Base '%s' is not a valid type"
> -                                % base)
> +    # Three types of unions, determined by discriminator.
>
> -    # If the union object has no member 'discriminator', it's an
> -    # ordinary union.
> -    if not discriminator:
> +    # If the value of member 'discriminator' is {}, it's an
> +    # anonymous union, and must not have a base.
> +    if discriminator == {}:
>          enum_define = None
> +        if base:
> +            raise QAPIExprError(expr_info,
> +                                "Anonymous union '%s' must not have a base"
> +                                % name)
>
> -    # Else if the value of member 'discriminator' is {}, it's an
> -    # anonymous union.
> -    elif discriminator == {}:
> +    # Else if the union object has no member 'discriminator', it's an
> +    # ordinary union.  For now, it must not have a base.

Since you're touching this anyway, you could s/ordinary/simple/.

> +    elif not discriminator:
>          enum_define = None
> +        if base:
> +            raise QAPIExprError(expr_info,
> +                                "Simple union '%s' must not have a base"
> +                                % name)
>
>      # Else, it's a flat union.
>      else:
> @@ -284,6 +305,12 @@ def check_union(expr, expr_info):
>              raise QAPIExprError(expr_info,
>                                  "Flat union '%s' must have a base field"
>                                  % name)
> +        base_fields = find_base_fields(base)
> +        if not base_fields:
> +            raise QAPIExprError(expr_info,
> +                                "Base '%s' is not a valid type"
> +                                % base)
> +
>          # The value of member 'discriminator' must name a member of the
>          # base type.
>          discriminator_type = base_fields.get(discriminator)
> @@ -301,15 +328,42 @@ def check_union(expr, expr_info):
>
>      # Check every branch
>      for (key, value) in members.items():
> -        # If this named member's value names an enum type, then all members
> +        # If the discriminator names an enum type, then all members
>          # of 'data' must also be members of the enum type.
> -        if enum_define and not key in enum_define['enum_values']:
> -            raise QAPIExprError(expr_info,
> -                                "Discriminator value '%s' is not found in "
> -                                "enum '%s'" %
> -                                (key, enum_define["enum_name"]))
> -        # Todo: add checking for values. Key is checked as above, value can 
> be
> -        # also checked here, but we need more functions to handle array case.
> +        if enum_define:
> +            if not key in enum_define['enum_values']:
> +                raise QAPIExprError(expr_info,
> +                                    "Discriminator value '%s' is not found 
> in "
> +                                    "enum '%s'" %
> +                                    (key, enum_define["enum_name"]))
> +
> +        # Otherwise, check for conflicts in the generated enum
> +        else:
> +            c_key = _generate_enum_string(key)
> +            if c_key in values:
> +                raise QAPIExprError(expr_info,
> +                                    "Union '%s' member '%s' clashes with 
> '%s'"
> +                                    % (name, key, values[c_key]))
> +            values[c_key] = key
> +
> +        # Ensure anonymous unions have no type conflicts.
> +        if discriminator == {}:
> +            if isinstance(value, list):
> +                raise QAPIExprError(expr_info,
> +                                    "Anonymous union '%s' member '%s' must "
> +                                    "not be array type" % (name, key))
> +            qtype = find_anonymous_member_qtype(value)
> +            if not qtype:
> +                raise QAPIExprError(expr_info,
> +                                    "Anonymous union '%s' member '%s' has "
> +                                    "invalid type '%s'" % (name, key, value))
> +            if qtype in types_seen:
> +                raise QAPIExprError(expr_info,
> +                                    "Anonymous union '%s' member '%s' has "
> +                                    "same QObject type as member '%s'"
> +                                    % (name, key, types_seen[qtype]))
> +            types_seen[qtype] = key
> +
>
>  def check_enum(expr, expr_info):
>      name = expr['enum']
[...]
> diff --git a/tests/qapi-schema/alternate-conflict-string.err 
> b/tests/qapi-schema/alternate-conflict-string.err
> index e69de29..a3b7b6d 100644
> --- a/tests/qapi-schema/alternate-conflict-string.err
> +++ b/tests/qapi-schema/alternate-conflict-string.err
> @@ -0,0 +1 @@
> +tests/qapi-schema/alternate-conflict-string.json:4: Anonymous union 
> 'MyUnion' member 'two' has same QObject type as member 'one'

Explains the problem in terms of the implementation.  That's okay for a
development tool, but what about "Anonymous union 'MyUnion' member 'two'
can't be distinguished from member 'one'"?

> diff --git a/tests/qapi-schema/alternate-conflict-string.exit 
> b/tests/qapi-schema/alternate-conflict-string.exit
> index 573541a..d00491f 100644
> --- a/tests/qapi-schema/alternate-conflict-string.exit
> +++ b/tests/qapi-schema/alternate-conflict-string.exit
> @@ -1 +1 @@
> -0
> +1
> diff --git a/tests/qapi-schema/alternate-conflict-string.json 
> b/tests/qapi-schema/alternate-conflict-string.json
> index 5fd1a47..a1b0bea 100644
> --- a/tests/qapi-schema/alternate-conflict-string.json
> +++ b/tests/qapi-schema/alternate-conflict-string.json
> @@ -1,4 +1,4 @@
> -# FIXME: we should reject anonymous unions with multiple string-like branches
> +# we reject anonymous unions with multiple string-like branches
>  { 'enum': 'Enum',
>    'data': [ 'hello', 'world' ] }
>  { 'union': 'MyUnion',
> diff --git a/tests/qapi-schema/alternate-conflict-string.out 
> b/tests/qapi-schema/alternate-conflict-string.out
> index e7b39a2..e69de29 100644
> --- a/tests/qapi-schema/alternate-conflict-string.out
> +++ b/tests/qapi-schema/alternate-conflict-string.out
> @@ -1,5 +0,0 @@
> -[OrderedDict([('enum', 'Enum'), ('data', ['hello', 'world'])]),
> - OrderedDict([('union', 'MyUnion'), ('discriminator', OrderedDict()), 
> ('data', OrderedDict([('one', 'str'), ('two', 'Enum')]))])]
> -[{'enum_name': 'Enum', 'enum_values': ['hello', 'world']},
> - {'enum_name': 'MyUnionKind', 'enum_values': None}]
> -[]
[...]
> diff --git a/tests/qapi-schema/union-base-no-discriminator.err 
> b/tests/qapi-schema/union-base-no-discriminator.err
> index e69de29..f33392d 100644
> --- a/tests/qapi-schema/union-base-no-discriminator.err
> +++ b/tests/qapi-schema/union-base-no-discriminator.err
> @@ -0,0 +1 @@
> +tests/qapi-schema/union-base-no-discriminator.json:12: Simple union 
> 'TestUnion' must not have a base
> diff --git a/tests/qapi-schema/union-base-no-discriminator.exit 
> b/tests/qapi-schema/union-base-no-discriminator.exit
> index 573541a..d00491f 100644
> --- a/tests/qapi-schema/union-base-no-discriminator.exit
> +++ b/tests/qapi-schema/union-base-no-discriminator.exit
> @@ -1 +1 @@
> -0
> +1
> diff --git a/tests/qapi-schema/union-base-no-discriminator.json 
> b/tests/qapi-schema/union-base-no-discriminator.json
> index b5da546..548a633 100644
> --- a/tests/qapi-schema/union-base-no-discriminator.json
> +++ b/tests/qapi-schema/union-base-no-discriminator.json
> @@ -1,4 +1,5 @@
> -# FIXME: either allow base in non-flat unions, or diagnose missing 
> discriminator
> +# for now, we reject a base for non-flat unions
> +# FIXME: might be a useful extension to allow later

This isn't a FIXME, it's an idea :)

>  { 'type': 'TestTypeA',
>    'data': { 'string': 'str' } }
>
> diff --git a/tests/qapi-schema/union-base-no-discriminator.out 
> b/tests/qapi-schema/union-base-no-discriminator.out
> index 505fd57..e69de29 100644
> --- a/tests/qapi-schema/union-base-no-discriminator.out
> +++ b/tests/qapi-schema/union-base-no-discriminator.out
> @@ -1,8 +0,0 @@
> -[OrderedDict([('type', 'TestTypeA'), ('data', OrderedDict([('string', 
> 'str')]))]),
> - OrderedDict([('type', 'TestTypeB'), ('data', OrderedDict([('integer', 
> 'int')]))]),
> - OrderedDict([('type', 'Base'), ('data', OrderedDict([('string', 'str')]))]),
> - OrderedDict([('union', 'TestUnion'), ('base', 'Base'), ('data', 
> OrderedDict([('value1', 'TestTypeA'), ('value2', 'TestTypeB')]))])]
> -[{'enum_name': 'TestUnionKind', 'enum_values': None}]
> -[OrderedDict([('type', 'TestTypeA'), ('data', OrderedDict([('string', 
> 'str')]))]),
> - OrderedDict([('type', 'TestTypeB'), ('data', OrderedDict([('integer', 
> 'int')]))]),
> - OrderedDict([('type', 'Base'), ('data', OrderedDict([('string', 'str')]))])]
> diff --git a/tests/qapi-schema/union-max.err b/tests/qapi-schema/union-max.err
> index e69de29..55ce439 100644
> --- a/tests/qapi-schema/union-max.err
> +++ b/tests/qapi-schema/union-max.err
> @@ -0,0 +1 @@
> +tests/qapi-schema/union-max.json:2: Union 'Union' member 'max' clashes with 
> '(automatic)'

Cryptic error message, but it'll do.

> diff --git a/tests/qapi-schema/union-max.exit 
> b/tests/qapi-schema/union-max.exit
> index 573541a..d00491f 100644
> --- a/tests/qapi-schema/union-max.exit
> +++ b/tests/qapi-schema/union-max.exit
> @@ -1 +1 @@
> -0
> +1
> diff --git a/tests/qapi-schema/union-max.json 
> b/tests/qapi-schema/union-max.json
> index 45648c4..d6ad986 100644
> --- a/tests/qapi-schema/union-max.json
> +++ b/tests/qapi-schema/union-max.json
> @@ -1,3 +1,3 @@
> -# FIXME: we should reject 'max' branch in a union, for collision with C enum
> +# we reject 'max' branch in a union, for collision with C enum
>  { 'union': 'Union',
>    'data': { 'max': 'int' } }
> diff --git a/tests/qapi-schema/union-max.out b/tests/qapi-schema/union-max.out
> index 2757d36..e69de29 100644
> --- a/tests/qapi-schema/union-max.out
> +++ b/tests/qapi-schema/union-max.out
> @@ -1,3 +0,0 @@
> -[OrderedDict([('union', 'Union'), ('data', OrderedDict([('max', 'int')]))])]
> -[{'enum_name': 'UnionKind', 'enum_values': None}]
> -[]

Feel free to address my comments on top.

Reviewed-by: Markus Armbruster <arm...@redhat.com>

Reply via email to