Skip to content

PEP 646: Add section on grammar changes #2039

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 18, 2021
217 changes: 198 additions & 19 deletions pep-0646.rst
Original file line number Diff line number Diff line change
Expand Up @@ -733,28 +733,208 @@ hopefully enable a thriving ecosystem of tools for analysing and verifying
shape properties of numerical computing programs.


Backwards Compatibility
=======================
Grammar Changes
===============

This PEP requires two grammar changes. Full diffs of ``python.gram``
and simple tests to confirm correct behaviour are available at
https://github.com/mrahtz/cpython/commits/pep646-grammar.

In order to use the star operator for unpacking of ``TypeVarTuple`` instances,
we would need to make two grammar changes:
Star expressions in indexes
---------------------------

1. Star expressions must be made valid in at least index operations.
For example, ``Tuple[*Ts]`` and ``Tuple[T1, *Ts, T2]`` would both
be valid. (This PEP does not allow multiple unpacked ``TypeVarTuple``
instances to appear in a single parameter list, so ``Tuple[*Ts1, *Ts2]``
would be a runtime error. Also note that star expressions would *not*
be valid in slice expressions - e.g. ``Tuple[*Ts:*Ts]`` is
nonsensical and should remain invalid.)
2. We would need to make '``*args: *Ts``' valid in function definitions.
The first grammar change enables use of star expressions in index operations (that is,
within square brackets), necessary to support star-unpacking of TypeVarTuples:

In both cases, at runtime the star operator would call ``Ts.__iter__()``.
This would, in turn, return an instance of a helper class, e.g.
``UnpackedTypeVarTuple``, whose ``repr`` would be ``*Ts``.
::

If these grammar changes are considered too burdensome, we could instead
simply use ``Unpack`` - though in this case it might be better for us to
first decide whether there's a better option.
DType = TypeVar('DType')
Shape = TypeVarTuple('Shape')
class Array(Generic[DType, *Shape]):
...

Before:

::

slices:
| slice !','
| ','.slice+ [',']

After:

::

slices:
| slice !','
| ','.(slice | starred_expression)+ [',']

As with star-unpacking in other contexts, the star operator calls ``__iter__``
on the callee, and adds the contents of the resulting iterator to the argument
passed to ``__getitem__``. For example, if we do ``foo[a, *b, c]``, and
``b.__iter__`` produces an iterator yielding ``d`` and ``e``,
``foo.__getitem__`` would receive ``(a, d, e, c)``.

To put it another way, note that ``x[..., *a, ...]`` produces the same result
as ``x[(..., a*, ...)]``` (with any slices ``i:j`` in ``...`` replaced with
``slice(i, j)``, with the one edge case that ``x[*a]`` becomes ``x[(*a,)]``).

TypeVarTuple Implementation
'''''''''''''''''''''''''''

With this grammar change, ``TypeVarTuple`` is implemented as follows.
Note that this implementation is useful only for the benefit of a) correct
``repr()`` and b) runtime analysers; static analysers would not use the
implementation.

::

class TypeVarTuple:
def __init__(self, name):
self._name = name
self._unpacked = UnpackedTypeVarTuple(name)
def __iter__(self):
yield self._unpacked
def __repr__(self):
return self._name

class UnpackedTypeVarTuple:
def __init__(self, name):
self._name = name
def __repr__(self):
return '*' + self._name

Implications
''''''''''''

This grammar change implies a number of additional changes in behaviour not
required by this PEP. We choose to allow these additional changes rather than
disallowing them at a syntax level in order to keep the syntax change as small
as possible.

First, the grammar change enables star-unpacking of other structures, such
as lists, within indexing operations:

::

idxs_to_select = (1, 2)
array[0, *idxs_to_select, -1] # Equivalent to [0, 1, 2, -1]

Second, more than one instance of a star-unpack can occur within an index:

::

array[*idxs_to_select, *idxs_to_select] # Equivalent to array[1, 2, 1, 2]

Note that this PEP disallows multiple unpacked TypeVarTuples within a single
type parameter list. This requirement would therefore need to be implemented
in type checking tools themselves rather than at the syntax level.

Third, slices may co-occur with starred expressions:

::

array[3:5, *idxs_to_select] # Equivalent to array[3:5, 1, 2]

However, note that slices involving starred expressions are still invalid:

::

# Syntax error
array[*idxs_start:*idxs_end]


``*args`` as a TypeVarTuple
---------------------------

The second change enables use of ``*args: *Ts`` in function definitions.

Before:

::

star_etc:
| '*' param_no_default param_maybe_default* [kwds]
| '*' ',' param_maybe_default+ [kwds]
| kwds

After:

::

star_etc:
| '*' param_no_default param_maybe_default* [kwds]
| '*' param_no_default_star_annotation param_maybe_default* [kwds] # New
| '*' ',' param_maybe_default+ [kwds]
| kwds

Where:

::

param_no_default_star_annotation:
| param_star_annotation ',' TYPE_COMMENT?
| param_star_annotation TYPE_COMMENT? &')'

param_star_annotation: NAME star_annotation

star_annotation: ':' star_expression

This accomplishes the desired outcome (making ``*args: *Ts`` not be a syntax
error) while matching the behaviour of star-unpacking in other contexts:
at runtime, ``__iter__`` is called on the starred object, and a tuple
containing the items of the resulting iterator is set as the type annotion
for ``args``. In other words, at runtime ``*args: *foo`` is equivalent to
``*args: tuple(foo)``.

::

>>> Ts = TypeVarTuple('Ts')
>>> def foo(*args: *Ts): pass # Equivalent to `*args: tuple(Ts)`
>>> foo.__annotations__
{'args': (*Ts,)}
# *Ts is the repr() of Ts._unpacked, an instance of UnpackedTypeVarTuple

Note that the only scenario in which this grammar change allows ``*Ts`` to be
used as a direct annotation (rather than being wrapped in e.g. ``Tuple[*Ts]``)
is ``*args``. Other uses are still invalid:

::

x: *Ts # Syntax error
def foo(x: *Ts): pass # Syntax error

Implications
''''''''''''

As with the first grammar change, this change also has a number of side effects.
In particular, the annotation of ``*args`` could be set to a starred object
other than a ``TypeVarTuple`` - for example, the following nonsensical
annotation is possible:

::

>>> foo = [1, 2, 3]
>>> def bar(*args: *foo): pass # Equivalent to `*args: tuple(foo)`
>>> bar.__annotations__
{'args': (1, 2, 3)}

Again, prevention of such annotations will need to be done by, say, static
checkers, rather than at the level of syntax.

Alternatives
------------

If these two grammar changes are considered too burdensome, there are two alternatives:

1. **Support change 1 but not change 2**. Starred expressions within indexes are
more important to us than the ability to annotation ``*args``.
Comment on lines +930 to +931
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

2. **Use ``Unpack`` instead** - though in this case it might be better for us to
reconsider our options to see whether there isn't another option which would be
more readable.

Backwards Compatibility
=======================

The ``Unpack`` version of the PEP should be back-portable to previous
versions of Python.
Expand All @@ -766,7 +946,6 @@ uses of the class will still work, and b) parameterised and unparameterised
versions of the class can be used together (relevant if, for example, library
code is updated to use parameters while user code is not, or vice-versa).


Reference Implementation
========================

Expand Down