KhronosGroup / NNEF-Docs

NNEF public repository
Apache License 2.0
14 stars 3 forks source link

Unpacking tuple results #12

Closed zoeoz closed 5 years ago

zoeoz commented 5 years ago

I have a few questions about unpacking tuple results. On p. 23 it says "fragments that have multiple results essentially return a tuple."

First, this quote appears in the context of extended syntax, but I assume it also applies to flat syntax as well (I don't remember seeing any other part of the document address the issue).

Second, it seems -- specifically -- what the word "essentially" in that statement means is "the results of a fragment are returned as elements of an implicit tuple if and only if the fragment has two or more results."

In other words, fragments that return only a single result do not return an implicit tuple.

The reason I've draw this conclusion is because it always seems to work. For example, suppose we have two fragment declarations:

fragment gloop1( a: integer ) -> ( point: (integer,integer) ) # result has no implicit tuple
fragment gloop2( a: integer ) -> ( point1: (integer,integer), point2: (integer,integer) ) # result is implicit tuple of tuples

and within a fragment body, each of the following would be valid assignments:

t1 = gloop1( 3 );  # no unpacking: t1 is type (integer,integer)
(x,y) = gloop1( 3 );  # unpacking: x and y are type integer

(t1,t2) = gloop2( 4 );  # partial unpacking: t1 and t2 are each type (integer,integer)
(t1,(x,y)) = gloop2( 4 );  # partial unpacking: t1 is type (integer,integer) and x, y are type integer
((x,y),t2) = gloop2( 4 );  # partial unpacking: x, y are type integer and t2 is type (integer,integer)
((x1,y1),(x2,y2)) = gloop2( 4 );  # full unpacking: x1, y1, x2, and y2 are type integer

However, things get dicey when explicit tuple grouping syntax is not used on the left side of the assignment, for example, consider the following:

t1, t2 = gloop2( 4 );  # what is this ???

does this assign t1 and t2 each results of type (integer,integer) or is this interpreted as a failed attempt to fully unpack the results, e.g., compare with:

x1, y1, x2, y2 = goop2( 4 );  # x1, y1, x2, and y2 are type integer ???

in other words, unpacking without explicit tuple grouping on the left leads to ambiguous situations. Note that no unpacking works fine:

t1 = gloop2( 4 );  # no unpacking: t1 is type ((integer,integer),(integer,integer))
gyenesvi commented 5 years ago

You are right about how the return types are formed: in case of a single return value, no tuple is formed, in case of multiple values, their types form a tuple type, and that's the return type of the fragment (the word 'essentially' means that the return type is not explicitly written as a single tuple type, but implicitly by listing the return names and types, just like the input parameters).

Upon assignment, the structure of the lhs and the return type must be compatible in terms of the length of the tuples. A tuple is formed by the , operator, and not by the (), so

(t1, t2) = gloop2( 4 );

is the same as

t1, t2 = gloop2( 4 );

So this results in t1 and t2 becoming 2-tuples. However,

x1, y1, x2, y2 = gloop2( 4 );

is not valid, because the lhs is a 4-tuple, but the return value of the rhs is a 2-tuple.

Note that unlike Python, NNEF is strongly typed, and a tuple type has a fixed length (like a struct in C), so a 2-tuple and a 4-tuple are two distinct and incompatible types. If you want to assign it as a 2-tuple of 2-tuples, you have to use parentheses:

(x1, y1), (x2, y2) = gloop2( 4 );
zoeoz commented 5 years ago

Is it safe to say that the , operator only forms a tuple at the top-most level of nesting?

I think this is where I've had some difficulties. We've developed a recursive descent parser for the NNEF syntax, and an explicit translation of the <tuple-lvalue-expr> grammar on p. 12 generates four nested 1-tuples for the left-hand side of

x1, y1, x2, y2 = gloop2( 4 );

But I think this is an artifact of using nested method calls in the recursive descent parsing. Abstractly, it seems the grammar does actually produce a <tuple-lvalue-expr>.

To make it work in practice like you describe above, we had to modify the recursive descent parsing to:

<array-lvalue-expr> ::= "[" [<lvalue-infix-expr> ("," <lvalue-infix-expr>)* ] "]"
<tuple-lvalue-expr> ::= "(" <lvalue-infix-expr> ("," <lvalue-infix-expr>)+ ")"
<lvalue-infix-expr> ::= <identifier> | <array-lvalue-expr> | <tuple-lvalue-expr>
<lvalue-expr> ::= <lvalue-infix-expr> ("," <lvalue-infix-expr>)*

Although I don't believe there's any need to change the specification, do you think this fairly represents an equivalent syntax?

gyenesvi commented 5 years ago

I think I understand what you mean by the , operator forming a tuple only at the top-most level of nesting and why you have rewritten the grammar this way, which seems to be okay, but I believe it is unnecessary. The reference parser provided by Khronos is also a recursive descent parser, and it can parse things the right way in our experience. I believe the key observation is that an expression that starts with ( can be either a single parenthesized expression or a tuple, which is decided later, when the parser sees a , or not. At that point, it knows if the returned expression will be a tuple or not. Furthermore, a tuple parsing is initiated either by being on the top level as you mention, or conditionally when seeing a (. You can have a look at the reference parser implementation for more info on this.

zoeoz commented 5 years ago

I think that clears it all up. I took a look at the reference parser, and it appears to be similar. The top level entry point parseTuple() begins parsing with or without the enclosing parenthesis and then calls the "infix" method parseValue(), which requires the enclosing parenthesis.