Skip to content

Commit 9997056

Browse files
authored
Custom EOF in Reader (#218)
1 parent d52b03a commit 9997056

File tree

4 files changed

+115
-41
lines changed

4 files changed

+115
-41
lines changed

src/basilisp/cli.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import readline # noqa: F401
44
import traceback
55
import types
6+
from typing import Any
67

78
import click
89
import pytest
@@ -28,10 +29,10 @@ def eval_file(filename: str, ctx: compiler.CompilerContext, module: types.Module
2829
return last
2930

3031

31-
def eval_str(s: str, ctx: compiler.CompilerContext, module: types.ModuleType):
32+
def eval_str(s: str, ctx: compiler.CompilerContext, module: types.ModuleType, eof: Any):
3233
"""Evaluate the forms in a string into a Python module AST node."""
33-
last = None
34-
for form in reader.read_str(s, resolver=runtime.resolve_alias):
34+
last = eof
35+
for form in reader.read_str(s, resolver=runtime.resolve_alias, eof=eof):
3536
last = compiler.compile_and_exec_form(form, ctx, module, source_filename='REPL Input')
3637
return last
3738

@@ -55,6 +56,7 @@ def repl(default_ns):
5556
repl_module = bootstrap_repl(default_ns)
5657
ctx = compiler.CompilerContext()
5758
ns_var = runtime.set_current_ns(default_ns)
59+
eof = object()
5860
while True:
5961
ns: runtime.Namespace = ns_var.value
6062
try:
@@ -69,7 +71,9 @@ def repl(default_ns):
6971
continue
7072

7173
try:
72-
result = eval_str(lsrc, ctx, ns.module)
74+
result = eval_str(lsrc, ctx, ns.module, eof)
75+
if result is eof:
76+
continue
7377
print(compiler.lrepr(result))
7478
repl_module.mark_repl_result(result)
7579
except reader.SyntaxError as e:
@@ -97,9 +101,10 @@ def run(file_or_code, code, in_ns):
97101
ctx = compiler.CompilerContext()
98102
ns_var = runtime.set_current_ns(in_ns)
99103
ns: runtime.Namespace = ns_var.value
104+
eof = object()
100105

101106
if code:
102-
print(compiler.lrepr(eval_str(file_or_code, ctx, ns.module)))
107+
print(compiler.lrepr(eval_str(file_or_code, ctx, ns.module, eof)))
103108
else:
104109
print(compiler.lrepr(eval_file(file_or_code, ctx, ns.module)))
105110

src/basilisp/core/__init__.lpy

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,9 +1012,15 @@
10121012
Note that read-string should not be used to read string input from
10131013
untrusted sources."
10141014
([s]
1015-
(read-string {} s))
1015+
(read-string {:eof :eofthrow} s))
10161016
([opts s]
1017-
(first (basilisp.reader/read-str s *resolver* *data-readers*))))
1017+
(first (basilisp.reader/read-str s
1018+
*resolver*
1019+
*data-readers*
1020+
(:eof opts)
1021+
(if (= (:eof opts) :eofthrow)
1022+
true
1023+
false)))))
10181024

10191025
(defn read
10201026
"Read the next form from the stream. If no stream is specified, uses
@@ -1029,9 +1035,21 @@
10291035
([]
10301036
(read *in*))
10311037
([stream]
1032-
(read {} stream))
1038+
(read stream true nil))
10331039
([opts stream]
1034-
(first (basilisp.reader/read stream *resolver* *data-readers*))))
1040+
(first (basilisp.reader/read stream
1041+
*resolver*
1042+
*data-readers*
1043+
(:eof opts)
1044+
(if (= (:eof opts) :eofthrow)
1045+
true
1046+
false))))
1047+
([stream eof-error? eof-value]
1048+
(first (basilisp.reader/read stream
1049+
*resolver*
1050+
*data-readers*
1051+
eof-value
1052+
eof-error?))))
10351053

10361054
(defn eval
10371055
"Evaluate a form (not a string) and return its result."

src/basilisp/reader.py

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,14 @@ class ReaderContext:
172172
'_resolve',
173173
'_in_anon_fn',
174174
'_syntax_quoted',
175-
'_gensym_env')
175+
'_gensym_env',
176+
'_eof')
176177

177-
def __init__(self, reader: StreamReader,
178+
def __init__(self,
179+
reader: StreamReader,
178180
resolver: Resolver = None,
179-
data_readers: DataReaders = None) -> None:
181+
data_readers: DataReaders = None,
182+
eof: Any = None) -> None:
180183
data_readers = Maybe(data_readers).or_else_get(lmap.Map.empty())
181184
for entry in data_readers:
182185
if not isinstance(entry.key, symbol.Symbol):
@@ -192,11 +195,16 @@ def __init__(self, reader: StreamReader,
192195
self._in_anon_fn: Deque[bool] = collections.deque([])
193196
self._syntax_quoted: Deque[bool] = collections.deque([])
194197
self._gensym_env: Deque[GenSymEnvironment] = collections.deque([])
198+
self._eof = eof
195199

196200
@property
197201
def data_readers(self) -> lmap.Map:
198202
return self._data_readers
199203

204+
@property
205+
def eof(self) -> Any:
206+
return self._eof
207+
200208
@property
201209
def reader(self) -> StreamReader:
202210
return self._reader
@@ -243,7 +251,7 @@ def is_syntax_quoted(self) -> bool:
243251
return False
244252

245253

246-
__EOF = 'EOF'
254+
EOF = 'EOF'
247255

248256

249257
def _with_loc(f: W) -> W:
@@ -954,7 +962,7 @@ def _read_comment(ctx: ReaderContext) -> LispReaderForm:
954962
reader.advance()
955963
return _read_next(ctx)
956964
if token == '':
957-
return __EOF
965+
return ctx.eof
958966
reader.advance()
959967

960968

@@ -963,8 +971,8 @@ def _read_next_consuming_comment(ctx: ReaderContext) -> LispForm:
963971
reader comments completely."""
964972
while True:
965973
v = _read_next(ctx)
966-
if v is __EOF:
967-
return __EOF
974+
if v is ctx.eof:
975+
return ctx.eof
968976
if v is COMMENT or isinstance(v, Comment):
969977
continue
970978
return v
@@ -1008,12 +1016,16 @@ def _read_next(ctx: ReaderContext) -> LispReaderForm: # noqa: C901
10081016
elif token == '@':
10091017
return _read_deref(ctx)
10101018
elif token == '':
1011-
return __EOF
1019+
return ctx.eof
10121020
else:
10131021
raise SyntaxError("Unexpected token '{token}'".format(token=token))
10141022

10151023

1016-
def read(stream, resolver: Resolver = None, data_readers: DataReaders = None) -> Iterable[LispForm]:
1024+
def read(stream,
1025+
resolver: Resolver = None,
1026+
data_readers: DataReaders = None,
1027+
eof: Any = EOF,
1028+
is_eof_error: bool = False) -> Iterable[LispForm]:
10171029
"""Read the contents of a stream as a Lisp expression.
10181030
10191031
Callers may optionally specify a namespace resolver, which will be used
@@ -1028,29 +1040,47 @@ def read(stream, resolver: Resolver = None, data_readers: DataReaders = None) ->
10281040
10291041
The caller is responsible for closing the input stream."""
10301042
reader = StreamReader(stream)
1031-
ctx = ReaderContext(reader, resolver=resolver, data_readers=data_readers)
1043+
ctx = ReaderContext(reader, resolver=resolver, data_readers=data_readers, eof=eof)
10321044
while True:
10331045
expr = _read_next(ctx)
1034-
if expr is __EOF:
1046+
if expr is ctx.eof:
1047+
if is_eof_error:
1048+
raise EOFError
10351049
return
10361050
if expr is COMMENT or isinstance(expr, Comment):
10371051
continue
10381052
yield expr
10391053

10401054

1041-
def read_str(s: str, resolver: Resolver = None, data_readers: DataReaders = None) -> Iterable[LispForm]:
1055+
def read_str(s: str,
1056+
resolver: Resolver = None,
1057+
data_readers: DataReaders = None,
1058+
eof: Any = None,
1059+
is_eof_error: bool = False) -> Iterable[LispForm]:
10421060
"""Read the contents of a string as a Lisp expression.
10431061
10441062
Keyword arguments to this function have the same meanings as those of
10451063
basilisp.reader.read."""
10461064
with io.StringIO(s) as buf:
1047-
yield from read(buf, resolver=resolver, data_readers=data_readers)
1048-
1049-
1050-
def read_file(filename: str, resolver: Resolver = None, data_readers: DataReaders = None) -> Iterable[LispForm]:
1065+
yield from read(buf,
1066+
resolver=resolver,
1067+
data_readers=data_readers,
1068+
eof=eof,
1069+
is_eof_error=is_eof_error)
1070+
1071+
1072+
def read_file(filename: str,
1073+
resolver: Resolver = None,
1074+
data_readers: DataReaders = None,
1075+
eof: Any = None,
1076+
is_eof_error: bool = False) -> Iterable[LispForm]:
10511077
"""Read the contents of a file as a Lisp expression.
10521078
10531079
Keyword arguments to this function have the same meanings as those of
10541080
basilisp.reader.read."""
10551081
with open(filename) as f:
1056-
yield from read(f, resolver=resolver, data_readers=data_readers)
1082+
yield from read(f,
1083+
resolver=resolver,
1084+
data_readers=data_readers,
1085+
eof=eof,
1086+
is_eof_error=is_eof_error)

tests/basilisp/reader_test.py

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ def ns_var(test_ns: str):
2525
yield runtime.set_current_ns(test_ns)
2626

2727

28-
def read_str_first(s, resolver: reader.Resolver = None, data_readers=None):
28+
def read_str_first(s: str,
29+
resolver: reader.Resolver = None,
30+
data_readers=None,
31+
is_eof_error: bool = False):
2932
"""Read the first form from the input string. If no form
3033
is found, return None."""
3134
try:
32-
return next(reader.read_str(s, resolver=resolver, data_readers=data_readers))
35+
return next(reader.read_str(s, resolver=resolver, data_readers=data_readers, is_eof_error=is_eof_error))
3336
except StopIteration:
3437
return None
3538

@@ -677,15 +680,32 @@ def test_invalid_meta_attachment():
677680

678681

679682
def test_comment_reader_macro():
680-
assert None is read_str_first('#_ (a list)')
681-
assert None is read_str_first('#_1')
682-
assert None is read_str_first('#_"string"')
683-
assert None is read_str_first('#_:keyword')
684-
assert None is read_str_first('#_symbol')
685-
assert None is read_str_first('#_[]')
686-
assert None is read_str_first('#_{}')
687-
assert None is read_str_first('#_()')
688-
assert None is read_str_first('#_#{}')
683+
with pytest.raises(EOFError):
684+
read_str_first('#_ (a list)', is_eof_error=True)
685+
686+
with pytest.raises(EOFError):
687+
read_str_first('#_1', is_eof_error=True)
688+
689+
with pytest.raises(EOFError):
690+
read_str_first('#_"string"', is_eof_error=True)
691+
692+
with pytest.raises(EOFError):
693+
read_str_first('#_:keyword', is_eof_error=True)
694+
695+
with pytest.raises(EOFError):
696+
read_str_first('#_symbol', is_eof_error=True)
697+
698+
with pytest.raises(EOFError):
699+
read_str_first('#_[]', is_eof_error=True)
700+
701+
with pytest.raises(EOFError):
702+
read_str_first('#_{}', is_eof_error=True)
703+
704+
with pytest.raises(EOFError):
705+
read_str_first('#_()', is_eof_error=True)
706+
707+
with pytest.raises(EOFError):
708+
read_str_first('#_#{}', is_eof_error=True)
689709

690710
assert kw.keyword('kw2') == read_str_first('#_:kw1 :kw2')
691711

@@ -719,11 +739,12 @@ def test_comment_reader_macro():
719739

720740

721741
def test_comment_line():
722-
assert read_str_first("; I'm a little comment short and stout") is None
723-
assert read_str_first(";; :kw1\n:kw2") == kw.keyword('kw2')
724-
assert read_str_first(""";; Comment
742+
assert None is read_str_first("; I'm a little comment short and stout")
743+
assert kw.keyword('kw2') == read_str_first(";; :kw1\n:kw2")
744+
assert llist.l(sym.symbol('form'), kw.keyword('keyword')) == read_str_first(
745+
""";; Comment
725746
(form :keyword)
726-
""") == llist.l(sym.symbol('form'), kw.keyword('keyword'))
747+
""")
727748

728749

729750
def test_function_reader_macro():

0 commit comments

Comments
 (0)