Skip to content

Commit 726a879

Browse files
authored
Allow empty let and loop forms (#382)
1 parent de49e91 commit 726a879

File tree

2 files changed

+44
-51
lines changed

2 files changed

+44
-51
lines changed

src/basilisp/lang/compiler/parser.py

Lines changed: 28 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,21 @@ def _clean_meta(meta: Optional[lmap.Map]) -> Optional[lmap.Map]:
497497
return None if len(new_meta) == 0 else new_meta
498498

499499

500+
def _body_ast(
501+
ctx: ParserContext, form: Union[llist.List, ISeq]
502+
) -> Tuple[Iterable[Node], Node]:
503+
"""Parse the form into a body of statement nodes and a single return
504+
expression node.
505+
506+
If the body is empty, return a constant node containing nil."""
507+
body = list(map(partial(_parse_ast, ctx), form))
508+
if body:
509+
*stmts, ret = body
510+
else:
511+
stmts, ret = [], _const_node(ctx, None)
512+
return stmts, ret
513+
514+
500515
def _with_meta(gen_node):
501516
"""Wraps the node generated by gen_node in a :with-meta AST node if the
502517
original form has meta.
@@ -764,13 +779,7 @@ def __deftype_classmethod(
764779

765780
params = args[1:]
766781
has_vargs, param_nodes = __deftype_method_param_bindings(ctx, params)
767-
768-
body = list(map(partial(_parse_ast, ctx), runtime.nthrest(form, 2)))
769-
if body:
770-
*stmts, ret = body
771-
else:
772-
stmts, ret = [], _const_node(ctx, None)
773-
782+
stmts, ret = _body_ast(ctx, runtime.nthrest(form, 2))
774783
method = ClassMethod(
775784
form=form,
776785
name=method_name,
@@ -824,12 +833,7 @@ def __deftype_method(
824833

825834
loop_id = genname(method_name)
826835
with ctx.new_recur_point(loop_id, param_nodes):
827-
body = list(map(partial(_parse_ast, ctx), runtime.nthrest(form, 2)))
828-
if body:
829-
*stmts, ret = body
830-
else:
831-
stmts, ret = [], _const_node(ctx, None)
832-
836+
stmts, ret = _body_ast(ctx, runtime.nthrest(form, 2))
833837
method = Method(
834838
form=form,
835839
name=method_name,
@@ -889,12 +893,7 @@ def __deftype_property(
889893

890894
assert not has_vargs, "deftype* properties may not have arguments"
891895

892-
body = list(map(partial(_parse_ast, ctx), runtime.nthrest(form, 2)))
893-
if body:
894-
*stmts, ret = body
895-
else:
896-
stmts, ret = [], _const_node(ctx, None)
897-
896+
stmts, ret = _body_ast(ctx, runtime.nthrest(form, 2))
898897
prop = PropertyMethod(
899898
form=form,
900899
name=method_name,
@@ -924,13 +923,7 @@ def __deftype_staticmethod(
924923
"""Emit a node for a :staticmethod member of a deftype* form."""
925924
with ctx.hide_parent_symbol_table(), ctx.new_symbol_table(method_name):
926925
has_vargs, param_nodes = __deftype_method_param_bindings(ctx, args)
927-
928-
body = list(map(partial(_parse_ast, ctx), runtime.nthrest(form, 2)))
929-
if body:
930-
*stmts, ret = body
931-
else:
932-
stmts, ret = [], _const_node(ctx, None)
933-
926+
stmts, ret = _body_ast(ctx, runtime.nthrest(form, 2))
934927
method = StaticMethod(
935928
form=form,
936929
name=method_name,
@@ -1287,12 +1280,7 @@ def __fn_method_ast( # pylint: disable=too-many-branches,too-many-locals
12871280

12881281
fn_loop_id = genname("fn_arity" if fnname is None else fnname.name)
12891282
with ctx.new_recur_point(fn_loop_id, param_nodes):
1290-
body = list(map(partial(_parse_ast, ctx), form.rest))
1291-
if body:
1292-
*stmts, ret = body
1293-
else:
1294-
stmts, ret = [], _const_node(ctx, None)
1295-
1283+
stmts, ret = _body_ast(ctx, form.rest)
12961284
method = FnMethod(
12971285
form=form,
12981286
loop_id=fn_loop_id,
@@ -1654,18 +1642,14 @@ def _let_ast(ctx: ParserContext, form: ISeq) -> Let:
16541642
assert form.first == SpecialForm.LET
16551643
nelems = count(form)
16561644

1657-
if nelems < 3:
1645+
if nelems < 2:
16581646
raise ParserException(
1659-
"let forms must have bindings and at least one body form", form=form
1647+
"let forms must have bindings vector and 0 or more body forms", form=form
16601648
)
16611649

16621650
bindings = runtime.nth(form, 1)
16631651
if not isinstance(bindings, vec.Vector):
16641652
raise ParserException("let bindings must be a vector", form=bindings)
1665-
elif len(bindings) == 0:
1666-
raise ParserException(
1667-
"let form must have at least one pair of bindings", form=bindings
1668-
)
16691653
elif len(bindings) % 2 != 0:
16701654
raise ParserException(
16711655
"let bindings must appear in name-value pairs", form=bindings
@@ -1689,13 +1673,13 @@ def _let_ast(ctx: ParserContext, form: ISeq) -> Let:
16891673
ctx.put_new_symbol(name, binding)
16901674

16911675
let_body = runtime.nthrest(form, 2)
1692-
*statements, ret = map(partial(_parse_ast, ctx), let_body)
1676+
stmts, ret = _body_ast(ctx, let_body)
16931677
return Let(
16941678
form=form,
16951679
bindings=vec.vector(binding_nodes),
16961680
body=Do(
16971681
form=let_body,
1698-
statements=vec.vector(statements),
1682+
statements=vec.vector(stmts),
16991683
ret=ret,
17001684
is_body=True,
17011685
env=ctx.get_node_env(),
@@ -1708,9 +1692,9 @@ def _loop_ast(ctx: ParserContext, form: ISeq) -> Loop:
17081692
assert form.first == SpecialForm.LOOP
17091693
nelems = count(form)
17101694

1711-
if nelems < 3:
1695+
if nelems < 2:
17121696
raise ParserException(
1713-
"loop forms must have bindings and at least one body form", form=form
1697+
"loop forms must have bindings vector and 0 or more body forms", form=form
17141698
)
17151699

17161700
bindings = runtime.nth(form, 1)
@@ -1740,13 +1724,13 @@ def _loop_ast(ctx: ParserContext, form: ISeq) -> Loop:
17401724

17411725
with ctx.new_recur_point(loop_id, binding_nodes):
17421726
loop_body = runtime.nthrest(form, 2)
1743-
*statements, ret = map(partial(_parse_ast, ctx), loop_body)
1727+
stmts, ret = _body_ast(ctx, loop_body)
17441728
loop_node = Loop(
17451729
form=form,
17461730
bindings=vec.vector(binding_nodes),
17471731
body=Do(
17481732
form=loop_body,
1749-
statements=vec.vector(statements),
1733+
statements=vec.vector(stmts),
17501734
ret=ret,
17511735
is_body=True,
17521736
env=ctx.get_node_env(),

tests/basilisp/compiler_test.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1951,10 +1951,14 @@ def test_let_num_elems(self, ns: runtime.Namespace):
19511951
with pytest.raises(compiler.CompilerException):
19521952
lcompile("(let*)")
19531953

1954-
with pytest.raises(compiler.CompilerException):
1955-
lcompile("(let* [a :kw])")
1954+
def test_let_may_have_empty_bindings(self, ns: runtime.Namespace):
1955+
assert None is lcompile("(let* [])")
1956+
assert kw.keyword("kw") == lcompile("(let* [] :kw)")
19561957

19571958
def test_let_bindings_must_be_vector(self, ns: runtime.Namespace):
1959+
with pytest.raises(compiler.CompilerException):
1960+
lcompile("(let* () :kw)")
1961+
19581962
with pytest.raises(compiler.CompilerException):
19591963
lcompile("(let* (a kw) a)")
19601964

@@ -1976,9 +1980,9 @@ def test_let_name_does_not_resolve(self, ns: runtime.Namespace):
19761980
with pytest.raises(compiler.CompilerException):
19771981
lcompile("(let* [a 'sym] c)")
19781982

1979-
def test_let_must_have_bindings(self, ns: runtime.Namespace):
1980-
with pytest.raises(compiler.CompilerException):
1981-
lcompile('(let* [] "string")')
1983+
def test_let_may_have_empty_body(self, ns: runtime.Namespace):
1984+
assert None is lcompile("(let* [])")
1985+
assert None is lcompile("(let* [a :kw])")
19821986

19831987
def test_let(self, ns: runtime.Namespace):
19841988
assert lcompile("(let* [a 1] a)") == 1
@@ -2118,8 +2122,9 @@ def test_loop_num_elems(self, ns: runtime.Namespace):
21182122
with pytest.raises(compiler.CompilerException):
21192123
lcompile("(loop*)")
21202124

2121-
with pytest.raises(compiler.CompilerException):
2122-
lcompile("(loop* [a :kw])")
2125+
def test_loop_may_have_empty_bindings(self, ns: runtime.Namespace):
2126+
assert None is lcompile("(loop* [])")
2127+
assert kw.keyword("kw") == lcompile("(loop* [] :kw)")
21232128

21242129
def test_loop_bindings_must_be_vector(self, ns: runtime.Namespace):
21252130
with pytest.raises(compiler.CompilerException):
@@ -2146,6 +2151,10 @@ def test_let_name_does_not_resolve(self, ns: runtime.Namespace):
21462151
with pytest.raises(compiler.CompilerException):
21472152
lcompile("(loop* [a 'sym] c)")
21482153

2154+
def test_loop_may_have_empty_body(self, ns: runtime.Namespace):
2155+
assert None is lcompile("(loop* [])")
2156+
assert None is lcompile("(loop* [a :kw])")
2157+
21492158
def test_loop_without_recur(self, ns: runtime.Namespace):
21502159
assert 1 == lcompile("(loop* [a 1] a)")
21512160
assert kw.keyword("keyword") == lcompile('(loop* [a :keyword b "string"] a)')

0 commit comments

Comments
 (0)