How to distinguish explicit assignment vs 'sugared' capture list item syntax in the AST?

given the current implementation in Sema, what is the best way to differentiate the capture list items in the following two cases from their representation in the AST?

let x = 0
_ = { [x] in _ = x }

vs

let x = 0
_ = { [x = x] in _ = x }

i.e. when one capture list item has the binding 'fully spelled out' vs one in which the shorthand, 'sugared' syntax is used.

IIUC the short version with no explicit assignment operator is automatically 'expanded' into the longer form, though i have not tracked down the exact mechanics involved. i initially assumed there'd be something in the AST with an 'implicit' bit set that would discriminate the cases, but if there is i've been unable to find it. so far the best i've come up with is to check if the pattern binding decl in the capture list has a positive source range extent, which feels... 'icky', but maybe that feeling is unjustified.

the relevant portion of the AST dump output for these cases looks like this (find the full example here: Compiler Explorer):

# [x] case
(capture_list type="() -> ()" location=/app/example.swift:2:5 range=[/app/example.swift:2:5 - line:2:20]
  (pattern_binding_decl decl_context=0x6099d6d083d8 range=[/app/example.swift:2:8 - line:2:8]
    (pattern_entry
      (pattern_named implicit type="Int" "x")
      (original_init=unresolved_decl_ref_expr type="<null>" name="x" function_ref=unapplied)
      (processed_init=declref_expr type="Int" location=/app/example.swift:2:8 range=[/app/example.swift:2:8 - line:2:8] decl="output.(file).x@/app/example.swift:1:5" function_ref=unapplied)))
# [x = x] case
(capture_list type="() -> ()" location=/app/example.swift:2:5 range=[/app/example.swift:2:5 - line:2:24]
  (pattern_binding_decl decl_context=0x64af2c03d3d8 range=[/app/example.swift:2:8 - line:2:12]
    (pattern_entry
      (pattern_named implicit type="Int" "x")
      (original_init=unresolved_decl_ref_expr type="<null>" name="x" function_ref=unapplied)
      (processed_init=declref_expr type="Int" location=/app/example.swift:2:12 range=[/app/example.swift:2:12 - line:2:12] decl="output.(file).x@/app/example.swift:1:5" function_ref=unapplied)))

note how the range=[...] for the pattern_binding_decl differs in the two cases, but not much else particularly relevant-looking appears to change.

It's done directly in the parser here:

we take the name from the token and then allow it to be parsed as an expression for the initializer.

Yeah this is a weird case because technically the user has written both the initializer and bound variable, it isn't really correct to say that either are not user-written when it comes to things like diagnostics and semantic functionality. This matches what we do for if let x {}.

Can you do it by checking the getEqualLoc on the PatternBindingEntry? That would probably be my preferred solution.

1 Like

I don't know if it's helpful, but Swift Testing has to handle these syntaxen when capturing values in exit tests. We're using swift-syntax of course, so that doesn't help you in C++ Land, but… anyway, where am I?

1 Like