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

Expose experimental edition features in descriptor sets #627

Merged
merged 7 commits into from
Nov 28, 2023

Conversation

timostamm
Copy link
Member

@timostamm timostamm commented Nov 21, 2023

This change exposes edition feature sets in a DescriptorSet, the wrapper around protobuf file descriptor messages provided by createDescriptorSet from @bufbuild/protobuf.

Each type (DescFile, DescMessage, DescField, etc.) receives a new method with this PR:

  /**
   * Get the edition features for this protobuf element.
   */
  getFeatures(): MergedFeatureSet;

The returned type is a protobuf message google.protobuf.FeatureSet, but with a modified type that marks all properties as required.

The value is resolved from the edition feature defaults (obtained at build time from protoc in #619), merged with the features of all parent elements and the current element. This implements the logic specified in the design documents for editions here and here.

Users can provide their own edition feature defaults via a new optional argument to createDescriptorSet. This can be useful for language-specific features, although usability is currently limited because we do not support extensions yet (tracked in #86), which makes it difficult to obtain the features at runtime.

This PR also updates the properties packed and packedByDefault of DescField (and DescExtension) to provide sensible values for editions that respect features. Test coverage for proto2 and proto3 has been added prior to this PR in #625 to guard against regressions.

@timostamm timostamm changed the title Expose editions features in descriptor sets Expose experimental edition features in descriptor sets Nov 22, 2023
@timostamm timostamm marked this pull request as ready for review November 22, 2023 11:09
Instead of assuming that EXPANDED means that the field is not packed, be specific and check for PACKED, because it's unlikely but possible that more values are added to the enumeration in the future.
@timostamm timostamm requested review from smaye81, jhump, srikrsna-buf and a team and removed request for jhump November 22, 2023 11:32
Copy link
Member

@jhump jhump left a comment

Choose a reason for hiding this comment

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

I have some questions and comments, but mostly LGTM.

// length-delimited types cannot be packed
return false;
default:
switch (file.edition) {
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps it doesn't matter since it seems to have been this way before, too, but I think this and the function above are missing a check that proto.label == LABEL_REPEATED.

Also, this default case seems a bit more verbose than I would have expected. I would instead have expected simply:

return proto.options?.packed ?? isPackedFieldByDefault(file, proto, resolveFeatures)

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree that it would make sense to check for LABEL_REPEATED. Didn't want to change behavior in this PR.

Also, this default case seems a bit more verbose than I would have expected.

Yes, I wonder how this will play out for other implementations. But the field packed of FieldOptions is indeed not populated at all with editions.

);

/**
* A merged google.protobuf.FeaturesSet, with all fields guaranteed to be set.
Copy link
Member

Choose a reason for hiding this comment

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

This will cause a compile issue if a new feature is added and the resulting generated code is paired with an older version of the runtime, right (since the code may be populating a MergedFeatureSet without setting all required fields)? I guess that's probably desirable.

Copy link
Member Author

Choose a reason for hiding this comment

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

If a new feature is added, I expect this to happen with the release of a new edition. For an upstream release, we generate the WKT, and also seed the edition defaults (see #619) at build time. The function createDescriptorSet uses the seeded defaults. As a result, createDescriptorSet can now parse descriptors including the new edition. The message FeatureSet will be in sync, since it is part of the WKT.

createDescriptorSet may have to be updated to provide a good user experience around a new feature. Then the runtime will have implement this feature. Then we can release full support for the new edition. It is possible to cut a release before we support the new future in the runtime, and only make it available in createDescriptorSet, but not in protoc-gen-es. We can sort out the exact details if we get there.

If createDescriptorSet is called with descriptors using an edition from the future, createFeatureResolver will raise an error because the edition is not supported yet. createDescriptorSet takes an optional argument to bring your own FeatureSetDefaults, allowing to use editions other than the ones supported by the @bufbuild/protobuf release. The getFeatures() methods will return FeatureSet classes (and still be valid MergedFeatureSet because it seems very unlikely that a feature will ever be removed from descriptors).

The optional argument to bring your own FeatureSetDefaults can also be used to provide language-specific extensions to the protobuf message FeatureSet. It would be a nice addition to @bufbuild/protoplugin to support this use case, but we really ought to support extensions first.

It's unfortunate that feature resolution is effectively split into build-time and runtime concerns. The logic could certainly be made completely dynamic (and #628 partially gets us there). But since we currently don't support extensions and don't embed descriptors in generated code, implementation is complex and DX is awkward. Since it will also affect bundle size, I think we're better off punting on this now, and reconsider the feature later on.

Comment on lines 55 to 56
const min = compiledFeatureSetDefaults.minimumEdition ?? 0;
const max = compiledFeatureSetDefaults.maximumEdition ?? 0;
Copy link
Member

Choose a reason for hiding this comment

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

Won't the ?? 0 cause comparison issues below? IIUC, I think a zero for max will cause this to always generate an error like Edition <edition> is later than the maximum supported edition EDITION_UNKNOWN, which could be confusing. Though I suppose it is also possible that the call to Edition[edition] to build the error message could also raise an exception.

Maybe a more explicit check with a better error message about the feature set defaults being incomplete/corrupt or something?

Copy link
Member Author

Choose a reason for hiding this comment

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

I kept this pretty close to the cc implementation, but had the same thoughts. Pushed up 3bc7274 with a little more validation.

For a google.protobuf.FeatureSetDefaults from the future, our generated Editions enumeration may be missing values. In this case, the error messages will be "Edition undefined is earlier ...". Not ideal, but the alternatives seem barely better, or prohibitively complex.

function validateMergedFeatures(
featureSet: FeatureSet,
): featureSet is MergedFeatureSet {
for (const p of [
Copy link
Member

Choose a reason for hiding this comment

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

So if we pair this set of fixed properties with a newer FeatureSet definition, we'll end up violating the type instead of this being an error, right?

Perhaps better would be to a constant literal of type MergedFeatureSet, so the compiler can verify it is correct (e.g. has all required properties), and then here loop over the keys of the constant.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for raising this question, I was meaning to take a closer look at forward compatibility. In 169354c, I'm adding the following comments to this function:

// When protoc generates google.protobuf.FeatureSetDefaults, it ensures that
// fields are not repeated or required, do not use oneof, and have a default
// value.
//
// When features for an element are resolved, features of the element and its
// parents are merged into the default FeatureSet for the edition. Because unset
// fields in the FeatureSet of an element do not unset the default FeatureSet
// values, a resolved FeatureSet is guaranteed to have all fields set. This is
// also the case for extensions to FeatureSet that a user might provide, and for
// features from the future.
//
// We cannot exhaustively validate correctness of FeatureSetDefaults at runtime
// without knowing the schema: If no value for a feature is provided, we do not
// know that it exists at all.
//
// As a sanity check, we validate that all fields known to our version of
// FeatureSet are set.

The gist is that I don't see an alternative to trusting the FeatureSetDefaults to be valid. If the defaults are valid, feature resolution will always yield a message with all fields set.

In ad1965d, I've updated the logic to use the field information embedded in the generated code to check fields. So this sanity check will always include all known fields.

@jhump
Copy link
Member

jhump commented Nov 24, 2023

(I'm still ooo, but happened to see the message in Slack so figured I could do a quick scan.)

@timostamm timostamm merged commit ae8a3ae into main Nov 28, 2023
5 checks passed
@timostamm timostamm deleted the tstamm/expose-edition-features branch November 28, 2023 10:43
@timostamm timostamm mentioned this pull request Nov 30, 2023
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.

3 participants