Skip to content

syntax: Add labeled block break#60481

Merged
Keno merged 2 commits intomasterfrom
kf/breakblocks
Jan 24, 2026
Merged

syntax: Add labeled block break#60481
Keno merged 2 commits intomasterfrom
kf/breakblocks

Conversation

@Keno
Copy link
Member

@Keno Keno commented Dec 25, 2025

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 convenience, the label may be ommitted, in which case it defaults to _, i.e. the above can be written as:

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, and I haven't looked at the implementation particularly carefully yet - for now I'm just interested in discussion of the syntax.

@Keno
Copy link
Member Author

Keno commented Jan 6, 2026

Capturing some discussion from this morning:

There appears to be general support for this version of the proposal, so I will move forwards with cleanup and getting this ready to merge. Some specific concerns:

  • @StefanKarpinski was concerned that if we want to move to a proper label syntax in the near future, this would introduce a duplicate syntax. However, I don't think any potential first class syntax is necessarily that close - it intersects with thinking about general syntax for scopes/lifetimes, which is not something I want to tackle immediately.

  • There was some general skepticism of the omission of the label. I will change this back to require _ in the @label invocation. We can reconsider in the future if it turns out annoying.

  • There was some general discussion of whether this syntax obsoletes ordinary @label/@goto. The answer is that the possible control flow of this feature is more restricted. In particular, the control flow is structured and the resulting CFG will be reducible. This distinction does not currently matter in the current IR representation, but there was some thoughts on making this more explicit in the IR in the future.

@Keno
Copy link
Member Author

Keno commented Jan 6, 2026

  • I will change this back to require _ in the @label invocation.

Updated accordingly

@jariji
Copy link
Contributor

jariji commented Jan 10, 2026

I haven't felt much need for labeled breaks, but the use case of cleanup statements, which I would like syntactic support for, I think would be well served by a built-in dedicated construct like https://github.com/c42f/ResourceContexts.jl.

@bbrehm
Copy link

bbrehm commented Jan 11, 2026

  1. Can we also get labeled continue?
  2. Can we get additional optional syntax a la end :somelabel for the end of a labeled block, which is a lowering error if the label of the end doesn't match the label of the block and otherwise is the same as end? This also fits well with labeled break, because this label sits at the point where break somelabel will goto

If one refactors code from "lots of loopy goto" to "semi-structured control flow that makes reducibility of the CFG apparent", then one needs to introduce a bunch of additional nested @label :somelabel begin ... end :somelabel blocks (one for each @label). This can make it hard to visually match what block ends where; and indentation might be a little too much visual clutter.

All that being said... isn't this all doable by having a macro label(the_label, the_code_block) and macro label(the_label, the_code_block, optionally_the_same_label_again)? Because the label of a labeled block can only be used inside this labeled block, so we can simply rewrite labeled break/continue statements inside into @goto?

(small caveat: rewriting into @goto is probably not OK until lowering gets its act together with respect to closure/capture boxing)

@jakobnissen
Copy link
Member

IIUC a labeled continue targeting the innermost loop is equivalent to a normal continue, and when targeting the Nth loop from the inside, it’s equivalent to a labeled break of the N-1th loop, so I don’t see the need

@GunnarFarneback
Copy link
Contributor

That doesn't sound right but feel free to see if you can reduce the break; continue cases in https://github.com/GunnarFarneback/Multibreak.jl/blob/master/test/tutorial.jl to just breaks.

it seemed like people strongly disliked the break break syntax

I'm much in favor of it. Sure, it doesn't scale great to deep nesting but for the common cases it's much more concise than the label syntax.

@bbrehm
Copy link

bbrehm commented Jan 12, 2026

and when targeting the Nth loop from the inside, it’s equivalent to a labeled break of the N-1th loop

Consider:

@label_for_continue_break :A while condition1
    #Block1
    while condition2
        #...
         continue :A 
    end
    #Block2
end :A

The continue :A will skip Block2.

I further like the fact that labeled begin with labeled continue supports blocks/loops that don't introduce scope, and are potentially clearer than a while true for loops that should run at least once, "do-while"-style.

@jakobnissen
Copy link
Member

You are right - I didn't think carefully enough.

@Keno
Copy link
Member Author

Keno commented Jan 14, 2026

Labeled continue is already implemented by this PR

Keno added a commit that referenced this pull request Jan 14, 2026
Syntax versioning (intentionally) uses a binding partitioned constant
to set the syntax version. This gives well defined meaning to changing
the parser of the course of a module's lifetime, but open's up the
question which world age `include` uses to determine the syntax version.
I think the correct answer is that `include` should look at the latest
world age, since it also implicitly raises the world age for the
statements it executes, so it would be somewhat inconsistent for the
parse to happen in the caller's world age. Fixes #60624. Also addresses
a somewhat hilarious interaction with backdating where the syntax
version depended on the value of `--depwarn` (causing test failures
in #60481).
@JeffBezanson
Copy link
Member

👍 I like the feature. Can we drop the colons to be consistent with @label and @goto? I think the colons are fine in a context where something could conceivably be a value, but I don't think there's any intention that the label could ever be computed. Is there?

@Keno
Copy link
Member Author

Keno commented Jan 15, 2026

There is no intention for the label to be computed. However, the value could be a variable of course as in break foo bar. In the original implementation of this, I found it a tad confusing to remember which one of those was the label and which was the value, thus the syntactic distinction, but I'm ok dropping it.

@JeffBezanson
Copy link
Member

To me break foo bar is less confusing than sometimes needing @label x and elsewhere @label :x.

@Keno
Copy link
Member Author

Keno commented Jan 15, 2026

Sure. We can also call the macro something different though, since you can't goto it either.

@oscardssmith
Copy link
Member

Triage brings up that break :label currently passes parsing (but throws a ParseError on lowering) which is suboptimal as it means this is currently valid syntax in macros.

julia> Meta.@dump(continue :label)
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol :
    2: Expr
      head: Symbol continue
      args: Array{Any}((0,))
    3: Symbol label

Triage is conflicted on the advantage over @goto, and has many proposals for syntax, none of which seem "right".

Things we want from any syntax:

  1. Currently parse error (if at all possible)
  2. Looks acceptably pretty
  3. Doesn't suggest misleading semantics

@Keno is going to vibecode the proposal with

var = for ... 
    break var 123 ...
end
var = for ... 
    ...
    continue var
    ...
end
var = let ... 
    ...
    break var
    ...
end

which we will discuss next time (unless he comes up with a better option before then.

Keno added a commit that referenced this pull request Jan 22, 2026
)

Syntax versioning (intentionally) uses a binding partitioned constant to
set the syntax version. This gives well defined meaning to changing the
parser of the course of a module's lifetime, but open's up the question
which world age `include` uses to determine the syntax version. I think
the correct answer is that `include` should look at the latest world
age, since it also implicitly raises the world age for the statements it
executes, so it would be somewhat inconsistent for the parse to happen
in the caller's world age. Fixes #60624. Also addresses a somewhat
hilarious interaction with backdating where the syntax version depended
on the value of `--depwarn` (causing test failures in #60481).

---------

Co-authored-by: Keno Fischer <Keno@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
@Keno
Copy link
Member Author

Keno commented Jan 22, 2026

@Keno is going to vibecode the proposal with

I implemented this, but I think I will not PR it - I think it's too clever.

I've played with a few more variants, but I keep coming back to the proposal as currently in this PR. I also think we should keep the : - I really think that identifier juxtaposition like

function break_with_underscore()
    @label x begin
        for i in 1:10
            if i > 5
                break x i * 3
            end
        end
        0
    end
end

is very jarring on the eyes. That said, maybe that can be mitigated with syntax highlighting - I'll play with that as well.

@Keno
Copy link
Member Author

Keno commented Jan 23, 2026

That said, maybe that can be mitigated with syntax highlighting - I'll play with that as well.

Screenshot 2026-01-22 at 7 51 23 PM

I think syntax highlighting does fix it. I'll go ahead and update this to the version with macro, but without :

@topolarity
Copy link
Member

topolarity commented Jan 23, 2026

I don't feel like the syntax highlighting is a slam dunk - that example only has two colors, so it's really isolating the label semantically but that doesn't seem typical to me.

I'm not sure how to color that, e.g., for Github's highlighter since that already uses red, white, and blue for

return i * 3

on its own

@Keno
Copy link
Member Author

Keno commented Jan 23, 2026

I don't feel like the syntax highlighting is a slam dunk - that example only has two colors, so it's really isolating the label semantically but that doesn't seem typical to me.

I just need it to be colored differently than any juxtaposed identifier.

@Keno Keno force-pushed the kf/breakblocks branch 2 times, most recently from 498f727 to e33e549 Compare January 23, 2026 21:03
@Keno Keno merged commit 8dab3f0 into master Jan 24, 2026
8 checks passed
@Keno Keno deleted the kf/breakblocks branch January 24, 2026 15:37
@nsajko nsajko added feature Indicates new feature / enhancement requests parser Language parsing and surface syntax labels Jan 24, 2026
@aviatesk
Copy link
Member

It seems that this PR needs further updates on the new lowering implementation.
It's a simple case, but JuliaLowering doesn't seem to handle this syntax:

julia> @label _ begin
           for i = 1:10
               x = i
               break _ x
           end
       end
1

julia> JuliaLowering.include_string(Main, """
       @label _ begin
           for i = 1:10
               x = i
               break _ x
           end
       end
       """)
ERROR: LoweringError:
    for i = 1:10
        x = i
        break _ x
#             ╙ ── Invalid break label: expected identifier
    end
end

Detailed provenance:
_
└─ _
   ├─ _
   │  └─ @ string:4
   └─ (macrocall @label _ (block (for (iteration (in i (call-i 1 : 10))) (block (= x i) (break _ x)))))
      └─ (macrocall (macro_name ✘ label) ✘ _ ✘ (block nothing ✘ (for nothing (iteration (in ✘ i ✘ = (call-i ✘ 1 : 10))) (block ✘ (= x ✘ = ✘ i) ✘ (break nothing ✘ _ ✘ x) ✘) nothing) ✘ nothing))
         └─ @ string:1

@Keno
Copy link
Member Author

Keno commented Jan 24, 2026

Thanks. Will fix. I changed the design of that back and forth like three times and must have forgotten to change it back

Keno added a commit that referenced this pull request Jan 25, 2026
Adds back JuliaLowering support for `break _` (ref
#60481 (comment))
Improves error message for label mismatch (fixes #60817)

Written by Claude
Keno added a commit that referenced this pull request Jan 26, 2026
Adds back JuliaLowering support for `break _` (ref
#60481 (comment))
Improves error message for label mismatch (fixes #60817)

Written by Claude
@mlechu
Copy link
Member

mlechu commented Jan 26, 2026

Is there any reason we can't make the #cont distinction with a new Expr head instead of string concatenation within the identifier?

This error message is a bit internal:

julia> @label foo begin
       continue foo
       end
ERROR: syntax: `break foo#cont` not in a block with label `foo#cont`
Stacktrace:
 [1] top-level scope
   @ REPL[1]:1

mlechu added a commit to mlechu/julia that referenced this pull request Jan 26, 2026
- symbolic_block -> symbolicblock (since I did symbolicgoto, symboliclabel too)
  - should fix Expr-incompatibility as well (default conversion)
- add new continue/break/symbolicblock forms to validator
- fix scope of new identifier in JL `@label` (hygiene by default means we need
  to specify the scope layer if we're generating new identifiers in the macro)
@Keno
Copy link
Member Author

Keno commented Jan 26, 2026

Is there any reason we can't make the #cont distinction with a new Expr head instead of string concatenation within the identifier?

I think that would be fine, but also doesn't seem necessary - we can just improve the error message. The important bit to me was the we could emit continue blocks in a macro, in case a package wants to have their own looping structure.

mlechu added a commit to mlechu/julia that referenced this pull request Jan 27, 2026
- symbolic_block -> symbolicblock (since I did symbolicgoto, symboliclabel too)
  - should fix Expr-incompatibility as well (default conversion)
- add new continue/break/symbolicblock forms to validator
- fix scope of new identifier in JL `@label` (hygiene by default means we need
  to specify the scope layer if we're generating new identifiers in the macro)
mlechu added a commit to mlechu/julia that referenced this pull request Jan 28, 2026
- symbolic_block -> symbolicblock (since I did symbolicgoto, symboliclabel too)
  - should fix Expr-incompatibility as well (default conversion)
- add new continue/break/symbolicblock forms to validator
- fix scope of new identifier in JL `@label` (hygiene by default means we need
  to specify the scope layer if we're generating new identifiers in the macro)
mlechu added a commit to mlechu/julia that referenced this pull request Jan 29, 2026
- symbolic_block -> symbolicblock (since I did symbolicgoto, symboliclabel too)
  - should fix Expr-incompatibility as well (default conversion)
- add new continue/break/symbolicblock forms to validator
- fix scope of new identifier in JL `@label` (hygiene by default means we need
  to specify the scope layer if we're generating new identifiers in the macro)
mlechu added a commit to mlechu/julia that referenced this pull request Jan 29, 2026
- symbolic_block -> symbolicblock (since I did symbolicgoto, symboliclabel too)
  - should fix Expr-incompatibility as well (default conversion)
- add new continue/break/symbolicblock forms to validator
- fix scope of new identifier in JL `@label` (hygiene by default means we need
  to specify the scope layer if we're generating new identifiers in the macro)
@GunnarFarneback
Copy link
Contributor

GunnarFarneback commented Jan 31, 2026

What's up with this?

$ mkdir testenv
$ cd testenv
$ echo -e '[compat]\njulia = "1"' > Project.toml
$ julia +nightly --project=.
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.14.0-DEV.1651 (2026-01-30)
 _/ |\__'_|_|_|\__'_|  |  Commit 7eb1edb73e0 (1 day old master)
|__/                   |

julia> @label _ begin break _ end
ERROR: ParseError:
# Error @ REPL[1]:1:16
@label _ begin break _ end
#              └─────┘ ── labeled `break` not supported in Julia version 1.13 < 1.14
Stacktrace:
 [1] top-level scope
   @ REPL:1

I want to update the Multibreak package to use these expressions, @static ifed on the version being high enough, and otherwise use the old goto.

@Keno
Copy link
Member Author

Keno commented Jan 31, 2026

It's a syntax versioned feature. @static if does not work because the inactive side still gets parsed:

# julia +1.13
julia> @static if false
       @label _ begin break _ end
eERROR: nParseError:
# Error @ REPL[1]:2:21
@static if false
@label _ begin break _ end
#                   └┘ ── unexpected token after break
Stacktrace:
 [1] top-level scope
   @ REPL:1

@GunnarFarneback
Copy link
Contributor

GunnarFarneback commented Jan 31, 2026

But inactive includes aren't parsed, are they?

@static if VERSION < v"1.14.0-DEV.1651"  # Or whatever is the right number.
    include("multibreak_v1.jl")  # Using goto
else
    include("multibreak_v2.jl")  # Using labeled break/continue
end

@Keno
Copy link
Member Author

Keno commented Jan 31, 2026

Correct, if you want to mix syntax versions in your packages (which is not really recommended), you can do

@static if VERSION < v"1.14.0-DEV.1651"  # Or whatever is the right number.
    include("multibreak_v1.jl")  # Using goto
else
    Base.Experimental.@set_syntax_version v"1.14"
    include("multibreak_v2.jl")  # Using labeled break/continue
end

Note that the syntax version change will affect all future includes. Maybe we should hav a version of the macro that changes it for just one include.

IanButterworth added a commit to JuliaLang/JuliaSyntax.jl that referenced this pull request Feb 1, 2026
Backports labeled break/continue support from JuliaLang/julia#60481
(commit 8dab3f0623) to support parsing Julia 1.14+ source code.

Co-authored-by: Claude <noreply@anthropic.com>
@GunnarFarneback
Copy link
Contributor

Actually I was a little silly not realizing that I didn't need the syntax but only the AST expressions in the code. I did need the syntax in the tests though, so the tips came in handy.

One more question, are there any potential problems with having unused symbolicblock expressions in macro output, or will they just go away at some point in the compilation?

@Keno
Copy link
Member Author

Keno commented Feb 1, 2026

One more question, are there any potential problems with having unused symbolicblock expressions in macro output, or will they just go away at some point in the compilation?

Will be gone after lowering.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Indicates new feature / enhancement requests parser Language parsing and surface syntax

Projects

None yet

Development

Successfully merging this pull request may close these issues.