Detect control flow in entities nested inside functions (#5336)

Right now, return_scope_stack is being used to determine whether logic
is in a function scope. However, we need to handle nested entities
inside function scopes. For example where this crashes right now:

```
base class C(B:! bool) {}

fn F() {
  class B {
    extend base: C(true or false);
  }
}
```

This is doing a few things to make this kind of code not crash:

- Split `scope_stack().Push` into `PushForDeclName`, `PushForEntity`,
`PushForExpr`, and `PushForFunction` so that better decisions can be
made about behaviors.
- Hide `return_scope_stack` in the API, instead using interfaces to get
at the underlying data.
- Also using `PushForFunction` to update it similar to the other stacks
that `ScopeStack` manages.
- Add `IsInFunctionScope` as the best way to determine presence in
function scope.
- Remove `PeekIsLexicalScope` since destruction really wants function
scope information anyways.
- Clean up `destroy_id_stack` handling to be for function scopes rather
than lexical scopes.
- Return after related `context.TODO`s in a couple more spots, so that
code doesn't proceed to add control flow in spite of the lack of
support.

---------

Co-authored-by: Geoff Romer <gromer@google.com>
This commit is contained in:
Jon Ross-Perkins
2025-04-23 19:03:53 +00:00
committed by GitHub
co-authored by Geoff Romer
parent 51498547c9
commit 03e693873b
22 changed files with 476 additions and 391 deletions
@@ -48,7 +48,6 @@ fn F() {
var explicit_return: ExplicitReturn;
var with_addr: WithAddr;
if (true) {
// TODO: Destroy in nested scope.
var in_scope: NoAddr;
}
}
@@ -476,22 +475,24 @@ fn G() { F({}); }
// CHECK:STDOUT: !if.then:
// CHECK:STDOUT: name_binding_decl {
// CHECK:STDOUT: %in_scope.patt: %NoAddr = binding_pattern in_scope
// CHECK:STDOUT: %.loc11: %NoAddr = var_pattern %in_scope.patt
// CHECK:STDOUT: %.loc10_5.1: %NoAddr = var_pattern %in_scope.patt
// CHECK:STDOUT: }
// CHECK:STDOUT: %in_scope.var: ref %NoAddr = var in_scope
// CHECK:STDOUT: %destroy.ref.4: %destroy.type.bc5 = name_ref destroy, imports.%Main.import_ref.7fe [concrete = constants.%destroy.60f]
// CHECK:STDOUT: %destroy.bound.4: <bound method> = bound_method %in_scope.var, %destroy.ref.4
// CHECK:STDOUT: %NoAddr.ref.loc11: type = name_ref NoAddr, imports.%Main.NoAddr [concrete = constants.%NoAddr]
// CHECK:STDOUT: %NoAddr.ref.loc10: type = name_ref NoAddr, imports.%Main.NoAddr [concrete = constants.%NoAddr]
// CHECK:STDOUT: %in_scope: ref %NoAddr = bind_name in_scope, %in_scope.var
// CHECK:STDOUT: br !if.else
// CHECK:STDOUT:
// CHECK:STDOUT: !if.else:
// CHECK:STDOUT: %.loc10_5.2: %NoAddr = bind_value %in_scope.var
// CHECK:STDOUT: %destroy.call.1: init %empty_tuple.type = call %destroy.bound.4(%.loc10_5.2)
// CHECK:STDOUT: %addr: %ptr.b4e = addr_of %with_addr.var
// CHECK:STDOUT: %destroy.call.1: init %empty_tuple.type = call %destroy.bound.3(%addr)
// CHECK:STDOUT: %destroy.call.2: init %empty_tuple.type = call %destroy.bound.3(%addr)
// CHECK:STDOUT: %.loc7_3.2: %ExplicitReturn = bind_value %explicit_return.var
// CHECK:STDOUT: %destroy.call.2: init %empty_tuple.type = call %destroy.bound.2(%.loc7_3.2)
// CHECK:STDOUT: %destroy.call.3: init %empty_tuple.type = call %destroy.bound.2(%.loc7_3.2)
// CHECK:STDOUT: %.loc6_3.2: %NoAddr = bind_value %no_addr.var
// CHECK:STDOUT: %destroy.call.3: init %empty_tuple.type = call %destroy.bound.1(%.loc6_3.2)
// CHECK:STDOUT: %destroy.call.4: init %empty_tuple.type = call %destroy.bound.1(%.loc6_3.2)
// CHECK:STDOUT: return
// CHECK:STDOUT: }
// CHECK:STDOUT: