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
Add support for top-level declaration and module annotations #4482
Comments
This all sounds like we wish to add metadata to different parts of the AST. The examples are varied, but the common point is that this metadata is not for the compiler itself, but only passed through and consumed by humans or tools downstream of the compiler. The collection of use-cases that we have in this issue is good, but I think that to go from there to adding a language feature we'd need to see more in detail what kind of information needs to be carried around. The previous proposal used comments, and there's overlap with the proposed I guess the bottom line is that the bar for adding a language feature at this point is somewhat high since we don't really want to be breaking things all the time if requirements change, and "something to pass metadata through" feels way too underspecified to be warmly welcome without being afraid to break it over time. It might also be that we don't need a new language feature at all for just passing metadata through. E.g. I don't see how we couldn't cover all the proposed usecases with a very generic |
Not quite. The deprecation annotation would cause the compiler to emit a warning. So, the compiler is a consumer in some cases.
I understand the reasoning here, but we typically break things about once every year. And without actually shipping a feature, it's harder to get feedback about what changes to the design are needed.
I find this idea problematic for a few reasons:
|
That sounds good to me, I'd suggest any of the Whatever syntax we use, it'd be nice if we could avoid introducing anything new that tramples on the existing namespace. Since we're sticking to top-level things for annotations, I'd suggest that means starting them with a symbol, since symbols are not allowed there otherwise currently, so there's no conflict.
I agree with top level only, as mentioned in my bit about 1. I think the list of things you suggest here covers it!
Sure, I can't really think of anything other than a deprecation annotation just now... maybe something about backend targets? It's only sort-of the compiler there though, as it depends on
I don't have any to say about this just now it's a while since I was in there.
I don't think they necessarily need to, aside from whatever is necessary in terms of
I'd say aside from them having a label and (maybe optional?) associated value that's all the structure they'd need. There's discussion to be had about lexical rules for the label though I guess. Given my suggestions about using a symbol I'm thinking we'd end up with something like:
I have no strong feelings about this, C# is the main language I can think of that I interacted with them. They were inoffensive. |
I think that we would want to support annotations on typeclass/instance members, which are not top-level. I could also see annotations on expressions/identifiers, but I recognize that this is a much larger space to look at. It can sometimes be subsumed by value level identity functions, but it assumes:
|
Ah... good point. That would be useful for inline directives, right? |
Would each annotation exist as its own declaration (e.g. 1 per line)? Or would they be grouped under a single declaration (e.g. many per line)? @Annotation1
@Annotation2
@Annotation3
foo :: String -> ...
foo = ... or
|
👍
If there's no difference then I'd maybe slightly prefer not having them grouped, but I don't feel particularly strongly about it - if it's easier to parse one over the other, then that one? 😄 |
I could see that as well. But for the time being, could that be considered outside the scope of this? Or put differently, could you provide an example where adding such an annotation would be helpful? |
I thought of another use case for annotations, this time at the module-level. ES modules have default exports. What if there was an annotation that indicated (at least for JS backends) which value to export as default in the final codegen? Given this.. @JsDefault=foo
module Foo where
foo :: String
foo = "bar"
baz :: Int
baz = 1 The outputted JS would be: export const foo = "bar"
export const baz = 1
export default foo; I could also imagine something like @JsDefault={foo, bar} which would output export default { foo, bar } |
My use case was call-site inlining annotations. Additionally, inlining annotations on local bindings could be useful. To me there isn't a distinction between "top-level" and "any declaration context". |
My main reason for not supporting that initially was to make this easier to implement and to deal with less edge cases. But, that presumption was mainly due to not knowing how much complexity supporting non-top-level declarations would be. That being said, I agree that this would be useful to have. Do you think it should be included in the initial implementation as opposed to being added later? I'm assuming yes. |
Taking a cursory glance at this using Rust. Rust's annotations are called attributes. Some key highlights:
Looking at some of the use cases they list for attributes, I'll update the opening thread with these as ideas:
|
I'd like to toss out a design sketch at this point. Syntax
Examples: {{Deprecated "use other thing instead"}}
{{NoInline, NoSpecialize}}
foo :: forall a. Show a => Int -> a -> String
foo 0 _ = "zero"
foo 1 _ = "one"
foo _ a = {{IgnoreWarning "DeprecatedFunction"}} showDeprecated a
data ColorEnum
= {{Repr 0}} Red
| {{Repr 1}} Green
| {{Repr 2}} Blue (Design notes: my main concern with picking a concrete syntax for annotations is making it unlikely to collide with other grammar elements now or in the future, as the grammar evolves. A SemanticsThe compiler is permitted to use annotations as advice to make internal decisions that do not affect which programs are accepted. Examples of things that a compiler may do with an annotation:
Examples of things that a compiler may not do with an annotation:
Alternate backends and consumers of CoreFn are recommended to adhere to the same guidelines when consuming annotations. The compiler will assume that any programs using annotations contain (reference) a module called (We'll add such a module to the prelude. It may also contain some basic annotation types such as the below.) Annotations are parsed, name checked, and type checked as expressions of type type Deprecated = String -> Annotation
type NoInline = Annotation
type NoSpecialize = Annotation
type IgnoreWarning = String -> Annotation In more sophisticated cases, polymorphism and type classes can be used to constrain and validate the arguments to an annotation head. An annotation that doesn't name check or type check is a compile-time error. (Design notes: my hope is that annotations can be a rich field for experimentation without requiring participation from the compiler team to gatekeep them. At the same time, I want there to be some guardrails against users accidentally typoing their annotations or using them in unintended ways. That means making the annotation definitions user-definable, importable, and typed is a must. There's a choice here between defining annotations as term-like or type-like; I wanted to go with term-like, because I think PureScript's machinery for using type classes to direct type inference is more powerful at the term level than anything we currently have at the type level. But the trade-off is that annotations are irrelevant at run time, which suggests that they should be type-like; also, unless the compiler handles them specially, term-like annotation functions would need to have useless definitions attached even though only the type signatures are relevant. I settled on a desugaring compromise that might seem inelegant but should deliver the best ergonomics of both extremes.) CoreFn outputAnnotations are attached at parse time and the compiler makes a best effort to keep them attached until CoreFn is emitted. The compiler must not remove annotations from modules, named bindings (top-level or inner), or constructor definitions. The compiler may remove inline annotations if the expressions to which they are attached are mangled beyond recognition. Annotations should be compiled to CoreFn the same way that expressions are, with annotation heads being represented as {{Deprecated "use other thing instead"}}
{{NoInline, NoSpecialize}}
foo :: ...
There may be more places in CoreFn that we want to add annotation data, to be determined as needs arise. |
In general, I think that's a really good proposal. A few questions:
Is there an issue with scoping? For example, can you annotate a module with an annotation that's imported in the module? I don't see a technical limitation with this, other than it being a little odd.
Is this just to be conservative (ie, force parens to make it obvious what it applies to) or is there a limitation? If we supported
Are annotations made available in CoreFn for data type fields?
To be clear, qualification is included in "CoreFn details"? |
Yeah, I don't see a problem with it either, but I also don't think it's an essential case to support. I'd be inclined to say try it until it complicates things in which case don't. Edit: Oh wait, you said imported in the module; I was thinking defined in the module. Yeah, we do need to support that then.
I was looking at record updates when I chose that, yeah. In principle I don't think there's an issue with letting keyword-initial productions also start with annotations; the way the grammar is currently set up I think it'd have to be added to those productions one by one if we don't want ambiguity with record updates, but that wouldn't be so bad (and maybe there's a clever reorg of the grammar I'm not seeing).
Good thought; I don't see why not.
Uh, yeah. I was confused here for a sec because |
Can you elaborate on this more? I don't really follow what you are envisioning regarding typeclasses, and why we should treat these type aliases somewhat magically rather than using |
Sure, let's say someone wants to define an annotation that holds a record, and they want the labels to be arbitrary but all the values need to be If you were trying to do this in executable code, you'd write something like this: class StringRecordRL (l :: RowList Type)
instance StringRecordRL Nil
instance StringRecordRL tail => StringRecordRL (Cons s String tail)
acceptsStringRecord :: forall r l. RowToList r l => StringRecordRL l => Record r -> Unit
acceptsStringRecord _ = unit As an annotation, instead, you'd write this: class StringRecordRL (l :: RowList Type)
instance StringRecordRL Nil
instance StringRecordRL tail => StringRecordRL (Cons s String tail)
type AcceptsStringRecord = forall r l. RowToList r l => StringRecordRL l => Record r -> Annotation And then the annotation, written If annotations were just types (defined with We could be less magic by dropping the constructor-like fiction and making annotations be real term declarations like acceptsStringRecord :: forall r l. RowToList r l => StringRecordRL l => Record r -> Annotation
acceptsStringRecord _ = Annotation I just don't like how verbose that is, or how it makes annotation functions look like they're meant to be evaluated at run time with other terms. |
I missed that the arguments are expr4. How does this interact with scoping (runtime arguments, type variables, given constraints)? I'm personally wary of using runtime syntax for this purpose. Would that be necessary if we had real data kinds? |
I think there's a pretty straightforward right thing to do in all the places I'm proposing allowing annotations: Uses the module-level scope to resolve names:
Uses the module-level scope plus any names made available in the parent context:
Uses the local scope that an expression in this position would use:
This is just a sketch but I don't see that much difficulty here; happy to discuss a specific example if you have one in mind. One thing that I think is nice about this is that annotations get a sort of limited quotation capability for free. We can write, for example: type Specialize :: forall a. a -> Annotation
{{Specialize fooInt}}
foo :: forall a. a -> Array a
foo = ...
fooInt :: Int -> Array Int
fooInt = ... and even if the specialization takes place as a post-compiler transformation, the compiler can flag a typo while it does name checking.
If we just had data kinds but not GADTs, I don't think we could do type class polymorphism tricks. If we had data kinds and GADTs but not quotation (or dependent types, I guess), we would have to use a string to name |
If the annotation takes constraints, how does that elaborate in the CoreFn annotation syntax? Are constraints omitted or are they elaborated to annotation arguments? Is it useful to have them elaborated (ie, get a hold of a dictionary reference in an annotation)? |
I don't have a specific use case in mind for using the dictionaries, but it might be useful for the compiler to solve for an instance dictionary expression in an annotation and then have a subsequent CoreFn transformation use that solved instance in some code that is being injected. On balance I think it's better to leave them in and ignore them than omit them and deviate from what CoreFn consumers already expect CoreFn to be able to contain. |
Summary
Add support for annotations on top-level declarations and modules.
Motivation
Marking a module or top-level declaration with additional information unrelated to its type or kind signature would be useful for some purpose (e.g. codegen, documentation, etc.). However, the current language does not support a way to add such an annotation to such declarations or modules. Examples of where this would be useful include:
Warn
type class can be used to indicate deprecation on values/functions, it cannot be used on types or data constructors.TemplateHaskell
, annotating a type is something that could be used by the IDE to generate optics for that type using this program.deriving via
#3302 were implemented.backend-optimizer
project could be encoded as annotations. This would enable the directive to be declared near its declaration site rather than in the module comment.export default
. See this comment for details.I will update this list with more examples as they arise.
Design Questions
I am proposing this feature because my previous attempt at something similar in #4442 was seemingly blocked on the reasoning that we should implement a "real" annotation feature rather than misuse comments for a similar purpose. In the event that this issue produces bikeshedding and comes to a halt, I suppose that will give more reason to continue implementing #4442.
Regardless, being unfamiliar with the core questions an annotation feature's design requires, I thought I would first open this issue and continue it by asking what design questions need to be addressed. I imagine it's something like the following:
Deprecated
annotation causes the compiler to emit a warning)?CoreFn
transformations, and whichCoreFn
node holds that annotation?The text was updated successfully, but these errors were encountered: