Skip to content

Commit 6299191

Browse files
authored
Loop special form (#317)
* Loop special form * Formatting * take-nth tests * Test that runtime and compiler special forms are the same * Loop tests * Nvm bad idea * Loop sequential destructuring * Associative destructuring loop tests * Collapse special form handlers * Formatting
1 parent d1dae79 commit 6299191

File tree

7 files changed

+447
-42
lines changed

7 files changed

+447
-42
lines changed

src/basilisp/core/__init__.lpy

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
(fn* let [&form & decl]
5151
(cons 'let* decl)))
5252

53+
(def ^:macro ^:redef loop
54+
(fn* loop [&form & decl]
55+
(cons 'loop* decl)))
56+
5357
(def ^:macro ^:redef fn
5458
(fn* fn [&form & decl]
5559
(with-meta
@@ -1011,6 +1015,16 @@
10111015
(when (> n 0)
10121016
(cons (f) (repeatedly (dec n) f))))))
10131017

1018+
(defn take-nth
1019+
"Return a lazy sequence of every nth element of coll."
1020+
[n coll]
1021+
(lazy-seq
1022+
(when (seq coll)
1023+
(if (<= n 0)
1024+
(repeat (first coll))
1025+
(cons (first coll)
1026+
(take-nth n (drop (dec n) (rest coll))))))))
1027+
10141028
(defn partition
10151029
"Return a lazy sequence of partitions of coll of size n at offsets
10161030
of step elements. If step is not given, steps of size n will be used
@@ -2121,3 +2135,30 @@
21212135
(mapcat destructure))]
21222136
`(let* [~@bindings]
21232137
~@body)))
2138+
2139+
(defn ^:private loop-with-destructuring
2140+
"Take a loop definition (an binding vector and 0 or more body
2141+
expressions) whose binding vector may or may not require destructuring
2142+
and return a loop binding vector and loop body."
2143+
[bindings body]
2144+
(let [defs (->> (take-nth 2 bindings)
2145+
(map destructure-def))
2146+
binding-vec (vec (mapcat (fn [ddef binding]
2147+
[(:name ddef) binding])
2148+
defs
2149+
(take-nth 2 (drop 1 bindings))))
2150+
inner-bindings (->> defs
2151+
(filter #(not= :symbol (:type %)))
2152+
(mapcat destructure-binding))
2153+
new-body (if (seq inner-bindings)
2154+
[`(let* [~@inner-bindings]
2155+
~@body)]
2156+
body)]
2157+
[binding-vec new-body]))
2158+
2159+
(defmacro ^:no-warn-on-redef loop
2160+
"Loop bindings with destructuring support."
2161+
[bindings & body]
2162+
(let [[bindings body] (loop-with-destructuring bindings body)]
2163+
`(loop* ~bindings
2164+
~@body)))

src/basilisp/lang/compiler.py

Lines changed: 130 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
_INTEROP_CALL = sym.symbol(".")
8484
_INTEROP_PROP = sym.symbol(".-")
8585
_LET = sym.symbol("let*")
86+
_LOOP = sym.symbol("loop*")
8687
_QUOTE = sym.symbol("quote")
8788
_RECUR = sym.symbol("recur")
8889
_THROW = sym.symbol("throw")
@@ -97,6 +98,7 @@
9798
_INTEROP_CALL,
9899
_INTEROP_PROP,
99100
_LET,
101+
_LOOP,
100102
_QUOTE,
101103
_RECUR,
102104
_THROW,
@@ -864,7 +866,7 @@ def _assert_recur_is_tail(ctx: CompilerContext, form: lseq.Seq) -> None: # noqa
864866
_assert_recur_is_tail(ctx, lseq.sequence([runtime.nth(child, 3)]))
865867
except IndexError:
866868
pass
867-
elif child.first == _LET:
869+
elif child.first in {_LET, _LOOP}:
868870
for binding, val in partition(runtime.nth(child, 1), 2):
869871
_assert_no_recur(ctx, lseq.sequence([binding]))
870872
_assert_no_recur(ctx, lseq.sequence([val]))
@@ -1468,6 +1470,110 @@ def let_32(a_33, b_34, c_35):
14681470
yield _node(ast.Call(func=_load_attr(outer_letname), args=[], keywords=[]))
14691471

14701472

1473+
def _loop_ast( # pylint:disable=too-many-locals
1474+
ctx: CompilerContext, form: llist.List
1475+
) -> ASTStream:
1476+
"""Generate a Python AST node for a loop special form.
1477+
1478+
Python code for a `loop*` binding like this:
1479+
(loop [s "abc"
1480+
len 0]
1481+
(if (seq s)
1482+
(recur (rest s)
1483+
(inc len))
1484+
len))
1485+
1486+
Should look roughly like this:
1487+
def loop_14():
1488+
1489+
def loop_15(s_16, len__17):
1490+
1491+
def lisp_if_19():
1492+
if_test_18 = basilisp.core.seq(s_16)
1493+
if None is if_test_18 or False is if_test_18:
1494+
return len__17
1495+
else:
1496+
return runtime_5._TrampolineArgs(False, basilisp.core.rest(
1497+
s_16), basilisp.core.inc(len__17))
1498+
return lisp_if_19()
1499+
s_12 = 'abc'
1500+
len__13 = 0
1501+
return runtime_5._trampoline(loop_15)(s_12, len__13)"""
1502+
assert form.first == _LOOP
1503+
assert isinstance(form[1], vec.Vector)
1504+
assert len(form) >= 3
1505+
1506+
# For a better description of what's going on below, peek up at _let_ast.
1507+
with ctx.new_symbol_table(genname("loop_st")) as st:
1508+
bindings = list(partition(form[1], 2))
1509+
1510+
arg_syms: Dict[
1511+
sym.Symbol, str
1512+
] = OrderedDict() # Mapping of binding symbols (turned into function parameter names) to munged name # noqa: E501
1513+
var_names = (
1514+
[]
1515+
) # Names of local Python variables bound to computed expressions prior to the function call
1516+
arg_deps = [] # Argument expression dependency nodes
1517+
arg_exprs = [] # Bound expressions are the expressions a name is bound to
1518+
for s, expr in bindings:
1519+
# Keep track of only the newest symbol and munged name in arg_syms, that way
1520+
# we are only calling the loop binding below with the most recent entry.
1521+
munged = genname(munge(s.name))
1522+
arg_syms[s] = munged
1523+
var_names.append(munged)
1524+
1525+
expr_deps, expr_node = _nodes_and_expr(_to_ast(ctx, expr))
1526+
1527+
# Don't add the new symbol until after we've processed the expression
1528+
_new_symbol(ctx, s, munged, _SYM_CTX_LOCAL, st=st)
1529+
1530+
arg_deps.append(expr_deps)
1531+
arg_exprs.append(_unwrap_node(expr_node))
1532+
1533+
# Generate an outer function to hold the entire loop expression (including bindings).
1534+
# We need to do this to guarantee that no binding expressions are executed as part of
1535+
# an assignment as a dependency node. This eager evaluation could leak out as part of
1536+
# (at least) if statements dependency nodes.
1537+
outer_loopname = genname("loop")
1538+
loop_fn_body: List[ast.AST] = []
1539+
1540+
# Generate a function to hold the body of the loop expression
1541+
loopname = genname("loop")
1542+
1543+
with ctx.new_recur_point("loop", vec.vector(arg_syms.keys())):
1544+
# Suppress shadowing warnings below since the shadow warnings will be
1545+
# emitted by calling _new_symbol in the loop above
1546+
args, body, vargs = _fn_args_body(
1547+
ctx,
1548+
vec.vector(arg_syms.keys()),
1549+
runtime.nthrest(form, 2),
1550+
warn_on_shadowed_var=False,
1551+
warn_on_shadowed_name=False,
1552+
)
1553+
loop_fn_body.append(_expressionize(body, loopname, args=args, vargs=vargs))
1554+
1555+
# Generate local variable assignments for processing loop bindings
1556+
var_names = seq(var_names).map(lambda n: ast.Name(id=n, ctx=ast.Store()))
1557+
for name, deps, expr in zip(var_names, arg_deps, arg_exprs):
1558+
loop_fn_body.extend(_unwrap_nodes(deps))
1559+
loop_fn_body.append(ast.Assign(targets=[name], value=expr))
1560+
1561+
loop_fn_body.append(
1562+
ast.Call(
1563+
func=ast.Call(
1564+
func=_TRAMPOLINE_FN_NAME, args=[_load_attr(loopname)], keywords=[]
1565+
),
1566+
args=seq(arg_syms.values())
1567+
.map(lambda n: ast.Name(id=n, ctx=ast.Load()))
1568+
.to_list(),
1569+
keywords=[],
1570+
)
1571+
)
1572+
1573+
yield _dependency(_expressionize(loop_fn_body, outer_loopname))
1574+
yield _node(ast.Call(func=_load_attr(outer_loopname), args=[], keywords=[]))
1575+
1576+
14711577
def _quote_ast(ctx: CompilerContext, form: llist.List) -> ASTStream:
14721578
"""Generate a Python AST Node for quoted forms.
14731579
@@ -1676,49 +1782,32 @@ def _var_ast(_: CompilerContext, form: llist.List) -> ASTStream:
16761782
yield _node(ast.Call(func=_FIND_VAR_FN_NAME, args=[base_sym], keywords=[]))
16771783

16781784

1785+
_SPECIAL_FORM_HANDLERS: Dict[
1786+
sym.Symbol, Callable[[CompilerContext, llist.List], ASTStream]
1787+
] = {
1788+
_DEF: _def_ast,
1789+
_FN: _fn_ast,
1790+
_IF: _if_ast,
1791+
_IMPORT: _import_ast,
1792+
_INTEROP_CALL: _interop_call_ast,
1793+
_INTEROP_PROP: _interop_prop_ast,
1794+
_DO: _do_ast,
1795+
_LET: _let_ast,
1796+
_LOOP: _loop_ast,
1797+
_QUOTE: _quote_ast,
1798+
_RECUR: _recur_ast,
1799+
_THROW: _throw_ast,
1800+
_TRY: _try_ast,
1801+
_VAR: _var_ast,
1802+
}
1803+
1804+
16791805
def _special_form_ast(ctx: CompilerContext, form: llist.List) -> ASTStream:
16801806
"""Generate a Python AST Node for any Lisp special forms."""
16811807
assert form.first in _SPECIAL_FORMS
1682-
which = form.first
1683-
if which == _DEF:
1684-
yield from _def_ast(ctx, form)
1685-
return
1686-
elif which == _FN:
1687-
yield from _fn_ast(ctx, form)
1688-
return
1689-
elif which == _IF:
1690-
yield from _if_ast(ctx, form)
1691-
return
1692-
elif which == _IMPORT:
1693-
yield from _import_ast(ctx, form) # type: ignore
1694-
return
1695-
elif which == _INTEROP_CALL:
1696-
yield from _interop_call_ast(ctx, form)
1697-
return
1698-
elif which == _INTEROP_PROP:
1699-
yield from _interop_prop_ast(ctx, form)
1700-
return
1701-
elif which == _DO:
1702-
yield from _do_ast(ctx, form)
1703-
return
1704-
elif which == _LET:
1705-
yield from _let_ast(ctx, form)
1706-
return
1707-
elif which == _QUOTE:
1708-
yield from _quote_ast(ctx, form)
1709-
return
1710-
elif which == _RECUR:
1711-
yield from _recur_ast(ctx, form)
1712-
return
1713-
elif which == _THROW:
1714-
yield from _throw_ast(ctx, form)
1715-
return
1716-
elif which == _TRY:
1717-
yield from _try_ast(ctx, form)
1718-
return
1719-
elif which == _VAR:
1720-
yield from _var_ast(ctx, form)
1721-
return
1808+
handle_special_form = _SPECIAL_FORM_HANDLERS.get(form.first, None)
1809+
if handle_special_form:
1810+
return handle_special_form(ctx, form)
17221811
raise CompilerException("Special form identified, but not handled") from None
17231812

17241813

src/basilisp/lang/runtime.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
_INTEROP_CALL = sym.symbol(".")
5656
_INTEROP_PROP = sym.symbol(".-")
5757
_LET = sym.symbol("let*")
58+
_LOOP = sym.symbol("loop*")
5859
_QUOTE = sym.symbol("quote")
5960
_RECUR = sym.symbol("recur")
6061
_THROW = sym.symbol("throw")
@@ -71,6 +72,7 @@
7172
_INTEROP_CALL,
7273
_INTEROP_PROP,
7374
_LET,
75+
_LOOP,
7476
_QUOTE,
7577
_RECUR,
7678
_THROW,

src/basilisp/repl.lpy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
(defn print-source
4141
"Print the source forms for a function."
42-
[v]
42+
[_]
4343
(throw (builtins/NotImplementedError)))
4444

4545
(defmacro source

tests/basilisp/compiler_test.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,42 @@ def test_let_warn_on_unused_name(ns: runtime.Namespace):
741741
)
742742

743743

744+
def test_loop(ns: runtime.Namespace):
745+
assert 1 == lcompile("(loop* [a 1] a)")
746+
assert kw.keyword("keyword") == lcompile('(loop* [a :keyword b "string"] a)')
747+
assert kw.keyword("value") == lcompile("(loop* [a :value b a] b)")
748+
assert lmap.map({kw.keyword("length"): 1}) == lcompile(
749+
"(loop* [a 1 b :length c {b a} a 4] c)"
750+
)
751+
assert 4 == lcompile("(loop* [a 1 b :length c {b a} a 4] a)")
752+
assert "LOWER" == lcompile('(loop* [a "lower"] (.upper a))')
753+
assert "string" == lcompile('(loop* [] "string")')
754+
755+
with pytest.raises(runtime.RuntimeException):
756+
lcompile("(loop* [a 'sym] c)")
757+
758+
code = """
759+
(import* io)
760+
(let* [reader (io/StringIO "string")
761+
writer (io/StringIO)]
762+
(loop* []
763+
(let* [c (.read reader 1)]
764+
(if (not= c "")
765+
(do
766+
(.write writer c)
767+
(recur))
768+
(.getvalue writer)))))"""
769+
assert "string" == lcompile(code)
770+
771+
code = """
772+
(loop* [s "tester"
773+
accum []]
774+
(if (seq s)
775+
(recur (rest s) (conj accum (first s)))
776+
(apply str accum)))"""
777+
assert "tester" == lcompile(code)
778+
779+
744780
def test_quoted_list(ns: runtime.Namespace):
745781
assert lcompile("'()") == llist.l()
746782
assert lcompile("'(str)") == llist.l(sym.symbol("str"))
@@ -892,6 +928,21 @@ def test_disallow_recur_outside_tail(ns: runtime.Namespace):
892928
with pytest.raises(compiler.CompilerException):
893929
lcompile('(fn [a] (let [a "a"] (recur a) a))')
894930

931+
with pytest.raises(compiler.CompilerException):
932+
lcompile('(fn [a] (loop* [a (recur "a")] a))')
933+
934+
with pytest.raises(compiler.CompilerException):
935+
lcompile('(fn [a] (loop* [a (do (recur "a"))] a))')
936+
937+
with pytest.raises(compiler.CompilerException):
938+
lcompile('(fn [a] (loop* [a (do :b (recur "a"))] a))')
939+
940+
with pytest.raises(compiler.CompilerException):
941+
lcompile('(fn [a] (loop* [a (do (recur "a") :c)] a))')
942+
943+
with pytest.raises(compiler.CompilerException):
944+
lcompile('(fn [a] (loop* [a "a"] (recur a) a))')
945+
895946
with pytest.raises(compiler.CompilerException):
896947
lcompile("(fn [a] (try (do (recur a) :b) (catch AttributeError _ nil)))")
897948

0 commit comments

Comments
 (0)