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.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

View File

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

View File

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

View File

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

View File

@ -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 = {

View File

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

View File

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

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