RFC: -flimit-debug-info + frame variable

Hello all,

With the expression evaluation part of -flimit-debug-info nearly
completed, I started looking at doing the same for the "frame variable"
command.

I have thought this would be simpler than the expression evaluator,
since we control most of that code. However, I have hit one snag, hence
this RFC.

The problem centers around how to implement
ValueObject::GetChildMemberWithName, which is the engine of the
subobject resultion in the "frame variable" command. Currently, this
function delegates most of the work to
CompilerType::GetIndexOfChildMemberWithName, which returns a list of (!)
indexes needed to access the relevant subobject. The list aspect is
important, because the desired object can be in a base class or in a C11
anonymous struct member.

The CompilerType instance in question belongs to the type system of the
module from which we retrieved the original variable. Therein lies the
problem -- this type system does not have complete information about the
contents of the base class subobjects.

Now, my question is what to do about it. At the moment, it seems to me
that the easiest solution to this problem would be to replace
CompilerType::GetIndexOfChildMemberWithName, with two new interfaces:
- Get(IndexOf)**Direct**ChildMemberWithName -- return any direct
children with the given name
- IsTransparent -- whether to descend into the type during name lookups
(i.e., is this an anonymous struct member)

The idea is that these two functions (in conjunction with existing
methods) can provide their answers even in a -flimit-debug-info setting,
and they also provide enough information for the caller to perform the
full name lookup himself. It would first check for direct members, and
if no matches are found, (recursively) proceed to look in all the
transparent members and base classes, switching type systems if the
current one does not contain the full type definition.

The downside of that is that this would hardcode a specific, c++-based,
algorithm which may not be suitable for all languages. Swift has a
fairly simple inheritance model, so I don't think this should be a
problem there, but for example python uses a slightly different method
to resolve ambiguities. The second downside is that a faithful
implementation of the c++ model, including the virtual inheritance
dominance is going to be fairly complicated.

The first issue could be solved by moving this logic into the clang
plugin, but making it independent of any specific type system instance.
The second issue is unavoidable, except by creating a unified view of
the full type in some scratch ast context, as we do for expression
evaluation.

That said, it's not clear to me how faithful do we need the "frame
variable" algorithm to be. The frame variable syntax does not precisely
follow the c++ semantics anyway. And a simple "recurse into subclasses"
algorithm is going to be understandable and be "close enough" under
nearly all circumstances. Virtual inheritance is used very seldomly, and
shadowing of members defined in a base class is even rarer.

While analysing this code I've found much more serious bugs (e.g.,
accessing a transparent member fetches a random other value if the class
it is in also has base cases; fetching a transparent member in a base
class does not work at all), which seem to have existed for quite some
time without being discovered.

For that reason I am tempted to just implement a basic "recurse into
subclasses" algorithm inside ValueObject::GetChildMemberWithName, and
not bother with the virtual inheritance details, nor with being able to
customize this algorithm to the needs of a specific language.

What do you think?

regards,
Pavel

Thanks for the write up!

I agree that the existing APIs are useful for exploring the types as they appear (and are completed within) in the module they came from. Now we are asking more complex questions from them. As with all software, things started out simple and have gotten quite a bit more complex as we went along and added interfaces and new requirements.

My first thought was that as soon as you dive into a CompilerType with a question about the type itself, or anything contained within, where any parts of the type can be incomplete, you can't just use a CompilerType on its own. Each CompilerType might be able to tell you the module, and possibly only a target for expressions ASTs or target ASTs for expression results, but it might not have access to a target if we have a type from a module.

So we need new API calls where you must supply a target so that you can find the type in other modules when you find something that we know is incomplete and need the real type. We can add new APIs to the TypeSystem class to take care of this and these APIs will need to take a target to allow finding types outside of the current module's types. If we add new APIs and switch any code that requires resolving of types on the fly over to using the new APIs, we might be able to leave the old APIs in place or remove them if they are no longer used after the refactor.

Or the other option is to try and leave the TypeSystem and CompilerType stuff alone and add a new "TargetType" class that has a target + CompilerType. And lookups on those types can be smart about where they grab types? They could still call through to new TypeSystem virtual functions that use the target for resolving types.

More comments inlined below between your paragraphs.

Hello all,

With the expression evaluation part of -flimit-debug-info nearly
completed, I started looking at doing the same for the "frame variable"
command.

I have thought this would be simpler than the expression evaluator,
since we control most of that code. However, I have hit one snag, hence
this RFC.

The problem centers around how to implement
ValueObject::GetChildMemberWithName, which is the engine of the
subobject resultion in the "frame variable" command. Currently, this
function delegates most of the work to
CompilerType::GetIndexOfChildMemberWithName, which returns a list of (!)
indexes needed to access the relevant subobject. The list aspect is
important, because the desired object can be in a base class or in a C11
anonymous struct member.

The CompilerType instance in question belongs to the type system of the
module from which we retrieved the original variable. Therein lies the
problem -- this type system does not have complete information about the
contents of the base class subobjects.

yes, and this requires a target (or a module list from the target to be more precise) in order to answer the questions.

Now, my question is what to do about it. At the moment, it seems to me
that the easiest solution to this problem would be to replace
CompilerType::GetIndexOfChildMemberWithName, with two new interfaces:
- Get(IndexOf)**Direct**ChildMemberWithName -- return any direct
children with the given name
- IsTransparent -- whether to descend into the type during name lookups
(i.e., is this an anonymous struct member)

The idea is that these two functions (in conjunction with existing
methods) can provide their answers even in a -flimit-debug-info setting,
and they also provide enough information for the caller to perform the
full name lookup himself. It would first check for direct members, and
if no matches are found, (recursively) proceed to look in all the
transparent members and base classes, switching type systems if the
current one does not contain the full type definition.

The downside of that is that this would hardcode a specific, c++-based,
algorithm which may not be suitable for all languages. Swift has a
fairly simple inheritance model, so I don't think this should be a
problem there, but for example python uses a slightly different method
to resolve ambiguities. The second downside is that a faithful
implementation of the c++ model, including the virtual inheritance
dominance is going to be fairly complicated.

Sounds like that can work easily for C/C++. I would prefer to leave things up to the type systems for name lookups so they can each do the lookup however they can by using the type itself and or looking up completed types in the target's modules. The fact that the current solution for name lookup relies on indexes was just a convenience and happened to work for C/C++ and the static typing we have had up until your new support. The index solution isn't required in any new solution.

The first issue could be solved by moving this logic into the clang
plugin, but making it independent of any specific type system instance.
The second issue is unavoidable, except by creating a unified view of
the full type in some scratch ast context, as we do for expression
evaluation.

Creating a complete new type in an AST would be a possible solution that avoids all of this, but then do we have ValueObject objects that each have a scratch AST? or do we fill up the target scratch AST up and hope for no collisions? We can avoid needing to copy the type over into the scratch AST if we know it has all completed information. So maybe ValueObjects can do a quick check on the type to see if anything requires completion and only copy the type over to the target scratch AST if needed. The main downfall there is we might end up completing a lot of the type when we don't need to when we make this copy. But it has the benefit of sharing code with the expression parser, so we know it would be kept up to date.

That said, it's not clear to me how faithful do we need the "frame
variable" algorithm to be. The frame variable syntax does not precisely
follow the c++ semantics anyway. And a simple "recurse into subclasses"
algorithm is going to be understandable and be "close enough" under
nearly all circumstances. Virtual inheritance is used very seldomly, and
shadowing of members defined in a base class is even rarer.

While analysing this code I've found much more serious bugs (e.g.,
accessing a transparent member fetches a random other value if the class
it is in also has base cases; fetching a transparent member in a base
class does not work at all), which seem to have existed for quite some
time without being discovered.

For that reason I am tempted to just implement a basic "recurse into
subclasses" algorithm inside ValueObject::GetChildMemberWithName, and
not bother with the virtual inheritance details, nor with being able to
customize this algorithm to the needs of a specific language.

What do you think?

IMHO it would be nice to let the TypeSystem class handle the GetChildMemberWithName() in a language specific kind of way. There could possibly be a default implementation in the TypeSystem code that matches what we are currently doing. This new API could have a target pointer as an argument to allow it to search in other modules in the target is specified. Swift always have complete definitions of types, so there are no worries for Swift that I know of.

I am not tied to my suggestion and really want to hear what others think on this as this are my thoughts after briefly thinking about the issue.

Greg

It seems like you are having to work hard in the ValueObject system because you don’t want to use single AST Type for the ValueObject’s type. Seems like it be much simpler if you could cons up a complete type in the ScratchASTContext, and then use the underlying TypeSystem to do the layout computation.

Preserving the full type in the scratch context also avoids other problems. For instance, suppose module A has a class that has an opaque reference to a type B. There is a full definition of B in modules B and C. If you make up a ValueObject for an object of type A resolving the full type to the one in Module B you can get into trouble. Suppose the next user step is over the dlclose of module B. When the local variable goes to see if it has changed, it will stumble across a type reference to a module that’s no longer present in the program. And if somebody calls RemoveOrphanedModules it won’t even be in the shared module cache.

You can try to finesse this by saying you can choose the type from the defining module so it can’t go away. But a) I don’t think you can know that for non-virtual classes in C++ and I don’t think you guarantee you can know how to do that for any given language.

I wonder if it wouldn’t be a better approach to build up a full compiler-type by importing the types you find into the scratch AST context. That way you know they can’t go away. And since you still have a full CompilerType for the variable, you can let the languages tell you where to find children based on their knowledge of the types.

Jim

BTW, Adrian already did something along these lines for the dynamic types of swift “frame variable” values. In that case, the dynamic type is quite likely from some entirely unrelated module. Swift makes a lot of use of protocols, so code is going to pass through your module that shares no actual types in common with it… So you really don’t want to pollute the module's TypeSystem with these unrelated dynamic types. His solution was to put the dynamic type results in the scratch AST context. I don’t remember the details of his implementation, but he probably does…

Jim

I do see the attractiveness of constructing of a full compiler type. The
reason I am hesitant to go that way, because it seems to me that this
would negate the two main benefits of the frame variable command over
the expression evaluator: a) it's fast; b) it's less likely to crash.

And while I don't think it will be as slow or as crashy as the
expression evaluator, the usage of the ast importer will force a lot
more types to be parsed than are strictly needed for this functionality.
And the insertion of all potentially conflicting types from different
modules into a single ast context is also somewhat worrying.

The dlclose issue is an interesting one. Presumably, we could ensure
that the module does not go away by storing a module shared (or weak?)
pointer somewhere inside the value object. BTW, how does this work with
ValueObject casts right now? If I cast a ValueObject to a CompilerType
belonging to a different module, does anything ensure this module does
not go away? Or when dereferencing a pointer to an type which is not
complete in the current module?

I'm hoping that this stuff won't be "hard work". I haven't prototyped
the code yet, but I am hoping to keep this lookup code in under 200 LOC.
And as Greg points out, there are ways to put this stuff into the type
system -- I'm just not sure whether that is needed given that the
ValueObject class is the only user of the GetIndexOfChildMemberWithName
interface. The whole function is pretty clearly designed with
ValueObject::GetChildMemberWithName in mind.

Another thing I like about this approach is that it will mostly use the
same code path for the limit and no-limit debug info scenarios. OTOH,
I'm pretty sure we would want to use the scratch context thingy only for
types that are really not complete in their own modules, which would
leave the scratch context method as a fairly complex, but rarely
exercised path.

pl

It seems like you are having to work hard in the ValueObject system because you don’t want to use single AST Type for the ValueObject’s type. Seems like it be much simpler if you could cons up a complete type in the ScratchASTContext, and then use the underlying TypeSystem to do the layout computation.

Preserving the full type in the scratch context also avoids other problems. For instance, suppose module A has a class that has an opaque reference to a type B. There is a full definition of B in modules B and C. If you make up a ValueObject for an object of type A resolving the full type to the one in Module B you can get into trouble. Suppose the next user step is over the dlclose of module B. When the local variable goes to see if it has changed, it will stumble across a type reference to a module that’s no longer present in the program. And if somebody calls RemoveOrphanedModules it won’t even be in the shared module cache.

You can try to finesse this by saying you can choose the type from the defining module so it can’t go away. But a) I don’t think you can know that for non-virtual classes in C++ and I don’t think you guarantee you can know how to do that for any given language.

I wonder if it wouldn’t be a better approach to build up a full compiler-type by importing the types you find into the scratch AST context. That way you know they can’t go away. And since you still have a full CompilerType for the variable, you can let the languages tell you where to find children based on their knowledge of the types.

I do see the attractiveness of constructing of a full compiler type. The
reason I am hesitant to go that way, because it seems to me that this
would negate the two main benefits of the frame variable command over
the expression evaluator: a) it's fast; b) it's less likely to crash.

And while I don't think it will be as slow or as crashy as the
expression evaluator, the usage of the ast importer will force a lot
more types to be parsed than are strictly needed for this functionality.
And the insertion of all potentially conflicting types from different
modules into a single ast context is also somewhat worrying.

I agree here. Frame variable should do as little as possible when dealing with a ValueObject and its type, so only completing the parts of the type we know are transparent it a good approach.

The dlclose issue is an interesting one. Presumably, we could ensure
that the module does not go away by storing a module shared (or weak?)
pointer somewhere inside the value object. BTW, how does this work with
ValueObject casts right now? If I cast a ValueObject to a CompilerType
belonging to a different module, does anything ensure this module does
not go away? Or when dereferencing a pointer to an type which is not
complete in the current module?

I am not sure dlclose is a problem, the module won't usually be cleaned up. And that shared library shouldn't have the definition we need and be able to be unloaded IIUC how the -flimit-debug-info stuff works.

I'm hoping that this stuff won't be "hard work". I haven't prototyped
the code yet, but I am hoping to keep this lookup code in under 200 LOC.
And as Greg points out, there are ways to put this stuff into the type
system -- I'm just not sure whether that is needed given that the
ValueObject class is the only user of the GetIndexOfChildMemberWithName
interface. The whole function is pretty clearly designed with
ValueObject::GetChildMemberWithName in mind.

We should be able to code it into ValueObject, or maybe just into TypeSystem base class?

It seems like you are having to work hard in the ValueObject system because you don’t want to use single AST Type for the ValueObject’s type. Seems like it be much simpler if you could cons up a complete type in the ScratchASTContext, and then use the underlying TypeSystem to do the layout computation.

Preserving the full type in the scratch context also avoids other problems. For instance, suppose module A has a class that has an opaque reference to a type B. There is a full definition of B in modules B and C. If you make up a ValueObject for an object of type A resolving the full type to the one in Module B you can get into trouble. Suppose the next user step is over the dlclose of module B. When the local variable goes to see if it has changed, it will stumble across a type reference to a module that’s no longer present in the program. And if somebody calls RemoveOrphanedModules it won’t even be in the shared module cache.

You can try to finesse this by saying you can choose the type from the defining module so it can’t go away. But a) I don’t think you can know that for non-virtual classes in C++ and I don’t think you guarantee you can know how to do that for any given language.

I wonder if it wouldn’t be a better approach to build up a full compiler-type by importing the types you find into the scratch AST context. That way you know they can’t go away. And since you still have a full CompilerType for the variable, you can let the languages tell you where to find children based on their knowledge of the types.

I do see the attractiveness of constructing of a full compiler type. The
reason I am hesitant to go that way, because it seems to me that this
would negate the two main benefits of the frame variable command over
the expression evaluator: a) it’s fast; b) it’s less likely to crash.

And while I don’t think it will be as slow or as crashy as the
expression evaluator, the usage of the ast importer will force a lot
more types to be parsed than are strictly needed for this functionality.
And the insertion of all potentially conflicting types from different
modules into a single ast context is also somewhat worrying.

Importation should be incremental as well, so this shouldn’t make things that much slower. And you shouldn’t ever be looking things up by name in this AST so you wouldn’t be led astray that way. You also are going to have to do pretty much the same job for “expr”, right? So you wouldn’t be opening new dangerous pathways.

OTOH, the AST’s are complex beasts, so I am not unmoved by your worries…

The dlclose issue is an interesting one. Presumably, we could ensure
that the module does not go away by storing a module shared (or weak?)
pointer somewhere inside the value object. BTW, how does this work with
ValueObject casts right now? If I cast a ValueObject to a CompilerType
belonging to a different module, does anything ensure this module does
not go away? Or when dereferencing a pointer to an type which is not
complete in the current module?

I don’t think at present we do anything smart about this. It’s just always bugged me at the back of my brain that we could get into trouble with this, and so I don’t want to do something that would make it worse, especially in a systemic way.

I’m hoping that this stuff won’t be “hard work”. I haven’t prototyped
the code yet, but I am hoping to keep this lookup code in under 200 LOC.
And as Greg points out, there are ways to put this stuff into the type
system – I’m just not sure whether that is needed given that the
ValueObject class is the only user of the GetIndexOfChildMemberWithName
interface. The whole function is pretty clearly designed with
ValueObject::GetChildMemberWithName in mind.

It seems fine to me to proceed along the lines you propose. If it ends up being smooth sailing, I can’t see any reason not to do it this way. When/If you end up having lots of corner cases to manage, would be the time to consider cutting back to using the real type system to back these computations.

Another thing I like about this approach is that it will mostly use the
same code path for the limit and no-limit debug info scenarios. OTOH,
I’m pretty sure we would want to use the scratch context thingy only for
types that are really not complete in their own modules, which would
leave the scratch context method as a fairly complex, but rarely
exercised path.

This is a legit concern, offset a bit by the fact that this is an area where it would be fairly easy to construct relevant test scenarios for corner cases.

Jim

I do see the attractiveness of constructing of a full compiler type. The
reason I am hesitant to go that way, because it seems to me that this
would negate the two main benefits of the frame variable command over
the expression evaluator: a) it's fast; b) it's less likely to crash.

And while I don't think it will be as slow or as crashy as the
expression evaluator, the usage of the ast importer will force a lot
more types to be parsed than are strictly needed for this functionality.
And the insertion of all potentially conflicting types from different
modules into a single ast context is also somewhat worrying.

Importation should be incremental as well, so this shouldn’t make things
that much slower. And you shouldn’t ever be looking things up by name
in this AST so you wouldn’t be led astray that way. You also are going
to have to do pretty much the same job for “expr”, right? So you
wouldn’t be opening new dangerous pathways.

The import is not as incremental as we might want, and it actually sort
of depends on what is the state of the source ast. Let's the source AST
has types A and B, and A depends on B in some way (say as a method
argument). Let's say that A is complete (parsed) and B isn't. While
importing A, the ast importer will import the method which has the B
argument, but whether it will not descend into B (and cause us to parse it).
If however, B happens to be B already parsed then it will import B and
all of its base classes (but not fields and methods).

On top of that we also have our own additions -- whenever we encounter a
method returning a pointer, we import the pointer target type (this has
to do with covariant return types). These things compound and so even a
simple import can end up importing quite a lot.

I actually tried making the ast importer more lazy -- I have a proof of
concept, but it required adding more explicit lookups into clang's Sema,
so that's why I haven't pursued it yet.

I could also try to disable some of these things for these frame
variable imports (they don't need methods at all), but then I would be
opening new dangerous pathways...

OTOH, the AST’s are complex beasts, so I am not unmoved by your worries...

Yeah... :slight_smile:

The dlclose issue is an interesting one. Presumably, we could ensure
that the module does not go away by storing a module shared (or weak?)
pointer somewhere inside the value object. BTW, how does this work with
ValueObject casts right now? If I cast a ValueObject to a CompilerType
belonging to a different module, does anything ensure this module does
not go away? Or when dereferencing a pointer to an type which is not
complete in the current module?

I don’t think at present we do anything smart about this. It’s just
always bugged me at the back of my brain that we could get into trouble
with this, and so I don’t want to do something that would make it worse,
especially in a systemic way.

Is there a reason we don't store a pointer to the module where the
TypeSystem came from? We could do either do that for all ValueObjects,
or just when the type system changes (casts, dereferences of incomplete
types, and now -flimit-debug-info) ?

I'm hoping that this stuff won't be "hard work". I haven't prototyped
the code yet, but I am hoping to keep this lookup code in under 200 LOC.
And as Greg points out, there are ways to put this stuff into the type
system -- I'm just not sure whether that is needed given that the
ValueObject class is the only user of the GetIndexOfChildMemberWithName
interface. The whole function is pretty clearly designed with
ValueObject::GetChildMemberWithName in mind.

It seems fine to me to proceed along the lines you propose. If it ends
up being smooth sailing, I can’t see any reason not to do it this way.
When/If you end up having lots of corner cases to manage, would be the
time to consider cutting back to using the real type system to back
these computations.

Ok, sounds good. Let me create a prototype for this, and we'll see how
it goes from there. It may take a while because I'm now entangled in
some line table stuff.

The dlclose issue is an interesting one. Presumably, we could ensure
that the module does not go away by storing a module shared (or weak?)
pointer somewhere inside the value object. BTW, how does this work with
ValueObject casts right now? If I cast a ValueObject to a CompilerType
belonging to a different module, does anything ensure this module does
not go away? Or when dereferencing a pointer to an type which is not
complete in the current module?

I am not sure dlclose is a problem, the module won't usually be

cleaned up. And that shared library shouldn't have the definition we
need and be able to be unloaded IIUC how the -flimit-debug-info stuff works.

In a well-behaved application, I think it shouldn't be possible to
dlclose a library if a library inheriting a type from it is still
loaded. However, there's no way to really guarantee that.

For example, and application might have two libraries with different
defintions of a class A, which don't cause conflict because the relevant
symbols are hidden. But when searching for a base class A from a third
library, we end up picking the wrong one. Or the same (odr) class is
defined in two libraries, and we pick the one which gets unloaded,
although the application actually uses the code from the other library.

Now we could try to be fancy and analyze module dependencies, symbol
visibility, etc. but it would still be pretty hard to guarantee that
this really always is the case.

pl

I do see the attractiveness of constructing of a full compiler type. The
reason I am hesitant to go that way, because it seems to me that this
would negate the two main benefits of the frame variable command over
the expression evaluator: a) it's fast; b) it's less likely to crash.

And while I don't think it will be as slow or as crashy as the
expression evaluator, the usage of the ast importer will force a lot
more types to be parsed than are strictly needed for this functionality.
And the insertion of all potentially conflicting types from different
modules into a single ast context is also somewhat worrying.

Importation should be incremental as well, so this shouldn’t make things
that much slower. And you shouldn’t ever be looking things up by name
in this AST so you wouldn’t be led astray that way. You also are going
to have to do pretty much the same job for “expr”, right? So you
wouldn’t be opening new dangerous pathways.

The import is not as incremental as we might want, and it actually sort
of depends on what is the state of the source ast. Let's the source AST
has types A and B, and A depends on B in some way (say as a method
argument). Let's say that A is complete (parsed) and B isn't. While
importing A, the ast importer will import the method which has the B
argument, but whether it will not descend into B (and cause us to parse it).
If however, B happens to be B already parsed then it will import B and
all of its base classes (but not fields and methods).

On top of that we also have our own additions -- whenever we encounter a
method returning a pointer, we import the pointer target type (this has
to do with covariant return types). These things compound and so even a
simple import can end up importing quite a lot.

I actually tried making the ast importer more lazy -- I have a proof of
concept, but it required adding more explicit lookups into clang's Sema,
so that's why I haven't pursued it yet.

Anything we can do along these lines will help folks with large projects. We have been getting slower in this area over the years. But I understand the need to tread with caution here.

I could also try to disable some of these things for these frame
variable imports (they don't need methods at all), but then I would be
opening new dangerous pathways...

OTOH, the AST’s are complex beasts, so I am not unmoved by your worries...

Yeah... :slight_smile:

The dlclose issue is an interesting one. Presumably, we could ensure
that the module does not go away by storing a module shared (or weak?)
pointer somewhere inside the value object. BTW, how does this work with
ValueObject casts right now? If I cast a ValueObject to a CompilerType
belonging to a different module, does anything ensure this module does
not go away? Or when dereferencing a pointer to an type which is not
complete in the current module?

I don’t think at present we do anything smart about this. It’s just
always bugged me at the back of my brain that we could get into trouble
with this, and so I don’t want to do something that would make it worse,
especially in a systemic way.

Is there a reason we don't store a pointer to the module where the
TypeSystem came from? We could do either do that for all ValueObjects,
or just when the type system changes (casts, dereferences of incomplete
types, and now -flimit-debug-info) ?

ValueObjects currently treat their types as a computed not stored entity. There’s not a "CompilerType m_type” ivar, only a pure virtual “CompilerType *GetCompilerType”. But I don’t know whether we’re taking use of that fact or not. But we could broadcast a “ModulesChanged” to the ValueObjects as well as to the Breakpoints and have them react to that.

I'm hoping that this stuff won't be "hard work". I haven't prototyped
the code yet, but I am hoping to keep this lookup code in under 200 LOC.
And as Greg points out, there are ways to put this stuff into the type
system -- I'm just not sure whether that is needed given that the
ValueObject class is the only user of the GetIndexOfChildMemberWithName
interface. The whole function is pretty clearly designed with
ValueObject::GetChildMemberWithName in mind.

It seems fine to me to proceed along the lines you propose. If it ends
up being smooth sailing, I can’t see any reason not to do it this way.
When/If you end up having lots of corner cases to manage, would be the
time to consider cutting back to using the real type system to back
these computations.

Ok, sounds good. Let me create a prototype for this, and we'll see how
it goes from there. It may take a while because I'm now entangled in
some line table stuff.

Excellent, I look forward to seeing what you come up with!

The dlclose issue is an interesting one. Presumably, we could ensure
that the module does not go away by storing a module shared (or weak?)
pointer somewhere inside the value object. BTW, how does this work with
ValueObject casts right now? If I cast a ValueObject to a CompilerType
belonging to a different module, does anything ensure this module does
not go away? Or when dereferencing a pointer to an type which is not
complete in the current module?

I am not sure dlclose is a problem, the module won't usually be

cleaned up. And that shared library shouldn't have the definition we
need and be able to be unloaded IIUC how the -flimit-debug-info stuff works.

In a well-behaved application, I think it shouldn't be possible to
dlclose a library if a library inheriting a type from it is still
loaded. However, there's no way to really guarantee that.

For example, and application might have two libraries with different
defintions of a class A, which don't cause conflict because the relevant
symbols are hidden. But when searching for a base class A from a third
library, we end up picking the wrong one. Or the same (odr) class is
defined in two libraries, and we pick the one which gets unloaded,
although the application actually uses the code from the other library.

Now we could try to be fancy and analyze module dependencies, symbol
visibility, etc. but it would still be pretty hard to guarantee that
this really always is the case.

Note, also, that C has opaque structs that we want to find cross module just like with C++ classes, but it doesn’t have ODR. So I don’t think we can count on ODR to help us out here.

Jim