I'm working on implementation for swift-evolution/NNNN-closures-as-structs.md at master · nickolas-pohilets/swift-evolution · GitHub. I'm pretty new to the compiler codebase, and still feeling a bit lost, but slowly making my way through.
I would appreciate some feedback on my implementation plan so far and some help with filling in the gaps.
Proposal describes being able to satisfy potential any protocol requirement with closure body. I'm not sure this is solvable in general case, so for now I've limited my scope only to a single callAsFunction
requirement.
Implementation consists of two parts:
- Detect if closure should be transformed at all, and if it should - what protocols it should conform to, which protocol requirement should be satisfied by the closure body, and types for associated types used in the signature of the protocol requirement.
- Do the AST re-writing.
First part
This is mostly about constraint solver.
I'm planning to introduce a new type, something like ClosureAsStructType
. This type should be used as a placeholder for the type of the structure created. If closure got assigned this type after type-checking - that means it should be re-writen. After re-writing this type will disappear. But depending on where re-writing will happen, it may escape from the constraint solver. Do I understand correctly that in order to be UNCHECKED_TYPE
it should not escape from constraint solver and be allocated in the constraint solver area?
Current logic always assigns FunctionType
to a closure. I'm planning to assign a fresh type variable to it, and create a disjunction of two constraints equality constraints - either to FunctionType
, or to a ClosureAsStructType
. Or should I keep FunctionType
assigned to a closure, and assign type variable to something else?
I suspect assigning a fresh type variable to every closure may cause a performance regression. But for now I'm concerned about getting something working, and will return to performance optimisations later.
I'm planning to update the score calculation system, so that overloads where closure literal matches with function type should win against overloads where closure get-s re-written. That's mostly for backward compatibility and can be reversed later. If we ever make function types true protocols, should be prioritise them differently compared to hand-written protocols? Or should we prioritise between generic vs existential types? Looks like currently existentials win for regular protocols (but I suspect there might be nuances), which is consistent with current decision to prefer function types to closure-as-struct.
When processing protocol conformance constraint against a ClosureAsStructType
, constraint is always solved (unless protocol requires a class), but matched protocol is recorded for the future processing. I should be able to backtrack matched protocols, so looks like it should be stored in the ConstraintSystem
and reverted by the SolverScope
.
I don't know yet how to handle protocols that don't have suitable requirement or where requirement is ambiguous. Probably they should be silently ignored if there are other overloads available.
To validate protocol requirements, I need to know that I've seen all the protocol requirements. How can I ensure that in the constraint solver?
Also, I need to match FunctionType
of the closure with the type of the protocol requirement. I think I can handle this by creating a bunch of constraints of existing kinds when processing protocol requirements. But maybe this should be a new kind of constraint between ClosureAsStructType
and FunctionType
.
I need to keep track of association between ClosureAsStructType
and related FunctionType
of the closure. Can I store reference to FunctionType
inside the ClosureAsStructType
, or should I use locators or some other mechanism?
I don't yet fully understand how SolverStep
's work. I'll keep reading the code, but maybe there are any other sources I could check? https://github.com/apple/swift/blob/master/docs/TypeChecker.rst does not seem to cover this.
Second part
First of all, what would be the place to do the re-writing? Should it happen when applying constraint solver solution or later?
I'll need to re-write closure expression into something which is an expression, but may contain function declaration inside. Any alternatives to wrapping all this into an immediately invoked closure? Can I even construct an AST for closure with return type that is declared inside the closure body? Alternatively I can sacrifice locality of the re-writing and put all the declarations for anonymous structs in the top of the parent scope and re-write closure expressions into sole structure instance construction.
Re-writing calls to super
may a bit challenging. Will SuperRefExpr
work fine, if it's Self
is not an actual self
of the struct, but a property in the struct?