From 595054abf9d784b14c842e9e049578f4cb45f070 Mon Sep 17 00:00:00 2001 From: Simeon David Schaub Date: Mon, 14 Oct 2024 01:21:21 +0200 Subject: [PATCH] support for-else and while-else 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 --- src/julia-parser.scm | 39 +++++++++++++++++++---- src/julia-syntax.scm | 24 +++++++++----- test/syntax.jl | 75 ++++++++++++++++++++++++++++++++++++-------- 3 files changed, 112 insertions(+), 26 deletions(-) diff --git a/src/julia-parser.scm b/src/julia-parser.scm index 891a26bb0ea49..1d31f9ea2c7b8 100644 --- a/src/julia-parser.scm +++ b/src/julia-parser.scm @@ -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 #\;)) diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index 4b3e6ae96898b..a87458b57345b 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -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 @@ -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) @@ -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))) @@ -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))) diff --git a/test/syntax.jl b/test/syntax.jl index c19721b5c54b3..918ff1a32f772 100644 --- a/test/syntax.jl +++ b/test/syntax.jl @@ -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 @@ -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 @@ -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