Clone a ClosureExpr and change all decl contexts within the body

I've stumbled across an interesting problem while trying to implement default values for the memberwise synthesized constructor for structures. For the most part I have a majority of expressions working, but I tried out closures and I'm running into an issue where I need to clone the closureexpr, which is fine, but I receive error: struct declaration cannot close over value '$0' defined in outer scope with the following piece of code:

struct X {
  var y: (Int) -> Int = { $0 }

  // Compiler Synthesized
  init(y: @escaping (Int) -> Int = { $0 })
}

This makes sense because the declrefexpr in the closure is still pointing to the variable initialization context, but I need to be able to change all decl contexts in the closure body to point to the default argument initializer. Right off the get go this doesn't sound like a "clean" solution because the closure could have many layers of different exprs, decls, or stmts that all point back to the pattern binding context. Is there a way to achieve this? Or is there possibly a better way of cloning the closureexpr?

Variable Pointing Context (just the good parts):
(declref_expr decl=X.pattern binding initializer.explicit)

The default value is generating something like the above instead of generating the below (which makes sense, but how can I walk through the whole body and fix it?).

Default Argument Pointing Context (just the good parts):
(declref_expr decl=X.init(y:).default argument initializer.explicit)

In this case it's not the DeclContext but the Decl itself you need to worry about, and that's a simpler problem. When cloning a type-checked closure, you can build a map of all the argument ParamDecls before and after and then replace the old ones in any DeclRefExprs you encounter.

The DeclContext thing is also tricky. You need to "reparent" each cloned declaration and DeclContext by giving them the correct parent DeclContext on creation. Since you're doing the clone as a tree-walk (presumably), it shouldn't be too hard to keep this information.

If there's one place we do this today, it would be in the implementation of lazy, which synthesizes a getter from the top-level expression. You can find at least some of this code in CodeSynthesis.cpp. But I can't remember if we actually do it by tree-walking—it's possible we just reuse the existing expression. (I hope we reparent closure contexts there but maybe we don't do that either!)

1 Like

Oh, that suggests one other option: instead of cloning, it might be worth making some secret helper function and then calling that from both places.

Ah yes, I see where lazy does something like this:

InitValue->walk(RecontextualizeClosures(Get));

This might be a good start.

On the other suggestion you had, are you suggesting something like the following?

struct X {
  var y: (Int) -> Int = yImpl

  // Compiler synthesized
  func yImpl(tmp0: Int) -> Int {
    return tmp0
  }

  // Compiler synthesized
  init(y: @escaping (Int) -> Int = yImpl)
}

Or are you suggesting something different?

Even simpler: something that will work in all cases and not just for closures.

var y: (Int) -> Int = yImpl()

func yImpl() -> (Int) -> Int {
  return { $0 }
}
1 Like

Wouldn’t I have to walk the closure anyway to set the decls to the function? Given a variable set to a closure, we would need to clone the closure anyway for the return statement no?

I'm thinking you'd just move it instead of cloning it. Then the only thing you have to do is set the closure's DeclContext, not anything else.

(Of course, there may be more than one top-level closure in the initial value expression, so you still have to do a walk.)

Thanks Jordan. When I have time today I’m going to play around with this. Is there a formal name that we can give this function?

Heh. lazy makes a second stored property with a name of y.storage; by analogy we should do something like y.initialValue or something. Having the dot in there makes sure it doesn't collide with anything else.

By the way, you'll want to make sure to make it final when in a class.

Hey Jordan, I was able to successfully implement this, mostly. I had to create a static function because I got the infamous cannot use instance member 'y.initialValue()' as a default parameter along with the property initializer error.

User writes:

struct X {
  var y: (Int) -> Int = { $0 }
}

Compiler Synthesizes:

struct X {
  var y: (Int) -> Int = X.y.initialValue()

  init(y: @escaping (Int) -> Int = X.y.initialValue())

  static func y.initialValue() -> (Int) -> Int {
    return { $0 }
  }
}

The only noticeable part about where this doesn't work is when I'm in the debug repl and I try :print_decl X. I get Assertion failed: (Range.isValid() && "range should be valid") when the repl attempts to print the default argument value with an invalid DeclNameLoc.

auto *funcRef = new (C) UnresolvedDeclRefExpr(name,
                                              DeclRefKind::Ordinary,
                                              DeclNameLoc());

Is there a way I can provide this a somewhat legitimate declnameloc?

Static function is definitely the way to go, yep.

I don't think you can provide a real DeclNameLoc(); instead you should go the other way and figure out what's expecting it to have a valid location.

Well I've found a solution where I can explicitly set the default value's string representation

SmallString<64> callText = decl->getName().str();
callText += ".";
callText += func->getName().str();
callText += "()";
auto stringRep = C.AllocateCopy(callText.str());
arg->setDefaultValueStringRepresentation(stringRep);

Given:

struct X {
  var y: (Int) -> Int = { $0 }
}

The initializer will print:

init(y: @escaping (Int) -> Int = X.y.initialValue())

The string representation is just for printing; it's not used for actual code generation.

Right and the crash was happening when the repl wanted to print the default value.

Oh, I see what you mean. Okay, this might be the best solution then. cc @harlanhaskins

I guess it should be fine since this decl is totally implicit. I wouldn't want this memberwise decl or the initialValue function itself to show up in parseable interfaces, though. As long as all these decls are internal, this should be fine. Great work!

1 Like

Hmm, this might not be so fine after all.

struct X {
  var y: (Int) -> Int = { $0 }
  @inlinable init() {}
}

If you add that static func, then it'll have to be @usableFromInline in a @_fixed_layout type.

struct X {
  var y: (Int) -> Int = X.y.initialValue()
  @usableFromInline
  static func y.initialValue() -> (Int) -> Int {
    return { $0 }
  }
  @inlinable init() {
    // implicit:
    //   this wouldn't work if y.initialValue wasn't @usableFromInline
    self.y = X.y.initialValue()
  }
}
1 Like

The memberwise constructor is not created if we find an explicitly declared constructuor in the struct (although this is something that I might look into):

struct X {
  var y = 0
  @inlinable init() {}
}

let x = X(y: 16) // argument passed to call that takes no arguments.
1 Like

I think it can still happen if the inlinable init is in an extension in the same file.

I don't think it's the end of the world if we have to make this function usable-from-inline. In fact, it should probably be @inlinable itself, since we don't want it to have extra overhead.

public struct X {
  public var y: (Int) -> Int = { $0 }
}

extension X {
  @inlinable init(y: Int) {}
}

_ = X()
_ = X(y: 6)
Terms of Service

Privacy Policy

Cookie Policy