Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP give ComponentInterface members a pointer to their parent. #1003

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

rfk
Copy link
Collaborator

@rfk rfk commented Aug 5, 2021

Here's something I wanted to toy with given recent work on refactoring our code-generation templates, and the desire for each interface member to be able to render itself. The issue is that sometimes we need to know information about the surrounding interface or its types in order to do the code generation, which shows up in awkward calls like:

{% if e.contains_object_references(ci) %}

What would it take to make this be just a plain method call without extra global context? Like so:

{% if e.contains_object_references() %}

In a garbage-collected language, we might conveniently give the Record, Enum etc objects a reference to their containing ComponentInterface and be done with it. That's hard to do in the obvious way in Rust, and the usual workarounds like handing out an Arc<RefCell<ComponentInterface>> seem extremely verbose/complex for our needs.

Here's a sketch of something that might work, although I'm not convinced of the value/complexity tradeoff.

Taking the Enum struct as an example, I've split it up into two parts:

  • EnumDescr is the data that describes an enum. Instances of this are owned by a ComponentInterface in a normal tree structure without backwards references.
  • Enum is a kind of aggregate transient pointer, combining a reference to an EnumDescr with a reference to its containing ComponentInterface. Instances of this are synthesized on demand when inspecting a ComponentInterface.

From the consumer's perspective, it calls ci.get_enum_definition("MyEnum") and receives an Enum struct, and it can call methods on the Enum struct to inspect its definition. But these methods all delegate to the underlying EnumDescr for the actual data.

It gets a little complicated in the details of nested strcuts like Variant and Field, and I'll leave some musings inline.

Thoughts?

@Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here
override fun destroy() {
when(this) {
{%- for variant in e.variants() %}
is {{ e.name()|class_name_kt }}.{{ variant.name()|class_name_kt }} -> {
{% for field in variant.fields() -%}
{%- if ci.type_contains_object_references(field.type_()) -%}
{%- if field.contains_object_references() -%}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a nice improvement in usability for the template logic.

pub struct Enum<'a> {
pub(super) parent: &'a ComponentInterface,
pub(super) descr: &'a EnumDescr,
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the Enum struct is now a kind of "fat pointer" thing that contains both a pointer to the data and parent ref to the containing object.

}

pub fn variants(&self) -> Vec<&Variant> {
self.variants.iter().collect()
pub fn variants(&self) -> Vec<Variant<'_, Self>> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, when consuming code lists the variants of the enum, so see that we create a bunch of Variant instances on demand with the appropriate pointers.

}

pub fn contains_object_references(&self, ci: &ComponentInterface) -> bool {
// *sigh* at the clone here, the relationship between a ComponentInterace
// and its contained types could use a bit of a cleanup.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey look, no more clone!


#[derive(Debug)]
pub struct Variant<'a, Parent: CINode> {
pub(super) parent: &'a Parent,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this starts to get a bit complicated...the Variant struct may be used as part of an Enum or as part of an Error, so its parent pointer type needs to be generic. The main property that we want from the parent pointer is the ability to get all the way up to the containing ComponentInterface, so I made a trait for that.

An alternative could be to have it point directly to the containing ComponentInterface rather than its containing object, which would make this more of an "owner" pointer than a "parent" pointer. But, I can imagine reasons why a variant might need to be able to inspect the properties of its containing Enum/Error.

}
}

impl<'a, Parent: CINode + 'a> CINode for Variant<'a, Parent> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could imagine generating these impls automatically from some sort of macro.

pub fn iter_enum_definitions(&self) -> Vec<Enum<'_>> {
self.enums
.iter()
.map(|e| Enum {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here again we see that we're synthesizing the aggregate pointer thingy on demand.

name: self.identifier.0.to_string(),
fields: self.members.body.convert(ci)?,
})
}
}

// Represents an individual field on a Record.
#[derive(Debug)]
pub struct Field<'a, Parent: CINode> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We re-use the Field struct in two places, for enum variants fields and record fields, hence the generic.

Which makes me wonder...I wonder if enum variants could actually contain a wrapped Record in the same way that errors contain a wrapped Enum...

@@ -172,7 +200,7 @@ mod test {
assert_eq!(record.fields().len(), 1);
assert_eq!(record.fields()[0].name(), "field");
assert_eq!(record.fields()[0].type_().canonical_name(), "u32");
assert!(!record.fields()[0].required);
assert!(!record.fields()[0].required());
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can no longer access the fields directly because they're hidden behind a pointer.

@rfk rfk requested a review from jhugman August 6, 2021 00:23
Copy link
Member

@badboy badboy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually looks simpler than I thought it would be.
It certainly does make the templating a bit easier, so I'm leaning on the side of "worth the complexity cost"

pub fn contains_unsigned_types(&self, ci: &ComponentInterface) -> bool {
self.variants().iter().any(|v| {
v.fields()
pub fn contains_unsigned_types(&self, _ci: &ComponentInterface) -> bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason to keep _ci here? Is it necessary or just work-in-progress for now?

@jhugman jhugman removed their request for review October 18, 2022 11:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants