Options for non-string-quoted meta attributes

Currently, we only support #[deftly(something = "value"))].

This is rather poor, especially for macros that want to take as input Rust code snippets (#56) or attributes to pass through somewhere (#109 (closed)). There are a few difficulties with improving this, notably: (1) possible ambiguity between an expression in quotes and an unquoted expression whose value is a string (2) overlap between some of the existing proposed syntaxes and #[deftly(nest(thing))].

The root question is this: what is the syntax of #[deftly]? Currently it is a nested tree of identifiers, which may have parens and then a further nested tree, or which may have values specified with =.

CC @nickm

Options

Consider ${Xmeta(scope(node)) as SOMETHING}.

Some options I came up with (not necessarily mutually exclusive):

  1. #[deftly(scope(node(CONTENT)))]

Ie, allow templates to reference the content (within parens) of a node in the tree, as arbitrary tokens. (This is what #56 is asking for.)

Advantages: similar to many other proc macros' input syntax.

Disadvantages: it is no longer clear whether a certain part of a #[deftly] is "structural" (ie, parsed in the uniform tree-like way by derive-deftly itself) or template-specific arbitrary Rust fragements to be passed through. This ambiguity makes the parsing and processing by derive-deftly much more complicated (especialiy, meta attribute use checking). It also makes things less clear for the human reader. You can't tell from just looking at it that node is structural and CONTENT is payload.

  1. #[deftly(scope(node = CONTENT))]

Ie, just allow unquoted values. This would work right now, without any trouble, for as ident, as ty, as path.

For as expr we suffer from an ambiguity. Is #[deftly(scope(node = "42"))] the integer literal 42 or the string literal "42"?

For as token_stream and as items, this cannot work because both items and token_strema can contain a comma, but it's comma that we're using to separate one meta item from the next.

  1. #[deftly(scope(node = { CONTENT }))] or #[deftly(scope(node { CONTENT }))]

    Ie, use { } for arbitrary content, with or without an equals. This would always work. But it is clumsy if the { } aren't actually needed to delimit CONTENT.

  2. Mixture of = CONTENT and = { CONTENT }.

    We could possibly allow some things to be bare and automatically strip one level of { }. (We do that already inside templates with names arguments to expansion keywords.) So #[deftly(scope(node = 42))] and #[deftly(scope(node = { 42 }))] would be equivalent (and indistinguishable by the template).

    Alternatively whether to strip { } could depend on the as. So as token_stream would always expect { }; as expr would allow them but only because { } can always surround a Rust expression; as vis would never allow them.

    This syntax would work reasonably well for attributes. You could say as attrs and it would expect CONTENT to be a series of #[...] (outer attributes; inner ones would presumably be forbidden).

Here are some options for how the template can control its input syntax:

  1. Template-level option to change how as expr behaves. Or, a new name for as expr that expects a Rust expression without surrounding quotes --- but what would we call it?

  2. Make derive-deftly 2.0 where as expr expects unquoted values. (There is a sophisticated semver scheme which can arrange that not all templates have to change at once. d-d 1.0 and 2.0 templates could both be applied to the same struct.)

  3. Allow #[deftly(scope(node = (VALUE)))], perhaps only for as expr.

    Advantage: the expansions of expressions already have ( ), so this is somehow natural.

    Disadvantage: the nested parens become even more confusing; the "meaning" of the piece (and thus of the close paren) depends on the presence of the =. We don't want = ( ) for other things than expressions, probably, so it would be a different syntax just for expressions.

Discussion

I am quite keen that one should be able to look at a #[deftly] attribute and know which parts of it are directed at the template ("payload") and which parts are structural (part of the meta identifier node tree, as in ${Xmeta(scope(node)) as ...}.

I think this rules out option 1. I quite like the = notation. It is pleasingly clear and mirrors much other Rust attribute stuff.

The most awkward case is as expr where the best syntax, #[deftly(scope(node = VALUE))] already means something else.

I think I don't mind if the as ... influences the accepted input syntax, and the interpretation of VALUE. It more or less already does that.

Proposal

  1. #[deftly(scope(node = NONEMPTHY RUST SNIPPET))] accepted for ty, path, ident, and vis.

  2. #[deftly(scope(node = { RUST SNIPPET } accepted for ty, path, ident, expr, items, vis, token_stream (ie everything except str).

    For expr, the { } are part of the expansion (and replace the ( ) that d-d normally inserts). (This is normally not very visible since { } are generally allowed around an expression.)

    For items and token_stream the { } are necessary to delimit the content, and we must not make them part of the expansion.

    For ty, path, ident, the { } are never necessary, and cannot be part of the expansion, but are accepted for orthogonality.

    For vis, the { } would in theory not ever be strictly necessary, but a visibility can be empty and #[deftly(scope(node(vis = )))] is very weird. So we require { } in that case, #[deftly(scope(node(vis = {})))].

  3. Invent as attrs which accepts #[deftly(scope(node = { #[ATTR] #[ATTR] }))].

    In future it might accept #[deftly(scope(node = #[ATTR], node = #[ATTR]))]. That would be compatible.

  4. Add a template option unquoted_exprs or similar, which causes the whole template to accept #[deftly(scope(node = EXPR))] and therefore changes the existing syntax #[deftly(scope(node = "TEXT"))] to be a string literal "TEXT" rather than parsing TEXT as Rust code.

  5. Add a ticket with the Breaking label, asking that unquoted_exprs become the default in 2.0.