From 4c38e2c9dd9bcbad6dd65db27258f25f41668aaf Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Fri, 21 Apr 2017 08:07:48 -0700 Subject: [PATCH] Rename reader macros to "sharp macros" (#1282) They're not actually reader macros, since their arguments are parsed s-expressions, like a regular macro, not pre-parsed source text. --- docs/language/api.rst | 42 +++++++---- docs/language/index.rst | 1 - docs/language/readermacros.rst | 73 ------------------- docs/tutorial.rst | 10 +-- hy/compiler.py | 25 +++---- hy/completer.py | 10 +-- hy/core/macros.hy | 2 +- hy/lex/lexer.py | 2 +- hy/lex/parser.py | 6 +- hy/macros.py | 26 +++---- tests/__init__.py | 2 +- ..._reader_macros.py => test_sharp_macros.py} | 4 +- .../{reader_macros.hy => sharp_macros.hy} | 30 ++++---- tests/test_lex.py | 6 +- 14 files changed, 89 insertions(+), 150 deletions(-) delete mode 100644 docs/language/readermacros.rst rename tests/macros/{test_reader_macros.py => test_sharp_macros.py} (76%) rename tests/native_tests/{reader_macros.hy => sharp_macros.hy} (71%) diff --git a/docs/language/api.rst b/docs/language/api.rst index ea0fb30..6fe6ea4 100644 --- a/docs/language/api.rst +++ b/docs/language/api.rst @@ -767,25 +767,39 @@ For example, 42 -defreader ---------- +defsharp +-------- -.. versionadded:: 0.9.12 +.. versionadded:: 0.13.0 -``defreader`` defines a reader macro, enabling you to restructure or -modify syntax. +``defsharp`` defines a sharp macro. A sharp macro is a unary macro that has the +same semantics as an ordinary macro defined with ``defmacro``, but can be +called without parentheses and with less whitespace. The name of a sharp macro +must be exactly one character long. It is called with the syntax ``#cFORM``, +where ``#`` is a literal sharp sign (hence the term "sharp macro"), ``c`` is +the name of the macro, and ``FORM`` is any form. Whitspace is forbidden between +``#`` and ``c``. Whitespace is allowed between ``c`` and ``FORM``, but not +required. .. code-block:: clj - => (defreader ^ [expr] (print expr)) - => #^(1 2 3 4) - (1 2 3 4) - => #^"Hello" - "Hello" + => (defsharp ♣ [expr] `[~expr ~expr]) + at 0x7f76d0271158> + => #♣5 + [5, 5] + => (setv x 0) + => #♣(+= x 1) + [None, None] + => x + 2 -.. seealso:: +In this example, if you used ``(defmacro ♣ ...)`` instead of ``(defsharp +♣ ...)``, you would call the macro as ``(♣ 5)`` or ``(♣ (+= x 1))``. - Section :ref:`Reader Macros ` +The syntax for calling sharp macros is similar to that of reader macros a la +Common Lisp's ``SET-MACRO-CHARACTER``. In fact, before Hy 0.13.0, sharp macros +were called "reader macros", and defined with ``defreader`` rather than +``defsharp``. True reader macros are not (yet) implemented in Hy. del --- @@ -1691,8 +1705,8 @@ will be 4 (``1+1 + 1+1``). .. versionadded:: 0.12.0 -The :ref:`reader macro` ``#@`` can be used as a shorthand -for ``with-decorator``. With ``#@``, the previous example becomes: +The sharp macro ``#@`` can be used as a shorthand for ``with-decorator``. With +``#@``, the previous example becomes: .. code-block:: clj diff --git a/docs/language/index.rst b/docs/language/index.rst index 143d01e..672063a 100644 --- a/docs/language/index.rst +++ b/docs/language/index.rst @@ -11,5 +11,4 @@ Contents: interop api core - readermacros internals diff --git a/docs/language/readermacros.rst b/docs/language/readermacros.rst deleted file mode 100644 index f633f9e..0000000 --- a/docs/language/readermacros.rst +++ /dev/null @@ -1,73 +0,0 @@ -.. _reader-macros: - -.. highlight:: clj - -============= -Reader Macros -============= - -Reader macros gives Lisp the power to modify and alter syntax on the fly. -You don't want Polish notation? A reader macro can easily do just that. Want -Clojure's way of having a regex? Reader macros can also do this easily. - - -Syntax -====== - -:: - - => (defreader ^ [expr] (print expr)) - => #^(1 2 3 4) - (1 2 3 4) - => #^"Hello" - "Hello" - => #^1+2+3+4+3+2 - 1+2+3+4+3+2 - -Hy has no literal for tuples. Lets say you dislike `(, ...)` and want something -else. This is a problem reader macros are able to solve in a neat way. - -:: - - => (defreader t [expr] `(, ~@expr)) - => #t(1 2 3) - (1, 2, 3) - -You could even do it like Clojure and have a literal for regular expressions! - -:: - - => (import re) - => (defreader r [expr] `(re.compile ~expr)) - => #r".*" - <_sre.SRE_Pattern object at 0xcv7713ph15#> - - -Implementation -============== - -``defreader`` takes a single character as symbol name for the reader macro; -anything longer will return an error. Implementation-wise, ``defreader`` -expands into a lambda covered with a decorator. This decorator saves the -lambda in a dictionary with its module name and symbol. - -:: - - => (defreader ^ [expr] (print expr)) - ;=> (with_decorator (hy.macros.reader ^) (fn [expr] (print expr))) - -``#`` expands into ``(dispatch_reader_macro ...)`` where the symbol -and expression is passed to the correct function. - -:: - - => #^() - ;=> (dispatch_reader_macro ^ ()) - => #^"Hello" - "Hello" - - -.. warning:: - Because of a limitation in Hy's lexer and parser, reader macros can't - redefine defined syntax such as ``()[]{}``. This will most likely be - addressed in the future. diff --git a/docs/tutorial.rst b/docs/tutorial.rst index c1432b3..38e951d 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -581,14 +581,14 @@ elements, so by the time program started executing, it actually reads: (+ 1 2 3) -Sometimes it's nice to have a very short name for a macro that doesn't take much -space or use extra parentheses. Reader macros can be pretty useful in these -situations (and since Hy operates well with unicode, we aren't running out of -characters that soon): +Sometimes it's nice to be able to call a one-parameter macro without +parentheses. Sharp macros allow this. The name of a sharp macro must be only +one character long, but since Hy operates well with Unicode, we aren't running +out of characters that soon: .. code-block:: clj - => (defreader ↻ [code] + => (defsharp ↻ [code] ... (setv op (last code) params (list (butlast code))) ... `(~op ~@params)) => #↻(1 2 3 +) diff --git a/hy/compiler.py b/hy/compiler.py index 2db237d..2933533 100644 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -34,7 +34,7 @@ from hy.lex.parser import hy_symbol_mangle import hy.macros from hy._compat import ( str_type, bytes_type, long_type, PY27, PY33, PY3, PY34, PY35, raise_empty) -from hy.macros import require, macroexpand, reader_macroexpand +from hy.macros import require, macroexpand, sharp_macroexpand import hy.importer import traceback @@ -2544,21 +2544,20 @@ class HyASTCompiler(object): return ret - @builds("defreader") + @builds("defsharp") @checkargs(min=2) - def compile_reader(self, expression): + def compile_sharp_macro(self, expression): expression.pop(0) name = expression.pop(0) - NOT_READERS = [":", "&"] - if name in NOT_READERS or len(name) > 1: - raise NameError("%s can't be used as a macro reader symbol" % name) + if name == ":" or name == "&" or len(name) > 1: + raise NameError("%s can't be used as a sharp macro name" % name) if not isinstance(name, HySymbol) and not isinstance(name, HyString): raise HyTypeError(name, ("received a `%s' instead of a symbol " - "for reader macro name" % type(name).__name__)) + "for sharp macro name" % type(name).__name__)) name = HyString(name).replace(name) new_expression = HyExpression([ - HyExpression([HySymbol("hy.macros.reader"), name]), + HyExpression([HySymbol("hy.macros.sharp"), name]), HyExpression([HySymbol("fn")] + expression), ]).replace(expression) @@ -2566,18 +2565,18 @@ class HyASTCompiler(object): return ret - @builds("dispatch_reader_macro") + @builds("dispatch_sharp_macro") @checkargs(exact=2) - def compile_dispatch_reader_macro(self, expression): - expression.pop(0) # dispatch-reader-macro + def compile_dispatch_sharp_macro(self, expression): + expression.pop(0) # dispatch-sharp-macro str_char = expression.pop(0) if not type(str_char) == HyString: raise HyTypeError( str_char, - "Trying to expand a reader macro using `{0}' instead " + "Trying to expand a sharp macro using `{0}' instead " "of string".format(type(str_char).__name__), ) - expr = reader_macroexpand(str_char, expression.pop(0), self) + expr = sharp_macroexpand(str_char, expression.pop(0), self) return self.compile(expr) @builds("eval_and_compile") diff --git a/hy/completer.py b/hy/completer.py index eaac593..830a61e 100644 --- a/hy/completer.py +++ b/hy/completer.py @@ -63,11 +63,11 @@ class Completer(object): builtins.__dict__, hy.macros._hy_macros[None], namespace] - self.reader_path = [hy.macros._hy_reader[None]] + self.sharp_path = [hy.macros._hy_sharp[None]] if '__name__' in namespace: module_name = namespace['__name__'] self.path.append(hy.macros._hy_macros[module_name]) - self.reader_path.append(hy.macros._hy_reader[module_name]) + self.sharp_path.append(hy.macros._hy_sharp[module_name]) def attr_matches(self, text): # Borrowed from IPython's completer @@ -104,10 +104,10 @@ class Completer(object): matches.append(k) return matches - def reader_matches(self, text): + def sharp_matches(self, text): text = text[1:] matches = [] - for p in self.reader_path: + for p in self.sharp_path: for k in p.keys(): if isinstance(k, string_types): if k.startswith(text): @@ -116,7 +116,7 @@ class Completer(object): def complete(self, text, state): if text.startswith("#"): - matches = self.reader_matches(text) + matches = self.sharp_matches(text) elif "." in text: matches = self.attr_matches(text) else: diff --git a/hy/core/macros.hy b/hy/core/macros.hy index 3177db8..7f6f4ba 100644 --- a/hy/core/macros.hy +++ b/hy/core/macros.hy @@ -250,7 +250,7 @@ (sys.exit ~retval)))) -(defreader @ [expr] +(defsharp @ [expr] (setv decorators (cut expr None -1) fndef (get expr -1)) `(with-decorator ~@decorators ~fndef)) diff --git a/hy/lex/lexer.py b/hy/lex/lexer.py index 4fe35e2..6d78d92 100644 --- a/hy/lex/lexer.py +++ b/hy/lex/lexer.py @@ -41,7 +41,7 @@ lg.add('QUASIQUOTE', r'`%s' % end_quote) lg.add('UNQUOTESPLICE', r'~@%s' % end_quote) lg.add('UNQUOTE', r'~%s' % end_quote) lg.add('HASHBANG', r'#!.*[^\r\n]') -lg.add('HASHREADER', r'#[^{]') +lg.add('HASHOTHER', r'#[^{]') # A regexp which matches incomplete strings, used to support # multi-line strings in the interpreter diff --git a/hy/lex/parser.py b/hy/lex/parser.py index 518caae..cf06c97 100644 --- a/hy/lex/parser.py +++ b/hy/lex/parser.py @@ -213,13 +213,13 @@ def term_unquote_splice(p): return HyExpression([HySymbol("unquote_splice"), p[1]]) -@pg.production("term : HASHREADER term") +@pg.production("term : HASHOTHER term") @set_quote_boundaries -def hash_reader(p): +def hash_other(p): st = p[0].getstr()[1] str_object = HyString(st) expr = p[1] - return HyExpression([HySymbol("dispatch_reader_macro"), str_object, expr]) + return HyExpression([HySymbol("dispatch_sharp_macro"), str_object, expr]) @pg.production("set : HLCURLY list_contents RCURLY") diff --git a/hy/macros.py b/hy/macros.py index 3ddeada..261697b 100644 --- a/hy/macros.py +++ b/hy/macros.py @@ -34,7 +34,7 @@ EXTRA_MACROS = [ ] _hy_macros = defaultdict(dict) -_hy_reader = defaultdict(dict) +_hy_sharp = defaultdict(dict) def macro(name): @@ -66,8 +66,8 @@ def macro(name): return _ -def reader(name): - """Decorator to define a reader macro called `name`. +def sharp(name): + """Decorator to define a sharp macro called `name`. This stores the macro `name` in the namespace for the module where it is defined. @@ -75,14 +75,14 @@ def reader(name): If the module where it is defined is in `hy.core`, then the macro is stored in the default `None` namespace. - This function is called from the `defreader` special form in the compiler. + This function is called from the `defsharp` special form in the compiler. """ def _(fn): module_name = fn.__module__ if module_name.startswith("hy.core"): module_name = None - _hy_reader[module_name][name] = fn + _hy_sharp[module_name][name] = fn return fn return _ @@ -106,7 +106,7 @@ def require(source_module, target_module, if prefix: prefix += "." - for d in _hy_macros, _hy_reader: + for d in _hy_macros, _hy_sharp: for name, macro in d[source_module].items(): seen_names.add(name) if all_macros: @@ -226,19 +226,19 @@ def macroexpand_1(tree, compiler): return tree -def reader_macroexpand(char, tree, compiler): - """Expand the reader macro "char" with argument `tree`.""" +def sharp_macroexpand(char, tree, compiler): + """Expand the sharp macro "char" with argument `tree`.""" load_macros(compiler.module_name) - reader_macro = _hy_reader[compiler.module_name].get(char) - if reader_macro is None: + sharp_macro = _hy_sharp[compiler.module_name].get(char) + if sharp_macro is None: try: - reader_macro = _hy_reader[None][char] + sharp_macro = _hy_sharp[None][char] except KeyError: raise HyTypeError( char, - "`{0}' is not a defined reader macro.".format(char) + "`{0}' is not a defined sharp macro.".format(char) ) - expr = reader_macro(tree) + expr = sharp_macro(tree) return replace_hy_obj(wrap_value(expr), tree) diff --git a/tests/__init__.py b/tests/__init__.py index 15a102e..1fbc6c6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -11,7 +11,7 @@ from .native_tests.unless import * # noqa from .native_tests.when import * # noqa from .native_tests.with_decorator import * # noqa from .native_tests.core import * # noqa -from .native_tests.reader_macros import * # noqa +from .native_tests.sharp_macros import * # noqa from .native_tests.operators import * # noqa from .native_tests.with_test import * # noqa from .native_tests.extra.anaphoric import * # noqa diff --git a/tests/macros/test_reader_macros.py b/tests/macros/test_sharp_macros.py similarity index 76% rename from tests/macros/test_reader_macros.py rename to tests/macros/test_sharp_macros.py index 0104d87..5f26ed6 100644 --- a/tests/macros/test_reader_macros.py +++ b/tests/macros/test_sharp_macros.py @@ -3,10 +3,10 @@ from hy.compiler import HyTypeError, HyASTCompiler from hy.lex import tokenize -def test_reader_macro_error(): +def test_sharp_macro_error(): """Check if we get correct error with wrong dispatch character""" try: - macroexpand(tokenize("(dispatch_reader_macro '- '())")[0], + macroexpand(tokenize("(dispatch_sharp_macro '- '())")[0], HyASTCompiler(__name__)) except HyTypeError as e: assert "with the character `-`" in str(e) diff --git a/tests/native_tests/reader_macros.hy b/tests/native_tests/sharp_macros.hy similarity index 71% rename from tests/native_tests/reader_macros.hy rename to tests/native_tests/sharp_macros.hy index a15468f..e5a9276 100644 --- a/tests/native_tests/reader_macros.hy +++ b/tests/native_tests/sharp_macros.hy @@ -1,34 +1,34 @@ (import [functools [wraps]]) -(defn test-reader-macro [] - "Test a basic reader macro" - (defreader ^ [expr] +(defn test-sharp-macro [] + "Test a basic sharp macro" + (defsharp ^ [expr] expr) (assert (= #^"works" "works"))) -(defn test-reader-macro-expr [] +(defn test-sharp-macro-expr [] "Test basic exprs like lists and arrays" - (defreader n [expr] + (defsharp n [expr] (get expr 1)) (assert (= #n[1 2] 2)) (assert (= #n(1 2) 2))) -(defn test-reader-macro-override [] +(defn test-sharp-macro-override [] "Test if we can override function symbols" - (defreader + [n] + (defsharp + [n] (+ n 1)) (assert (= #+2 3))) -(defn test-reader-macros-macros [] - "Test if defreader is actually a macro" - (defreader t [expr] +(defn test-sharp-macros-macros [] + "Test if defsharp is actually a macro" + (defsharp t [expr] `(, ~@expr)) (def a #t[1 2 3]) @@ -37,16 +37,16 @@ (assert (= (, 1 2 3) a))) -(defn test-reader-macro-string-name [] - "Test if defreader accepts a string as a macro name." +(defn test-sharp-macro-string-name [] + "Test if defsharp accepts a string as a macro name." - (defreader "." [expr] + (defsharp "." [expr] expr) (assert (= #."works" "works"))) -(defn test-builtin-decorator-reader [] +(defn test-builtin-decorator-sharp [] (defn increment-arguments [func] "Increments each argument passed to the decorated function." ((wraps func) @@ -68,7 +68,7 @@ (assert (= "foo" (. foo --name--))) (assert (= "Bar." (. foo --doc--))) - ;; We can use the #@ reader macro to apply more than one decorator + ;; We can use the #@ sharp macro to apply more than one decorator #@(increment-arguments increment-arguments (defn double-foo [&rest args &kwargs kwargs] diff --git a/tests/test_lex.py b/tests/test_lex.py index 514696b..5c14e1a 100644 --- a/tests/test_lex.py +++ b/tests/test_lex.py @@ -320,10 +320,10 @@ def test_complex(): assert entry == HySymbol("j") -def test_reader_macro(): - """Ensure reader macros are handles properly""" +def test_sharp_macro(): + """Ensure sharp macros are handled properly""" entry = tokenize("#^()") - assert entry[0][0] == HySymbol("dispatch_reader_macro") + assert entry[0][0] == HySymbol("dispatch_sharp_macro") assert entry[0][1] == HyString("^") assert len(entry[0]) == 3