Repository: hylang/hy

Issue:
[RFC] Annotation syntax
I've been thinking about the best way to handle annotations for a while, especially with Python 3.8 shipping type comment parsing in the built-in ast module. The main issue is that I want the syntax to be consistent across function definitions, variables, etc.

Here's what I've currently come up with:

```hy
; VARIABLE ANNOTATIONS

(setv x &of int 1)

; GENERIC TYPE SHORTHAND

; Instead of e.g. (get List int) for List[int] which sucks
(setv x &of (of List int) 1)

; FUNCTION ANNOTATIONS
(defn myfunc [x &of int y &of str] ...)
; Should we use nested braces instead for consistency with defaults?
; Arguably yes but now we lose consistency with setv.
(defn myfunc [[x &of int 2]])  ; like def myfunc(x: int = 2)
; Or we could only require nested braces for said defaults, e.g.
(defn myfunc [x &of int y &of int [z &of int 2]])  ; def myfunc(x: int, y: int, z: int = 2)

; TYPE COMMENT ALTERNATIVES

; Type comments suck with Hy's style so let's try something else.
; On Python 3.6 we can just lift these out to variable annotations but 3.5 might be messier
(for [[x &of int y &of int] pairs] ...)
(with [(thing) x &of int] ...)

; However we still need to support a # type: ignore equivalent, maybe:
(#ignore
  ...)
```

Some random thoughts:

- This could probably be shipped in a "tech preview" state at first to give us some time to try it out and amend what's necessarily, while letting people know that a bit of potential breakage wouldn't be unexpected.
- Do we really want to support 3.5? 3.6 has already been out for a while, and this would likely be far simpler if we can restrict what needs to be compiled to type comments to just `# type: ignore`. Heck, outside of `# type: ignore`, everything shown here should would in pre-3.8 *without* 
requiring typed_ast.


Discussion:
I haven't really been in the loop on the whole typing annotations thing. I understand that Python itself ignores them, so what software is currently in use that uses them?
Function and variable annotations are still present in various forms at runtime, the part that Python ignores is the *meaning* of the annotations. That's interpreted by type checking tools.

There are two reasons I think this would be great:
- It would make room for static types in Hy. Sure we could argue about whether that fits with the Lisp ideology, but it would probably be useful for many people.
- We're starting to see libraries take advantage of the presence of these annotations at runtime. E.g. Python 3.7+ adds dataclasses which are kinda like mutable named tuples / the attrs library, and there's Pydantic which uses these type annotations to perform validation of JSON-style data.
> the part that Python ignores is the *meaning* of the annotations

Yes, that's what I mean.

Regarding the proposal, an infix syntax, where e.g. `(List &of int)` isn't the same kind of form as `(List foo int)`, doesn't seem ideal for Lisp. Can you think of a prefix-style way to accomplish this?
I guess we could just do `(&of List int)` though that's pretty much identical in looks to get. It just feels like `(get List int)` to represent a type is rather...odd. 
Yeah, using the name `get` for that is confusing, so using `of` or something instead makes sense even if it's formally redundant.
Actually `of` could be a bit different, e.g. `(of Dict int str)` would compile to `Dict[int, str]`, but the equivalent `get` would compile to `Dict[int][str]`. I guess then `of` could just be something like:

```hy
(defmacro of [item &rest args]
  `(get item (, ~@args)))
```
Okay so I updated the original proposal, what do you think of the last two points as well?
Can we get a prefix syntax for scalars as well, like `(of x int)` instead of `x &of int`? As a bonus, we can then support whatever syntax for nested types we want purely through our design of `of`'s behavior as a special form, like `(of x (List int))` or whatever.

> This could probably be shipped in a "tech preview" state

That's fine. Technically, the whole language is still a tech preview.

> Do we really want to support 3.5?

Ignoring 3.5 is probably fine. Its end-of-life is in a little over a year, on 13 Sep 2020.
I mean I do understand that, but it looks a bit ugly:

```hy
(setv (of x int))  ; ?
(defn myfunc [(of x int)])  ; how would this even work?
```

Also the generic syntax can be used outside of that, e.g. how would this work in the other syntax:

```hy
(setv some-var &of Any {})
(cast (of Dict str str) some-var)  ; this is Dict[str, str]
```

> Ignoring 3.5 is probably fine. Its end-of-life is in a little over a year, on 13 Sep 2020.

~~Sweet less work for me then.~~
> `(setv (of x int))`

This probably shouldn't be legal because it isn't an assignment. It would make more sense to allow `of` at the top level.

> `(defn myfunc [(of x int)])  ; how would this even work?`

Presuming you can't declare the types of a function without defining it in Python, this shouldn't be legal, either.

> e.g. how would this work in the other syntax:
>
> `(setv some-var &of Any {})`

As `(setv (of some-var Any) {})`.

> `(cast (of Dict str str) some-var)`

`(cast (of (Dict str str)) some-var)`.
Hmm I don't know, I get the idea of prefix syntax being better given that this is Lisp, but it also feels rather verbose...

Looking at the syntax some other lisps use:

- Typed Clojure: Not a fan overall because it seems a bit inconsistent.
  - For variables: `(ann variable Type)`, separately from the assignment.
  - For functions, `(fn [arg :- Type])`, so this is more like the `&of` proposal.
- Racket:
  - For variables: `(: variable Type)`, separately from the assignment.
  - For functions, `(fn [arg : Type])`, again more like `&of` in being infix.

Also in the setv I forgot to have a value for the variable, pretend it's being assigned something, and the `defn` had an empty body so you could use your imagination... But both were supposed to be normal assignments / definitions.
I see. I think a prefix syntax for both `setv` and `defn` should be able to work the same way, using the rule that a symbol `foo` can be replaced with `(of foo Type)`, so e.g.

    (defn myfun [a &optional b [c 2]] …)

could be fully typed as

    (defn myfun [(of a int) &optional (of b int) [(of c int) 2]] …)

I think the only verbosity required beyond your original proposal will be more parentheses, at least in some circumstances.
> I haven't really been in the loop on the whole typing annotations thing. I understand that Python itself ignores them, so what software is currently in use that uses them?

I heavily use type annotations, and not as a nice-to-have, Package `pydantic` reads and validates JSON inputs and populates the corresponding Python object. Mini-web framework  `fastapi` (really intended for rest endpoints) heavily uses `pydantic` to the effect that JS objects are mapped to Python objects. 

Here's a simplified example: with `pydantic`'s metaclass `BaseModel` once can do something like

```
class AgeSpec(BaseModel):
    age : int
class MatureSpec(BaseModel):
    is_mature : bool
```


then in your `app.py` you might write

```
@app.post('/is_mature/')
def old_enough(age_input : AgeSpec) -> MatureSpec:
    return MatureSpec({is_mature: age_input.age >=21})
```

Python syntax for the data models is especially concise and readable by non-Python readers (read: node devs), but there is really no reason standard Python can't be used as a DSL for data models. OTOH it would be useful to write the app itself in Hy.

There's already a lot of controversy about syntax, so let me add the possibility of making `defn/t` a specialized constructor for typed functions (since they are not the norm in current Python usage). This may open type declaration syntax options without messing too much with the existing `defn` syntax. 
I can say that only I and Ryan (@refi64) are particularly active on Hy right now, and I have no plans to implement them because I'm not interested in them. Ryan, do you think you'll take a crack at this soon?
I went and deleted a comment that asked whether type annotations were on the horizon; on second thought it seemed way too imposing. In the mean time someone responded. Sorry.
I'm overbooked as I've never been for the next two months but following that I could help set up a "business model" (maybe a bounty system, Patreon, etc). I have some experience doing this for nonprofits professionally. Hy (and its developers) deserve more attention and resources. 
See #957. Speaking for myself, I have a full-time job and I'm not looking for more paid work right now, but others might be interested.
I definitely do really want to do this soon, though I am still having some second thoughts on the desired syntax as well...
Sorry for barging in like this, but I was just about to start hacking on something like this myself -- as a heavy Pydantic user, not having annotations directly in Hy stings a bit.

As far as I can tell, previous discussion here on this topic has converged on Clojure-style annotations with a caret; see #640 and #1482. Syntax-wise I would thus propose that, as it seems unobtrusive and nicely compatible with the use of `of` to annotate specialized types. So you would get:

```hy
; VARIABLE ANNOTATIONS
 
(setv ^int x 1)

; GENERIC TYPE SHORTHAND
 
; Instead of e.g. (get List int) for List[int] which sucks
(setv ^(of List int) x 1)

; FUNCTION ANNOTATIONS
; function args work like vars
(defn myfunc [^int x ^int y [^int z 2]])  ; def myfunc(x: int, y: int, z: int = 2)
; the return type could be after the function name like in Clojure
(defn myfunc ^(of List str) [^int x ^str y] ...)  ; def myfunc(x: int, y: str) -> List[str]: ...
; or before the function name for consistency, and possibly easier implementation
(defn ^(of List str) myfunc [^int x ^str y] ...)  ; def myfunc(x: int, y: str) -> List[str]: ...
``` 
The type comment equivalents would also work as above, just with the preposed caret syntax. Thoughts?
That seems reasonable.
If everyone agrees I can go for that. One thing to note: we can technically do stuff like:

```python
x: 'X' = X()
```

How would we represent that with the above syntax? `(setv ^"X" x)`, correct?

This does also still leave how to do declarations without a definition. Maybe:

```
(^int x)  # x: int
(^(of List int) y)  # y: List[int]
```

We are also flipping the order from Python (annotation goes first here with the proposal), but since this is Lisp the normal parsing difficulties don't apply.
To the former question, yeah, you'd just put any expression after the caret and it would be written into `__annotations__`. This also allows working with libraries such as Plac, which use string annotations in function definitions.

The latter one is a good question. I think for top-level declarations this would work fine, but also I'm primarily thinking of using pure declarations in a classdef context, coming from pydantic. Here for me the most "natural" extension of the existing syntax would be something like:

```hy
(defclass Point []
  [^int x
   ^int y])

; or with defaults

(defclass Point []
  [^int x 2
   ^int y 2])
```
I'm not sure how annoying this would be to implement in the reader though, given that in this syntax you can also get cases like this:
```hy
; x: int
; y = 2
(defclass Patho []
  [^int x y 2])
```
[We dropped the `defclass` attribute list though](https://github.com/hylang/hy/pull/1789), so that exact syntax wouldn't work anyway.

Maybe could just do something like `(^int x)`, then if Hy sees an expression where the first term is annotation syntax, it compiles it down to `x: int`?
Ah, I didn’t catch that. That makes everything a lot easier! I fully support your idea then.

For whatever my support counts (I figure it's not much and that's okay), I like it. 
I would add that there's some small value in aping Clojure syntax styles: we get to piggyback on Clojure tools for the various editors. (The VSCode Hy mode has subtle defects and there's none for Sublime Text, but in both Clojure syntax coloring works fine; and Parinfer for VSCode seems to be married to Clojure.)
While implementing this, I realized the standalone annotation syntax I proposed:

```hy
(^int x) ; x: int
``` 

is actually *really* painful to implement. The reason is that, like most other Hy magic operators, `^int` gets desurgared to `(annotate* int)`, so `(^int x)` becomes `((annotate* int) x)`, which is rather ugly to compile.

I also couldn't help but notice we lose some symmetry:

```python
a: int 
b: int 
c: int = 0 
d: int = 0
```

Vs the somewhat awkward (and badly indented, I'm typing this on mobile so please forgive):

```hy
(^int a)
(^int b)
(setv 
 ^int c 0 
 ^int d 0)
``` 

Here's my thought: what if setv could do plain annotations? They stick out easily, we could just drop off the value:

```hy
(setv 
 ^int a 
 ^int b 
 ^int c 0 
 ^int d 0)
```

It does have the odd attribute that, well, setv has "set" but plain type declarations aren't really *setting* anything in the traditional sense. (Though they are setting the annotation, maybe that counts?)

(I can't help but feel like this would actually look nicer if it were still def, I guess hindsight is everything ¯\\\_(ツ)\_/¯)
> `((annotate* int) x)`, which is rather ugly to compile.

I don't think this should be difficult. You just need to insert a check in `compile_expression` that delegates to a new method for `annotate*`. Admittedly, I haven't tried this, so maybe there's something I haven't foreseen.
Hmm it's actually not too bad, though it's worth noting that Python also treats annotations in the AST as an assignment without a value, so the setv idea isn't too bad. ¯\\\_(ツ)\_/¯

Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -23,6 +23,7 @@
 import textwrap
 import pkgutil
 import traceback
+import itertools
 import importlib
 import inspect
 import types
@@ -337,6 +338,15 @@ def mklist(*items, **kwargs):
    return make_hy_model(HyList, items, kwargs.get('rest'))
 
 
+# Parse an annotation setting.
+OPTIONAL_ANNOTATION = maybe(pexpr(sym("annotate*") + FORM) >> (lambda x: x[0]))
+
+
+def is_annotate_expression(model):
+    return (isinstance(model, HyExpression) and model and isinstance(model[0], HySymbol)
+            and model[0] == HySymbol("annotate*"))
+
+
 class HyASTCompiler(object):
     """A Hy-to-Python AST compiler"""
 
@@ -1334,44 +1344,83 @@ def compile_augassign_expression(self, expr, root, target, values):
         return ret + asty.AugAssign(
             expr, target=target, value=ret.force_expr, op=op())
 
-    @special("setv", [many(FORM + FORM)])
+    @special("setv", [many(OPTIONAL_ANNOTATION + FORM + FORM)])
     @special((PY38, "setx"), [times(1, 1, SYM + FORM)])
-    def compile_def_expression(self, expr, root, pairs):
-        if not pairs:
+    def compile_def_expression(self, expr, root, decls):
+        if not decls:
             return asty.Name(expr, id='None', ctx=ast.Load())
+
         result = Result()
-        for pair in pairs:
-            result += self._compile_assign(root, *pair)
+        is_assignment_expr = root == HySymbol("setx")
+        for decl in decls:
+            if is_assignment_expr:
+                ann = None
+                name, value = decl
+            else:
+                ann, name, value = decl
+
+            result += self._compile_assign(ann, name, value,
+                                           is_assignment_expr=is_assignment_expr)
         return result
 
-    def _compile_assign(self, root, name, result):
+    @special(["annotate*"], [FORM, FORM])
+    def compile_basic_annotation(self, expr, root, ann, target):
+        return self._compile_assign(ann, target, None)
 
-        if name in [HySymbol(x) for x in ("None", "True", "False")]:
-            raise self._syntax_error(name,
-                "Can't assign to `{}'".format(name))
+    def _compile_assign(self, ann, name, value, *, is_assignment_expr = False):
+        # Ensure that assignment expressions have a result and no annotation.
+        assert not is_assignment_expr or (value is not None and ann is None)
 
-        result = self.compile(result)
         ld_name = self.compile(name)
 
-        if isinstance(ld_name.expr, ast.Call):
-            raise self._syntax_error(name,
-                "Can't assign to a callable: `{}'".format(name))
+        annotate_only = value is None
+        if annotate_only:
+            result = Result()
+        else:
+            result = self.compile(value)
+
+        invalid_name = False
+        if ann is not None:
+            # An annotation / annotated assignment is more strict with the target expression.
+            invalid_name = not isinstance(ld_name.expr, (ast.Name, ast.Attribute, ast.Subscript))
+        else:
+            invalid_name = (str(name) in ("None", "True", "False")
+                            or isinstance(ld_name.expr, ast.Call))
+
+        if invalid_name:
+            raise self._syntax_error(name, "illegal target for {}".format(
+                                        "annotation" if annotate_only else "assignment"))
 
         if (result.temp_variables
                 and isinstance(name, HySymbol)
                 and '.' not in name):
             result.rename(name)
-            if root != HySymbol("setx"):
+            if not is_assignment_expr:
                 # Throw away .expr to ensure that (setv ...) returns None.
                 result.expr = None
         else:
             st_name = self._storeize(name, ld_name)
-            node = (asty.NamedExpr
-                if root == HySymbol("setx")
-                else asty.Assign)
+
+            if ann is not None:
+                ann_result = self.compile(ann)
+                result = ann_result + result
+
+            if is_assignment_expr:
+                node = asty.NamedExpr
+            elif ann is not None:
+                if not PY36:
+                    raise self._syntax_error(name, "Variable annotations are not supported on "
+                                                   "Python <=3.6")
+
+                node = lambda x, **kw: asty.AnnAssign(x, annotation=ann_result.force_expr,
+                                                      simple=int(isinstance(name, HySymbol)),
+                                                      **kw)
+            else:
+                node = asty.Assign
+
             result += node(
                 name if hasattr(name, "start_line") else result,
-                value=result.force_expr,
+                value=result.force_expr if not annotate_only else None,
                 target=st_name, targets=[st_name])
 
         return result
@@ -1430,39 +1479,60 @@ def make_not(operand):
         # The starred version is for internal use (particularly, in the
         # definition of `defn`). It ensures that a FunctionDef is
         # produced rather than a Lambda.
+        OPTIONAL_ANNOTATION,
         brackets(
-            many(NASYM),
-            maybe(sym("&optional") + many(NASYM | brackets(SYM, FORM))),
-            maybe(sym("&rest") + NASYM),
-            maybe(sym("&kwonly") + many(NASYM | brackets(SYM, FORM))),
-            maybe(sym("&kwargs") + NASYM)),
+            many(OPTIONAL_ANNOTATION + NASYM),
+            maybe(sym("&optional") + many(OPTIONAL_ANNOTATION
+                                            + (NASYM | brackets(SYM, FORM)))),
+            maybe(sym("&rest") + OPTIONAL_ANNOTATION + NASYM),
+            maybe(sym("&kwonly") + many(OPTIONAL_ANNOTATION
+                                        + (NASYM | brackets(SYM, FORM)))),
+            maybe(sym("&kwargs") + OPTIONAL_ANNOTATION + NASYM)),
         many(FORM)])
-    def compile_function_def(self, expr, root, params, body):
-
+    def compile_function_def(self, expr, root, returns, params, body):
         force_functiondef = root in ("fn*", "fn/a")
         node = asty.AsyncFunctionDef if root == "fn/a" else asty.FunctionDef
+        ret = Result()
+
+        # NOTE: Our evaluation order of return type annotations is
+        # different from Python: Python evalautes them after the argument
+        # annotations / defaults (as that's where they are in the source),
+        # but Hy evaluates them *first*, since here they come before the #
+        # argument list. Therefore, it would be more confusing for
+        # readability to evaluate them after like Python.
+
+        ret = Result()
+        returns_ann = None
+        if returns is not None:
+            returns_result = self.compile(returns)
+            ret += returns_result
 
         mandatory, optional, rest, kwonly, kwargs = params
-        optional, defaults, ret = self._parse_optional_args(optional)
-        kwonly, kw_defaults, ret2 = self._parse_optional_args(kwonly, True)
-        ret += ret2
-        main_args = mandatory + optional
 
-        main_args, kwonly, [rest], [kwargs] = (
-            [[x and asty.arg(x, arg=ast_str(x), annotation=None)
-              for x in o]
-             for o in (main_args or [], kwonly or [], [rest], [kwargs])])
+        optional = optional or []
+        kwonly = kwonly or []
+
+        mandatory_ast, _, ret = self._compile_arguments_set(mandatory, False, ret)
+        optional_ast, optional_defaults, ret = self._compile_arguments_set(optional, True, ret)
+        kwonly_ast, kwonly_defaults, ret = self._compile_arguments_set(kwonly, False, ret)
+
+        rest_ast = kwargs_ast = None
+
+        if rest is not None:
+            [rest_ast], _, ret = self._compile_arguments_set([rest], False, ret)
+        if kwargs is not None:
+            [kwargs_ast], _, ret = self._compile_arguments_set([kwargs], False, ret)
 
         args = ast.arguments(
-            args=main_args, defaults=defaults,
-            vararg=rest,
+            args=mandatory_ast + optional_ast, defaults=optional_defaults,
+            vararg=rest_ast,
             posonlyargs=[],
-            kwonlyargs=kwonly, kw_defaults=kw_defaults,
-            kwarg=kwargs)
+            kwonlyargs=kwonly_ast, kw_defaults=kwonly_defaults,
+            kwarg=kwargs_ast)
 
         body = self._compile_branch(body)
 
-        if not force_functiondef and not body.stmts:
+        if not force_functiondef and not body.stmts and returns is None:
             return ret + asty.Lambda(expr, args=args, body=body.force_expr)
 
         if body.expr:
@@ -1474,27 +1544,48 @@ def compile_function_def(self, expr, root, params, body):
                     name=name,
                     args=args,
                     body=body.stmts or [asty.Pass(expr)],
-                    decorator_list=[])
+                    decorator_list=[],
+                    returns=returns_result.force_expr if returns is not None else None)
 
         ast_name = asty.Name(expr, id=name, ctx=ast.Load())
         ret += Result(expr=ast_name, temp_variables=[ast_name, ret.stmts[-1]])
         return ret
 
-    def _parse_optional_args(self, expr, allow_no_default=False):
-        # [a b [c 5] d] → ([a, b, c, d], [None, None, 5, d], <ret>)
-        names, defaults, ret = [], [], Result()
-        for x in expr or []:
-            sym, value = (
-                x if isinstance(x, HyList)
-                else (x, None) if allow_no_default
-                else (x, HySymbol('None').replace(x)))
-            names.append(sym)
-            if value is None:
-                defaults.append(None)
+    def _compile_arguments_set(self, decls, implicit_default_none, ret):
+        args_ast = []
+        args_defaults = []
+
+        for ann, decl in decls:
+            default = None
+
+            # funcparserlib will check to make sure that the only times we
+            # ever have a HyList here are due to a default value.
+            if isinstance(decl, HyList):
+                sym, default = decl
+            else:
+                sym = decl
+                if implicit_default_none:
+                    default = HySymbol('None').replace(sym)
+
+            if ann is not None:
+                ret += self.compile(ann)
+                ann_ast = ret.force_expr
+            else:
+                ann_ast = None
+
+            if default is not None:
+                ret += self.compile(default)
+                args_defaults.append(ret.force_expr)
             else:
-                ret += self.compile(value)
-                defaults.append(ret.force_expr)
-        return names, defaults, ret
+                # Note that the only time any None should ever appear here
+                # is in kwargs, since the order of those with defaults vs
+                # those without isn't significant in the same way as
+                # positional args.
+                args_defaults.append(None)
+
+            args_ast.append(asty.arg(sym, arg=ast_str(sym), annotation=ann_ast))
+
+        return args_ast, args_defaults, ret
 
     @special("return", [maybe(FORM)])
     def compile_return(self, expr, root, arg):
@@ -1543,12 +1634,22 @@ def _rewire_init(self, expr):
             return expr
 
         new_args = []
-        pairs = list(expr[1:])
-        while pairs:
-            k, v = (pairs.pop(0), pairs.pop(0))
+        decls = list(expr[1:])
+        while decls:
+            if is_annotate_expression(decls[0]):
+                # Handle annotations.
+                ann = decls.pop(0)
+            else:
+                ann = None
+
+            k, v = (decls.pop(0), decls.pop(0))
             if ast_str(k) == "__init__" and isinstance(v, HyExpression):
                 v += HyExpression([HySymbol("None")])
-            new_args.extend([k, v])
+
+            if ann is not None:
+                new_args.append(ann)
+
+            new_args.extend((k, v))
         return HyExpression([HySymbol("setv")] + new_args).replace(expr)
 
     @special("dispatch-tag-macro", [STR, FORM])
@@ -1607,7 +1708,7 @@ def compile_inline_python(self, expr, root, code):
         return Result(stmts=o) if exec_mode else o
 
     @builds_model(HyExpression)
-    def compile_expression(self, expr):
+    def compile_expression(self, expr, *, allow_annotation_expression=False):
         # Perform macro expansions
         expr = macroexpand(expr, self.module, self)
         if not isinstance(expr, HyExpression):
@@ -1623,17 +1724,20 @@ def compile_expression(self, expr):
         func = None
 
         if isinstance(root, HySymbol):
-
             # First check if `root` is a special operator, unless it has an
             # `unpack-iterable` in it, since Python's operators (`+`,
             # etc.) can't unpack. An exception to this exception is that
             # tuple literals (`,`) can unpack. Finally, we allow unpacking in
             # `.` forms here so the user gets a better error message.
             sroot = ast_str(root)
-            if (sroot in _special_form_compilers or sroot in _bad_roots) and (
+
+            bad_root = sroot in _bad_roots or (sroot == ast_str("annotate*")
+                                               and not allow_annotation_expression)
+
+            if (sroot in _special_form_compilers or bad_root) and (
                     sroot in (mangle(","), mangle(".")) or
                     not any(is_unpack("iterable", x) for x in args)):
-                if sroot in _bad_roots:
+                if bad_root:
                     raise self._syntax_error(expr,
                         "The special form '{}' is not allowed here".format(root))
                 # `sroot` is a special operator. Get the build method and
@@ -1684,6 +1788,11 @@ def compile_expression(self, expr):
                                        attr=ast_str(root),
                                        ctx=ast.Load())
 
+        elif is_annotate_expression(root):
+            # Flatten and compile the annotation expression.
+            ann_expr = HyExpression(root + args).replace(root)
+            return self.compile_expression(ann_expr, allow_annotation_expression=True)
+
         if not func:
             func = self.compile(root)
 
diff --git a/hy/lex/lexer.py b/hy/lex/lexer.py
--- a/hy/lex/lexer.py
+++ b/hy/lex/lexer.py
@@ -10,7 +10,8 @@
 
 # A regexp for something that should end a quoting/unquoting operator
 # i.e. a space or a closing brace/paren/curly
-end_quote = r'(?![\s\)\]\}])'
+end_quote_set = r'\s\)\]\}'
+end_quote = r'(?![%s])' % end_quote_set
 
 identifier = r'[^()\[\]{}\'"\s;]+'
 
@@ -25,6 +26,7 @@
 lg.add('QUASIQUOTE', r'`%s' % end_quote)
 lg.add('UNQUOTESPLICE', r'~@%s' % end_quote)
 lg.add('UNQUOTE', r'~%s' % end_quote)
+lg.add('ANNOTATION', r'\^(?![=%s])' % end_quote_set)
 lg.add('DISCARD', r'#_')
 lg.add('HASHSTARS', r'#\*+')
 lg.add('BRACKETSTRING', r'''(?x)
diff --git a/hy/lex/parser.py b/hy/lex/parser.py
--- a/hy/lex/parser.py
+++ b/hy/lex/parser.py
@@ -134,6 +134,12 @@ def term_unquote_splice(state, p):
     return HyExpression([HySymbol("unquote-splice"), p[1]])
 
 
+@pg.production("term : ANNOTATION term")
+@set_quote_boundaries
+def term_annotation(state, p):
+    return HyExpression([HySymbol("annotate*"), p[1]])
+
+
 @pg.production("term : HASHSTARS term")
 @set_quote_boundaries
 def term_hashstars(state, p):


Test patch:
diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy
--- a/tests/native_tests/language.hy
+++ b/tests/native_tests/language.hy
@@ -68,15 +68,15 @@
   "NATIVE: test that setv doesn't work on names Python can't assign to
   and that we can't mangle"
   (try (eval '(setv None 1))
-       (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
+       (except [e [SyntaxError]] (assert (in "illegal target for assignment" (str e)))))
   (try (eval '(defn None [] (print "hello")))
-       (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
+       (except [e [SyntaxError]] (assert (in "illegal target for assignment" (str e)))))
   (try (eval '(setv False 1))
-       (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
+       (except [e [SyntaxError]] (assert (in "illegal target for assignment" (str e)))))
   (try (eval '(setv True 0))
-       (except [e [SyntaxError]] (assert (in "Can't assign to" (str e)))))
+       (except [e [SyntaxError]] (assert (in "illegal target for assignment" (str e)))))
   (try (eval '(defn True [] (print "hello")))
-       (except [e [SyntaxError]] (assert (in "Can't assign to" (str e))))))
+       (except [e [SyntaxError]] (assert (in "illegal target for assignment" (str e))))))
 
 
 (defn test-setv-pairs []
@@ -908,6 +908,22 @@
   (assert (= mooey.__name__ "mooey")))
 
 
+(defn test-defn-annotations []
+  "NATIVE: test that annotations in defn work"
+
+  (defn f [^int p1 p2 ^str p3 &optional ^str o1 ^int [o2 0]
+           &rest ^str rest &kwonly ^str k1 ^int [k2 0] &kwargs ^bool kwargs])
+
+  (assert (= (. f __annotations__ ["p1"]) int))
+  (assert (= (. f __annotations__ ["p3"]) str))
+  (assert (= (. f __annotations__ ["o1"]) str))
+  (assert (= (. f __annotations__ ["o2"]) int))
+  (assert (= (. f __annotations__ ["rest"]) str))
+  (assert (= (. f __annotations__ ["k1"]) str))
+  (assert (= (. f __annotations__ ["k2"]) int))
+  (assert (= (. f __annotations__ ["kwargs"]) bool)))
+
+
 (defn test-return []
 
   ; `return` in main line
diff --git a/tests/native_tests/py36_only_tests.hy b/tests/native_tests/py36_only_tests.hy
--- a/tests/native_tests/py36_only_tests.hy
+++ b/tests/native_tests/py36_only_tests.hy
@@ -6,6 +6,7 @@
 ;; conftest.py skips this file when running on Python <3.6.
 
 (import [asyncio [get-event-loop sleep]])
+(import [typing [get-type-hints List Dict]])
 
 
 (defn run-coroutine [coro]
@@ -38,6 +39,20 @@
         (else (setv x (+ x 50))))
       (assert (= x 53)))))
 
+(defn test-variable-annotations []
+  (defclass AnnotationContainer []
+    (setv ^int x 1 y 2)
+    (^bool z))
+
+  (setv annotations (get-type-hints AnnotationContainer))
+  (assert (= (get annotations "x") int))
+  (assert (= (get annotations "z") bool)))
+
+(defn test-of []
+  (assert (= (of str) str))
+  (assert (= (of List int) (get List int)))
+  (assert (= (of Dict str str) (get Dict (, str str)))))
+
 (defn test-pep-487 []
   (defclass QuestBase []
     (defn --init-subclass-- [cls swallow &kwargs kwargs]