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
This commit is contained in:
Foxboron 2013-12-15 17:47:24 +01:00
parent 1a701d4dc4
commit c9fdd40c9f
9 changed files with 140 additions and 10 deletions

View File

@ -36,6 +36,7 @@ from hy.models.float import HyFloat
from hy.models.list import HyList from hy.models.list import HyList
from hy.models.dict import HyDict from hy.models.dict import HyDict
import hy.macros
from hy.macros import require, macroexpand from hy.macros import require, macroexpand
from hy._compat import str_type, long_type from hy._compat import str_type, long_type
import hy.importer import hy.importer
@ -1753,6 +1754,19 @@ class HyASTCompiler(object):
bases=bases_expr, bases=bases_expr,
body=body.stmts) 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") @builds("defmacro")
@checkargs(min=1) @checkargs(min=1)
def compile_macro(self, expression): def compile_macro(self, expression):
@ -1768,16 +1782,30 @@ class HyASTCompiler(object):
HyExpression([HySymbol("fn")] + expression), HyExpression([HySymbol("fn")] + expression),
]).replace(expression) ]).replace(expression)
# Compile-time hack: we want to get our new macro now ret = self._compile_time_hack(new_expression)
# 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)
# We really want to have a `hy` import to get hy.macro in return ret
ret = self.compile(new_expression)
ret.add_imports('hy', [None]) @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 return ret

View File

@ -25,7 +25,6 @@
;;; These macros form the hy language ;;; These macros form the hy language
;;; They are automatically required in every module, except inside hy.core ;;; They are automatically required in every module, except inside hy.core
(defmacro for [args &rest body] (defmacro for [args &rest body]
"shorthand for nested foreach loops: "shorthand for nested foreach loops:
(for [x foo y bar] baz) -> (for [x foo y bar] baz) ->
@ -144,3 +143,13 @@
(setv -args (cdr (car -args)))) (setv -args (cdr (car -args))))
`(apply ~-fun [~@-args] (dict (sum ~-okwargs []))))) `(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))))

View File

@ -40,6 +40,7 @@ lg.add('QUASIQUOTE', r'`%s' % end_quote)
lg.add('UNQUOTESPLICE', r'~@%s' % end_quote) lg.add('UNQUOTESPLICE', r'~@%s' % end_quote)
lg.add('UNQUOTE', r'~%s' % end_quote) lg.add('UNQUOTE', r'~%s' % end_quote)
lg.add('HASHBANG', r'#!.*[^\r\n]') lg.add('HASHBANG', r'#!.*[^\r\n]')
lg.add('HASHREADER', r'#.')
lg.add('STRING', r'''(?x) lg.add('STRING', r'''(?x)

View File

@ -150,6 +150,15 @@ def term_unquote_splice(p):
return HyExpression([HySymbol("unquote_splice"), p[1]]) 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") @pg.production("dict : LCURLY list_contents RCURLY")
@set_boundaries @set_boundaries
def t_dict(p): def t_dict(p):

View File

@ -40,6 +40,8 @@ EXTRA_MACROS = [
] ]
_hy_macros = defaultdict(dict) _hy_macros = defaultdict(dict)
_hy_reader = defaultdict(dict)
_hy_reader_chars = set()
def macro(name): def macro(name):
@ -63,6 +65,30 @@ def macro(name):
return _ 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): def require(source_module, target_module):
"""Load the macros from `source_module` in the namespace of """Load the macros from `source_module` in the namespace of
`target_module`. `target_module`.
@ -75,6 +101,11 @@ def require(source_module, target_module):
for name, macro in macros.items(): for name, macro in macros.items():
refs[name] = macro 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 # type -> wrapping function mapping for _wrap_value
_wrappers = { _wrappers = {

View File

@ -11,3 +11,4 @@ from .native_tests.unless import * # noqa
from .native_tests.when import * # noqa from .native_tests.when import * # noqa
from .native_tests.with_decorator import * # noqa from .native_tests.with_decorator import * # noqa
from .native_tests.core import * # noqa from .native_tests.core import * # noqa
from .native_tests.reader_macros import * # noqa

View File

@ -252,3 +252,11 @@ def test_complex():
assert entry == HyComplex("1.0j") assert entry == HyComplex("1.0j")
entry = tokenize("(j)")[0][0] entry = tokenize("(j)")[0][0]
assert entry == HySymbol("j") 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

View File

@ -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)

View File

@ -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))))