Skip to content

Commit

Permalink
fix hy.eval trying to access hy.&reader
Browse files Browse the repository at this point in the history
  • Loading branch information
scauligi committed Aug 14, 2023
1 parent b193fc8 commit fd38da6
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 48 deletions.
2 changes: 2 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Bug Fixes
* Fixed incomplete recognition of macro calls with a unary dotted
head like `((. defn) f [])`.
* `~@ #*` now produces a syntax error instead of a nonsensical result.
* Fixed `hy.eval` failing on `defreader` or `require` forms that
install a new reader.

0.27.0 (released 2023-07-06)
=============================
Expand Down
22 changes: 12 additions & 10 deletions hy/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,16 +372,18 @@ def hy2py_worker(source, options, filename=None, parent_module=None, output_file
) as output_file:

def printing_source(hst):
for node in hst:
if options.with_source:
print(node, file=output_file)
yield node

hst = hy.models.Lazy(
printing_source(read_many(source, filename, skip_shebang=True))
)
hst.source = source
hst.filename = filename
def _printing_gen(hst):
for node in hst:
if options.with_source:
print(node, file=output_file)
yield node
printing_hst = hy.models.Lazy(_printing_gen(hst))
printing_hst.source = hst.source
printing_hst.filename = hst.filename
printing_hst.reader = hst.reader
return printing_hst

hst = printing_source(read_many(source, filename, skip_shebang=True))

with filtered_hy_exceptions():
module_name = source_path.stem if source_path else Path(filename).name
Expand Down
5 changes: 3 additions & 2 deletions hy/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
as_model,
is_unpack,
)
from hy.reader import mangle
from hy.reader import mangle, HyReader
from hy.scoping import ScopeGlobal

hy_ast_compile_flags = 0
Expand Down Expand Up @@ -795,6 +795,7 @@ def hy_compile(

filename = getattr(tree, "filename", filename)
source = getattr(tree, "source", source)
reader = getattr(tree, "reader", None)

tree = as_model(tree)
if not isinstance(tree, Object):
Expand All @@ -804,7 +805,7 @@ def hy_compile(

compiler = compiler or HyASTCompiler(module, filename=filename, source=source)

with compiler.scope:
with HyReader.using_reader(reader, create=False), compiler.scope:
result = compiler.compile(tree)
expr = result.force_expr

Expand Down
2 changes: 1 addition & 1 deletion hy/core/macros.hy
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
~@(if docstr [docstr] [])
~@body)))
(eval-when-compile
(setv (get hy.&reader.reader-macros ~dispatch-key)
(setv (get (. (hy.reader.HyReader.current-reader) reader-macros) ~dispatch-key)
(get _hy_reader_macros ~dispatch-key)))))


Expand Down
2 changes: 1 addition & 1 deletion hy/core/result_macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -1849,7 +1849,7 @@ def compile_require(compiler, expr, root, entries):
mkexpr(
dotted("hy.macros.enable-readers"),
"None",
dotted("hy.&reader"),
mkexpr(dotted("hy.reader.HyReader.current-reader")),
[reader_assignments],
),
),
Expand Down
6 changes: 3 additions & 3 deletions hy/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import hy
from hy.compiler import hy_compile
from hy.reader import read_many
from hy.reader import read_many, HyReader


@contextmanager
Expand Down Expand Up @@ -118,7 +118,7 @@ def _hy_source_to_code(self, data, path, _optimize=-1):
if os.environ.get("HY_MESSAGE_WHEN_COMPILING"):
print("Compiling", path, file=sys.stderr)
source = data.decode("utf-8")
hy_tree = read_many(source, filename=path, skip_shebang=True)
hy_tree = read_many(source, filename=path, skip_shebang=True, reader=HyReader())
with loader_module_obj(self) as module:
data = hy_compile(hy_tree, module)

Expand All @@ -139,7 +139,7 @@ def _hy_compile_source(pathname, source):
sys.modules[mname] = types.ModuleType(mname)
return compile(
hy_compile(
read_many(source.decode("UTF-8"), filename=pathname, skip_shebang=True),
read_many(source.decode("UTF-8"), filename=pathname, skip_shebang=True, reader=HyReader()),
sys.modules[mname],
),
pathname,
Expand Down
6 changes: 4 additions & 2 deletions hy/reader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ def read_many(stream, filename="<string>", reader=None, skip_shebang=False):
source = stream.read()
stream.seek(pos)

m = hy.models.Lazy((reader or HyReader()).parse(
reader = reader or HyReader()
m = hy.models.Lazy(reader.parse(
stream, filename, skip_shebang))
m.source = source
m.filename = filename
m.reader = reader
return m


Expand All @@ -52,5 +54,5 @@ def read(stream, filename=None, reader=None):
except StopIteration:
raise EOFError()
else:
m.source, m.filename = it.source, it.filename
m.source, m.filename, m.reader = it.source, it.filename, it.reader
return m
74 changes: 45 additions & 29 deletions hy/reader/hy_reader.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"Character reader for parsing Hy source."

import codecs
from contextlib import contextmanager, nullcontext
from itertools import islice

import hy
Expand Down Expand Up @@ -116,6 +117,7 @@ class HyReader(Reader):
###

NON_IDENT = set("()[]{};\"'`~")
_current_reader = None

def __init__(self):
super().__init__()
Expand All @@ -128,6 +130,27 @@ def __init__(self):
self.reader_macros[tag[1:]] = self.reader_table.pop(tag)


@classmethod
def current_reader(cls, override=None, create=True):
return override or HyReader._current_reader or (cls() if create else None)

@contextmanager
def as_current_reader(self):
old_reader = HyReader._current_reader
HyReader._current_reader = self
try:
yield
finally:
HyReader._current_reader = old_reader

@classmethod
@contextmanager
def using_reader(cls, override=None, create=True):
reader = cls.current_reader(override, create)
with reader.as_current_reader() if reader else nullcontext():
yield


def fill_pos(self, model, start):
"""Attach line/col information to a model.
Expand Down Expand Up @@ -159,8 +182,6 @@ def read_default(self, key):
def parse(self, stream, filename=None, skip_shebang=False):
"""Yields all `hy.models.Object`'s in `source`
Additionally exposes `self` as ``hy.&reader`` during read/compile time.
Args:
source:
Hy source to be parsed.
Expand All @@ -178,17 +199,7 @@ def parse(self, stream, filename=None, skip_shebang=False):
if c == "\n":
break

rname = mangle("&reader")
old_reader = getattr(hy, rname, None)
setattr(hy, rname, self)

try:
yield from self.parse_forms_until("")
finally:
if old_reader is None:
delattr(hy, rname)
else:
setattr(hy, rname, old_reader)
yield from self.parse_forms_until("")

###
# Reading forms
Expand All @@ -210,23 +221,28 @@ def try_parse_one_form(self):
fully parsing a form.
LexException: If there is an error during form parsing.
"""
try:
self.slurp_space()
c = self.getc()
start = self._pos
if not c:
raise PrematureEndOfInput.from_reader(
"Premature end of input while attempting to parse one form", self
with self.as_current_reader():
try:
self.slurp_space()
c = self.getc()
start = self._pos
if not c:
raise PrematureEndOfInput.from_reader(
"Premature end of input while attempting to parse one form", self
)
handler = self.reader_table.get(c)
model = handler(self, c) if handler else self.read_default(c)
if model is not None:
model = self.fill_pos(model, start)
model.reader = self
return model
return None
except LexException:
raise
except Exception as e:
raise LexException.from_reader(
str(e) or "Exception thrown attempting to parse one form", self
)
handler = self.reader_table.get(c)
model = handler(self, c) if handler else self.read_default(c)
return self.fill_pos(model, start) if model is not None else None
except LexException:
raise
except Exception as e:
raise LexException.from_reader(
str(e) or "Exception thrown attempting to parse one form", self
)

def parse_one_form(self):
"""Read from the stream until a form is parsed.
Expand Down
44 changes: 44 additions & 0 deletions tests/native_tests/reader_macros.hy
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
types
contextlib [contextmanager]
hy.errors [HyMacroExpansionError]
hy.reader [HyReader]
hy.reader.exceptions [PrematureEndOfInput]

pytest)
Expand Down Expand Up @@ -89,3 +90,46 @@
(eval-module #[[(require tests.resources.tlib :readers [taggart] :readers [upper!])]]))
(with [(pytest.raises hy.errors.HyRequireError)]
(eval-module #[[(require tests.resources.tlib :readers [not-a-real-reader])]])))

(defn test-eval-read []
;; https://github.com/hylang/hy/issues/2291
;; hy.eval should not raise an exception when
;; defining readers using hy.read or with quoted forms
(with [module (temp-module "<test>")]
(hy.eval (hy.read "(defreader r 5)") :module module)
(hy.eval '(defreader test-read 4) :module module)
(hy.eval '(require tests.resources.tlib :readers [upper!]) :module module)
;; these reader macros should not exist in any current reader
(for [tag #("#r" "#test-read" "#upper!")]
(print tag)
(with [(pytest.raises hy.errors.HySyntaxError)]
(print (hy.read tag))))
;; but they should be installed in the module
(setv reader (HyReader))
(hy.macros.enable-readers module reader "ALL")
(for [[s val] [["#r" 5]
["#test-read" 4]
["#upper! \"hi there\"" "HI THERE"]]]
(assert (= (hy.eval (hy.read s :reader reader) :module module) val))))

;; passing a reader explicitly should work as expected
(with [module (temp-module "<test>")]
(setv reader (HyReader))
(defn eval1 [s]
(hy.eval (hy.read s :reader reader) :module module))
(eval1 "(defreader fbaz 32)")
(eval1 "(require tests.resources.tlib :readers [upper!])")
(assert (= (eval1 "#fbaz") 32))
(assert (= (eval1 "#upper! \"hello\"") "HELLO"))))


(defn test-interleaving-readers []
(with [module1 (temp-module "<one>")
module2 (temp-module "<two>")]
(setv stream1 (hy.read-many #[[(do (defreader foo "foo1") (defreader bar "bar1")) #foo #bar]])
stream2 (hy.read-many #[[(do (defreader foo "foo2") (defreader bar "bar2")) #foo #bar]])
valss [[None None] ["foo1" "foo2"] ["bar1" "bar2"]])
(for [[form1 form2 vals] (zip stream1 stream2 valss)]
(assert (= vals
[(hy.eval form1 :module module1)
(hy.eval form2 :module module2)])))))

0 comments on commit fd38da6

Please sign in to comment.