diff mbox series

[v4,01/14] qapi: Parse numeric values

Message ID 20190624173935.25747-2-mreitz@redhat.com
State New
Headers show
Series block: Try to create well-typed json:{} filenames | expand

Commit Message

Max Reitz June 24, 2019, 5:39 p.m. UTC
Signed-off-by: Max Reitz <mreitz@redhat.com>
---
 tests/qapi-schema/bad-type-int.json      |  1 -
 tests/qapi-schema/enum-int-member.json   |  1 -
 scripts/qapi/common.py                   | 25 ++++++++++++++++++++----
 scripts/qapi/introspect.py               |  2 ++
 tests/qapi-schema/bad-type-int.err       |  2 +-
 tests/qapi-schema/enum-int-member.err    |  2 +-
 tests/qapi-schema/leading-comma-list.err |  2 +-
 7 files changed, 26 insertions(+), 9 deletions(-)

Comments

Markus Armbruster Nov. 14, 2019, 9:15 a.m. UTC | #1
Max Reitz <mreitz@redhat.com> writes:

> Signed-off-by: Max Reitz <mreitz@redhat.com>
> ---
>  tests/qapi-schema/bad-type-int.json      |  1 -
>  tests/qapi-schema/enum-int-member.json   |  1 -
>  scripts/qapi/common.py                   | 25 ++++++++++++++++++++----
>  scripts/qapi/introspect.py               |  2 ++
>  tests/qapi-schema/bad-type-int.err       |  2 +-
>  tests/qapi-schema/enum-int-member.err    |  2 +-
>  tests/qapi-schema/leading-comma-list.err |  2 +-
>  7 files changed, 26 insertions(+), 9 deletions(-)
>
> diff --git a/tests/qapi-schema/bad-type-int.json b/tests/qapi-schema/bad-type-int.json
> index 56fc6f8126..81355eb196 100644
> --- a/tests/qapi-schema/bad-type-int.json
> +++ b/tests/qapi-schema/bad-type-int.json
> @@ -1,3 +1,2 @@
>  # we reject an expression with a metatype that is not a string
> -# FIXME: once the parser understands integer inputs, improve the error message
>  { 'struct': 1, 'data': { } }
> diff --git a/tests/qapi-schema/enum-int-member.json b/tests/qapi-schema/enum-int-member.json
> index 6c9c32e149..6958440c6d 100644
> --- a/tests/qapi-schema/enum-int-member.json
> +++ b/tests/qapi-schema/enum-int-member.json
> @@ -1,3 +1,2 @@
>  # we reject any enum member that is not a string
> -# FIXME: once the parser understands integer inputs, improve the error message
>  { 'enum': 'MyEnum', 'data': [ 1 ] }
> diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
> index d61bfdc526..3396ea4a09 100644
> --- a/scripts/qapi/common.py
> +++ b/scripts/qapi/common.py
> @@ -498,6 +498,8 @@ class QAPISchemaParser(object):
>              raise QAPISemError(info, "Unknown pragma '%s'" % name)
>  
>      def accept(self, skip_comment=True):
> +        num_match = re.compile(r'([-+]?inf|nan|[-+0-9.][0-9a-f.ex]*)')
> +
>          while True:
>              self.tok = self.src[self.cursor]
>              self.pos = self.cursor

This is yet another extension over plain JSON.  RFC 8259:

      number = [ minus ] int [ frac ] [ exp ]
      decimal-point = %x2E       ; .
      digit1-9 = %x31-39         ; 1-9
      e = %x65 / %x45            ; e E
      exp = e [ minus / plus ] 1*DIGIT
      frac = decimal-point 1*DIGIT
      int = zero / ( digit1-9 *DIGIT )
      minus = %x2D               ; -
      plus = %x2B                ; +
      zero = %x30                ; 0

Extensions are acceptable when we have an actual use for it, and we
document them properly.

Isn't the parenthesis in your regular expression redundant?

What use do you have in mind for 'inf' and 'nan'?

Why accept leading '+' as in '+123'?

Why accept empty integral part as in '.123'?

Why accept '.xe.'?  Kidding you, that must be a bug in your regexp.
Please decide what number syntax you'd like to accept, then specify it
in docs/devel/qapi-code-gen.txt, so we can first discuss the
specification, and then check the regexp implements it.

docs/devel/qapi-code-gen.txt update goes here:

    === Schema syntax ===

    Syntax is loosely based on JSON (http://www.ietf.org/rfc/rfc8259.txt).
    Differences:

    * Comments: start with a hash character (#) that is not part of a
      string, and extend to the end of the line.

    * Strings are enclosed in 'single quotes', not "double quotes".

    * Strings are restricted to printable ASCII, and escape sequences to
      just '\\'.

--> * Numbers and null are not supported.

Hrmm, commit 9d55380b5a "qapi: Remove null from schema language" left
two instances in error messages behind.  I'll fix them.

> @@ -584,7 +586,22 @@ class QAPISchemaParser(object):
>                      return
>                  self.line += 1
>                  self.line_pos = self.cursor
> -            elif not self.tok.isspace():
> +            elif self.tok.isspace():
> +                pass
> +            elif num_match.match(self.src[self.pos:]):
> +                match = num_match.match(self.src[self.pos:]).group(0)

Sadly, the walrus operator is Python 3.8.

> +                try:
> +                    self.val = int(match, 0)
> +                except ValueError:
> +                    try:
> +                        self.val = float(match)
> +                    except ValueError:
> +                        raise QAPIParseError(self,
> +                                '"%s" is not a valid integer or float' % match)
> +
> +                self.cursor += len(match) - 1
> +                return
> +            else:
>                  raise QAPIParseError(self, 'Stray "%s"' % self.tok)

Any particular reason for putting the number case last?

>  
>      def get_members(self):
> @@ -617,9 +634,9 @@ class QAPISchemaParser(object):
>          if self.tok == ']':
>              self.accept()
>              return expr
> -        if self.tok not in "{['tfn":
> +        if self.tok not in "{['tfn-+0123456789.i":

This is getting a bit ugly.  Let's not worry about it now.

>              raise QAPIParseError(self, 'Expected "{", "[", "]", string, '
> -                                 'boolean or "null"')
> +                                 'boolean, number or "null"')
>          while True:
>              expr.append(self.get_expr(True))
>              if self.tok == ']':
> @@ -638,7 +655,7 @@ class QAPISchemaParser(object):
>          elif self.tok == '[':
>              self.accept()
>              expr = self.get_values()
> -        elif self.tok in "'tfn":
> +        elif self.tok in "'tfn-+0123456789.i":
>              expr = self.val
>              self.accept()
>          else:
> diff --git a/scripts/qapi/introspect.py b/scripts/qapi/introspect.py
> index f62cf0a2e1..6a61dd831f 100644
> --- a/scripts/qapi/introspect.py
> +++ b/scripts/qapi/introspect.py
> @@ -57,6 +57,8 @@ def to_qlit(obj, level=0, suppress_first_indent=False):
>          ret += indent(level) + '}))'
>      elif isinstance(obj, bool):
>          ret += 'QLIT_QBOOL(%s)' % ('true' if obj else 'false')
> +    elif isinstance(obj, int) and obj >= -(2 ** 63) and obj < 2 ** 63:
> +        ret += 'QLIT_QNUM(%i)' % obj

Please explain the range check.

>      else:
>          assert False                # not implemented
>      if level > 0:
> diff --git a/tests/qapi-schema/bad-type-int.err b/tests/qapi-schema/bad-type-int.err
> index da89895404..e22fb4f655 100644
> --- a/tests/qapi-schema/bad-type-int.err
> +++ b/tests/qapi-schema/bad-type-int.err
> @@ -1 +1 @@
> -tests/qapi-schema/bad-type-int.json:3:13: Stray "1"
> +tests/qapi-schema/bad-type-int.json:2: 'struct' key must have a string value

Test needs a rename, assuming it's not redundant now.

> diff --git a/tests/qapi-schema/enum-int-member.err b/tests/qapi-schema/enum-int-member.err
> index 071c5213d8..112175f79d 100644
> --- a/tests/qapi-schema/enum-int-member.err
> +++ b/tests/qapi-schema/enum-int-member.err
> @@ -1 +1 @@
> -tests/qapi-schema/enum-int-member.json:3:31: Stray "1"
> +tests/qapi-schema/enum-int-member.json:2: Member of enum 'MyEnum' requires a string name

This one's name is still good.

> diff --git a/tests/qapi-schema/leading-comma-list.err b/tests/qapi-schema/leading-comma-list.err
> index f5c870bb9c..fa9c80aa57 100644
> --- a/tests/qapi-schema/leading-comma-list.err
> +++ b/tests/qapi-schema/leading-comma-list.err
> @@ -1 +1 @@
> -tests/qapi-schema/leading-comma-list.json:2:13: Expected "{", "[", "]", string, boolean or "null"
> +tests/qapi-schema/leading-comma-list.json:2:13: Expected "{", "[", "]", string, boolean, number or "null"
Max Reitz Nov. 14, 2019, 9:50 a.m. UTC | #2
On 14.11.19 10:15, Markus Armbruster wrote:
> Max Reitz <mreitz@redhat.com> writes:
> 
>> Signed-off-by: Max Reitz <mreitz@redhat.com>
>> ---
>>  tests/qapi-schema/bad-type-int.json      |  1 -
>>  tests/qapi-schema/enum-int-member.json   |  1 -
>>  scripts/qapi/common.py                   | 25 ++++++++++++++++++++----
>>  scripts/qapi/introspect.py               |  2 ++
>>  tests/qapi-schema/bad-type-int.err       |  2 +-
>>  tests/qapi-schema/enum-int-member.err    |  2 +-
>>  tests/qapi-schema/leading-comma-list.err |  2 +-
>>  7 files changed, 26 insertions(+), 9 deletions(-)
>>
>> diff --git a/tests/qapi-schema/bad-type-int.json b/tests/qapi-schema/bad-type-int.json
>> index 56fc6f8126..81355eb196 100644
>> --- a/tests/qapi-schema/bad-type-int.json
>> +++ b/tests/qapi-schema/bad-type-int.json
>> @@ -1,3 +1,2 @@
>>  # we reject an expression with a metatype that is not a string
>> -# FIXME: once the parser understands integer inputs, improve the error message
>>  { 'struct': 1, 'data': { } }
>> diff --git a/tests/qapi-schema/enum-int-member.json b/tests/qapi-schema/enum-int-member.json
>> index 6c9c32e149..6958440c6d 100644
>> --- a/tests/qapi-schema/enum-int-member.json
>> +++ b/tests/qapi-schema/enum-int-member.json
>> @@ -1,3 +1,2 @@
>>  # we reject any enum member that is not a string
>> -# FIXME: once the parser understands integer inputs, improve the error message
>>  { 'enum': 'MyEnum', 'data': [ 1 ] }
>> diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
>> index d61bfdc526..3396ea4a09 100644
>> --- a/scripts/qapi/common.py
>> +++ b/scripts/qapi/common.py
>> @@ -498,6 +498,8 @@ class QAPISchemaParser(object):
>>              raise QAPISemError(info, "Unknown pragma '%s'" % name)
>>  
>>      def accept(self, skip_comment=True):
>> +        num_match = re.compile(r'([-+]?inf|nan|[-+0-9.][0-9a-f.ex]*)')
>> +
>>          while True:
>>              self.tok = self.src[self.cursor]
>>              self.pos = self.cursor
> 
> This is yet another extension over plain JSON.  RFC 8259:
> 
>       number = [ minus ] int [ frac ] [ exp ]
>       decimal-point = %x2E       ; .
>       digit1-9 = %x31-39         ; 1-9
>       e = %x65 / %x45            ; e E
>       exp = e [ minus / plus ] 1*DIGIT
>       frac = decimal-point 1*DIGIT
>       int = zero / ( digit1-9 *DIGIT )
>       minus = %x2D               ; -
>       plus = %x2B                ; +
>       zero = %x30                ; 0
> 
> Extensions are acceptable when we have an actual use for it, and we
> document them properly.

Well, it isn’t really an extension, because this isn’t a JSON parser but
just something that accepts anything that looks like a number and then
lets Python try a conversion on it.

> Isn't the parenthesis in your regular expression redundant?

You’re right, but on second thought, maybe I should surround it by \<
and \>.

> What use do you have in mind for 'inf' and 'nan'?

I could imagine inf being a useful default value, actually.  nan,
probably not so much.

> Why accept leading '+' as in '+123'?
> 
> Why accept empty integral part as in '.123'?
> 
> Why accept '.xe.'?  Kidding you, that must be a bug in your regexp.

Well, kind of.

I wanted to accept anything that looks in any way like a number and then
let Python try to convert it.  That’s also the reason why the case comes
last.

For that reason, I decided to keep the regex as simple as possible,
because the attempted conversions would reject anything that isn’t (to
Python) a valid number later.

It was my impression that the QAPI schema isn’t really JSON anyway and
that our QAPI schema parser isn’t a JSON parser.  Under that assumption
it simply seemed useful to me to accept anything that could potentially
be a number to Python and convert it.

Now, honestly, I still don’t see the point of having a strict JSON
“parser” here, but if you insist.  Seems possible to do in a regex.

Though I do think it makes sense to support hex integers as an extension.

> Please decide what number syntax you'd like to accept, then specify it
> in docs/devel/qapi-code-gen.txt, so we can first discuss the
> specification, and then check the regexp implements it.
> 
> docs/devel/qapi-code-gen.txt update goes here:
> 
>     === Schema syntax ===
> 
>     Syntax is loosely based on JSON (http://www.ietf.org/rfc/rfc8259.txt).
>     Differences:
> 
>     * Comments: start with a hash character (#) that is not part of a
>       string, and extend to the end of the line.
> 
>     * Strings are enclosed in 'single quotes', not "double quotes".
> 
>     * Strings are restricted to printable ASCII, and escape sequences to
>       just '\\'.
> 
> --> * Numbers and null are not supported.

OK.

> Hrmm, commit 9d55380b5a "qapi: Remove null from schema language" left
> two instances in error messages behind.  I'll fix them.
> 
>> @@ -584,7 +586,22 @@ class QAPISchemaParser(object):
>>                      return
>>                  self.line += 1
>>                  self.line_pos = self.cursor
>> -            elif not self.tok.isspace():
>> +            elif self.tok.isspace():
>> +                pass
>> +            elif num_match.match(self.src[self.pos:]):
>> +                match = num_match.match(self.src[self.pos:]).group(0)
> 
> Sadly, the walrus operator is Python 3.8.
> 
>> +                try:
>> +                    self.val = int(match, 0)
>> +                except ValueError:
>> +                    try:
>> +                        self.val = float(match)
>> +                    except ValueError:
>> +                        raise QAPIParseError(self,
>> +                                '"%s" is not a valid integer or float' % match)
>> +
>> +                self.cursor += len(match) - 1
>> +                return
>> +            else:
>>                  raise QAPIParseError(self, 'Stray "%s"' % self.tok)
> 
> Any particular reason for putting the number case last?

Because the match is so broad.

>>  
>>      def get_members(self):
>> @@ -617,9 +634,9 @@ class QAPISchemaParser(object):
>>          if self.tok == ']':
>>              self.accept()
>>              return expr
>> -        if self.tok not in "{['tfn":
>> +        if self.tok not in "{['tfn-+0123456789.i":
> 
> This is getting a bit ugly.  Let's not worry about it now.
> 
>>              raise QAPIParseError(self, 'Expected "{", "[", "]", string, '
>> -                                 'boolean or "null"')
>> +                                 'boolean, number or "null"')
>>          while True:
>>              expr.append(self.get_expr(True))
>>              if self.tok == ']':
>> @@ -638,7 +655,7 @@ class QAPISchemaParser(object):
>>          elif self.tok == '[':
>>              self.accept()
>>              expr = self.get_values()
>> -        elif self.tok in "'tfn":
>> +        elif self.tok in "'tfn-+0123456789.i":
>>              expr = self.val
>>              self.accept()
>>          else:
>> diff --git a/scripts/qapi/introspect.py b/scripts/qapi/introspect.py
>> index f62cf0a2e1..6a61dd831f 100644
>> --- a/scripts/qapi/introspect.py
>> +++ b/scripts/qapi/introspect.py
>> @@ -57,6 +57,8 @@ def to_qlit(obj, level=0, suppress_first_indent=False):
>>          ret += indent(level) + '}))'
>>      elif isinstance(obj, bool):
>>          ret += 'QLIT_QBOOL(%s)' % ('true' if obj else 'false')
>> +    elif isinstance(obj, int) and obj >= -(2 ** 63) and obj < 2 ** 63:
>> +        ret += 'QLIT_QNUM(%i)' % obj
> 
> Please explain the range check.

Will do.

>>      else:
>>          assert False                # not implemented
>>      if level > 0:
>> diff --git a/tests/qapi-schema/bad-type-int.err b/tests/qapi-schema/bad-type-int.err
>> index da89895404..e22fb4f655 100644
>> --- a/tests/qapi-schema/bad-type-int.err
>> +++ b/tests/qapi-schema/bad-type-int.err
>> @@ -1 +1 @@
>> -tests/qapi-schema/bad-type-int.json:3:13: Stray "1"
>> +tests/qapi-schema/bad-type-int.json:2: 'struct' key must have a string value
> 
> Test needs a rename, assuming it's not redundant now.

I’m not adding a test here, it’s just the value has changed in
4d42815587063d.

Thanks for reviewing!

Max

>> diff --git a/tests/qapi-schema/enum-int-member.err b/tests/qapi-schema/enum-int-member.err
>> index 071c5213d8..112175f79d 100644
>> --- a/tests/qapi-schema/enum-int-member.err
>> +++ b/tests/qapi-schema/enum-int-member.err
>> @@ -1 +1 @@
>> -tests/qapi-schema/enum-int-member.json:3:31: Stray "1"
>> +tests/qapi-schema/enum-int-member.json:2: Member of enum 'MyEnum' requires a string name
> 
> This one's name is still good.
> 
>> diff --git a/tests/qapi-schema/leading-comma-list.err b/tests/qapi-schema/leading-comma-list.err
>> index f5c870bb9c..fa9c80aa57 100644
>> --- a/tests/qapi-schema/leading-comma-list.err
>> +++ b/tests/qapi-schema/leading-comma-list.err
>> @@ -1 +1 @@
>> -tests/qapi-schema/leading-comma-list.json:2:13: Expected "{", "[", "]", string, boolean or "null"
>> +tests/qapi-schema/leading-comma-list.json:2:13: Expected "{", "[", "]", string, boolean, number or "null"
Markus Armbruster Nov. 14, 2019, 12:01 p.m. UTC | #3
Max Reitz <mreitz@redhat.com> writes:

> On 14.11.19 10:15, Markus Armbruster wrote:
>> Max Reitz <mreitz@redhat.com> writes:
>> 
>>> Signed-off-by: Max Reitz <mreitz@redhat.com>
>>> ---
>>>  tests/qapi-schema/bad-type-int.json      |  1 -
>>>  tests/qapi-schema/enum-int-member.json   |  1 -
>>>  scripts/qapi/common.py                   | 25 ++++++++++++++++++++----
>>>  scripts/qapi/introspect.py               |  2 ++
>>>  tests/qapi-schema/bad-type-int.err       |  2 +-
>>>  tests/qapi-schema/enum-int-member.err    |  2 +-
>>>  tests/qapi-schema/leading-comma-list.err |  2 +-
>>>  7 files changed, 26 insertions(+), 9 deletions(-)
>>>
>>> diff --git a/tests/qapi-schema/bad-type-int.json b/tests/qapi-schema/bad-type-int.json
>>> index 56fc6f8126..81355eb196 100644
>>> --- a/tests/qapi-schema/bad-type-int.json
>>> +++ b/tests/qapi-schema/bad-type-int.json
>>> @@ -1,3 +1,2 @@
>>>  # we reject an expression with a metatype that is not a string
>>> -# FIXME: once the parser understands integer inputs, improve the error message
>>>  { 'struct': 1, 'data': { } }
>>> diff --git a/tests/qapi-schema/enum-int-member.json b/tests/qapi-schema/enum-int-member.json
>>> index 6c9c32e149..6958440c6d 100644
>>> --- a/tests/qapi-schema/enum-int-member.json
>>> +++ b/tests/qapi-schema/enum-int-member.json
>>> @@ -1,3 +1,2 @@
>>>  # we reject any enum member that is not a string
>>> -# FIXME: once the parser understands integer inputs, improve the error message
>>>  { 'enum': 'MyEnum', 'data': [ 1 ] }
>>> diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
>>> index d61bfdc526..3396ea4a09 100644
>>> --- a/scripts/qapi/common.py
>>> +++ b/scripts/qapi/common.py
>>> @@ -498,6 +498,8 @@ class QAPISchemaParser(object):
>>>              raise QAPISemError(info, "Unknown pragma '%s'" % name)
>>>  
>>>      def accept(self, skip_comment=True):
>>> +        num_match = re.compile(r'([-+]?inf|nan|[-+0-9.][0-9a-f.ex]*)')
>>> +
>>>          while True:
>>>              self.tok = self.src[self.cursor]
>>>              self.pos = self.cursor
>> 
>> This is yet another extension over plain JSON.  RFC 8259:
>> 
>>       number = [ minus ] int [ frac ] [ exp ]
>>       decimal-point = %x2E       ; .
>>       digit1-9 = %x31-39         ; 1-9
>>       e = %x65 / %x45            ; e E
>>       exp = e [ minus / plus ] 1*DIGIT
>>       frac = decimal-point 1*DIGIT
>>       int = zero / ( digit1-9 *DIGIT )
>>       minus = %x2D               ; -
>>       plus = %x2B                ; +
>>       zero = %x30                ; 0
>> 
>> Extensions are acceptable when we have an actual use for it, and we
>> document them properly.
>
> Well, it isn’t really an extension, because this isn’t a JSON parser but
> just something that accepts anything that looks like a number and then
> lets Python try a conversion on it.

I'm totally cool with deviating from JSON, all I really care about is
proper schema language documentation.

If we stick to JSON form number syntax, this is easy: replace "Numbers
and null are not supported" by "null is not supported", done.
Implementation should also be easy enough: convert RFC 8259's EBNF to a
regexp, feed the matched string to Python's number parser, done.

Fancier syntax we'd need to document ourselves.  I'm willing to deal
with that if we have a sufficiently compelling use for them.

>> Isn't the parenthesis in your regular expression redundant?
>
> You’re right, but on second thought, maybe I should surround it by \<
> and \>.
>
>> What use do you have in mind for 'inf' and 'nan'?
>
> I could imagine inf being a useful default value, actually.  nan,
> probably not so much.
>
>> Why accept leading '+' as in '+123'?
>> 
>> Why accept empty integral part as in '.123'?
>> 
>> Why accept '.xe.'?  Kidding you, that must be a bug in your regexp.
>
> Well, kind of.
>
> I wanted to accept anything that looks in any way like a number and then
> let Python try to convert it.  That’s also the reason why the case comes
> last.
>
> For that reason, I decided to keep the regex as simple as possible,
> because the attempted conversions would reject anything that isn’t (to
> Python) a valid number later.

Ah, now I see.

> It was my impression that the QAPI schema isn’t really JSON anyway and
> that our QAPI schema parser isn’t a JSON parser.  Under that assumption
> it simply seemed useful to me to accept anything that could potentially
> be a number to Python and convert it.
>
> Now, honestly, I still don’t see the point of having a strict JSON
> “parser” here, but if you insist.  Seems possible to do in a regex.
>
> Though I do think it makes sense to support hex integers as an extension.

Let's start simple & stupid.  Extensions can be added on top.
Hexadecimal integers may well be compelling enough to justify an
extension.

>> Please decide what number syntax you'd like to accept, then specify it
>> in docs/devel/qapi-code-gen.txt, so we can first discuss the
>> specification, and then check the regexp implements it.
>> 
>> docs/devel/qapi-code-gen.txt update goes here:
>> 
>>     === Schema syntax ===
>> 
>>     Syntax is loosely based on JSON (http://www.ietf.org/rfc/rfc8259.txt).
>>     Differences:
>> 
>>     * Comments: start with a hash character (#) that is not part of a
>>       string, and extend to the end of the line.
>> 
>>     * Strings are enclosed in 'single quotes', not "double quotes".
>> 
>>     * Strings are restricted to printable ASCII, and escape sequences to
>>       just '\\'.
>> 
>> --> * Numbers and null are not supported.
>
> OK.
>
>> Hrmm, commit 9d55380b5a "qapi: Remove null from schema language" left
>> two instances in error messages behind.  I'll fix them.
>> 
>>> @@ -584,7 +586,22 @@ class QAPISchemaParser(object):
>>>                      return
>>>                  self.line += 1
>>>                  self.line_pos = self.cursor
>>> -            elif not self.tok.isspace():
>>> +            elif self.tok.isspace():
>>> +                pass
>>> +            elif num_match.match(self.src[self.pos:]):
>>> +                match = num_match.match(self.src[self.pos:]).group(0)
>> 
>> Sadly, the walrus operator is Python 3.8.
>> 
>>> +                try:
>>> +                    self.val = int(match, 0)
>>> +                except ValueError:
>>> +                    try:
>>> +                        self.val = float(match)
>>> +                    except ValueError:
>>> +                        raise QAPIParseError(self,
>>> +                                '"%s" is not a valid integer or float' % match)
>>> +
>>> +                self.cursor += len(match) - 1
>>> +                return
>>> +            else:
>>>                  raise QAPIParseError(self, 'Stray "%s"' % self.tok)
>> 
>> Any particular reason for putting the number case last?
>
> Because the match is so broad.
>
>>>  
>>>      def get_members(self):
>>> @@ -617,9 +634,9 @@ class QAPISchemaParser(object):
>>>          if self.tok == ']':
>>>              self.accept()
>>>              return expr
>>> -        if self.tok not in "{['tfn":
>>> +        if self.tok not in "{['tfn-+0123456789.i":
>> 
>> This is getting a bit ugly.  Let's not worry about it now.
>> 
>>>              raise QAPIParseError(self, 'Expected "{", "[", "]", string, '
>>> -                                 'boolean or "null"')
>>> +                                 'boolean, number or "null"')
>>>          while True:
>>>              expr.append(self.get_expr(True))
>>>              if self.tok == ']':
>>> @@ -638,7 +655,7 @@ class QAPISchemaParser(object):
>>>          elif self.tok == '[':
>>>              self.accept()
>>>              expr = self.get_values()
>>> -        elif self.tok in "'tfn":
>>> +        elif self.tok in "'tfn-+0123456789.i":
>>>              expr = self.val
>>>              self.accept()
>>>          else:
>>> diff --git a/scripts/qapi/introspect.py b/scripts/qapi/introspect.py
>>> index f62cf0a2e1..6a61dd831f 100644
>>> --- a/scripts/qapi/introspect.py
>>> +++ b/scripts/qapi/introspect.py
>>> @@ -57,6 +57,8 @@ def to_qlit(obj, level=0, suppress_first_indent=False):
>>>          ret += indent(level) + '}))'
>>>      elif isinstance(obj, bool):
>>>          ret += 'QLIT_QBOOL(%s)' % ('true' if obj else 'false')
>>> +    elif isinstance(obj, int) and obj >= -(2 ** 63) and obj < 2 ** 63:
>>> +        ret += 'QLIT_QNUM(%i)' % obj
>> 
>> Please explain the range check.
>
> Will do.
>
>>>      else:
>>>          assert False                # not implemented
>>>      if level > 0:
>>> diff --git a/tests/qapi-schema/bad-type-int.err b/tests/qapi-schema/bad-type-int.err
>>> index da89895404..e22fb4f655 100644
>>> --- a/tests/qapi-schema/bad-type-int.err
>>> +++ b/tests/qapi-schema/bad-type-int.err
>>> @@ -1 +1 @@
>>> -tests/qapi-schema/bad-type-int.json:3:13: Stray "1"
>>> +tests/qapi-schema/bad-type-int.json:2: 'struct' key must have a string value
>> 
>> Test needs a rename, assuming it's not redundant now.
>
> I’m not adding a test here, it’s just the value has changed in
> 4d42815587063d.

The test name 'bad-type-int' is now bad, because the int in it is no
longer bad (pardon my lame puns).

> Thanks for reviewing!

Better late than never, I guess...  You're welcome!

>
> Max
>
>>> diff --git a/tests/qapi-schema/enum-int-member.err b/tests/qapi-schema/enum-int-member.err
>>> index 071c5213d8..112175f79d 100644
>>> --- a/tests/qapi-schema/enum-int-member.err
>>> +++ b/tests/qapi-schema/enum-int-member.err
>>> @@ -1 +1 @@
>>> -tests/qapi-schema/enum-int-member.json:3:31: Stray "1"
>>> +tests/qapi-schema/enum-int-member.json:2: Member of enum 'MyEnum' requires a string name
>> 
>> This one's name is still good.
>> 
>>> diff --git a/tests/qapi-schema/leading-comma-list.err b/tests/qapi-schema/leading-comma-list.err
>>> index f5c870bb9c..fa9c80aa57 100644
>>> --- a/tests/qapi-schema/leading-comma-list.err
>>> +++ b/tests/qapi-schema/leading-comma-list.err
>>> @@ -1 +1 @@
>>> -tests/qapi-schema/leading-comma-list.json:2:13: Expected "{", "[", "]", string, boolean or "null"
>>> +tests/qapi-schema/leading-comma-list.json:2:13: Expected "{", "[", "]", string, boolean, number or "null"
diff mbox series

Patch

diff --git a/tests/qapi-schema/bad-type-int.json b/tests/qapi-schema/bad-type-int.json
index 56fc6f8126..81355eb196 100644
--- a/tests/qapi-schema/bad-type-int.json
+++ b/tests/qapi-schema/bad-type-int.json
@@ -1,3 +1,2 @@ 
 # we reject an expression with a metatype that is not a string
-# FIXME: once the parser understands integer inputs, improve the error message
 { 'struct': 1, 'data': { } }
diff --git a/tests/qapi-schema/enum-int-member.json b/tests/qapi-schema/enum-int-member.json
index 6c9c32e149..6958440c6d 100644
--- a/tests/qapi-schema/enum-int-member.json
+++ b/tests/qapi-schema/enum-int-member.json
@@ -1,3 +1,2 @@ 
 # we reject any enum member that is not a string
-# FIXME: once the parser understands integer inputs, improve the error message
 { 'enum': 'MyEnum', 'data': [ 1 ] }
diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
index d61bfdc526..3396ea4a09 100644
--- a/scripts/qapi/common.py
+++ b/scripts/qapi/common.py
@@ -498,6 +498,8 @@  class QAPISchemaParser(object):
             raise QAPISemError(info, "Unknown pragma '%s'" % name)
 
     def accept(self, skip_comment=True):
+        num_match = re.compile(r'([-+]?inf|nan|[-+0-9.][0-9a-f.ex]*)')
+
         while True:
             self.tok = self.src[self.cursor]
             self.pos = self.cursor
@@ -584,7 +586,22 @@  class QAPISchemaParser(object):
                     return
                 self.line += 1
                 self.line_pos = self.cursor
-            elif not self.tok.isspace():
+            elif self.tok.isspace():
+                pass
+            elif num_match.match(self.src[self.pos:]):
+                match = num_match.match(self.src[self.pos:]).group(0)
+                try:
+                    self.val = int(match, 0)
+                except ValueError:
+                    try:
+                        self.val = float(match)
+                    except ValueError:
+                        raise QAPIParseError(self,
+                                '"%s" is not a valid integer or float' % match)
+
+                self.cursor += len(match) - 1
+                return
+            else:
                 raise QAPIParseError(self, 'Stray "%s"' % self.tok)
 
     def get_members(self):
@@ -617,9 +634,9 @@  class QAPISchemaParser(object):
         if self.tok == ']':
             self.accept()
             return expr
-        if self.tok not in "{['tfn":
+        if self.tok not in "{['tfn-+0123456789.i":
             raise QAPIParseError(self, 'Expected "{", "[", "]", string, '
-                                 'boolean or "null"')
+                                 'boolean, number or "null"')
         while True:
             expr.append(self.get_expr(True))
             if self.tok == ']':
@@ -638,7 +655,7 @@  class QAPISchemaParser(object):
         elif self.tok == '[':
             self.accept()
             expr = self.get_values()
-        elif self.tok in "'tfn":
+        elif self.tok in "'tfn-+0123456789.i":
             expr = self.val
             self.accept()
         else:
diff --git a/scripts/qapi/introspect.py b/scripts/qapi/introspect.py
index f62cf0a2e1..6a61dd831f 100644
--- a/scripts/qapi/introspect.py
+++ b/scripts/qapi/introspect.py
@@ -57,6 +57,8 @@  def to_qlit(obj, level=0, suppress_first_indent=False):
         ret += indent(level) + '}))'
     elif isinstance(obj, bool):
         ret += 'QLIT_QBOOL(%s)' % ('true' if obj else 'false')
+    elif isinstance(obj, int) and obj >= -(2 ** 63) and obj < 2 ** 63:
+        ret += 'QLIT_QNUM(%i)' % obj
     else:
         assert False                # not implemented
     if level > 0:
diff --git a/tests/qapi-schema/bad-type-int.err b/tests/qapi-schema/bad-type-int.err
index da89895404..e22fb4f655 100644
--- a/tests/qapi-schema/bad-type-int.err
+++ b/tests/qapi-schema/bad-type-int.err
@@ -1 +1 @@ 
-tests/qapi-schema/bad-type-int.json:3:13: Stray "1"
+tests/qapi-schema/bad-type-int.json:2: 'struct' key must have a string value
diff --git a/tests/qapi-schema/enum-int-member.err b/tests/qapi-schema/enum-int-member.err
index 071c5213d8..112175f79d 100644
--- a/tests/qapi-schema/enum-int-member.err
+++ b/tests/qapi-schema/enum-int-member.err
@@ -1 +1 @@ 
-tests/qapi-schema/enum-int-member.json:3:31: Stray "1"
+tests/qapi-schema/enum-int-member.json:2: Member of enum 'MyEnum' requires a string name
diff --git a/tests/qapi-schema/leading-comma-list.err b/tests/qapi-schema/leading-comma-list.err
index f5c870bb9c..fa9c80aa57 100644
--- a/tests/qapi-schema/leading-comma-list.err
+++ b/tests/qapi-schema/leading-comma-list.err
@@ -1 +1 @@ 
-tests/qapi-schema/leading-comma-list.json:2:13: Expected "{", "[", "]", string, boolean or "null"
+tests/qapi-schema/leading-comma-list.json:2:13: Expected "{", "[", "]", string, boolean, number or "null"