Skip to content

Commit 36b779d

Browse files
fix: add support for comprehensions with conditionals (#132)
Comprehension support works by parsing the underlying generators, and generators with `if` conditions have `filter` as their `Expr` form's second argument. This previously wasn't supported, as the generator parsing code expected the `for` iteration's `=` to be the second arg instead. This change adds support for the case of generators with filters, and adds relevant tests. (Also adds two - currently broken - tests where there is closure-boxing in the comprehension.)
1 parent fe89ed1 commit 36b779d

File tree

4 files changed

+57
-9
lines changed

4 files changed

+57
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# News
22

3+
## v1.0.4 - 2025-08-25
4+
5+
- Support comprehensions with conditionals, e.g., `@resumable function f1(); [i for i in 1:10 if i<5]; end` which previously led to "Illegal expression" errors.
6+
37
## v1.0.3 - 2025-03-24
48

59
- Internal changes to `fsmi_generator` to support julia 1.12

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ license = "MIT"
55
desc = "C# sharp style generators a.k.a. semi-coroutines for Julia."
66
authors = ["Ben Lauwens and volunteer maintainers"]
77
repo = "https://github.com/JuliaDynamics/ResumableFunctions.jl.git"
8-
version = "1.0.3"
8+
version = "1.0.4"
99

1010
[deps]
1111
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"

src/utils.jl

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -338,19 +338,31 @@ function scoping(s::Symbol, scope; new = false)
338338
return lookup_rhs!(s, scope)
339339
end
340340

341-
function scope_generator(expr, scope)
342-
expr.head !== :generator && error("Illegal generator expression: $(expr)")
343-
# first the generator case
341+
function scope_generator_inner(expr, scope)
344342
for i in 2:length(expr.args)
345-
!(expr.args[i] isa Expr && expr.args[i].head === :(=)) && error("Illegal expression in generator: $(expr.args[i])")
343+
!(expr.args[i] isa Expr && expr.args[i].head === :(=)) &&
344+
error("Illegal expression in generator: $(expr.args[i])")
346345
expr.args[i].args[2] = scoping(expr.args[i].args[2], scope)
347346
end
348347
# now create new scope
349348
push!(scope.scope_stack, Dict())
350349
for i in 2:length(expr.args)
351350
expr.args[i].args[1] = lookup_lhs!(expr.args[i].args[1], scope, new = true)
352351
end
352+
end
353+
354+
function scope_generator(expr, scope)
355+
expr.head !== :generator && error("Illegal generator expression: $(expr)")
353356

357+
has_filter = length(expr.args) == 2 && expr.args[2] isa Expr && expr.args[2].head === :filter
358+
if has_filter
359+
ex = expr.args[2]
360+
scope_generator_inner(ex, scope)
361+
# now apply scoping to the filter condition expression
362+
ex.args[1] = scoping(ex.args[1], scope)
363+
else
364+
scope_generator_inner(expr, scope)
365+
end
354366
expr.args[1] = scoping(expr.args[1], scope)
355367
pop!(scope.scope_stack)
356368
return expr
@@ -432,10 +444,6 @@ function scoping(expr::Expr, scope)
432444
end
433445
return expr
434446
end
435-
if expr.head === :generator
436-
expr = scope_generator(expr)
437-
return expr
438-
end
439447

440448
if expr.head === :(=)
441449
# One special case, where we need to have both LHS and RHS at our hands

test/test_main.jl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,42 @@ end
403403
end
404404
end
405405
@test collect(test_comprehension5()) == [2, 3, 4, 3, 4, 5]
406+
407+
@resumable function test_comprehension_state()
408+
c = 1
409+
@yield [i*c for i in 1:5]
410+
c += 1
411+
@yield [i*c for i in 1:5]
412+
end
413+
@test_broken collect(test_comprehension_state()) == [[1, 2, 3, 4, 5], [2, 4, 6, 8, 10]]
414+
415+
@resumable function test_comprehension_if()
416+
@yield [i for i in 1:10 if i < 5]
417+
end
418+
@test collect(test_comprehension_if()) == [[1, 2, 3, 4]]
419+
420+
@resumable function test_comprehension_if_multi_iterators()
421+
@yield [i + j for i in 1:3, j in 1:3 if i <= j]
422+
end
423+
@test collect(test_comprehension_if_multi_iterators()) == [[2, 3, 4, 4, 5, 6]]
424+
425+
@resumable function test_comprehension_if_nested_for()
426+
@yield [i * j for i in 1:3 for j in 1:3 if i * j < 5]
427+
end
428+
@test collect(test_comprehension_if_nested_for()) == [[1, 2, 3, 2, 4, 3]]
429+
430+
@resumable function test_comprehension_if_mixed()
431+
@yield [i + j + k for i in 1:2, j in 1:2 for k in 1:2 if i + j + k == 4]
432+
end
433+
@test collect(test_comprehension_if_mixed()) == [[4, 4, 4]]
434+
435+
@resumable function test_comprehension_if_state()
436+
c = 1
437+
@yield [i for i in 1:10 if i < c]
438+
c += 1
439+
@yield [i for i in 1:10 if i < c]
440+
end
441+
@test_broken collect(test_comprehension_if_state()) == [Int[], [1]]
406442
end
407443

408444
@testset "test_ref" begin

0 commit comments

Comments
 (0)