From the review of: SILBuilder ergonomics for improved debug scope handling #16355
Author: @vedantk
I'm responding in the forum because I'd like to have Vedant and Adrian's comments recorded for posterity.
@Adrian_Prantl says:
I don't think that there is a case where an instruction's scope should ever be changed without also changing its location. For all practical purposes scope and location form an inseparable unit. (The one exception is inlining, because the scope happens to be the data structure where the inline history of an instruction is stored, but that's almost an implementation detail).
Good. That was my original understanding until I once asked about the purpose of SILBuilderWithScope
and was told something along the lines that we can't break the debugger by jumping between scopes.
If instructions are simply supposed to propagate their location info, that's much more straightforward. Now I really don't understand the purpose of SILBuilderWithScope
, If scope and location are tied together, why is one enforced during construction and another independently passed per instruction. Why don't we derive the scope from the debug location. Wouldn't that have avoided all our inconsistent debug info problems?
It would help to enumerate the use cases and provide a strawman API even if this PR doesn't implement everything. Caveat: this is off the top of my head, so I'm sure we'll see the reality is more complex once we look at replacing actual use cases.
Case #1: Polymorphism, and cases we haven't thought of
A noninstantiable base class can exist for polymorphic use cases and future extension:
class SILBuilderBase {
protected:
SILBuilderBase();
public:
virtual createSomeSILInstr(WithOperands);
}
Case #2: Replacement
I'd say the vastly predominate case is instruction replacement. That should be the simplest and strictest:
/// This corresponds to a single insert point the inherits scope+location from /// an existing instruction.
class SILBuilder final: public SILBuilderBase {
// Insert instructions here and inherit debug location from this position.
SILBuilder(SILInstruction *atPosition);
}
Case #3: Code Motion
A secondary, but important use case is code motion, where we separate debug info from insertion point. I see a couple options:
a. Provide another constructor:
// Insert instructions at this position with the given debug location
// *and scope*, which should almost always be derived from location.
// Use this for code motion.
SILBuilder(SILInstruction *atPosition, SILLocation withLoc);
What's wrong with keeping it simple?
b. Use a more specific class name to force people to think harder about it.
// Use this for code motion.
class SILBuilderWithLocation final: SILBuilderBase {
// Insert instructions at this position with the given debug location
// *and scope*, which should almost always be derived from location.
SILBuilder(SILInstruction *atPosition, SILLocation withLoc);
};
Adding another class name doesn't seem very beneficial to me. Either way, I don't think it would be hard for developers to pick the right API for the use case as long as the rules are simple and the API is clearly commented. The underlying issue causing current confusion seems to be the separate handling of debug scope vs. location.
Now, with the simple APIs above, can we eliminate the location argument from all the createInstruction
entry points? It really doesn't belong there.
Case #4: Inlining
It seems easy enough provide more flexible class where scope and location aren't tied together.
SILBuilderWithDebugScope final: SILBuilderBase {
SILBuilder(SILInstruction *atPosition, SILLocation withLoc,
SILDebugScope withScope);
}
As in Case #3, I would be fine with that being just another constructor on plain old SILBuilder.
Case #5: SILGen
SILGen already has its own builder. So why is this a case? Can't it simply construct a SILBuilderWithDebugScope whenever it creates an instruction?
Case #6: Code no-one wants to touch yet
I'd be fine with a DeprecatedSILBuilder initially while we cleanup passes.