Skip to content

Commit 6e3f1fd

Browse files
authored
Support yield statement for creating coroutines (#652)
* Support `yield` statement for creating coroutines * Here you have it * Forgot this lad
1 parent 52d95d8 commit 6e3f1fd

File tree

8 files changed

+171
-5
lines changed

8 files changed

+171
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
* Added support for hierarchies (#633)
1313
* Added support for several more utility Namespace and Var utility functions (#636)
1414
* Added `basilisp.io` namespace with polymorphic reader and writer functions (#645)
15+
* Added support for coroutines and generators using `yield` syntax (#652)
1516

1617
### Changed
1718
* PyTest is now an optional extra dependency, rather than a required dependency (#622)

src/basilisp/io.lpy

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

1616
(extend-protocol Coercions
1717
nil
18-
(as-path [f] nil)
18+
(as-path [_] nil)
1919

2020
python/str
2121
(as-path [f] (pathlib/Path f))

src/basilisp/lang/compiler/analyzer.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,11 @@
121121
from basilisp.lang.compiler.nodes import Set as SetNode
122122
from basilisp.lang.compiler.nodes import SetBang, SpecialFormNode, Throw, Try, VarRef
123123
from basilisp.lang.compiler.nodes import Vector as VectorNode
124-
from basilisp.lang.compiler.nodes import WithMeta, deftype_or_reify_python_member_names
124+
from basilisp.lang.compiler.nodes import (
125+
WithMeta,
126+
Yield,
127+
deftype_or_reify_python_member_names,
128+
)
125129
from basilisp.lang.interfaces import IMeta, IRecord, ISeq, IType, IWithMeta
126130
from basilisp.lang.runtime import Var
127131
from basilisp.lang.typing import CompilerOpts, LispForm, ReaderForm
@@ -2903,6 +2907,32 @@ def _var_ast(form: ISeq, ctx: AnalyzerContext) -> VarRef:
29032907
)
29042908

29052909

2910+
def _yield_ast(form: ISeq, ctx: AnalyzerContext) -> Yield:
2911+
assert form.first == SpecialForm.YIELD
2912+
2913+
if ctx.func_ctx is None:
2914+
raise AnalyzerException(
2915+
"yield forms may not appear in function context", form=form
2916+
)
2917+
2918+
nelems = count(form)
2919+
if nelems not in {1, 2}:
2920+
raise AnalyzerException(
2921+
"yield forms must contain 1 or 2 elements, as in: (yield [expr])", form=form
2922+
)
2923+
2924+
if nelems == 2:
2925+
with ctx.expr_pos():
2926+
expr = _analyze_form(runtime.nth(form, 1), ctx)
2927+
return Yield(
2928+
form=form,
2929+
expr=expr,
2930+
env=ctx.get_node_env(pos=ctx.syntax_position),
2931+
)
2932+
else:
2933+
return Yield.expressionless(form, ctx.get_node_env(pos=ctx.syntax_position))
2934+
2935+
29062936
SpecialFormHandler = Callable[[ISeq, AnalyzerContext], SpecialFormNode]
29072937
_SPECIAL_FORM_HANDLERS: Mapping[sym.Symbol, SpecialFormHandler] = {
29082938
SpecialForm.AWAIT: _await_ast,
@@ -2924,6 +2954,7 @@ def _var_ast(form: ISeq, ctx: AnalyzerContext) -> VarRef:
29242954
SpecialForm.THROW: _throw_ast,
29252955
SpecialForm.TRY: _try_ast,
29262956
SpecialForm.VAR: _var_ast,
2957+
SpecialForm.YIELD: _yield_ast,
29272958
}
29282959

29292960

src/basilisp/lang/compiler/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class SpecialForm:
2525
THROW = sym.symbol("throw")
2626
TRY = sym.symbol("try")
2727
VAR = sym.symbol("var")
28+
YIELD = sym.symbol("yield")
2829

2930

3031
AMPERSAND = sym.symbol("&")

src/basilisp/lang/compiler/generator.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
from basilisp.lang.compiler.nodes import Set as SetNode
9494
from basilisp.lang.compiler.nodes import SetBang, Throw, Try, VarRef
9595
from basilisp.lang.compiler.nodes import Vector as VectorNode
96-
from basilisp.lang.compiler.nodes import WithMeta
96+
from basilisp.lang.compiler.nodes import WithMeta, Yield
9797
from basilisp.lang.interfaces import IMeta, IRecord, ISeq, ISeqable, IType
9898
from basilisp.lang.runtime import CORE_NS
9999
from basilisp.lang.runtime import NS_VAR_NAME as LISP_NS_VAR
@@ -710,8 +710,6 @@ def statementize(e: ast.AST) -> ast.AST:
710710
ast.With,
711711
ast.FunctionDef,
712712
ast.Return,
713-
ast.Yield,
714-
ast.YieldFrom,
715713
ast.Global,
716714
ast.ClassDef,
717715
ast.AsyncFunctionDef,
@@ -2815,6 +2813,17 @@ def _try_to_py_ast(ctx: GeneratorContext, node: Try) -> GeneratedPyAST:
28152813
)
28162814

28172815

2816+
@_with_ast_loc_deps
2817+
def _yield_to_py_ast(ctx: GeneratorContext, node: Yield) -> GeneratedPyAST:
2818+
assert node.op == NodeOp.YIELD
2819+
if node.expr is None:
2820+
return GeneratedPyAST(node=ast.Yield(value=None))
2821+
expr_ast = gen_py_ast(ctx, node.expr)
2822+
return GeneratedPyAST(
2823+
node=ast.Yield(value=expr_ast.node), dependencies=expr_ast.dependencies
2824+
)
2825+
2826+
28182827
##########
28192828
# Symbols
28202829
##########
@@ -3607,6 +3616,7 @@ def _const_node_to_py_ast(ctx: GeneratorContext, lisp_ast: Const) -> GeneratedPy
36073616
NodeOp.SET_BANG: _set_bang_to_py_ast,
36083617
NodeOp.THROW: _throw_to_py_ast,
36093618
NodeOp.TRY: _try_to_py_ast,
3619+
NodeOp.YIELD: _yield_to_py_ast,
36103620
NodeOp.VAR: _var_sym_to_py_ast,
36113621
NodeOp.VECTOR: _vec_to_py_ast,
36123622
NodeOp.WITH_META: _with_meta_to_py_ast, # type: ignore

src/basilisp/lang/compiler/nodes.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ class NodeOp(Enum):
104104
VAR = kw.keyword("var")
105105
VECTOR = kw.keyword("vector")
106106
WITH_META = kw.keyword("with-meta")
107+
YIELD = kw.keyword("yield")
107108

108109

109110
T = TypeVar("T")
@@ -937,6 +938,21 @@ class WithMeta(Node[LispForm]):
937938
raw_forms: IPersistentVector[LispForm] = vec.PersistentVector.empty()
938939

939940

941+
@attr.s(auto_attribs=True, frozen=True, slots=True)
942+
class Yield(Node[SpecialForm]):
943+
form: SpecialForm
944+
expr: Optional[Node]
945+
env: NodeEnv
946+
children: Sequence[kw.Keyword] = vec.v(EXPR)
947+
op: NodeOp = NodeOp.YIELD
948+
top_level: bool = False
949+
raw_forms: IPersistentVector[LispForm] = vec.PersistentVector.empty()
950+
951+
@classmethod
952+
def expressionless(cls, form: SpecialForm, env: NodeEnv):
953+
return cls(form=form, expr=None, env=env, children=vec.PersistentVector.empty())
954+
955+
940956
SpecialFormNode = Union[
941957
Await,
942958
Def,
@@ -959,4 +975,5 @@ class WithMeta(Node[LispForm]):
959975
Throw,
960976
Try,
961977
VarRef,
978+
Yield,
962979
]

src/basilisp/lang/runtime.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
_THROW = sym.symbol("throw")
114114
_TRY = sym.symbol("try")
115115
_VAR = sym.symbol("var")
116+
_YIELD = sym.symbol("yield")
116117
_SPECIAL_FORMS = lset.s(
117118
_AWAIT,
118119
_CATCH,
@@ -136,6 +137,7 @@
136137
_THROW,
137138
_TRY,
138139
_VAR,
140+
_YIELD,
139141
)
140142

141143

tests/basilisp/compiler_test.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5676,3 +5676,107 @@ def test_var_reader_literal(self, lcompile: CompileFn, ns: runtime.Namespace):
56765676
v = lcompile(code)
56775677
assert v == Var.find_in_ns(sym.symbol(ns_name), sym.symbol("some-var"))
56785678
assert v.value == "a value"
5679+
5680+
5681+
class TestYield:
5682+
@pytest.mark.parametrize(
5683+
"code",
5684+
[
5685+
"(yield)",
5686+
"(yield :a)",
5687+
"(let* [v :a] (yield v))",
5688+
],
5689+
)
5690+
def test_yield_must_be_in_fn_context(self, lcompile: CompileFn, code: str):
5691+
with pytest.raises(compiler.CompilerException):
5692+
lcompile(code)
5693+
5694+
@pytest.mark.parametrize(
5695+
"code",
5696+
[
5697+
"(fn [] (yield :two :items))",
5698+
"(fn [] (yield :three :items :now))",
5699+
],
5700+
)
5701+
def test_yield_num_elems(self, lcompile: CompileFn, code: str):
5702+
with pytest.raises(compiler.CompilerException):
5703+
lcompile(code)
5704+
5705+
def test_yield_control_only(self, lcompile: CompileFn, ns: runtime.Namespace):
5706+
f = lcompile(
5707+
"""
5708+
(def state (atom nil))
5709+
(fn []
5710+
(reset! state :started)
5711+
(yield)
5712+
(reset! state :done))
5713+
"""
5714+
)
5715+
state = ns.find(sym.symbol("state")).value
5716+
coro = f()
5717+
assert None is state.deref()
5718+
assert None is next(coro)
5719+
assert kw.keyword("started") == state.deref()
5720+
assert None is next(coro, None)
5721+
assert kw.keyword("done") == state.deref()
5722+
5723+
def test_yield_value(self, lcompile: CompileFn, ns: runtime.Namespace):
5724+
f = lcompile(
5725+
"""
5726+
(def state (atom nil))
5727+
(fn []
5728+
(reset! state :started)
5729+
(yield :yielding)
5730+
(reset! state :done))
5731+
"""
5732+
)
5733+
state = ns.find(sym.symbol("state")).value
5734+
coro = f()
5735+
assert None is state.deref()
5736+
assert kw.keyword("yielding") == next(coro)
5737+
assert kw.keyword("started") == state.deref()
5738+
assert None is next(coro, None)
5739+
assert kw.keyword("done") == state.deref()
5740+
5741+
def test_yield_as_coroutine(self, lcompile: CompileFn, ns: runtime.Namespace):
5742+
f = lcompile(
5743+
"""
5744+
(def state (atom nil))
5745+
(fn []
5746+
(reset! state :started)
5747+
(let [v (yield :yielding)]
5748+
(reset! state v)))
5749+
"""
5750+
)
5751+
state = ns.find(sym.symbol("state")).value
5752+
coro = f()
5753+
assert None is state.deref()
5754+
assert kw.keyword("yielding") == next(coro)
5755+
assert kw.keyword("started") == state.deref()
5756+
with pytest.raises(StopIteration):
5757+
coro.send(kw.keyword("coroutine-value"))
5758+
assert kw.keyword("coroutine-value") == state.deref()
5759+
5760+
def test_yield_as_coroutine_with_multiple_yields(
5761+
self, lcompile: CompileFn, ns: runtime.Namespace
5762+
):
5763+
f = lcompile(
5764+
"""
5765+
(def state (atom nil))
5766+
(fn []
5767+
(reset! state :started)
5768+
(let [v (yield :yielding)]
5769+
(reset! state v)
5770+
(yield)
5771+
(reset! state :done)))
5772+
"""
5773+
)
5774+
state = ns.find(sym.symbol("state")).value
5775+
coro = f()
5776+
assert None is state.deref()
5777+
assert kw.keyword("yielding") == next(coro)
5778+
assert kw.keyword("started") == state.deref()
5779+
assert None is coro.send(kw.keyword("coroutine-value"))
5780+
assert kw.keyword("coroutine-value") == state.deref()
5781+
assert None is next(coro, None)
5782+
assert kw.keyword("done") == state.deref()

0 commit comments

Comments
 (0)