Skip to content

Add labeled break and continue statements - #219

Open
SPY wants to merge 4 commits into
luau-lang:masterfrom
SPY:syntax-labeled-break-continue
Open

Add labeled break and continue statements#219
SPY wants to merge 4 commits into
luau-lang:masterfrom
SPY:syntax-labeled-break-continue

Conversation

@SPY

@SPY SPY commented Jul 7, 2026

Copy link
Copy Markdown

@aatxe aatxe changed the title Add labeled break and return statements Add labeled break and continue statements Jul 7, 2026
@Dekkonot

Dekkonot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

My preference would be to have the label follow the do I think, since it makes it more obvious that it's attached to that scope rather than just the loop. It also solves some of the ambiguity problems of less ugly syntax (my apologies to anyone who is in love with ::label::) because the position is already a syntax error with something as simple as a colon:

for i = 1, 10 do: label
  if i == 8 then
    continue label
  end
end

While I'm not strictly proposing this syntax (it gets weird due to e.g. do:label("foo"):lower()), it gives us more options than making it a prefix to the keywords.

repeat has the same property, which is that a lot of symbols after it are just outright syntax errors right now.

@bradsharp

Copy link
Copy Markdown
Contributor

While this would be a nice feature I am not sure about the syntax to to add a label. Could we lean into attributes here with something like @label <name>? Alternately, could this be a number where break 1 is equivalent to break and break 2 would break the outer loop?

@nnullcolumn

nnullcolumn commented Jul 8, 2026

Copy link
Copy Markdown

While this would be a nice feature I am not sure about the syntax to to add a label. Could we lean into attributes here with something like @label <name>? Alternately, could this be a number where break 1 is equivalent to break and break 2 would break the outer loop?

break 1 or break 2, albeit admirably minimal, seem like they could cause mistakes - e.g. moving nested code from one spot to another. Attributes historically haven't changed the runtime semantics of programs (apart from arguably native in niche float ways), so it feels bad to overload them for control flow. I like the solution dekkonot proposed even if it allows people to mangle their syntax a bit.

@SPY

SPY commented Jul 8, 2026

Copy link
Copy Markdown
Author

My preference would be to have the label follow the do I think, since it makes it more obvious that it's attached to that scope rather than just the loop.

I like it.

my apologies to anyone who is in love with ::label::

Never was attached to it. It looks ugly :)

@Nicant02

Nicant02 commented Jul 8, 2026

Copy link
Copy Markdown

I like this, something like this would be nice to have.

I enjoy the extended do block syntax, an uncommon pattern I do sometimes when I need to check a bunch of different things that chain off of each other is something like:

-- execution starts
repeat
	local character = Player.Character
	if not character then
		break
	end
	
	local humanoid = character:FindFirstChildOfClass("Humanoid")
	if not humanoid then
		break
	end
	ApplyDamage(humanoid, 10)
until true
-- execution continues

@InfraredGodYT

Copy link
Copy Markdown

My preference would be to have the label follow the do I think, since it makes it more obvious that it's attached to that scope rather than just the loop.

I like it.

my apologies to anyone who is in love with ::label::

Never was attached to it. It looks ugly :)

I was unsure about the syntax at first, but using do: label sealed the deal for me. If this isn’t too complex to add, it’ll definitely be a nice addition.

@alexmccord

alexmccord commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Even label: (do|for|while|repeat) and break label are syntax that can be supported retroactively. No need for ::label:: gunk or postfix kw: label. And quite frankly, I don't want to read for k, v in t do: label, I want to read label: for k, v in t do.

@nnullcolumn

Copy link
Copy Markdown

Even label: (do|for|while|repeat) and break label are syntax that can be supported retroactively. No need for ::label:: gunk or postfix kw: label. And quite frankly, I don't want to read for k, v in t do: label, I want to read label: for k, v in t do.

imo, label: for is crazy visual noise and looks like a type annotation in all the wrong ways

@SPY

SPY commented Jul 16, 2026

Copy link
Copy Markdown
Author

After internal discussion with the team we decided to split this PR in two moving away labeled do blocks.
The reason for that is potential inconsistency: do blocks going to support only labeled breaks, introduction of unlabeled breaks for do blocks breaks backward compatibility with code like:

for i in 1,10 do
  if i > 5 then do break end end
end

We still consider breaking labeled do blocks reasonable language feature, but we think it deserves own RFC.
If there is no significant objections for labeled loops/break/continue I'm going to merge that RFC next week

Comment thread docs/syntax-labeled-break-continue.md Outdated
Comment thread docs/syntax-labeled-break-continue.md Outdated
Comment thread docs/syntax-labeled-break-continue.md Outdated
Comment thread docs/syntax-labeled-break-continue.md Outdated

@nnullcolumn nnullcolumn left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this opinion is orthogonal to my feedback, but in addition to many quality concerns (a lot of the wording here is excessively fluffy imo - llms are well known for that), i think merging an ai-generated rfc as-is sets a potentially antisocial precedent for future collaborators.

- It reuses `return` with different semantics than the enclosing function, which can confuse readers.
- Luau's type checker must reason through the IIFE boundary, which complicates type narrowing and return type inference.
- It is stylistically alien — the construct exists solely to work around a language limitation rather than to express intent.
- If compiled without optimization(or inlining doesn't happen), it introduces a closure allocation and call overhead.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there should be a space before the parenthesized text here


## Summary

Introduce a labeled form of loop, and extend `break` and `continue` to optionally reference a label. The label is written as a colon-suffix on the loop's `do` keyword — `for i = 1, 10 do: outer` — making it structurally bound to the loop it names. This allows structured non-local exits from nested loops without workarounds such as flag variables or immediately-invoked function expressions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think 'a labeled form of loop', 'extend break and continue', 'label is written as a colon-suffix on the loop's do keyword', etc... are good things to communicate, but could be said more concisely by replacing this entire section with something along the lines of 'introduce named control flow manipulation in loops with do: label, repeat: label, break label, and continue label'

end
```

**Immediately-invoked function expressions (IIFEs)** are commonly used in Luau codebases:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'commonly used' seems very subjective, there are lots of luau codebases without this pattern. perhaps 'may be used' would be better?


A label is in scope from the `do:` (or `repeat:`) token to the end of the enclosing block. It is visible in all nested blocks — but only for the purpose of `break`/`continue`; a label cannot be referenced in any other context, and it is never captured as an upvalue.

Label names must be unique within their immediately enclosing block. Redeclaring a label name at the same scope level is a compile-time error. Shadowing an outer label with an inner label of the same name is permitted, following the same rules as variable shadowing.

@nnullcolumn nnullcolumn Jul 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Label names must be unique within their immediately enclosing block.

this wording makes no sense to me. if the meaning is what follows it, why have this at all?

Redeclaring a label name at the same scope level is a compile-time error. Shadowing an outer label with an inner label of the same name is permitted, following the same rules as variable shadowing.

why a compile error for making the same label twice in the same scope? does this imply:

repeat: retries
    local ok, err = attempt()
    if err == "fatal" then break retries end
until ok or retries_exceeded()
-- invalid
repeat: retries
    local ok, err = attempt_other()
    if err == "fatal" then break retries end
until ok or retries_exceeded()

if so, that seems extremely different from local shadowing semantics - those scopes are completely unrelated, since the label stops existing as soon as you leave the loop. secondly, there already exist lints for shadowing locals. i'd argue if anything we should lint or compile error the opposite way - inner shadowing disallowed because it results in rules which may be significantly more confusing, but otherwise there should just be no limitation imo


**`repeat` asymmetry.** All other labeled constructs attach the label to `do`, which is the shared block-opening keyword. `repeat` has no `do`, so its label sits on `repeat` itself (`repeat: name`). This is a minor inconsistency programmers must learn.

**Contextual keyword extension.** `continue` is already a contextual keyword. `break label` and `continue label` require the parser to recognize that the identifier following `break`/`continue` on the same line is a label reference. This is straightforward but must be carefully specified to avoid edge cases at line boundaries.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is straightforward but must be carefully specified to avoid edge cases at line boundaries.

could this be explained further? i don't really see how this results in any edge cases, and line boundaries feel orthogonal


### Interaction with `continue`

`continue` is already a contextual keyword in Luau. The labeled form `continue Name` is parsed the same way: `continue` followed by an identifier on the same line. Since `continue` only appears inside loop bodies and cannot start a function call, there is no ambiguity with expression statements.

@nnullcolumn nnullcolumn Jul 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since continue only appears inside loop bodies and cannot start a function call, there is no ambiguity with expression statements.

this sentence doesn't make a lot of sense. why would a keyword start a function call, and how is that related? additionally, expressions and statements are fundamentally disjoint in luau - there are no expression statements. there are things which are valid as expressions and valid as statements, but to me this paragraph seems really unnecessary generally?


### Type checking

Labels have no direct type implications. The type checker treats labeled `break` the same as an unconditional `break` for the purposes of control-flow analysis: it is a definite exit from the labeled statement. Labeled `continue` is treated as a definite jump to the loop condition of the labeled statement.

@nnullcolumn nnullcolumn Jul 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that doesn't make a lot of sense, you say it has no implications and then explain how it behaves in control flow analysis. isn't non-local jumping from an inner loop an inherently more complicated control flow which wasn't possible before? even the drawbacks section addresses this directly.

the behavior here is also worded somewhat confusingly explaining the implications of this, possibly because it seemingly ignores potential implications


### Bytecode

`break label` compiles to the same `JUMP` instruction as an unlabeled `break`, targeting the instruction immediately following the labeled statement(extra `CLOSEUPVALS` for all outer loops should be also emitted though). `continue label` compiles to the same `JUMP` as an unlabeled `continue`, targeting the loop condition of the labeled statement. No new bytecode instructions are required.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space missing before parenthesized section here


### Bytecode

`break label` compiles to the same `JUMP` instruction as an unlabeled `break`, targeting the instruction immediately following the labeled statement(extra `CLOSEUPVALS` for all outer loops should be also emitted though). `continue label` compiles to the same `JUMP` as an unlabeled `continue`, targeting the loop condition of the labeled statement. No new bytecode instructions are required.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps the CLOSEUPVALS bytecode should be mentioned in the drawbacks section? a vm developer would have to tell me if that's super significant because idrk the implementation of that


## Drawbacks

**`repeat` asymmetry.** All other labeled constructs attach the label to `do`, which is the shared block-opening keyword. `repeat` has no `do`, so its label sits on `repeat` itself (`repeat: name`). This is a minor inconsistency programmers must learn.

@nnullcolumn nnullcolumn Jul 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although i'm not against a very exhaustive drawbacks section, i'm not sure this item is very significant. it could also be merged with the later one that talks about the learnability of this syntax.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants