From 7c2a00fb8f00779f0d3b2eaf9d256f46d01d0a42 Mon Sep 17 00:00:00 2001 From: Simeon Schaub Date: Thu, 9 Sep 2021 15:52:00 -0400 Subject: [PATCH 1/4] add try/catch/else This PR allows the use of `else` inside try-blocks, which is only taken if no exception was caught inside `try`. It is still combinable with `finally` as well. If an error is thrown inside the `else` block, the current semantics are that the error does not get caught and is thrown like normal, but the `finally` block is still run afterwards. This seemed like the most sensible option to me. I am not very confident about the implementation of linearization for `trycatchelse` here, so I would appreciate it if @JeffBezanson could give this a thorough review, so that I don't miss any edge cases. I thought we had an issue for this already, but I couldn't find anything. `else` might also not be the best keyword here, so maybe we can come up with something clearer. But it of course has the advantage that it is already a Julia keyword, so we don't need to add a new one. --- base/show.jl | 5 ++- src/ast.scm | 7 ++++ src/julia-parser.scm | 34 ++++++++++++++----- src/julia-syntax.scm | 52 +++++++++++++++++----------- src/utils.scm | 1 + test/syntax.jl | 81 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 151 insertions(+), 29 deletions(-) diff --git a/base/show.jl b/base/show.jl index f3110d5981429..5ae5d543357da 100644 --- a/base/show.jl +++ b/base/show.jl @@ -2131,12 +2131,15 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In elseif head === :line && 1 <= nargs <= 2 show_linenumber(io, args...) - elseif head === :try && 3 <= nargs <= 4 + elseif head === :try && 3 <= nargs <= 5 iob = IOContext(io, beginsym=>false) show_block(iob, "try", args[1], indent, quote_level) if is_expr(args[3], :block) show_block(iob, "catch", args[2] === false ? Any[] : args[2], args[3]::Expr, indent, quote_level) end + if nargs >= 5 && is_expr(args[5], :block) + show_block(iob, "else", Any[], args[5]::Expr, indent, quote_level) + end if nargs >= 4 && is_expr(args[4], :block) show_block(iob, "finally", Any[], args[4]::Expr, indent, quote_level) end diff --git a/src/ast.scm b/src/ast.scm index e5148a507a4fd..a1615cc01e2fe 100644 --- a/src/ast.scm +++ b/src/ast.scm @@ -209,6 +209,13 @@ "\n" (indented-block (cdr (cadddr e)) ilvl)) "") + (if (length> e 5) + (let ((els (cadddddr e))) + (if (and (pair? els) (eq? (car els) 'block)) + (string (string.rep " " ilvl) "else\n" + (indented-block (cdr els) ilvl)) + "")) + "") (if (length> e 4) (let ((fin (caddddr e))) (if (and (pair? fin) (eq? (car fin) 'block)) diff --git a/src/julia-parser.scm b/src/julia-parser.scm index dac32bdf81066..8fac67a3278be 100644 --- a/src/julia-parser.scm +++ b/src/julia-parser.scm @@ -1509,29 +1509,33 @@ (let loop ((nxt (peek-token s)) (catchb #f) (catchv #f) - (finalb #f)) + (finalb #f) + (elseb #f)) (take-token s) (cond ((eq? nxt 'end) (list* 'try try-block (or catchv '(false)) - (or catchb (if finalb '(false) (error "try without catch or finally"))) - (if finalb (list finalb) '()))) + (or catchb (if (or finalb elseb) '(false) (error "try without catch, else or finally"))) + (cond (elseb (list (or finalb '(false)) elseb)) + (finalb (list finalb)) + (else '())))) ((and (eq? nxt 'catch) (not catchb)) (let ((nl (memv (peek-token s) '(#\newline #\;)))) (if (eqv? (peek-token s) #\;) (take-token s)) - (if (memq (require-token s) '(end finally)) + (if (memq (require-token s) '(end finally else)) (loop (require-token s) '(block) #f - finalb) + finalb + elseb) (let* ((loc (line-number-node s)) (var (if nl #f (parse-eq* s))) (var? (and (not nl) (or (symbol? var) (and (length= var 2) (eq? (car var) '$)) (error (string "invalid syntax \"catch " (deparse var) "\""))))) - (catch-block (if (eq? (require-token s) 'finally) + (catch-block (if (memq (require-token s) '(finally else)) `(block ,(line-number-node s)) (parse-block s)))) (loop (require-token s) @@ -1543,16 +1547,28 @@ '() (cdr catch-block)))) (if var? var '(false)) - finalb))))) + finalb + elseb))))) ((and (eq? nxt 'finally) (not finalb)) - (let ((fb (if (eq? (require-token s) 'catch) + (let ((fb (if (eq? (require-token s) '(catch else)) '(block) (parse-block s)))) (loop (require-token s) catchb catchv - fb))) + fb + elseb))) + ((and (eq? nxt 'else) + (not elseb)) + (let ((eb (if (eq? (require-token s) '(catch finally)) + '(block) + (parse-block s)))) + (loop (require-token s) + catchb + catchv + finalb + eb))) (else (expect-end-error nxt 'try)))))) ((return) (let ((t (peek-token s))) (if (or (eqv? t #\newline) (closing-token? t)) diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index 1b40704b36c3a..35acc10eec6fb 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -1332,25 +1332,29 @@ (let ((tryb (cadr e)) (var (caddr e)) (catchb (cadddr e))) - (cond ((length= e 5) + (cond ((and (length> e 4) (not (equal? (caddddr e) '(false)))) (if (has-unmatched-symbolic-goto? tryb) (error "goto from a try/finally block is not permitted")) - (let ((finalb (cadddr (cdr e)))) + (let ((finalb (caddddr e))) (expand-forms `(tryfinally - ,(if (not (equal? catchb '(false))) - `(try ,tryb ,var ,catchb) - `(scope-block ,tryb)) + ,(if (and (equal? catchb '(false)) (length= e 5)) + `(scope-block ,tryb) + `(try ,tryb ,var ,catchb (false) ,@(cdddddr e))) (scope-block ,finalb))))) - ((length= e 4) - (expand-forms - (if (symbol-like? var) - `(trycatch (scope-block ,tryb) - (scope-block - (block (= ,var (the_exception)) - ,catchb))) - `(trycatch (scope-block ,tryb) - (scope-block ,catchb))))) + ((length> e 3) + (and (length> e 6) (error "invalid \"try\" form")) + (let ((elseb (if (length= e 6) (cdddddr e) '()))) + (expand-forms + `(,(if (null? elseb) 'trycatch 'trycatchelse) + (scope-block ,tryb) + (scope-block + ,(if (symbol-like? var) + `(scope-block + (block (= ,var (the_exception)) + ,catchb)) + `(scope-block ,catchb))) + ,@elseb)))) (else (error "invalid \"try\" form"))))) @@ -3587,7 +3591,7 @@ f(x) = yt(x) ((eq? (car e) 'symboliclabel) (kill) #t) - ((memq (car e) '(if elseif trycatch tryfinally)) + ((memq (car e) '(if elseif trycatch tryfinally trycatchelse)) (let ((prev (table.clone live))) (if (eager-any (lambda (e) (begin0 (visit e) (kill))) @@ -3653,7 +3657,7 @@ f(x) = yt(x) (and cv (vinfo:asgn cv) (vinfo:capt cv))))) (define (toplevel-preserving? e) - (and (pair? e) (memq (car e) '(if elseif block trycatch tryfinally)))) + (and (pair? e) (memq (car e) '(if elseif block trycatch tryfinally trycatchelse)))) (define (map-cl-convert exprs fname lam namemap defined toplevel interp opaq) (if toplevel @@ -4446,9 +4450,10 @@ f(x) = yt(x) ;; (= tok (enter L)) - push handler with catch block at label L, yielding token ;; (leave n) - pop N exception handlers ;; (pop_exception tok) - pop exception stack back to state of associated enter - ((trycatch tryfinally) + ((trycatch tryfinally trycatchelse) (let ((handler-token (make-ssavalue)) (catch (make-label)) + (els (and (eq? (car e) 'trycatchelse) (make-label))) (endl (make-label)) (last-finally-handler finally-handler) (finally (if (eq? (car e) 'tryfinally) (new-mutable-var) #f)) @@ -4465,11 +4470,20 @@ f(x) = yt(x) ;; handler block postfix (if (and val v1) (emit-assignment val v1)) (if tail - (begin (if v1 (emit-return v1)) + (begin (if els + (begin (if (and (not val) v1) (emit v1)) + (emit '(leave 1))) + (if v1 (emit-return v1))) (if (not finally) (set! endl #f))) (begin (emit '(leave 1)) - (emit `(goto ,endl)))) + (emit `(goto ,(or els endl))))) (set! handler-level (- handler-level 1)) + ;; emit else block + (if els + (begin (mark-label els) + (let ((v3 (compile (cadddr e) break-labels value tail))) ;; emit else block code + (if val (emit-assignment val v3))) + (emit `(goto ,endl)))) ;; emit either catch or finally block (mark-label catch) (emit `(leave 1)) diff --git a/src/utils.scm b/src/utils.scm index c1a893102053c..7be6b2999a90c 100644 --- a/src/utils.scm +++ b/src/utils.scm @@ -79,6 +79,7 @@ (define (caddddr x) (car (cdr (cdr (cdr (cdr x)))))) (define (cdddddr x) (cdr (cdr (cdr (cdr (cdr x)))))) +(define (cadddddr x) (car (cdddddr x))) (define (table.clone t) (let ((nt (table))) diff --git a/test/syntax.jl b/test/syntax.jl index 14b17d6eaa0b2..a8386f869aa6e 100644 --- a/test/syntax.jl +++ b/test/syntax.jl @@ -2977,3 +2977,84 @@ macro m42220() end @test @m42220()() isa Vector{Float64} @test @m42220()(Bool) isa Vector{Bool} + +@testset "try else" begin + fails(f) = try f() catch; true else false end + @test fails(error) + @test !fails(() -> 1 + 2) + + err = try + try + 1 + 2 + else + error("foo") + end + catch e + e + end + @test err == ErrorException("foo") + + x = 0 + err = try + try + 1 + 2 + else + error("foo") + finally + x += 1 + end + catch e + e + end + @test err == ErrorException("foo") + @test x == 1 + + x = 0 + err = try + try + 1 + 2 + else + 3 + 4 + finally + x += 1 + end + catch e + e + end + @test err == 3 + 4 + @test x == 1 + + x = 0 + err = try + try + 1 + 2 + catch + 5 + 6 + else + 3 + 4 + finally + x += 1 + end + catch e + e + end + @test err == 3 + 4 + @test x == 1 + + x = 0 + err = try + try + error() + catch + 5 + 6 + else + 3 + 4 + finally + x += 1 + end + catch e + e + end + @test err == 5 + 6 + @test x == 1 +end From 2bbf9242f2645708905750c5b8a486a5646c74e0 Mon Sep 17 00:00:00 2001 From: Simeon Schaub Date: Thu, 16 Sep 2021 14:45:36 -0400 Subject: [PATCH 2/4] require else after catch --- src/julia-parser.scm | 4 +++- test/syntax.jl | 22 +++++++--------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/julia-parser.scm b/src/julia-parser.scm index 8fac67a3278be..e29e7d605131e 100644 --- a/src/julia-parser.scm +++ b/src/julia-parser.scm @@ -1515,7 +1515,7 @@ (cond ((eq? nxt 'end) (list* 'try try-block (or catchv '(false)) - (or catchb (if (or finalb elseb) '(false) (error "try without catch, else or finally"))) + (or catchb (if finalb '(false) (error "try without catch or finally"))) (cond (elseb (list (or finalb '(false)) elseb)) (finalb (list finalb)) (else '())))) @@ -1561,6 +1561,8 @@ elseb))) ((and (eq? nxt 'else) (not elseb)) + (if (or (not catchb) finalb) + (error "else inside try block needs to be immediately after catch")) (let ((eb (if (eq? (require-token s) '(catch finally)) '(block) (parse-block s)))) diff --git a/test/syntax.jl b/test/syntax.jl index a8386f869aa6e..32838480da3d6 100644 --- a/test/syntax.jl +++ b/test/syntax.jl @@ -2983,9 +2983,15 @@ end @test fails(error) @test !fails(() -> 1 + 2) + @test_throws ParseError Meta.parse("try foo() else bar() end") + @test_throws ParseError Meta.parse("try foo() else bar() catch; baz() end") + @test_throws ParseError Meta.parse("try foo() catch; baz() finally foobar() else bar() end") + @test_throws ParseError Meta.parse("try foo() finally foobar() else bar() catch; baz() end") + err = try try 1 + 2 + catch else error("foo") end @@ -2998,6 +3004,7 @@ end err = try try 1 + 2 + catch else error("foo") finally @@ -3009,21 +3016,6 @@ end @test err == ErrorException("foo") @test x == 1 - x = 0 - err = try - try - 1 + 2 - else - 3 + 4 - finally - x += 1 - end - catch e - e - end - @test err == 3 + 4 - @test x == 1 - x = 0 err = try try From 0da865db2a63385b01d90f98c83da2000bf1bfb4 Mon Sep 17 00:00:00 2001 From: Simeon Schaub Date: Fri, 24 Sep 2021 14:49:41 -0400 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Jeff Bezanson --- src/julia-parser.scm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/julia-parser.scm b/src/julia-parser.scm index e29e7d605131e..cf681ab9cdee3 100644 --- a/src/julia-parser.scm +++ b/src/julia-parser.scm @@ -1551,7 +1551,7 @@ elseb))))) ((and (eq? nxt 'finally) (not finalb)) - (let ((fb (if (eq? (require-token s) '(catch else)) + (let ((fb (if (memq (require-token s) '(catch else)) '(block) (parse-block s)))) (loop (require-token s) @@ -1563,7 +1563,7 @@ (not elseb)) (if (or (not catchb) finalb) (error "else inside try block needs to be immediately after catch")) - (let ((eb (if (eq? (require-token s) '(catch finally)) + (let ((eb (if (eq? (require-token s) 'finally) '(block) (parse-block s)))) (loop (require-token s) From 4528068fd41fc8deb1ccfb311f08e0fa1362823e Mon Sep 17 00:00:00 2001 From: Simeon David Schaub Date: Sun, 17 Oct 2021 22:08:03 -0400 Subject: [PATCH 4/4] add NEWS --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index f908da52b06f7..882fdc203e636 100644 --- a/NEWS.md +++ b/NEWS.md @@ -12,6 +12,8 @@ New language features to enforce the involved function calls to be (or not to be) inlined. ([#41312]) * The default behavior of observing `@inbounds` declarations is now an option via `auto` in `--check-bounds=yes|no|auto` ([#41551]) * New function `eachsplit(str)` for iteratively performing `split(str)`. +* `try`-blocks can now optionally have an `else`-block which is executed right after the main body only if + no errors were thrown. ([#42211]) Language changes ----------------