Skip to content

Commit 7cc187a

Browse files
authored
Allow arithmetic symbols in the reader (#119)
* Allow arithmetic symbols in the reader * A few additional cases
1 parent c327e16 commit 7cc187a

File tree

2 files changed

+106
-44
lines changed

2 files changed

+106
-44
lines changed

basilisp/reader.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from basilisp.lang.typing import LispForm, IterableLispForm
2323
from basilisp.util import Maybe
2424

25-
ns_name_chars = re.compile(r'\w|-|\+|\*|\?|/|\=|\\|!|&|%')
25+
ns_name_chars = re.compile(r'\w|-|\+|\*|\?|/|\=|\\|!|&|%|>|<')
2626
begin_num_chars = re.compile('[0-9\-]')
2727
num_chars = re.compile('[0-9]')
2828
whitespace_chars = re.compile('[\s,]')
@@ -219,11 +219,14 @@ def _read_namespaced(ctx: ReaderContext, allowed_suffix: Optional[str] = None) -
219219
reader.next_token()
220220
if has_ns:
221221
raise SyntaxError("Found '/'; expected word character")
222-
has_ns = True
223-
ns = name
224-
name = []
225-
if len(ns) == 0:
226-
raise SyntaxError("Found ''; expected namespace")
222+
elif len(name) == 0:
223+
name.append('/')
224+
else:
225+
if '/' in name:
226+
raise SyntaxError("Found '/' after '/'")
227+
has_ns = True
228+
ns = name
229+
name = []
227230
elif ns_name_chars.match(token):
228231
reader.next_token()
229232
name.append(token)
@@ -236,7 +239,17 @@ def _read_namespaced(ctx: ReaderContext, allowed_suffix: Optional[str] = None) -
236239
else:
237240
break
238241

239-
return None if not has_ns else ''.join(ns), ''.join(name)
242+
ns_str = None if not has_ns else ''.join(ns)
243+
name_str = ''.join(name)
244+
245+
# A small exception for the symbol '/ used for division
246+
if ns_str is None:
247+
if '/' in name_str and name_str != '/':
248+
raise SyntaxError("'/' character disallowed in names")
249+
250+
assert ns_str is None or len(ns_str) > 0
251+
252+
return ns_str, name_str
240253

241254

242255
def _read_coll(ctx: ReaderContext, f: Callable[[Collection[Any]], Union[
@@ -398,7 +411,7 @@ def _read_map(ctx: ReaderContext) -> lmap.Map:
398411

399412
def _read_num(ctx: ReaderContext) -> MaybeNumber:
400413
"""Return a numeric (integer or float) from the input stream."""
401-
chars = []
414+
chars: List[str] = []
402415
reader = ctx.reader
403416
is_float = False
404417
while True:
@@ -407,6 +420,11 @@ def _read_num(ctx: ReaderContext) -> MaybeNumber:
407420
following_token = reader.next_token()
408421
if not begin_num_chars.match(following_token):
409422
reader.pushback()
423+
try:
424+
for _ in chars:
425+
reader.pushback()
426+
except IndexError:
427+
raise SyntaxError("Requested to pushback too many characters onto StreamReader")
410428
return _read_sym(ctx)
411429
chars.append(token)
412430
continue

tests/reader_test.py

Lines changed: 80 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -142,28 +142,47 @@ def test_float():
142142

143143

144144
def test_kw():
145-
assert read_str_first(":kw") == kw.keyword("kw")
146-
assert read_str_first(":kebab-kw") == kw.keyword("kebab-kw")
147-
assert read_str_first(":underscore_kw") == kw.keyword("underscore_kw")
148-
assert read_str_first(":kw?") == kw.keyword("kw?")
149-
assert read_str_first(":+") == kw.keyword("+")
150-
assert read_str_first(":?") == kw.keyword("?")
151-
assert read_str_first(":=") == kw.keyword("=")
152-
assert read_str_first(":!") == kw.keyword("!")
153-
assert read_str_first(":-") == kw.keyword("-")
154-
assert read_str_first(":*") == kw.keyword("*")
155-
assert read_str_first(":*muffs*") == kw.keyword("*muffs*")
156-
assert read_str_first(":yay!") == kw.keyword("yay!")
157-
158-
assert read_str_first(":ns/kw") == kw.keyword("kw", ns="ns")
159-
assert read_str_first(":qualified.ns/kw") == kw.keyword(
160-
"kw", ns="qualified.ns")
161-
assert read_str_first(":really.qualified.ns/kw") == kw.keyword(
162-
"kw", ns="really.qualified.ns")
145+
assert kw.keyword("kw") == read_str_first(":kw")
146+
assert kw.keyword("kebab-kw") == read_str_first(":kebab-kw")
147+
assert kw.keyword("underscore_kw") == read_str_first(":underscore_kw")
148+
assert kw.keyword("kw?") == read_str_first(":kw?")
149+
assert kw.keyword("+") == read_str_first(":+")
150+
assert kw.keyword("?") == read_str_first(":?")
151+
assert kw.keyword("=") == read_str_first(":=")
152+
assert kw.keyword("!") == read_str_first(":!")
153+
assert kw.keyword("-") == read_str_first(":-")
154+
assert kw.keyword("*") == read_str_first(":*")
155+
assert kw.keyword("/") == read_str_first(":/")
156+
assert kw.keyword(">") == read_str_first(":>")
157+
assert kw.keyword("->") == read_str_first(":->")
158+
assert kw.keyword("->>") == read_str_first(":->>")
159+
assert kw.keyword("-->") == read_str_first(":-->")
160+
assert kw.keyword("--------------->") == read_str_first(":--------------->")
161+
assert kw.keyword("<") == read_str_first(":<")
162+
assert kw.keyword("<-") == read_str_first(":<-")
163+
assert kw.keyword("<--") == read_str_first(":<--")
164+
assert kw.keyword("<body>") == read_str_first(":<body>")
165+
assert kw.keyword("*muffs*") == read_str_first(":*muffs*")
166+
assert kw.keyword("yay!") == read_str_first(":yay!")
167+
168+
assert kw.keyword("kw", ns="ns") == read_str_first(":ns/kw")
169+
assert kw.keyword("kw", ns="qualified.ns") == read_str_first(
170+
":qualified.ns/kw")
171+
assert kw.keyword("kw", ns="really.qualified.ns") == read_str_first(
172+
":really.qualified.ns/kw")
173+
174+
with pytest.raises(reader.SyntaxError):
175+
read_str_first("://")
163176

164177
with pytest.raises(reader.SyntaxError):
165178
read_str_first(":ns//kw")
166179

180+
with pytest.raises(reader.SyntaxError):
181+
read_str_first(":some/ns/sym")
182+
183+
with pytest.raises(reader.SyntaxError):
184+
read_str_first(":ns/sym/")
185+
167186
with pytest.raises(reader.SyntaxError):
168187
read_str_first(":/kw")
169188

@@ -178,31 +197,56 @@ def test_literals():
178197

179198

180199
def test_symbol():
181-
assert read_str_first("sym") == sym.symbol("sym")
182-
assert read_str_first("kebab-sym") == sym.symbol("kebab-sym")
183-
assert read_str_first("underscore_sym") == sym.symbol("underscore_sym")
184-
assert read_str_first("sym?") == sym.symbol("sym?")
185-
assert read_str_first("+") == sym.symbol("+")
186-
assert read_str_first("?") == sym.symbol("?")
187-
assert read_str_first("=") == sym.symbol("=")
188-
assert read_str_first("!") == sym.symbol("!")
189-
assert read_str_first("-") == sym.symbol("-")
190-
assert read_str_first("*") == sym.symbol("*")
191-
assert read_str_first("*muffs*") == sym.symbol("*muffs*")
192-
assert read_str_first("yay!") == sym.symbol("yay!")
193-
194-
assert read_str_first("ns/sym") == sym.symbol("sym", ns="ns")
195-
assert read_str_first("qualified.ns/sym") == sym.symbol(
196-
"sym", ns="qualified.ns")
197-
assert read_str_first("really.qualified.ns/sym") == sym.symbol(
198-
"sym", ns="really.qualified.ns")
200+
assert sym.symbol("sym") == read_str_first("sym")
201+
assert sym.symbol("kebab-sym") == read_str_first("kebab-sym")
202+
assert sym.symbol("underscore_sym") == read_str_first("underscore_sym")
203+
assert sym.symbol("sym?") == read_str_first("sym?")
204+
assert sym.symbol("+") == read_str_first("+")
205+
assert sym.symbol("?") == read_str_first("?")
206+
assert sym.symbol("=") == read_str_first("=")
207+
assert sym.symbol("!") == read_str_first("!")
208+
assert sym.symbol("-") == read_str_first("-")
209+
assert sym.symbol("*") == read_str_first("*")
210+
assert sym.symbol("/") == read_str_first("/")
211+
assert sym.symbol(">") == read_str_first(">")
212+
assert sym.symbol("->") == read_str_first("->")
213+
assert sym.symbol("->>") == read_str_first("->>")
214+
assert sym.symbol("-->") == read_str_first("-->")
215+
assert sym.symbol("<") == read_str_first("<")
216+
assert sym.symbol("<-") == read_str_first("<-")
217+
assert sym.symbol("<--") == read_str_first("<--")
218+
assert sym.symbol("<body>") == read_str_first("<body>")
219+
assert sym.symbol("*muffs*") == read_str_first("*muffs*")
220+
assert sym.symbol("yay!") == read_str_first("yay!")
221+
222+
assert sym.symbol("sym", ns="ns") == read_str_first("ns/sym")
223+
assert sym.symbol(
224+
"sym", ns="qualified.ns") == read_str_first("qualified.ns/sym")
225+
assert sym.symbol(
226+
"sym", ns="really.qualified.ns") == read_str_first("really.qualified.ns/sym")
227+
228+
with pytest.raises(reader.SyntaxError):
229+
read_str_first("//")
199230

200231
with pytest.raises(reader.SyntaxError):
201232
read_str_first("ns//sym")
202233

234+
with pytest.raises(reader.SyntaxError):
235+
read_str_first("some/ns/sym")
236+
237+
with pytest.raises(reader.SyntaxError):
238+
read_str_first("ns/sym/")
239+
203240
with pytest.raises(reader.SyntaxError):
204241
read_str_first("/sym")
205242

243+
with pytest.raises(reader.SyntaxError):
244+
# This will raise because the default pushback depth of the
245+
# reader.StreamReader instance used by the reader is 5, so
246+
# we are unable to pushback more - characters consumed by
247+
# reader._read_num trying to parse a number.
248+
read_str_first("------->")
249+
206250

207251
def test_str():
208252
assert read_str_first('""') == ''

0 commit comments

Comments
 (0)