Skip to content

Commit 88eedc4

Browse files
committed
RFC: syntax: Implement for/while-then & labeled, multi-level break
This implements multi-level and labeled break as contemplated in #5334. In addition this adds support for break-with-value (#22891) as well as for-then (aka for-else #1289). Also while-then of course. All three features are syntax gated to 1.14 syntax. The syntax for multi-level break is as follows: ``` for i = 1:10 for j = 1:10 break break (i, j) end end # Loop evaluate to `(1,1) ``` The break value can be `continue` in which case the next innermost loop is continued: ``` julia> for i = 1:3 for j = 1:3 i > 1 && j == 2 && break continue @show (i,j) end @show i end (i, j) = (1, 1) (i, j) = (1, 2) (i, j) = (1, 3) i = 1 (i, j) = (2, 1) (i, j) = (3, 1) ``` For more deeply nested loops, the loop can be annotated with a `@label` and the the break or continue can be targeted using `@goto`: ``` julia> @Label outer for a = 1:2 for b = 1:3 for c = 1:4 b > 1 && c == 2 && @goto outer break end @show b end @show a end b = 1 ``` Naturally `continue` is supported here as well. The syntax and semantics for `for-then` are as proposed in the issue: ``` function has5(iter) return for x in iter x == 5 && break true then false end end ``` Any `break` (including multi-level and labeled) skips the corresponding loop's `then` block, which ordinarily would run at loop completion.
1 parent 1806b0b commit 88eedc4

File tree

10 files changed

+666
-60
lines changed

10 files changed

+666
-60
lines changed

AGENTS.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ After making changes, run static analysis checks:
3333
- Tests can also be rerun individually with `clang-sa-<filename>`, `clang-sagc-<filename>` or `clang-tidy-<filename>`.
3434
- If `clang-sagc-<filename>` fails, it may require adding `JL_GC_PUSH` statements, or `JL_GC_PROMISE_ROOTED` statements., or require fixing locks. Remember arguments are assumed rooted, so check the callers to make sure that is handled. If the value is being temporarily moved around in a struct or arraylist, `JL_GC_PROMISE_ROOTED(struct->field)` may be needed as a statement (it return void) immediately after reloading the struct before any use of struct. Put that promise as early in the code as is legal, near the definition not the use.
3535

36+
### Lowering (Scheme) changes
37+
38+
If you made changes to `src/julia-syntax.scm` (the lowering code), you need to rebuild
39+
with `make -C src`. This is faster than a full rebuild but you must ensure the flisp
40+
boot files are regenerated:
41+
- If `make -C src` doesn't recompile flisp, remove `src/julia_flisp.boot` and `src/julia_flisp.boot.inc` then run `make -C src` again.
42+
- Alternatively, touch the source file before running make: `touch src/julia-syntax.scm && make -C src`
43+
44+
For debugging flisp/Scheme code:
45+
- Use `prn` to print debug output (not `io.princ` or `write`)
46+
- Look for commented debug printing near "malformed expression" errors to get flisp stack traces
47+
48+
Note: Revise does NOT reload lowering changes - you must rebuild.
49+
3650
## Using Revise
3751

3852
If you have made changes to files included in the system image (base/ or stdlib/),

JuliaSyntax/src/integration/expr.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,9 +427,23 @@ end
427427
# Add extra line number node for the `end` of the block. This may seem
428428
# useless but it affects code coverage.
429429
push!(args[2].args, endloc)
430+
# Handle then clause: (for iter body (then then_body)) -> (for iter body then_body)
431+
if length(args) >= 3
432+
then_clause = args[3]
433+
if @isexpr(then_clause, :then)
434+
args[3] = then_clause.args[1] # Extract the block from the then clause
435+
end
436+
end
430437
elseif k == K"while"
431438
# Line number node for the `end` of the block as in `for` loops.
432439
push!(args[2].args, endloc)
440+
# Handle then clause: (while cond body (then then_body)) -> (while cond body then_body)
441+
if length(args) >= 3
442+
then_clause = args[3]
443+
if @isexpr(then_clause, :then)
444+
args[3] = then_clause.args[1] # Extract the block from the then clause
445+
end
446+
end
433447
elseif k in KSet"tuple vect braces"
434448
# Move parameters blocks to args[1]
435449
_reorder_parameters!(args, 1)

JuliaSyntax/src/julia/kinds.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ register_kinds!(JuliaSyntax, 0, [
235235
"else"
236236
"elseif"
237237
"end"
238+
"then"
238239
"END_BLOCK_CONTINUATION_KEYWORDS"
239240
"BEGIN_CONTEXTUAL_KEYWORDS"
240241
# contextual keywords

JuliaSyntax/src/julia/parser.jl

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ end
460460

461461
# Parse a block, but leave emitting the block up to the caller.
462462
function parse_block_inner(ps::ParseState, down::F) where {F <: Function}
463-
parse_Nary(ps, down, KSet"NewlineWs ;", KSet"end else elseif catch finally")
463+
parse_Nary(ps, down, KSet"NewlineWs ;", KSet"end else elseif catch finally then")
464464
end
465465

466466
# ";" at the top level produces a sequence of top level expressions
@@ -1889,17 +1889,33 @@ function parse_resword(ps::ParseState)
18891889
elseif word == K"while"
18901890
# while cond body end ==> (while cond (block body))
18911891
# while x < y \n a \n b \n end ==> (while (call-i x < y) (block a b))
1892+
# while cond body then else_body end ==> (while cond (block body) (then (block else_body)))
18921893
bump(ps, TRIVIA_FLAG)
18931894
parse_cond(ps)
18941895
parse_block(ps)
1896+
if peek(ps) == K"then"
1897+
min_supported_version(v"1.14", ps, mark, "loop then clause")
1898+
then_mark = position(ps)
1899+
bump(ps, TRIVIA_FLAG) # consume 'then'
1900+
parse_block(ps)
1901+
emit(ps, then_mark, K"then")
1902+
end
18951903
bump_closing_token(ps, K"end")
18961904
emit(ps, mark, K"while")
18971905
elseif word == K"for"
18981906
# for x in xs end ==> (for (iteration (in x xs)) (block))
18991907
# for x in xs, y in ys \n a \n end ==> (for (iteration (in x xs) (in y ys)) (block a))
1908+
# for x in xs body then else_body end ==> (for (iteration ...) (block body) (then (block else_body)))
19001909
bump(ps, TRIVIA_FLAG)
19011910
parse_iteration_specs(ps)
19021911
parse_block(ps)
1912+
if peek(ps) == K"then"
1913+
min_supported_version(v"1.14", ps, mark, "loop then clause")
1914+
then_mark = position(ps)
1915+
bump(ps, TRIVIA_FLAG) # consume 'then'
1916+
parse_block(ps)
1917+
emit(ps, then_mark, K"then")
1918+
end
19031919
bump_closing_token(ps, K"end")
19041920
emit(ps, mark, K"for")
19051921
elseif word == K"let"
@@ -2058,15 +2074,44 @@ function parse_resword(ps::ParseState)
20582074
parse_eq(ps)
20592075
end
20602076
emit(ps, mark, K"return")
2061-
elseif word in KSet"break continue"
2062-
# break ==> (break)
2077+
elseif word == K"break"
2078+
# Extended break syntax (1.14+):
2079+
# break ==> (break)
2080+
# break x ==> (break x)
2081+
# break break ==> (break (break))
2082+
# break break x ==> (break (break x))
2083+
# break continue ==> (break (continue))
2084+
bump(ps, TRIVIA_FLAG)
2085+
k = peek(ps)
2086+
if k == K"NewlineWs" || is_closing_token(ps, k)
2087+
# break\n ==> (break)
2088+
emit(ps, mark, K"break")
2089+
elseif k == K"break"
2090+
# break break ... ==> (break (break ...))
2091+
min_supported_version(v"1.14", ps, mark, "multi-level break")
2092+
parse_resword(ps) # Recursive parse of inner break
2093+
emit(ps, mark, K"break")
2094+
elseif k == K"continue"
2095+
# break continue ==> (break (continue))
2096+
min_supported_version(v"1.14", ps, mark, "break continue")
2097+
inner_mark = position(ps)
2098+
bump(ps, TRIVIA_FLAG)
2099+
emit(ps, inner_mark, K"continue")
2100+
emit(ps, mark, K"break")
2101+
else
2102+
# break x ==> (break x)
2103+
min_supported_version(v"1.14", ps, mark, "break with value")
2104+
parse_eq(ps)
2105+
emit(ps, mark, K"break")
2106+
end
2107+
elseif word == K"continue"
20632108
# continue ==> (continue)
20642109
bump(ps, TRIVIA_FLAG)
2065-
emit(ps, mark, word)
2110+
emit(ps, mark, K"continue")
20662111
k = peek(ps)
20672112
if !(k in KSet"NewlineWs ; ) : EndMarker" || (k == K"end" && !ps.end_symbol))
20682113
recover(is_closer_or_newline, ps, TRIVIA_FLAG,
2069-
error="unexpected token after $(untokenize(word))")
2114+
error="unexpected token after continue")
20702115
end
20712116
elseif word in KSet"module baremodule"
20722117
# module A end ==> (module A (block))

JuliaSyntax/src/julia/tokenize.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,7 @@ K"public",
12981298
K"quote",
12991299
K"return",
13001300
K"struct",
1301+
K"then",
13011302
K"try",
13021303
K"using",
13031304
K"while",

JuliaSyntax/test/parser.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,19 @@ tests = [
551551
# break/continue
552552
"break" => "(break)"
553553
"continue" => "(continue)"
554+
# Extended break syntax (1.14+)
555+
((v=v"1.14",), "break x") => "(break x)"
556+
((v=v"1.14",), "break 42") => "(break 42)"
557+
((v=v"1.14",), "break f(x)") => "(break (call f x))"
558+
((v=v"1.14",), "break break") => "(break (break))"
559+
((v=v"1.14",), "break break x") => "(break (break x))"
560+
((v=v"1.14",), "break break break")=> "(break (break (break)))"
561+
((v=v"1.14",), "break continue") => "(break (continue))"
562+
((v=v"1.14",), "break break continue") => "(break (break (continue)))"
563+
# Extended break should error on older versions
564+
((v=v"1.13",), "break x") => "(break (error) x)"
565+
((v=v"1.13",), "break break") => "(break (error) (break))"
566+
((v=v"1.13",), "break continue") => "(break (error) (continue))"
554567
# module/baremodule
555568
"module A end" => "(module A (block))"
556569
"baremodule A end" => "(module-bare A (block))"

base/essentials.jl

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,83 @@ macro goto(name::Symbol)
958958
return esc(Expr(:symbolicgoto, name))
959959
end
960960

961+
# Extended break syntax: labeled loops (Julia 1.14+)
962+
963+
"""
964+
@label name loop
965+
966+
Labels a `for` or `while` loop with the symbolic name `name`, allowing targeted
967+
break/continue via `@goto name break` or `@goto name continue`.
968+
969+
!!! compat "Julia 1.14"
970+
Labeled loops require Julia 1.14 or later.
971+
972+
# Example
973+
```julia
974+
@label outer for i in 1:10
975+
for j in 1:10
976+
if i * j > 50
977+
@goto outer break (i, j) # break outer loop with value
978+
end
979+
end
980+
end
981+
```
982+
"""
983+
macro label(name::Symbol, loop)
984+
if !(loop isa Expr && loop.head in (:for, :while))
985+
throw(ArgumentError("@label must be followed by a for or while loop"))
986+
end
987+
return esc(Expr(:labeled_loop, name, loop))
988+
end
989+
990+
"""
991+
@goto name break [value]
992+
@goto name continue
993+
994+
Break from or continue a labeled loop.
995+
996+
!!! compat "Julia 1.14"
997+
Labeled loop break/continue requires Julia 1.14 or later.
998+
999+
# Examples
1000+
```julia
1001+
@goto outer break # break from @label outer loop
1002+
@goto outer break x # break with value x
1003+
@goto outer continue # continue the @label outer loop
1004+
```
1005+
"""
1006+
macro goto(name::Symbol, action::Symbol)
1007+
if action === :break
1008+
return esc(Expr(:labeled_break, name, nothing))
1009+
elseif action === :continue
1010+
return esc(Expr(:labeled_continue, name))
1011+
else
1012+
throw(ArgumentError("@goto label must be followed by break or continue"))
1013+
end
1014+
end
1015+
1016+
macro goto(name::Symbol, action::Symbol, value)
1017+
if action !== :break
1018+
throw(ArgumentError("@goto label continue does not accept a value"))
1019+
end
1020+
return esc(Expr(:labeled_break, name, value))
1021+
end
1022+
1023+
# Handle @goto label break value when parsed as @goto label (break value)
1024+
macro goto(name::Symbol, break_expr::Expr)
1025+
if break_expr.head === :break
1026+
if isempty(break_expr.args)
1027+
return esc(Expr(:labeled_break, name, nothing))
1028+
else
1029+
return esc(Expr(:labeled_break, name, break_expr.args[1]))
1030+
end
1031+
elseif break_expr.head === :continue
1032+
return esc(Expr(:labeled_continue, name))
1033+
else
1034+
throw(ArgumentError("@goto label must be followed by break or continue"))
1035+
end
1036+
end
1037+
9611038
# linear indexing
9621039
function getindex(A::Array, i::Int)
9631040
@_noub_if_noinbounds_meta

0 commit comments

Comments
 (0)