RFC-0086: Updates to RFC-0050: FIDL Attributes Syntax

RFC-0086: Updates to RFC-0050: FIDL Attributes Syntax
StatusAccepted
Areas
  • FIDL
Description

Specifies a new syntax for attributes in the FIDL language.

Issues
Gerrit change
Authors
Reviewers
Date submitted (year-month-day)2021-03-09
Date reviewed (year-month-day)2021-04-07

Summary

This document describes a new syntax for attributes in the FIDL language.

See also:

Motivation

FIDL attributes provide a clear way to attach compile-time metadata to FIDL declarations, allowing authors to pass extra information to the FIDL compiler pertaining to documentation (Doc, NoDoc) compile-time validation (ForDeprecatedCBindings), code generation (Transitional, Transport), desired API availability (Discoverable), and so on. In addition to these "official" attributes, FIDL authors are allowed to define their own "custom" attributes, which do not affect compilation, but are still attached to the resulting FIDL JSON Intermediate Representation for potential downstream use.

Three properties of the existing attributes syntax are lacking:

  • Multiple attributes are written as a single, comma-separated declaration, like so: [Discoverable, NoDoc, Transport = "Channel"]. This is inflexible, and it can be unclear that the attributes are independent of one another.
  • Attributes are currently only able to take either zero or one argument, but not more.
  • When attributes do take arguments, the values are always strings. For example, the MaxBytes attribute takes a stringified number as its argument, like so: [MaxBytes="128"]. This is confusing, especially when the remainder of FIDL is predictably typed.

The FIDL syntax is currently undergoing a major migration as part of the RFC-0050: Syntax Revamp effort. This presents a good opportunity to implement syntax changes for attributes as well.

Design

Syntax

Each attribute is a single declaration, with the name of the attribute (lower_snake_cased by convention) being directly preceded by an @ symbol. For declarations that carry multiple attributes, there is no preferred ordering between attributes. For example, the attribute declaration [Discoverable, NoDoc, Transport = "Channel"] may now be written as:

@discoverable
@no_doc
@transport("Channel")
protocol P {};

If necessary, the attribute name is optionally followed by a set of parentheses, containing a comma-separated list of one or more arguments. An argument may be any valid FIDL constant. For example:

const DEFAULT_TRANSPORT string = "Channel";

@transport(DEFAULT_TRANSPORT)
protocol P {};

Arguments must be denoted with a lower_snake_cased "keyword" syntax similar to that found in Python, except for attributes that only take one argument, which must omit the keyword. Argument typing and the presence of required arguments are only validated for attributes native to fidlc. For such attributes, the compiler may default omitted optional arguments to some predetermined value. Consider the following example usages of a mock @native attribute which takes two required arguments (req_a and req_b), and two optional ones(opt_a and opt_b):

const C bool = true;
@native(req_a="Foo",req_b=3)                  // OK: neither opt arg is set
@native(req_a="Foo",req_b=3,opt_c=C)          // OK: only opt_a is set
@native(req_a="Foo",req_b=3,opt_d=-4)         // OK: only opt_b is set
@native(req_a="Foo",req_b=3,opt_c=C,opt_d=-4) // OK: both opt args are set
@native(req_a="Foo",req_b=3,opt_d=-4,opt_c=C) // OK: arg order is irrelevant
@native(opt_d=-4,req_a="Foo",req_b=3)         // OK: arg order is irrelevant
@native(req_b=3)                              // Error: missing req_a
@native(req_a="Foo")                          // Error: missing req_b
type S = struct {};

Author-defined custom attributes are schema-less, and are described solely by their presence in a FIDL file. The compiler has no way of validating the number of arguments, whether or not they are required, and what their proper types are (though this may change should we choose to implement "In-FIDL" attribute schemas, as described in the Alternatives section below). Thus, only the grammatical correctness of the attribute signature is enforced. An example of an author-defined attribute, @custom, is shown here:

@custom(a="Bar",b=true) // OK: grammatically correct
@custom("Bar",true)     // Error: bad grammar - multiple args require keywords
@custom("Bar")          // OK: correct grammar (though signature now unclear)
@custom(true)           // OK: correct grammar (signature even more unclear)
@custom()               // Error: bad grammar - cannot have empty arguments list
@custom                 // OK: grammatically correct
type S = struct {};

Formally, the modified BNF grammar for the new FIDL attribute syntax is:

attribute = "@", IDENTIFIER , ( "(" , constant | attribute-args, ")" ) ;
attribute-args = attribute-arg | attribute-arg, "," attribute-args;
attribute-arg = IDENTIFIER , "=" , constant;

RFC-0040: Identifier Uniqueness applies to attribute names. This means that different casings and consecutive underscores in attribute names are reduced to a single common name during canonical name resolution. The following example thus causes a scoping clash, generating an error at compile time:

@foo_bar
@FooBar   // Error: re-used attribute name "foo_bar"
@fooBar   // Error: re-used attribute name "foo_bar"
@Foo_Bar  // Error: re-used attribute name "foo_bar"
@foo__bar // Error: re-used attribute name "foo_bar"
@FOOBar   // Error: re-used attribute name "foo_bar"
type S = struct {};

JSON IR

The FIDL JSON Intermediate Representation schema for attributes reflects the new syntax. The schema's attribute definition now has a location field to track the in-file position of the attribute declaration. The value field has been replaced by an arguments field, which stores the value of each argument as a name/value pair, with the latter taking the schema of a #/definitions/constant. For attributes that only accept one argument, and thus have no keyword name in the source, the sole argument's name defaults to "value".

Additionally, compose stanzas and reserved table/union members were previously not able to carry attributes. This RFC rectifies these oversights, adding a new #/definitions/compose definition as a property on #/definitions/interface, and formally attaching a maybe_attributes property to #/definitions/table-member.

In sum, this RFC introduces three new schema definitions (#/definitions/attribute-arg, #/definitions/attribute-args, and #/definitions/compose) and modifies three others (#/definitions/attribute, #/definitions/interface, and #/definitions/table-member). The new attribute definitions will appear as follows:

"attribute-arg": {
  {
    "description": "Definition of an attribute argument",
    "type": "object",
    "required": [
        "name",
        "value",
    ],
    "properties": {
      "name": {
        "description": "Name of the attribute argument",
        "type": "string",
      },
      "value": {
        "description": "Value of the attribute argument",
        "$ref": "#/definitions/constant",
      },
      "location": {
        "description": "Source location of the attribute argument",
        "$ref": "#/definitions/location"
      },
    },
  },
},
"attribute-args": {
  {
    "description": "Definition of an attributes argument list",
    "type": "array",
    "items": {
      "description": "List of arguments",
      "$ref": "#/definitions/attribute-arg",
    },
  },
},
"attribute": {
  {
    "description": "Definition of an attribute",
    "type": "object",
    "required": [
        "name",
        "arguments",
        "location",
    ],
    "properties": {
      "name": {
        "description": "Attribute name",
        "type": "string",
      },
      "arguments": {
        "description": "Attribute arguments",
        "$ref": "#/definitions/attribute-args",
      },
      "location": {
        "description": "Source location of the declaration",
        "$ref": "#/definitions/location",
      },
    },
  },
},
"compose": {
  {
    "description": "Compose member of an interface declaration",
    "type": "object",
    "required": [
        "name",
        "location",
    ],
    "properties": {
      "name": {
        "$ref": "#/definitions/compound-identifier",
        "description": "Name of the composed interface"
      },
      "maybe_attributes": {
        "description": "Optional list of attributes of the compose declaration",
        "$ref": "#/definitions/attributes-list",
      },
      "location": {
        "description": "Source location of the compose declaration",
        "$ref": "#/definitions/location",
      },
    },
  },
},

Continuing with the @native example from the previous section, the maybe_attributes field of its JSON IR output for type S would be:

"maybe_attributes": [
  {
    "name": "native",
    "arguments": [
      // Note: the omitted opt_d is not included in the IR
      {
        "name": "req_a",
        "value": {
          "kind": "literal",
          "value": "Foo",
          "expression": "\"Foo\"",
          "value": {
            "kind": "string",
            "value": "Foo",
            "expression": "\"Foo\"",
          },
        },
      },
      {
        "name": "req_b",
        "value": {
          "kind": "literal",
          "value": "3",
          "expression": "3",
          "literal": {
            "kind": "numeric",
            "value": "3",
            "expression": "3",
          },
        },
      },
      {
        "name": "opt_c",
        "value": {
          "kind": "identifier",
          "value": "true",
          "expression": "C",
          "identifier": "example/C",
        },
      },
    ],
    "location": {
        "filename": "/path/to/src/fidl/file/example.fidl",
        "line": 4,
        "column": 0,
        "length": 36,
    },
  },
],

Case Study: FIDL Versioning

RFC-0083: FIDL Versioning describes an attribute-based syntax for appending versioning metadata to FIDL declarations and their members. In particular, it defines a new attribute, @available, which takes the optional arguments platform, since, removed, deprecated, and note. For example:

@available(since=2)
type User = table {
  // Was created with struct at version 2, so no @available attribute is needed.
  1: is_admin bool;
  // Deprecated in favor of, and eventually replaced by, the name field.
  @available(deprecated=3,removed=4,note="use UTF-8 `name` instead")
  2: uid vector<uint8>;
  // The current (unreleased) version constrains this definition.
  // Placing "removed" before "since" is discouraged, but won't fail compilation.
  @available(removed="HEAD",since=3)
  3: name string;
  @available(since="HEAD")
  3: name string:60;
};

It is worth noting that arguments that reference numbered platform versions (namely since, removed, and deprecated) may also take the special string "HEAD". This means that these arguments do not have a single, easily resolvable type such as uint8. Such constructs are permitted because the specific type validation rules for an "official" attribute like @available are hard-coded into the compiler itself. Other, more subtle rules, such as the restriction that only @available attributes attached to library declarations may carry the platform argument, are handled in this bespoke manner as well.

Implementation

This proposal will be implemented as part of the broader RFC-0050 FIDL syntax conversion. All FIDL files written in the "new" syntax will be expected to conform to the changes laid out in this RFC, and the formal FIDL grammar will be updated to reflect its design at the same time as the rest of RFC-0050.

In addition, the schema definition will be updated to accommodate the JSON IR changes specified by this document. However, it is important to keep the JSON IR schema static during the actual migration to the syntax defined by RFC-0050, as ensuring that the pre- and post-migration syntaxes produce the same IR will be an important tool for verifying the accuracy of the new compiler. Thus, the update to the attribute definition proposed by this document must happen prior to any RFC-0050 migration, to ensure that the JSON IR does not change while that process is ongoing.

Performance

These syntax changes are unlikely to have a performance impact.

Security Considerations

These syntax changes are unlikely to have a major security impact. They do have the minor benefit of making potential future security-validating attributes easier to write and reason about.

Privacy Considerations

These syntax changes are unlikely to have a major privacy impact. They do have the minor benefit of making potential future privacy-validating attributes easier to write and reason about.

Testing

These syntax changes are unlikely to have a major testing impact. They do have the minor benefit of making potential future test-instrumenting attributes easier to write and reason about.

Documentation

All relevant documentation and examples will be updated to feature the new syntax as part of the broader RFC-0050 documentation update. In particular, the reference documentation for official FIDL attributes will be updated to reflect the rules stipulated by this design, and to note valid argument types and implied defaults for each attribute.

Drawbacks, Alternatives, and Unknowns

In-FIDL Attribute Schemas

There is an almost infinite design space of possible syntaxes, and this section will not try to address all of them. However, one option that received serious consideration was allowing the interfaces of annotation functions to be defined "with FIDL." This alternative syntax and the rationale for its rejection are described below.

Consider the following FIDL file, with the interface of its custom attribute defined inline:

type MyAttrArgs = table {
  1: foo string;
  2: bar int32;
  3: baz bool;
};
@myAttr(MyAttrArgs{foo: "abc", bar: 1, baz: false})
type MyStruct = struct {...};

This design has the benefit of "eating our own dogfood:" FIDL is an interface definition language, so why not define the interfaces to our built-in, compiler-aware attribute functions with it as well? Further, this allows for custom, user-defined attributes to be enabled at some point in the future, though how the compiler would attach such user-defined meta information to the generated bindings code remains an open question.

This design path is ultimately rejected for trying to do too much. None of the use cases for attributes in the foreseeable future require such in-language definition capabilities, and it is not even clear that enabling user-defined attributes is a desirable future goal. The syntax proposed by this document is simpler and more familiar to users of other popular languages such as Rust and Python.

Nothing in the current design explicitly precludes the implementation of In-FIDL attribute schemas, so this alternative remains a viable option for future extensions to the attributes syntax.

Use String Literals for Attribute Arguments

One possible alteration to the design specified in this document is to refrain from allowing typed arguments, and to instead retain the old regime of requiring that all argument values be string literals. Consider the following example illustrating such a design:

const MAX_STR string = "123";
const MAX_NUM uint64 = 123;

@max_handles(123)     // Error: cannot use non-string types
@max_handles(MAX_STR) // Error: cannot use const identifiers
@max_handles(MAX_NUM) // Error: cannot use non-string types or const identifiers
@max_handles("123")   // OK: only string literals are allowed
type S = struct {};

The upside of this design is the simplicity of implementation it affords. To accommodate typed attribute arguments, backends will need a relatively more complex ingestion logic that properly accounts for the various possible types for each of the attributes' arguments. Where previously a simple cast from the string "123" to the int 123 would have sufficed, backends now need to handle the entirety of the #/definitions/constant schema. This additional implementation cost is multiplied by the number of backends being supported.

The benefit of allowing typed attributes is that it centralizes this type casting logic. For example, consider the attribute declaration @my_custom_attr("3."). If each backend is expected to do its own type casting, one may decide that "3." is a valid value to cast as an integer, while another may not. It would be difficult to catch all subtleties of this ilk, leading to backends inevitably diverging in their attribute implementations. Enshrining one canonical understanding of how attribute types behave in fidlc eliminates such inconsistencies.

Rejected Minor Alterations

The attribute syntax described in this document explicitly states that the ordering between multiple attributes attached to a single declaration is irrelevant. An alternative to this would have been to enforce alphabetical order. This was rejected because author-defined custom attributes, as well as future fidlc-native attributes, may reference one another in ways that may benefit from using specific orderings for clarity. Consider the following two author-defined custom attributes, whose order would need to be confusingly reversed if alphabetical ordering were required:

@this_attr("Foo")
@test_for_this_attr(false)
protocol P {};

Additionally, camelCase was considered as the recommended casing convention for the attribute syntax. This recommendation was ultimately rejected, as none of the other casing suggestions in the FIDL Style Guide implement camelCase, and it was judged to be overly confusing to add a new casing style for attributes alone.

Prior Art and References

This RFC is an evolution of the syntax defined in RFC-0050: Syntax Revamp, and will thus modify the formal FIDL Grammar.

This proposal drew on a number of existing "attribute-like" implementations in other languages, specifically:

  • Python's decorator syntax and keyword argument design serve as inspiration for some of the design choices seen in this document, such as the use of the @ symbol and referencing arguments by keyword.
  • Rust's attribute specification, while superficially different on some syntactic choices, is also conceptually similar to this proposal.
  • Cap'n Proto's annotations schemas were considered as a reference point for a potentially more featureful alternative to the syntax proposed in this document.