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!)
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?
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?
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!
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.
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.