[cfe-dev] PATCH: Cleanup function redeclaration representations
Chris Lattner
clattner at apple.com
Sat May 3 23:59:03 PDT 2008
On May 1, 2008, at 6:57 PM, Doug Gregor wrote:
> Chris - there's a patch in here that you probably want to take a
> look at.
Reading the backthread, it is a very interesting and nasty topic. I
strongly agree with the meta points that Argiris is making:
It is bad for clients if parsing new decls causes old decls to change
(e.g. due to swapping). Looking forward, this will be a significant
hindrance if we want to do things like incremental reparsing. It also
breaks a number of useful invariants for clients: the codegen
assertion in the testsuite is basically due to the argument list for a
functiondecl changing between the first time the client (codegen) sees
a decl and the second time it sees it [the first time it sees it, the
function is "void()" the second time it is "void(int)"]. Decls
changing is surprising and weird. :)
It is also nice to be able to capture (as Argiris says) information
that is reasonably close to the way the user wrote it. This would
argue for representing:
void foo(int x, int y = 4);
void foo(int x = 12, int y);
... as two functiondecls, each of which that have *one* default
argument specified.
> On Fri, Apr 25, 2008 at 4:42 AM, Argiris Kirtzidis
> <akyrtzi at gmail.com> wrote:
>> Doug Gregor wrote:
>> How about
>>
>> (1) -#2 is checked for error diagnostics by considering default
>> args from
>> both #1 and #2
>>
>>
>> And in the end, only #1 gets the merged args. This is the same as
>> only #2
>> gets the merged args.
>
> I went a slightly different way... both #1 and #2 get the merged args
> (so each shows the complete state at that time), but we use
> CXXDefauiltArgExpr for default arguments that were the result of
> merging default arguments. That way, we always know exactly which
> parameter (in which declaration) the default argument originally came
> from.
Ok, so this is effectively handle the above as:
void foo(int x, int y = 4);
void foo(int x = 12, int y = <defaultargexpr 4>);
Where <defaultargexpr 4> points to the '4' in the previous foo
prototype? This way you can distinguish between arguments that are
explicitly specified from those that were inherited from previous
declarations? How does this handle cases like this:
void foo()
void foo(int x);
void foo()
case?
>> Actually, I'm thinking that maybe there could be a common
>> convention that,
>> not only functions, but vars and namespaces should follow.
>> An extended namespace definition could be regarded as a
>> redeclaration.
>
> Yes, agreed. I've moved addRedeclaration (and its brethren) into
> ScopedDecl, so the same approach can apply to extern variables,
> functions, namespaces, classes, etc.
Nice. I think it is very important to pick a model and be consistent.
>> I agree that it is philosophical, thus I don't really care much
>> about the
>> name pointing to the first or the last decl.
>> What bothers me is the "swapping content" bit. It seems to me that
>> feeding
>> to the consumer the same pointer for, essentially, different
>> declaration objects is a source of complexity and possible subtle
>> bugs,
>> without much benefit.
>
> I was never quite thrilled with the swapping bit, either.
Unfortunately, I did a minor cleanup of the swapping code before
reading the whole thread. Maybe that code can just vanish :)
>> I hope I'm not coming off as too pedantic :)
>
> It's a C++ front end, so it pays to be pedantic.
:)
> Anyway, you've convinced me. The attached patch makes a few changes to
> the way redeclarations are handled:
>
> 1) The swapping behavior is gone; instead, we merge into the
> redeclaration and into the original declaration.
I'm concerned about this. It violates the sanctity of the original
declaration :)
> 2) The addRedeclaration/getNextRedeclaration function has moved into
> ScopedDecl, so it can be used for other kinds of declarations
> 3) We use a DenseMap to store the redeclaration links, since so few
> actual ScopedDecls will have redeclarations
> 4) We keep track of where merged default arguments came from, so
> that we can provide better diagnostics for them.
I like this approach much better than the 'swapping' approach.
However, it still has a some funny effects.
Here is a crazy idea which may be completely impractical :). It seems
that we have two different desires here: 1) represent declarations as
the user wrote them so that clients can have a simple model where
decls are just streaming by. Some of these clients also want the
ability to walk the list of redefinitions. 2) provide sema (and some
other clients) an 'aggregate' view of the decl where all the info is
merged together into one place. In the previous example:
void foo(int x, int y = 4);
void foo(int x = 12, int y);
We want the clients to be able to see those two decls, but also one
aggregate decl of:
void foo(int x = 12, int y = 4);
This saves each client from having to walk the entire list of
redeclarations to pull together all the default arguments.
Lets assume that functions are usually not redefined, and when they
are that the redefinitions are often exactly the same (plus perhaps a
body). Given this, maybe a spin on the 'canonical type' idea would
work.
Ignoring efficiency, imagine if the first time we parsed a decl that
we actually created two versions of the decl: one version (the parsed
version) that represents the decl as written, and a second version
(the aggregate version) that is updated as other redecls are parsed.
After parsing the decl the first time, the two decls are exactly the
same:
void foo(int x, int y = 4); void foo(int x, int y = 4);
When parsing the redeclaration, we create another 'parse decl' for the
redeclaration:
void foo(int x, int y = 4); void foo(int x, int y = 4);
void foo(int x = 12, int y);
Then we call mergedecl, and it updates the aggregate version to
contain all the goop [this is a technical term] merged together for
the two declarations:
void foo(int x, int y = 4);
void foo(int x = 12, int y); void foo(int x = 12, int y = 4);
If we later see a definition (e.g. "void foo(int x, int y) {}"), we
parse it, but add the actual body to the aggregate version:
void foo(int x, int y = 4);
void foo(int x = 12, int y);
void foo(int x, int y)<defn>; void foo(int x = 12, int y = 4) {}
Instead of adding the body to the "parsed version" of the proto, we
add it to the aggregate version, and use a flag on the 'parsed
version' to say that it was responsible for providing the definition
or not.
Again, if we ignore efficiency, I think this provides an interesting
sweet spot: all of these [re]decls would be linked together, so
clients could walk the list. Also, parsing a redefinition does not
cause the previous definition to change. Clients who care about the
aggregate semantics of a decl could just always look at the 'aggregate
version' of the decl (similar to the 'canonical' version of a type),
which would always have the union of information known about the
decl. This is also reasonably easy to implement: merge decls just
handles the construction and updating of the aggregate version, and it
has all the information it could ever want to provide really accurate
diagnostics.
To me, the only bad thing about this approach is that it is
(incredibly!) memory inefficient. Allocating twice the number of
decls in the common case (where there is only one decls for each
object) is badness. However, this case is also the trivial case where
the aggregate and the parsed decl are exactly the same. :) As such, I
propose that we add two fields to ScopeDecl (?): a 'next
redeclaration' pointer and 'is aggregate redecl' bool.
It would actually be implemented as:
I. Sema handles the ActOnFooDecl Action by creating a new decl object,
verifying that it is self consistent, and then calling mergedecls
unconditionally.
II. Mergedecls gets the decl, sees that it is the first declaration
for the object. Since the 'parsed' and 'aggregate' declaration is the
same, set 'nextredecl' pointer to point to itself, and set
'isaggregate' to true. This saves allocation of a second decl in the
common case. For example, imagine we just parsed:
void foo(int x, int y); // #1
we now get a "foo" decl, which I'll write schematically as (numbering
redeclarations, really the numbers would be pointers of course):
1: foo [nextredecl=1, isaggregate=true] void foo(int x, int y);
Scope[foo] = 1 // This is the scope chain for foo
III. The next time MergeDecls is called, there are two possibilities:
first if the redecl is exactly the same (no information is added) then
existing redecl is set as the next pointer and isaggregate is set to
false:
void foo(int x, int y); // #2
1: foo [nextredecl=1, isaggregate=true] void foo(int x, int y);
2: foo [nextredecl=1, isaggregate=false] void foo(int x, int y);
Scope[foo] = 2
At this point, the scope object says that "#2" is the last
redeclaration of 'foo'. From that, we can walk the 'nextredecl' list
to see #1. When we get to it, we see that the next link is the
aggregate version, so clients can decide whether they want to get the
aggregate version, or just stop if they want to see decls
corresponding to what the user wrote.
IV. When mergedecls is called and there *is* new information, two
things can happen. So far, there is not an explicit 'aggregate'
version. As such, we now create a new aggregate version of it ("A"),
and update things:
void foo(int x, int y = 0); // #3
A: foo [nextredecl=A, isaggregate=true] void foo(int x, int y = 0);
1: foo [nextredecl=A, isaggregate=true] void foo(int x, int y);
2: foo [nextredecl=1, isaggregate=false] void foo(int x, int y);
3: foo [nextredecl=2, isaggregate=false] void foo(int x, int y = 0);
Scope[foo] = 3
At this point, users can walk the redefn list and see all the 'parsed'
definitions 1/2/3 and other clients can also get the full aggregate
version (A). We can distinguish between "recycled" aggregate versions
and explicit aggregate versions because 'nextredecl' points to 'this'
in the reuse case.
V. The second case when adding information is that the explicit
aggregate version already exists. In that case, we just update (A):
void foo(int x = 12, int y) // #4
A: foo [nextredecl=A, isaggregate=true] void foo(int x = 12, int y =
0);
1: foo [nextredecl=A, isaggregate=true] void foo(int x, int y);
2: foo [nextredecl=1, isaggregate=false] void foo(int x, int y);
3: foo [nextredecl=2, isaggregate=false] void foo(int x, int y = 0);
4: foo [nextredecl=3, isaggregate=false] void foo(int x = 12, int y);
Scope[foo] = 4
etc. Of course, the bool and pointer can be swizzeled together so the
bool goes in the low bit of the pointer.
I think this is a fairly reasonable sweet spot, what do you guys think?
-Chris
More information about the cfe-dev
mailing list