From c9fdd40c9f1537e55bb334c49d7aa21e3e29f315 Mon Sep 17 00:00:00 2001 From: Foxboron Date: Sun, 15 Dec 2013 17:47:24 +0100 Subject: [PATCH] Hy reader macros #377 Added first iteration of reader macros Refactored defmacro and defreader Added test inn hy/tests/lex/test_lex.py Added new test in hy/tests/native/tests Added new test in hy/tests/macros. changed the error given in the dispatch macro and added some handling for missing symbol and invalid characters --- hy/compiler.py | 46 +++++++++++++++++++++++------ hy/core/macros.hy | 11 ++++++- hy/lex/lexer.py | 1 + hy/lex/parser.py | 9 ++++++ hy/macros.py | 31 +++++++++++++++++++ tests/__init__.py | 1 + tests/lex/test_lex.py | 8 +++++ tests/macros/test_reader_macros.py | 11 +++++++ tests/native_tests/reader_macros.hy | 32 ++++++++++++++++++++ 9 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 tests/macros/test_reader_macros.py create mode 100644 tests/native_tests/reader_macros.hy diff --git a/hy/compiler.py b/hy/compiler.py index 8d848b6..36dd12a 100644 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -36,6 +36,7 @@ from hy.models.float import HyFloat from hy.models.list import HyList from hy.models.dict import HyDict +import hy.macros from hy.macros import require, macroexpand from hy._compat import str_type, long_type import hy.importer @@ -1753,6 +1754,19 @@ class HyASTCompiler(object): bases=bases_expr, body=body.stmts) + def _compile_time_hack(self, expression): + """Compile-time hack: we want to get our new macro now + We must provide __name__ in the namespace to make the Python + compiler set the __module__ attribute of the macro function.""" + hy.importer.hy_eval(expression, + compile_time_ns(self.module_name), + self.module_name) + + # We really want to have a `hy` import to get hy.macro in + ret = self.compile(expression) + ret.add_imports('hy', [None]) + return ret + @builds("defmacro") @checkargs(min=1) def compile_macro(self, expression): @@ -1768,16 +1782,30 @@ class HyASTCompiler(object): HyExpression([HySymbol("fn")] + expression), ]).replace(expression) - # Compile-time hack: we want to get our new macro now - # We must provide __name__ in the namespace to make the Python - # compiler set the __module__ attribute of the macro function. - hy.importer.hy_eval(new_expression, - compile_time_ns(self.module_name), - self.module_name) + ret = self._compile_time_hack(new_expression) - # We really want to have a `hy` import to get hy.macro in - ret = self.compile(new_expression) - ret.add_imports('hy', [None]) + return ret + + @builds("defreader") + @checkargs(min=2, max=3) + def compile_reader(self, expression): + expression.pop(0) + name = expression.pop(0) + NOT_READERS = [":", "&"] + if name in NOT_READERS: + raise NameError("%s can't be used as a macro reader symbol" % name) + if not isinstance(name, HySymbol): + raise HyTypeError(name, + ("received a `%s' instead of a symbol " + "for reader macro name" % type(name).__name__)) + name = HyString(name).replace(name) + new_expression = HyExpression([ + HySymbol("with_decorator"), + HyExpression([HySymbol("hy.macros.reader"), name]), + HyExpression([HySymbol("fn")] + expression), + ]).replace(expression) + + ret = self._compile_time_hack(new_expression) return ret diff --git a/hy/core/macros.hy b/hy/core/macros.hy index 026fc86..d7c8b15 100644 --- a/hy/core/macros.hy +++ b/hy/core/macros.hy @@ -25,7 +25,6 @@ ;;; These macros form the hy language ;;; They are automatically required in every module, except inside hy.core - (defmacro for [args &rest body] "shorthand for nested foreach loops: (for [x foo y bar] baz) -> @@ -144,3 +143,13 @@ (setv -args (cdr (car -args)))) `(apply ~-fun [~@-args] (dict (sum ~-okwargs []))))) + + +(defmacro dispatch-reader-macro [char &rest body] + "Dispatch a reader macro based on the character" + (import [hy.macros]) + (setv str_char (get char 1)) + (if (not (in str_char hy.macros._hy_reader_chars)) + (raise (hy.compiler.HyTypeError char (.format "There is no reader macro with the character `{0}`" str_char)))) + `(do (import [hy.macros [_hy_reader]]) + ((get (get _hy_reader --name--) ~char) ~(get body 0)))) diff --git a/hy/lex/lexer.py b/hy/lex/lexer.py index ce3eae3..d4a5638 100644 --- a/hy/lex/lexer.py +++ b/hy/lex/lexer.py @@ -40,6 +40,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('STRING', r'''(?x) diff --git a/hy/lex/parser.py b/hy/lex/parser.py index e4a3ece..70d43f8 100644 --- a/hy/lex/parser.py +++ b/hy/lex/parser.py @@ -150,6 +150,15 @@ def term_unquote_splice(p): return HyExpression([HySymbol("unquote_splice"), p[1]]) +@pg.production("term : HASHREADER term") +@set_quote_boundaries +def hash_reader(p): + st = p[0].getstr()[1] + str_object = HyExpression([HySymbol("quote"), HyString(st)]) + expr = HyExpression([HySymbol("quote"), p[1]]) + return HyExpression([HySymbol("dispatch_reader_macro"), str_object, expr]) + + @pg.production("dict : LCURLY list_contents RCURLY") @set_boundaries def t_dict(p): diff --git a/hy/macros.py b/hy/macros.py index 1074fc5..4e2389c 100644 --- a/hy/macros.py +++ b/hy/macros.py @@ -40,6 +40,8 @@ EXTRA_MACROS = [ ] _hy_macros = defaultdict(dict) +_hy_reader = defaultdict(dict) +_hy_reader_chars = set() def macro(name): @@ -63,6 +65,30 @@ def macro(name): return _ +def reader(name): + """Decorator to define a macro called `name`. + + This stores the macro `name` in the namespace for the module where it is + defined. + + 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 `defmacro` 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 + + # Ugly hack to get some error handling + _hy_reader_chars.add(name) + return fn + return _ + + def require(source_module, target_module): """Load the macros from `source_module` in the namespace of `target_module`. @@ -75,6 +101,11 @@ def require(source_module, target_module): for name, macro in macros.items(): refs[name] = macro + readers = _hy_reader[source_module] + reader_refs = _hy_reader[target_module] + for name, reader in readers.items(): + reader_refs[name] = reader + # type -> wrapping function mapping for _wrap_value _wrappers = { diff --git a/tests/__init__.py b/tests/__init__.py index 5e935a2..582ceae 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -11,3 +11,4 @@ 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 diff --git a/tests/lex/test_lex.py b/tests/lex/test_lex.py index 359999b..bda1e3f 100644 --- a/tests/lex/test_lex.py +++ b/tests/lex/test_lex.py @@ -252,3 +252,11 @@ def test_complex(): assert entry == HyComplex("1.0j") entry = tokenize("(j)")[0][0] assert entry == HySymbol("j") + + +def test_reader_macro(): + """Ensure reader macros are handles properly""" + entry = tokenize("#^()") + assert entry[0][0] == HySymbol("dispatch_reader_macro") + assert entry[0][1] == HyExpression([HySymbol("quote"), HyString("^")]) + assert len(entry[0]) == 3 diff --git a/tests/macros/test_reader_macros.py b/tests/macros/test_reader_macros.py new file mode 100644 index 0000000..791a046 --- /dev/null +++ b/tests/macros/test_reader_macros.py @@ -0,0 +1,11 @@ +from hy.macros import macroexpand +from hy.compiler import HyTypeError +from hy.lex import tokenize + + +def test_reader_macro_error(): + """Check if we get correct error with wrong disptach character""" + try: + macroexpand(tokenize("(dispatch_reader_macro '- '())")[0], __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/reader_macros.hy new file mode 100644 index 0000000..e43220b --- /dev/null +++ b/tests/native_tests/reader_macros.hy @@ -0,0 +1,32 @@ +(defn test-reader-macro [] + "Test a basic redaer macro" + (defreader ^ [expr] + expr) + + (assert (= #^"works" "works"))) + + +(defn test-reader-macro-expr [] + "Test basic exprs like lists and arrays" + (defreader n [expr] + (get expr 1)) + + (assert (= #n[1 2] 2)) + (assert (= #n(1 2) 2))) + + +(defn test-reader-macro-override [] + "Test if we can override function symbols" + (defreader + [n] + (+ n 1)) + + (assert (= #+2 3))) + + +(defn test-reader-macro-compile-docstring [] + "Test if we can compile with a docstring" + (try + (defreader d [] + "Compiles with docstrings") + (except [Exception] + (assert False))))