Skip to content

Commit

Permalink
support for-else and while-else
Browse files Browse the repository at this point in the history
This adopts the semantics discussed in #1289, namely the `else` block is executed whenever the loop never runs. Unlike Python, `break` and `continue` are irrelevant. Multidimensional loops are not supported since there is some ambiguity whether e.g.

```julia
for i in 1:3, j in 1:0
    print(1)
else
    print(2)
end
```

should print 2 once, thrice or maybe not even at all.

Currently only supported in the flisp parser, so requires `JULIA_USE_FLISP_PARSER=1`. I could use some guidance on the necessary steps to add this to JuliaSyntax as well - AFAIU this would also require #56110 first.

closes #1289
  • Loading branch information
simeonschaub committed Oct 14, 2024
1 parent 67c93b9 commit 595054a
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 26 deletions.
39 changes: 33 additions & 6 deletions src/julia-parser.scm
Original file line number Diff line number Diff line change
Expand Up @@ -1397,14 +1397,41 @@
(if (eq? word 'quote)
(list 'quote blk)
blk))))
((while) (begin0 (list 'while (parse-cond s) (append (parse-block s) (list (line-number-node s))))
(expect-end s word)))
((while)
(let* ((con (parse-cond s))
(body (parse-block s))
(nxt (require-token s)))
(take-token s)
(case nxt
((end)
`(while ,con
,(append body (list (line-number-node s)))))
((else)
(let ((else-body (parse-block s)))
(expect-end s word)
`(while ,con
,body
,(append else-body (list (line-number-node s))))))
(else
(error (string "unexpected \"" nxt "\""))))))
((for)
(let* ((ranges (parse-comma-separated-iters s))
(body (parse-block s)))
(expect-end s word)
`(for ,(if (length= ranges 1) (car ranges) (cons 'block ranges))
,(append body (list (line-number-node s))))))
(ranges (if (length= ranges 1) (car ranges) (cons 'block ranges)))
(body (parse-block s))
(nxt (require-token s)))
(take-token s)
(case nxt
((end)
`(for ,ranges
,(append body (list (line-number-node s)))))
((else)
(let ((else-body (parse-block s)))
(expect-end s word)
`(for ,ranges
,body
,(append else-body (list (line-number-node s))))))
(else
(error (string "unexpected \"" nxt "\""))))))

((let)
(let ((binds (if (memv (peek-token s) '(#\newline #\;))
Expand Down
24 changes: 17 additions & 7 deletions src/julia-syntax.scm
Original file line number Diff line number Diff line change
Expand Up @@ -1823,7 +1823,9 @@
(if ,g ,g
,(loop (cdr tail)))))))))))

(define (expand-for lhss itrs body)
(define (expand-for lhss itrs body . else-body)
(if (and (length> lhss 1) (not (null? else-body)))
(error "multi-dimensional for-loops are not allowed to have else-blocks"))
(define (outer? x) (and (pair? x) (eq? (car x) 'outer)))
(let ((copied-vars ;; variables not declared `outer` are copied in the innermost loop
;; TODO: maybe filter these to remove vars not assigned in the loop
Expand Down Expand Up @@ -1867,7 +1869,8 @@
(_do_while
(block ,body
(= ,next (call (top iterate) ,coll ,state)))
(call (top not_int) (call (core ===) ,next (null))))))))))))
(call (top not_int) (call (core ===) ,next (null))))
,@else-body))))))))

;; wrap `expr` in a function appropriate for consuming values from given ranges
(define (func-for-generator-ranges expr range-exprs flat outervars)
Expand Down Expand Up @@ -2134,10 +2137,17 @@
(list* (car e) (expand-condition (cadr e)) (map expand-forms (cddr e))))

(define (expand-while e)
`(break-block loop-exit
(_while ,(expand-condition (cadr e))
(break-block loop-cont
(scope-block ,(blockify (expand-forms (caddr e))))))))
(if (length= e 3)
`(break-block loop-exit
(_while ,(expand-condition (cadr e))
(break-block loop-cont
(scope-block ,(blockify (expand-forms (caddr e)))))))
`(break-block loop-exit
(if ,(expand-condition (cadr e))
(_do_while (break-block loop-cont
(scope-block ,(blockify (expand-forms (caddr e)))))
,(expand-condition (cadr e)))
(scope-block ,(blockify (expand-forms (cadddr e))))))))

(define (expand-vcat e
(vcat '((top vcat)))
Expand Down Expand Up @@ -2798,7 +2808,7 @@
(let ((ranges (if (eq? (car (cadr e)) 'block)
(cdr (cadr e))
(list (cadr e)))))
(expand-forms (expand-for (map cadr ranges) (map caddr ranges) (caddr e)))))
(expand-forms (apply expand-for (map cadr ranges) (map caddr ranges) (cddr e)))))

'&& (lambda (e) (expand-forms (expand-and e)))
'|\|\|| (lambda (e) (expand-forms (expand-or e)))
Expand Down
75 changes: 62 additions & 13 deletions test/syntax.jl
Original file line number Diff line number Diff line change
Expand Up @@ -537,18 +537,19 @@ end
# make sure that incomplete tags are detected correctly
# (i.e. error messages in src/julia-parser.scm must be matched correctly
# by the code in base/client.jl)
for (str, tag) in Dict("" => :none, "\"" => :string, "#=" => :comment, "'" => :char,
"`" => :cmd, "begin;" => :block, "quote;" => :block,
"let;" => :block, "for i=1;" => :block, "function f();" => :block,
"f() do x;" => :block, "module X;" => :block, "mutable struct X;" => :block,
"struct X;" => :block, "(" => :other, "[" => :other,
"for" => :other, "function" => :other,
"f() do" => :other, "module" => :other, "mutable struct" => :other,
"struct" => :other,
"quote" => using_JuliaSyntax ? :block : :other,
"let" => using_JuliaSyntax ? :block : :other,
"begin" => using_JuliaSyntax ? :block : :other,
)
@testset "incomplete tags: $str => $tag" for (str, tag) in Dict(
"" => :none, "\"" => :string, "#=" => :comment, "'" => :char,
"`" => :cmd, "begin;" => :block, "quote;" => :block,
"let;" => :block, "for i=1;" => :other, "function f();" => :block,
"f() do x;" => :block, "module X;" => :block, "mutable struct X;" => :block,
"struct X;" => :block, "(" => :other, "[" => :other,
"for" => :other, "function" => :other,
"f() do" => :other, "module" => :other, "mutable struct" => :other,
"struct" => :other,
"quote" => using_JuliaSyntax ? :block : :other,
"let" => using_JuliaSyntax ? :block : :other,
"begin" => using_JuliaSyntax ? :block : :other,
)
@test Base.incomplete_tag(Meta.parse(str, raise=false)) == tag
end

Expand Down Expand Up @@ -2429,7 +2430,7 @@ end
@test x == 6

# issue #36196
@test_parseerror "(for i=1; println())" "\"for\" at none:1 expected \"end\", got \")\""
@test_parseerror "(for i=1; println())" "unexpected \")\""
@test_parseerror "(try i=1; println())" "\"try\" at none:1 expected \"end\", got \")\""

# issue #36272
Expand Down Expand Up @@ -3987,3 +3988,51 @@ end
@test f45494() === (0,)

@test_throws "\"esc(...)\" used outside of macro expansion" eval(esc(:(const x=1)))

@testset "for else" begin
a = Int[]
for i in 1:0
push!(a, 1)
else
push!(a, 2)
end
@test a == [2]

a = Int[]
for i in 1:3
push!(a, 1)
else
push!(a, 2)
end
@test a == [1, 1, 1]

@test_throws "multi-dimensional for-loops are not allowed to have else-blocks" eval(:(
for i in 1:0, j in 1:3
print(1)
else
print(2)
end
))
end

@testset "while else" begin
a = Int[]
i = 1
while i 0
push!(a, 1)
i += 1
else
push!(a, 2)
end
@test a == [2]

a = Int[]
i = 1
while i 3
push!(a, 1)
i += 1
else
push!(a, 2)
end
@test a == [1, 1, 1]
end

0 comments on commit 595054a

Please sign in to comment.