Repository: hylang/hy

Issue:
Parsing `^` is weird
A wart in the syntax is that `^` marks an annotation unless it's followed by whitespace or `=`. The `^` and `^=` macros are rarely used, so I think it's safe to rename them. Then `^` will only be for annotations. Another option is to use some other syntax for annotation, like `#^`, and let `^` be a regular character.


Discussion:
I'd prefer the `#^` option of the two, so that all the Python operators still translate 1:1.

Alternatively, would it be possible to (ab)use keywords to represent annotations somehow? Since it would map nicely with the Python colon syntax.
> so that all the Python operators still translate 1:1

We're still stuck with `=` for `==` and `setv` for `=`. ;_;

> Alternatively, would it be possible to (ab)use keywords to represent annotations somehow?

Not that I can think of. What do you have in mind?
I was thinking something along the lines of `:int` etc, but now that I'm typing this I'm realizing it wouldn't work for anything more complicated (eg `(of list int)`).

I think `#^` works well, since our annotation syntax already works like a reader macro.
I'd be good with `#^`. it'd slot right into the code that parses `#*` & `#**` easily enough

Resolving patch:
diff --git a/hy/core/result_macros.py b/hy/core/result_macros.py
--- a/hy/core/result_macros.py
+++ b/hy/core/result_macros.py
@@ -69,8 +69,9 @@ def pvalue(root, wanted):
     return pexpr(sym(root) + wanted) >> (lambda x: x[0])
 
 
-# Parse an annotation setting.
-OPTIONAL_ANNOTATION = maybe(pvalue("annotate", FORM))
+def maybe_annotated(target):
+    return pexpr(sym("annotate") + target + FORM) | target >> (lambda x: (x, None))
+
 
 # ------------------------------------------------
 # * Fundamentals
@@ -402,7 +403,7 @@ def compile_augassign_expression(compiler, expr, root, target, values):
 # ------------------------------------------------
 
 
-@pattern_macro("setv", [many(OPTIONAL_ANNOTATION + FORM + FORM)])
+@pattern_macro("setv", [many(maybe_annotated(FORM) + FORM)])
 @pattern_macro(((3, 8), "setx"), [times(1, 1, SYM + FORM)])
 def compile_def_expression(compiler, expr, root, decls):
     if not decls:
@@ -415,7 +416,7 @@ def compile_def_expression(compiler, expr, root, decls):
             ann = None
             name, value = decl
         else:
-            ann, name, value = decl
+            (name, ann), value = decl
 
         result += compile_assign(
             compiler, ann, name, value, is_assignment_expr=is_assignment_expr
@@ -424,7 +425,7 @@ def compile_def_expression(compiler, expr, root, decls):
 
 
 @pattern_macro(["annotate"], [FORM, FORM])
-def compile_basic_annotation(compiler, expr, root, ann, target):
+def compile_basic_annotation(compiler, expr, root, target, ann):
     return compile_assign(compiler, ann, target, None)
 
 
@@ -1368,8 +1369,8 @@ def compile_catch_expression(compiler, expr, var, exceptions, body):
 # ------------------------------------------------
 
 NASYM = some(lambda x: isinstance(x, Symbol) and x not in (Symbol("/"), Symbol("*")))
-argument = OPTIONAL_ANNOTATION + (NASYM | brackets(NASYM, FORM))
-varargs = lambda unpack_type, wanted: OPTIONAL_ANNOTATION + pvalue(unpack_type, wanted)
+argument = maybe_annotated(NASYM | brackets(NASYM, FORM))
+varargs = lambda unpack_type, wanted: maybe_annotated(pvalue(unpack_type, wanted))
 kwonly_delim = some(lambda x: x == Symbol("*"))
 lambda_list = brackets(
     maybe(many(argument) + sym("/")),
@@ -1380,11 +1381,12 @@ def compile_catch_expression(compiler, expr, var, exceptions, body):
 )
 
 
-@pattern_macro(["fn", "fn/a"], [OPTIONAL_ANNOTATION, lambda_list, many(FORM)])
-def compile_function_lambda(compiler, expr, root, returns, params, body):
+@pattern_macro(["fn", "fn/a"], [maybe_annotated(lambda_list), many(FORM)])
+def compile_function_lambda(compiler, expr, root, params, body):
+    params, returns = params
     posonly, args, rest, kwonly, kwargs = params
     has_annotations = returns is not None or any(
-        isinstance(param, tuple) and param[0] is not None
+        isinstance(param, tuple) and param[1] is not None
         for param in (posonly or []) + args + kwonly + [rest, kwargs]
     )
     args, ret = compile_lambda_list(compiler, params)
@@ -1406,9 +1408,10 @@ def compile_function_lambda(compiler, expr, root, returns, params, body):
 
 @pattern_macro(
     ["defn", "defn/a"],
-    [maybe(brackets(many(FORM))), OPTIONAL_ANNOTATION, SYM, lambda_list, many(FORM)],
+    [maybe(brackets(many(FORM))), maybe_annotated(SYM), lambda_list, many(FORM)],
 )
-def compile_function_def(compiler, expr, root, decorators, returns, name, params, body):
+def compile_function_def(compiler, expr, root, decorators, name, params, body):
+    name, returns = name
     node = asty.FunctionDef if root == "defn" else asty.AsyncFunctionDef
     decorators, ret, _ = compiler._compile_collect(decorators[0] if decorators else [])
     args, ret2 = compile_lambda_list(compiler, params)
@@ -1497,7 +1500,7 @@ def compile_lambda_list(compiler, params):
 
     posonly_parms = posonly_parms or []
 
-    is_positional_arg = lambda x: isinstance(x[1], Symbol)
+    is_positional_arg = lambda x: isinstance(x[0], Symbol)
     invalid_non_default = next(
         (
             arg
@@ -1508,7 +1511,7 @@ def compile_lambda_list(compiler, params):
     )
     if invalid_non_default:
         raise compiler._syntax_error(
-            invalid_non_default[1], "non-default argument follows default argument"
+            invalid_non_default[0], "non-default argument follows default argument"
         )
 
     posonly_ast, posonly_defaults, ret = compile_arguments_set(
@@ -1549,7 +1552,7 @@ def compile_arguments_set(compiler, decls, ret, is_kwonly=False):
     args_ast = []
     args_defaults = []
 
-    for ann, decl in decls:
+    for decl, ann in decls:
         default = None
 
         # funcparserlib will check to make sure that the only times we
@@ -1852,13 +1855,13 @@ def compile_assert_expression(compiler, expr, root, test, msg):
     )
 
 
-@pattern_macro("let", [brackets(many(OPTIONAL_ANNOTATION + FORM + FORM)), many(FORM)])
+@pattern_macro("let", [brackets(many(maybe_annotated(FORM) + FORM)), many(FORM)])
 def compile_let(compiler, expr, root, bindings, body):
     res = Result()
     bindings = bindings[0]
     scope = compiler.scope.create(ScopeLet)
 
-    for ann, target, value in bindings:
+    for (target, ann), value in bindings:
         res += compile_assign(compiler, ann, target, value, let_scope=scope)
 
     with scope:
diff --git a/hy/lex/hy_reader.py b/hy/lex/hy_reader.py
--- a/hy/lex/hy_reader.py
+++ b/hy/lex/hy_reader.py
@@ -308,17 +308,6 @@ def unquote(self, key):
             self.parse_one_form(),
         )
 
-    @reader_for("^")
-    def annotate_or_xor(self, _):
-        next_char = self.peekc()
-        if not next_char or isnormalizedspace(next_char):
-            return sym("^")
-        elif next_char == "=":
-            self.getc()
-            return sym("^=")
-        else:
-            return mkexpr("annotate", self.parse_one_form())
-
     ###
     # Sequences
     ###
@@ -352,6 +341,11 @@ def tag_dispatch(self, key):
                 "Premature end of input while attempting dispatch", self
             )
 
+        if self.peek_and_getc("^"):
+            typ = self.parse_one_form()
+            target = self.parse_one_form()
+            return mkexpr("annotate", target, typ)
+
         tag = None
         # try dispatching tagged ident
         ident = self.read_ident(just_peeking=True)


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
@@ -754,8 +754,8 @@
 
 (defn test-defn-annotations []
 
-  (defn ^int f [^(get List int) p1 p2 ^str p3 ^str [o1 None] ^int [o2 0]
-           ^str #* rest ^str k1 ^int [k2 0] ^bool #** kwargs])
+  (defn #^int f [#^(get List int) p1 p2 #^str p3 #^str [o1 None] #^int [o2 0]
+           #^str #* rest #^str k1 #^int [k2 0] #^bool #** kwargs])
 
   (assert (is (. f __annotations__ ["return"]) int))
   (for [[k v] (.items (dict
@@ -1766,8 +1766,8 @@ cee"} dee" "ey bee\ncee dee"))
 
 (defn test-variable-annotations []
   (defclass AnnotationContainer []
-    (setv ^int x 1 y 2)
-    (^bool z))
+    (setv #^int x 1 y 2)
+    (#^bool z))
 
   (setv annotations (get-type-hints AnnotationContainer))
   (assert (= (get annotations "x") int))
diff --git a/tests/native_tests/py3_10_only_tests.hy b/tests/native_tests/py3_10_only_tests.hy
--- a/tests/native_tests/py3_10_only_tests.hy
+++ b/tests/native_tests/py3_10_only_tests.hy
@@ -76,8 +76,8 @@
     0))
 
   (defclass [dataclass] Point []
-    (^int x)
-    (^int y))
+    (#^int x)
+    (#^int y))
 
   (assert (= 0 (match (Point 1 0) (Point 1 :y var) var)))
   (assert (is None (match (Point 0 0) (Point 1 :y var) var)))
@@ -163,8 +163,8 @@
   (assert (= [x y] [3 4])))
 
 (defclass [dataclass] Point []
-  (^int x)
-  (^int y))
+  (#^int x)
+  (#^int y))
 
 (defn test-let-match-pattern []
   (setv [x y] [1 2]
diff --git a/tests/native_tests/reader_macros.hy b/tests/native_tests/reader_macros.hy
--- a/tests/native_tests/reader_macros.hy
+++ b/tests/native_tests/reader_macros.hy
@@ -3,6 +3,7 @@
   sys
   types
   contextlib [contextmanager]
+  hy.errors [HyMacroExpansionError]
 
   pytest)
 
@@ -43,6 +44,13 @@
     (eval-isolated (next it) module)
     (assert (= (next it) '"hello, world!"))))
 
+(defn test-bad-reader-macro-name []
+  (with [(pytest.raises HyMacroExpansionError)]
+    (eval-module "(defreader :a-key '1)"))
+
+  (with [(pytest.raises HyMacroExpansionError)]
+    (eval-module "(defreader ^foo '1)")))
+
 (defn test-require-readers []
   (with [module (temp-module "<test>")]
     (setv it (hy.read-many #[[(require tests.resources.tlib :readers [upper])
diff --git a/tests/resources/pydemo.hy b/tests/resources/pydemo.hy
--- a/tests/resources/pydemo.hy
+++ b/tests/resources/pydemo.hy
@@ -51,8 +51,8 @@ Call me Ishmael. Some years ago—never mind how long precisely—having little
 (setv boolexpr (and (or True False) (not (and True False))))
 (setv condexpr (if "" "x" "y"))
 (setv mylambda (fn [x] (+ x "z")))
-(setv annotated-lambda-ret (fn ^int [] 1))
-(setv annotated-lambda-params (fn [^int a * ^str [b "hello world!"]] #(a b)))
+(setv annotated-lambda-ret (fn #^int [] 1))
+(setv annotated-lambda-params (fn [#^int a * #^str [b "hello world!"]] #(a b)))
 
 (setv fstring1 f"hello {(+ 1 1)} world")
 (setv p "xyzzy")