I’m not convinced (yet) that using such a different AST for None-aware access operators makes sense. Yes, it would simplify the short-circuiting, at what cost though?
In addition to the NoneAwareAttribute and NoneAwareSubscript nodes, we’d likely need to add at least three more AttributeTail, SubscriptTail and CallTail. If I didn’t miss something, the expression a.b?.c[0].func() would then be parsed roughly like this:
ast.NoneAwareAttribute(
base=ast.Attribute(
expr=ast.Name(identifier="a"),
identifier="b"
)
tail=[
ast.AttributeTail(identifier="c"),
ast.SubscriptTail(slice=ast.Constant(value=0)),
ast.AttributeTail(identifier="func")
ast.CallTail(args=[], keyword=[])
]
)
Reusing parts of the Attribute, Subscript and Call nodes might be challenging as one is left-recursive whereas the *Tail variants are right-recursive. This will not only affect CPython itself for the bytecode generation where we might need to duplicate or refactor code but also every other consumer of the AST. It might also be at least a bit surprising that a normal attribute access (.) can be parsed as two separate nodes depending on the context.
What I like about just adding primary '?' '.' NAME and primary '?' '[' slices ']' to the primary rule is that it shows these can be used almost interchangeably with the other access operators. Yes, consumers will need to adjust their Attribute, Subscript and Call node handling to add support for short-circuiting, but IMO that’s a fairly minor ask compared to the alternative mentioned above.
–
In comparison, adding a group attribute to all expression nodes is a bit tedious but it does make a surprising amount of sense actually. In a way it’s similar to the other location attributes. It also helps that a group will only ever have exactly one topmost expression node.
My implementation can be found here: Comparing python:main...cdce8p:syntax-none-aware-access-operators · python/cpython · GitHub
In particular the last commit: Add group attribute for expressions · python/cpython@fb46569 · GitHub