Skip to content

Commit 1ccaa8e

Browse files
committed
Add extended block-break syntax
This is a redo of #60367 taking into account various feedback on that PR. In particular, it seemed like people strongly disliked the `break break` syntax for two primary reasons: 1. It scales poorly to multiple loops `break break break break` 2. It introduced footguns if extra loops are introduced between the loop and the break. Instead, the consensus seemed to be that labeled break should be the only facility available. Thus, this implements a proper labeled break facility that looks as follows: ``` @Label name for i = 1:10 for j = 1:10 break name (i, j) end end # evaluate to `(1,1) ``` The idea is to re-use the `@label` macro for now, but possibly promote this to syntax in a future version if it gains widespread use. Note that parser changes are still required, since the `break` syntax is currently an error. However, compat.jl could provide `@goto break name val`, which parses fine. `continue` is extended with label support as well: ``` @Label outer while true for i = 1:10 a[i] && continue outer end [...] end ``` However, the feature is not restricted to loops. A particular use case is to replace cleanup blocks written like ``` cond1 && @goto error cond2 && @goto error return true @Label error error("foo") ``` by a labeled begin/end block: ``` @Label error begin cond1 && break error cond2 && break error return true end error("foo") ``` This doesn't save much typing work, but it makes this kind of pattern much more structured, e.g. for code-folding in IDEs. A similar pattern replaces the `for-then` construction originally proposed: ``` result = @Label result begin for x in arr pred(x) && break result x end default end ``` For anonymous blocks, use `@label _ begin ... end` with `break _`: ``` result = @Label _ begin for x in arr pred(x) && break _ x end default end ``` I've taken the liberty of converting some base code for testing and to give an idea of what the syntax looks like in practice, but didn't go through particularly comprehensively. These changes should be considered extended usage examples. Largely written by Claude.
1 parent f132175 commit 1ccaa8e

File tree

22 files changed

+1097
-338
lines changed

22 files changed

+1097
-338
lines changed

JuliaLowering/src/JuliaLowering.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ using .JuliaSyntax: highlight, Kind, @KSet_str, is_leaf, children, numchildren,
1616
head, kind, flags, has_flags, filename, first_byte, last_byte, byte_range,
1717
sourcefile, source_location, span, sourcetext, is_literal, is_infix_op_call,
1818
is_postfix_op_call, @isexpr, SyntaxHead, is_syntactic_operator,
19+
is_contextual_keyword,
1920
SyntaxGraph, SyntaxTree, SyntaxList, NodeId, SourceRef, SourceAttrType,
2021
ensure_attributes, ensure_attributes!, delete_attributes, new_id!, hasattr,
2122
setattr, setattr!, syntax_graph, is_compatible_graph,

JuliaLowering/src/desugaring.jl

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4449,10 +4449,40 @@ function expand_forms_2(ctx::DesugaringContext, ex::SyntaxTree, docs=nothing)
44494449
elseif k == K"="
44504450
expand_assignment(ctx, ex)
44514451
elseif k == K"break"
4452-
numchildren(ex) > 0 ? ex :
4452+
nc = numchildren(ex)
4453+
if nc == 0
44534454
@ast ctx ex [K"break" "loop_exit"::K"symbolic_label"]
4455+
else
4456+
@chk nc <= 2 (ex, "Too many arguments to break")
4457+
label = ex[1]
4458+
label_kind = kind(label)
4459+
# Convert Symbol (from Expr conversion) to symbolic_label
4460+
if label_kind == K"Symbol"
4461+
label = @ast ctx label label.name_val::K"symbolic_label"
4462+
elseif !(label_kind == K"Identifier" || label_kind == K"symbolic_label" ||
4463+
is_contextual_keyword(label_kind))
4464+
throw(LoweringError(label, "Invalid break label: expected identifier"))
4465+
end
4466+
if nc == 2
4467+
@ast ctx ex [K"break" label expand_forms_2(ctx, ex[2])]
4468+
else
4469+
@ast ctx ex [K"break" label]
4470+
end
4471+
end
44544472
elseif k == K"continue"
4455-
@ast ctx ex [K"break" "loop_cont"::K"symbolic_label"]
4473+
nc = numchildren(ex)
4474+
if nc == 0
4475+
@ast ctx ex [K"break" "loop_cont"::K"symbolic_label"]
4476+
else
4477+
@chk nc == 1 (ex, "Too many arguments to continue")
4478+
label = ex[1]
4479+
label_kind = kind(label)
4480+
if !(label_kind == K"Identifier" || label_kind == K"Placeholder" ||
4481+
label_kind == K"Symbol" || is_contextual_keyword(label_kind))
4482+
throw(LoweringError(label, "Invalid continue label: expected identifier"))
4483+
end
4484+
@ast ctx ex [K"break" string(label.name_val, "#cont")::K"symbolic_label"]
4485+
end
44564486
elseif k == K"comparison"
44574487
expand_forms_2(ctx, expand_compare_chain(ctx, ex))
44584488
elseif k == K"doc"
@@ -4613,6 +4643,11 @@ function expand_forms_2(ctx::DesugaringContext, ex::SyntaxTree, docs=nothing)
46134643
]
46144644
elseif k == K"inert" || k == K"inert_syntaxtree"
46154645
ex
4646+
elseif k == K"symbolic_block"
4647+
# @label name body -> (symbolic_block name expanded_body)
4648+
# The @label macro inserts the continue block for loops, so we just expand the body
4649+
@chk numchildren(ex) == 2
4650+
@ast ctx ex [K"symbolic_block" ex[1] expand_forms_2(ctx, ex[2])]
46164651
elseif k == K"gc_preserve"
46174652
s = ssavar(ctx, ex)
46184653
r = ssavar(ctx, ex)

JuliaLowering/src/kinds.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ function _register_kinds()
4141
"symbolic_label"
4242
# Goto named label
4343
"symbolic_goto"
44+
# Labeled block for `@label name expr` (block break)
45+
"symbolic_block"
4446
# Internal initializer for struct types, for inner constructors/functions
4547
"new"
4648
"splatnew"

JuliaLowering/src/linear_ir.jl

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ struct JumpTarget{Attrs}
2929
label::SyntaxTree{Attrs}
3030
handler_token_stack::SyntaxList{Attrs, Vector{NodeId}}
3131
catch_token_stack::SyntaxList{Attrs, Vector{NodeId}}
32+
result_var::Union{SyntaxTree{Attrs}, Nothing} # for symbolic_block valued breaks
3233
end
3334

34-
function JumpTarget(label::SyntaxTree{Attrs}, ctx) where {Attrs}
35-
JumpTarget{Attrs}(label, copy(ctx.handler_token_stack), copy(ctx.catch_token_stack))
35+
function JumpTarget(label::SyntaxTree{Attrs}, ctx, result_var=nothing) where {Attrs}
36+
JumpTarget{Attrs}(label, copy(ctx.handler_token_stack), copy(ctx.catch_token_stack), result_var)
3637
end
3738

3839
struct JumpOrigin{Attrs}
@@ -79,6 +80,7 @@ struct LinearIRContext{Attrs} <: AbstractLoweringContext
7980
finally_handlers::Vector{FinallyHandler{Attrs}}
8081
symbolic_jump_targets::Dict{String,JumpTarget{Attrs}}
8182
symbolic_jump_origins::Vector{JumpOrigin{Attrs}}
83+
symbolic_block_labels::Set{String} # labels that are symbolic blocks (not allowed as @goto targets)
8284
meta::Dict{Symbol, Any}
8385
mod::Module
8486
end
@@ -91,7 +93,7 @@ function LinearIRContext(ctx, is_toplevel_thunk, lambda_bindings, return_type)
9193
is_toplevel_thunk, lambda_bindings, Dict{IdTag,IdTag}(), rett,
9294
Dict{String,JumpTarget{Attrs}}(), SyntaxList(ctx), SyntaxList(ctx),
9395
Vector{FinallyHandler{Attrs}}(), Dict{String,JumpTarget{Attrs}}(),
94-
Vector{JumpOrigin{Attrs}}(), Dict{Symbol, Any}(), ctx.mod)
96+
Vector{JumpOrigin{Attrs}}(), Set{String}(), Dict{Symbol, Any}(), ctx.mod)
9597
end
9698

9799
function current_lambda_bindings(ctx::LinearIRContext)
@@ -309,6 +311,14 @@ function emit_break(ctx, ex)
309311
ty = name == "loop_exit" ? "break" : "continue"
310312
throw(LoweringError(ex, "$ty must be used inside a `while` or `for` loop"))
311313
end
314+
# Handle valued break (break name val)
315+
if numchildren(ex) >= 2
316+
if isnothing(target.result_var)
317+
throw(LoweringError(ex, "break with value not allowed for label `$name`"))
318+
end
319+
val = compile(ctx, ex[2], true, false)
320+
emit_assignment(ctx, ex, target.result_var, val)
321+
end
312322
if !isempty(ctx.finally_handlers)
313323
handler = last(ctx.finally_handlers)
314324
if length(target.handler_token_stack) < length(handler.target.handler_token_stack)
@@ -683,7 +693,9 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos)
683693
end_label = make_label(ctx, ex)
684694
name = ex[1].name_val
685695
outer_target = get(ctx.break_targets, name, nothing)
686-
ctx.break_targets[name] = JumpTarget(end_label, ctx)
696+
# Inherit result_var from outer symbolicblock if present
697+
outer_result_var = isnothing(outer_target) ? nothing : outer_target.result_var
698+
ctx.break_targets[name] = JumpTarget(end_label, ctx, outer_result_var)
687699
compile(ctx, ex[2], false, false)
688700
if isnothing(outer_target)
689701
delete!(ctx.break_targets, name)
@@ -694,12 +706,44 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos)
694706
if needs_value
695707
compile(ctx, nothing_(ctx, ex), needs_value, in_tail_pos)
696708
end
709+
elseif k == K"symbolic_block"
710+
name = ex[1].name_val
711+
if haskey(ctx.symbolic_jump_targets, name) || name in ctx.symbolic_block_labels
712+
throw(LoweringError(ex, "Label `$name` defined multiple times"))
713+
end
714+
push!(ctx.symbolic_block_labels, name)
715+
end_label = make_label(ctx, ex)
716+
result_var = if needs_value || in_tail_pos
717+
rv = new_local_binding(ctx, ex, "$(name)_result")
718+
emit_assignment(ctx, ex, rv, nothing_(ctx, ex))
719+
rv
720+
else
721+
nothing
722+
end
723+
outer_target = get(ctx.break_targets, name, nothing)
724+
ctx.break_targets[name] = JumpTarget(end_label, ctx, result_var)
725+
body_val = compile(ctx, ex[2], !isnothing(result_var), false)
726+
if !isnothing(result_var) && !isnothing(body_val)
727+
emit_assignment(ctx, ex, result_var, body_val)
728+
end
729+
if isnothing(outer_target)
730+
delete!(ctx.break_targets, name)
731+
else
732+
ctx.break_targets[name] = outer_target
733+
end
734+
emit(ctx, end_label)
735+
if in_tail_pos
736+
emit_return(ctx, ex, result_var)
737+
nothing
738+
else
739+
result_var
740+
end
697741
elseif k == K"break"
698742
emit_break(ctx, ex)
699743
elseif k == K"symbolic_label"
700744
label = emit_label(ctx, ex)
701745
name = ex.name_val
702-
if haskey(ctx.symbolic_jump_targets, name)
746+
if haskey(ctx.symbolic_jump_targets, name) || name in ctx.symbolic_block_labels
703747
throw(LoweringError(ex, "Label `$name` defined multiple times"))
704748
end
705749
push!(ctx.symbolic_jump_targets, name=>JumpTarget(label, ctx))
@@ -950,6 +994,10 @@ function compile_body(ctx::LinearIRContext, ex)
950994
name = origin.goto.name_val
951995
target = get(ctx.symbolic_jump_targets, name, nothing)
952996
if isnothing(target)
997+
# Check if it's a symbolic block label
998+
if name in ctx.symbolic_block_labels
999+
throw(LoweringError(origin.goto, "cannot use @goto to jump to @label block `$name`"))
1000+
end
9531001
throw(LoweringError(origin.goto, "label `$name` referenced but not defined"))
9541002
end
9551003
i = origin.index
@@ -1172,7 +1220,7 @@ loops, etc) to gotos and exception handling to enter/leave. We also convert
11721220
Vector{FinallyHandler{Attrs}}(),
11731221
Dict{String, JumpTarget{Attrs}}(),
11741222
Vector{JumpOrigin{Attrs}}(),
1175-
Dict{Symbol, Any}(), ctx.mod)
1223+
Set{String}(), Dict{Symbol, Any}(), ctx.mod)
11761224
res = compile_lambda(_ctx, reparent(_ctx, ex))
11771225
_ctx, res
11781226
end

JuliaLowering/src/scope_analysis.jl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,12 @@ function _find_scope_decls!(ctx, scope, ex)
220220
k === K"constdecl" && numchildren(ex) == 2)
221221
_find_scope_decls!(ctx, scope, ex[2])
222222
end
223+
elseif k === K"symbolic_block"
224+
# Only recurse into the body (second child), not the label name (first child)
225+
_find_scope_decls!(ctx, scope, ex[2])
226+
elseif k === K"break" && numchildren(ex) >= 2
227+
# For break with value, only recurse into the value expression (second child), not the label
228+
_find_scope_decls!(ctx, scope, ex[2])
223229
elseif needs_resolution(ex) && !(k === K"scope_block" || k === K"lambda")
224230
for e in children(ex)
225231
_find_scope_decls!(ctx, scope, e)
@@ -343,6 +349,10 @@ function _resolve_scopes(ctx, ex::SyntaxTree,
343349
ex
344350
elseif k == K"softscope"
345351
newleaf(ctx, ex, K"TOMBSTONE")
352+
elseif k == K"break" && numchildren(ex) >= 2
353+
# For break with value (break label value), process the value expression but not the label
354+
# This must come BEFORE !needs_resolution check since K"break" is in is_quoted
355+
@ast ctx ex [K"break" ex[1] _resolve_scopes(ctx, ex[2], scope)]
346356
elseif !needs_resolution(ex)
347357
ex
348358
elseif k == K"local"
@@ -507,6 +517,9 @@ function _resolve_scopes(ctx, ex::SyntaxTree,
507517
@assert numchildren(ex) === 2
508518
assignment_kind = bk == :global ? K"constdecl" : K"="
509519
@ast ctx ex _resolve_scopes(ctx, [assignment_kind ex[1] ex[2]], scope)
520+
elseif k == K"symbolic_block"
521+
# Only recurse into the body (second child), not the label name (first child)
522+
@ast ctx ex [K"symbolic_block" ex[1] _resolve_scopes(ctx, ex[2], scope)]
510523
else
511524
mapchildren(e->_resolve_scopes(ctx, e, scope), ctx, ex)
512525
end
@@ -600,6 +613,11 @@ function analyze_variables!(ctx, ex)
600613
@assert b.kind === :global || b.is_ssa || haskey(ctx.lambda_bindings.locals_capt, b.id)
601614
elseif k == K"Identifier"
602615
@assert false
616+
elseif k == K"break" && numchildren(ex) >= 2
617+
# For break with value, only analyze the value expression (second child), not the label
618+
# This must come BEFORE !needs_resolution check since K"break" is in is_quoted
619+
analyze_variables!(ctx, ex[2])
620+
return
603621
elseif !needs_resolution(ex)
604622
return
605623
elseif k == K"static_eval"
@@ -676,6 +694,9 @@ function analyze_variables!(ctx, ex)
676694
ctx.graph, ctx.bindings, ctx.mod, ctx.scopes, lambda_bindings,
677695
ctx.method_def_stack, ctx.closure_bindings)
678696
foreach(e->analyze_variables!(ctx2, e), ex[3:end]) # body & return type
697+
elseif k == K"symbolic_block"
698+
# Only analyze the body (second child), not the label name (first child)
699+
analyze_variables!(ctx, ex[2])
679700
else
680701
foreach(e->analyze_variables!(ctx, e), children(ex))
681702
end

JuliaLowering/src/syntax_macros.jl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,35 @@ function Base.var"@label"(__context__::MacroContext, ex)
4949
@ast __context__ ex ex=>K"symbolic_label"
5050
end
5151

52+
function Base.var"@label"(__context__::MacroContext, name, body)
53+
# Handle `@label _ body` (anonymous) or `@label name body` (named)
54+
k = kind(name)
55+
if k == K"Identifier"
56+
# `@label _ body` or `@label name body` - plain identifier
57+
elseif is_contextual_keyword(k)
58+
# Contextual keyword used as label name (e.g., `@label outer body`)
59+
else
60+
throw(MacroExpansionError(name, "Expected identifier for block label"))
61+
end
62+
# If body is a syntactic loop, wrap its body in a continue block
63+
# This allows `continue name` to work by breaking to `name#cont`
64+
body_kind = kind(body)
65+
if body_kind == K"for" || body_kind == K"while"
66+
cont_name = string(name.name_val, "#cont")
67+
loop_body = body[2]
68+
wrapped_body = @ast __context__ loop_body [K"symbolic_block"
69+
cont_name::K"Identifier"
70+
loop_body
71+
]
72+
if body_kind == K"for"
73+
body = @ast __context__ body [K"for" body[1] wrapped_body]
74+
else # while
75+
body = @ast __context__ body [K"while" body[1] wrapped_body]
76+
end
77+
end
78+
@ast __context__ __context__.macrocall [K"symbolic_block" name body]
79+
end
80+
5281
function Base.var"@goto"(__context__::MacroContext, ex)
5382
@chk kind(ex) == K"Identifier"
5483
@ast __context__ ex ex=>K"symbolic_goto"

JuliaSyntax/src/julia/parser.jl

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2058,15 +2058,60 @@ function parse_resword(ps::ParseState)
20582058
parse_eq(ps)
20592059
end
20602060
emit(ps, mark, K"return")
2061-
elseif word in KSet"break continue"
2062-
# break ==> (break)
2063-
# continue ==> (continue)
2061+
elseif word == K"continue"
2062+
# continue ==> (continue)
2063+
# continue _ ==> (continue _) [1.14+]
2064+
# continue label ==> (continue label) [1.14+]
20642065
bump(ps, TRIVIA_FLAG)
2065-
emit(ps, mark, word)
20662066
k = peek(ps)
2067-
if !(k in KSet"NewlineWs ; ) : EndMarker" || (k == K"end" && !ps.end_symbol))
2068-
recover(is_closer_or_newline, ps, TRIVIA_FLAG,
2069-
error="unexpected token after $(untokenize(word))")
2067+
if k in KSet"NewlineWs ; ) EndMarker" || (k == K"end" && !ps.end_symbol)
2068+
# continue with no arguments
2069+
emit(ps, mark, K"continue")
2070+
elseif ps.range_colon_enabled && k == K":"
2071+
# Ternary case: `cond ? continue : x`
2072+
emit(ps, mark, K"continue")
2073+
elseif k == K"Identifier" || is_contextual_keyword(k)
2074+
# continue label - plain identifier or contextual keyword as label
2075+
bump(ps)
2076+
emit(ps, mark, K"continue")
2077+
min_supported_version(v"1.14", ps, mark, "labeled `continue`")
2078+
else
2079+
# Error: unexpected token after continue
2080+
emit(ps, mark, K"continue")
2081+
end
2082+
elseif word == K"break"
2083+
# break ==> (break)
2084+
# break _ ==> (break _) [1.14+]
2085+
# break _ val ==> (break _ val) [1.14+]
2086+
# break label ==> (break label) [1.14+]
2087+
# break label val ==> (break label val) [1.14+]
2088+
bump(ps, TRIVIA_FLAG)
2089+
function parse_break_value(ps, mark)
2090+
k2 = peek(ps)
2091+
if k2 in KSet"NewlineWs ; ) : EndMarker" || (k2 == K"end" && !ps.end_symbol)
2092+
# break label
2093+
emit(ps, mark, K"break")
2094+
else
2095+
# break label value
2096+
parse_eq(ps)
2097+
emit(ps, mark, K"break")
2098+
end
2099+
min_supported_version(v"1.14", ps, mark, "labeled `break`")
2100+
end
2101+
k = peek(ps)
2102+
if k in KSet"NewlineWs ; ) EndMarker" || (k == K"end" && !ps.end_symbol)
2103+
# break with no arguments
2104+
emit(ps, mark, K"break")
2105+
elseif ps.range_colon_enabled && k == K":"
2106+
# Ternary case: `cond ? break : x`
2107+
emit(ps, mark, K"break")
2108+
elseif k == K"Identifier" || is_contextual_keyword(k)
2109+
# break label [value] - plain identifier or contextual keyword as label
2110+
bump(ps)
2111+
parse_break_value(ps, mark)
2112+
else
2113+
# Error: unexpected token after break
2114+
emit(ps, mark, K"break")
20702115
end
20712116
elseif word in KSet"module baremodule"
20722117
# module A end ==> (module A (block))

JuliaSyntax/test/parser.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,13 @@ tests = [
551551
# break/continue
552552
"break" => "(break)"
553553
"continue" => "(continue)"
554+
# break/continue with labels (plain identifiers only)
555+
"break _" => "(break _)"
556+
"break _ x" => "(break _ x)"
557+
"break label" => "(break label)"
558+
"break label x" => "(break label x)"
559+
"continue _" => "(continue _)"
560+
"continue label" => "(continue label)"
554561
# module/baremodule
555562
"module A end" => "(module A (block))"
556563
"baremodule A end" => "(module-bare A (block))"

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ New language features
1111
- `` (U+U+1D45), `` (U+1D4B), `` (U+1DB2), `˱` (U+02F1), `˲` (U+02F2), and `` (U+2094) can now also be used as
1212
operator suffixes, accessible as `\^alpha`, `\^epsilon`, `\^ltphi`, `\_<`, `\_>`, and `\_schwa` at the REPL
1313
([#60285]).
14+
- The `@label` macro can now create labeled blocks that can be exited early with `break name [value]`. Use
15+
`@label name expr` for named blocks or `@label _ expr` for anonymous blocks. The `continue` statement also
16+
supports labels with `continue name` to continue a labeled loop ([#60481]).
1417

1518
Language changes
1619
----------------

0 commit comments

Comments
 (0)