Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ After making changes, run static analysis checks:
- Tests can also be rerun individually with `clang-sa-<filename>`, `clang-sagc-<filename>` or `clang-tidy-<filename>`.
- 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.

### Lowering (Scheme) changes

If you made changes to `src/julia-syntax.scm` (the lowering code), you need to rebuild
with `make -C src`. This is faster than a full rebuild but you must ensure the flisp
boot files are regenerated:
- 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.
- Alternatively, touch the source file before running make: `touch src/julia-syntax.scm && make -C src`

For debugging flisp/Scheme code:
- Use `prn` to print debug output (not `io.princ` or `write`)
- Look for commented debug printing near "malformed expression" errors to get flisp stack traces

Note: Revise does NOT reload lowering changes - you must rebuild.

## Using Revise

If you have made changes to files included in the system image (base/ or stdlib/),
Expand Down
14 changes: 14 additions & 0 deletions JuliaSyntax/src/integration/expr.jl
Original file line number Diff line number Diff line change
Expand Up @@ -427,9 +427,23 @@ end
# Add extra line number node for the `end` of the block. This may seem
# useless but it affects code coverage.
push!(args[2].args, endloc)
# Handle then clause: (for iter body (then then_body)) -> (for iter body then_body)
if length(args) >= 3
then_clause = args[3]
if @isexpr(then_clause, :then)
args[3] = then_clause.args[1] # Extract the block from the then clause
end
end
elseif k == K"while"
# Line number node for the `end` of the block as in `for` loops.
push!(args[2].args, endloc)
# Handle then clause: (while cond body (then then_body)) -> (while cond body then_body)
if length(args) >= 3
then_clause = args[3]
if @isexpr(then_clause, :then)
args[3] = then_clause.args[1] # Extract the block from the then clause
end
end
elseif k in KSet"tuple vect braces"
# Move parameters blocks to args[1]
_reorder_parameters!(args, 1)
Expand Down
1 change: 1 addition & 0 deletions JuliaSyntax/src/julia/kinds.jl
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ register_kinds!(JuliaSyntax, 0, [
"else"
"elseif"
"end"
"then"
"END_BLOCK_CONTINUATION_KEYWORDS"
"BEGIN_CONTEXTUAL_KEYWORDS"
# contextual keywords
Expand Down
55 changes: 50 additions & 5 deletions JuliaSyntax/src/julia/parser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ end

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

# ";" at the top level produces a sequence of top level expressions
Expand Down Expand Up @@ -1889,17 +1889,33 @@ function parse_resword(ps::ParseState)
elseif word == K"while"
# while cond body end ==> (while cond (block body))
# while x < y \n a \n b \n end ==> (while (call-i x < y) (block a b))
# while cond body then else_body end ==> (while cond (block body) (then (block else_body)))
bump(ps, TRIVIA_FLAG)
parse_cond(ps)
parse_block(ps)
if peek(ps) == K"then"
min_supported_version(v"1.14", ps, mark, "loop then clause")
then_mark = position(ps)
bump(ps, TRIVIA_FLAG) # consume 'then'
parse_block(ps)
emit(ps, then_mark, K"then")
end
bump_closing_token(ps, K"end")
emit(ps, mark, K"while")
elseif word == K"for"
# for x in xs end ==> (for (iteration (in x xs)) (block))
# for x in xs, y in ys \n a \n end ==> (for (iteration (in x xs) (in y ys)) (block a))
# for x in xs body then else_body end ==> (for (iteration ...) (block body) (then (block else_body)))
bump(ps, TRIVIA_FLAG)
parse_iteration_specs(ps)
parse_block(ps)
if peek(ps) == K"then"
min_supported_version(v"1.14", ps, mark, "loop then clause")
then_mark = position(ps)
bump(ps, TRIVIA_FLAG) # consume 'then'
parse_block(ps)
emit(ps, then_mark, K"then")
end
bump_closing_token(ps, K"end")
emit(ps, mark, K"for")
elseif word == K"let"
Expand Down Expand Up @@ -2058,15 +2074,44 @@ function parse_resword(ps::ParseState)
parse_eq(ps)
end
emit(ps, mark, K"return")
elseif word in KSet"break continue"
# break ==> (break)
elseif word == K"break"
# Extended break syntax (1.14+):
# break ==> (break)
# break x ==> (break x)
# break break ==> (break (break))
# break break x ==> (break (break x))
# break continue ==> (break (continue))
bump(ps, TRIVIA_FLAG)
k = peek(ps)
if k == K"NewlineWs" || is_closing_token(ps, k)
# break\n ==> (break)
emit(ps, mark, K"break")
elseif k == K"break"
# break break ... ==> (break (break ...))
min_supported_version(v"1.14", ps, mark, "multi-level break")
parse_resword(ps) # Recursive parse of inner break
emit(ps, mark, K"break")
elseif k == K"continue"
# break continue ==> (break (continue))
min_supported_version(v"1.14", ps, mark, "break continue")
inner_mark = position(ps)
bump(ps, TRIVIA_FLAG)
emit(ps, inner_mark, K"continue")
emit(ps, mark, K"break")
else
# break x ==> (break x)
min_supported_version(v"1.14", ps, mark, "break with value")
parse_eq(ps)
emit(ps, mark, K"break")
end
elseif word == K"continue"
# continue ==> (continue)
bump(ps, TRIVIA_FLAG)
emit(ps, mark, word)
emit(ps, mark, K"continue")
k = peek(ps)
if !(k in KSet"NewlineWs ; ) : EndMarker" || (k == K"end" && !ps.end_symbol))
recover(is_closer_or_newline, ps, TRIVIA_FLAG,
error="unexpected token after $(untokenize(word))")
error="unexpected token after continue")
end
elseif word in KSet"module baremodule"
# module A end ==> (module A (block))
Expand Down
1 change: 1 addition & 0 deletions JuliaSyntax/src/julia/tokenize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,7 @@ K"public",
K"quote",
K"return",
K"struct",
K"then",
K"try",
K"using",
K"while",
Expand Down
13 changes: 13 additions & 0 deletions JuliaSyntax/test/parser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,19 @@ tests = [
# break/continue
"break" => "(break)"
"continue" => "(continue)"
# Extended break syntax (1.14+)
((v=v"1.14",), "break x") => "(break x)"
((v=v"1.14",), "break 42") => "(break 42)"
((v=v"1.14",), "break f(x)") => "(break (call f x))"
((v=v"1.14",), "break break") => "(break (break))"
((v=v"1.14",), "break break x") => "(break (break x))"
((v=v"1.14",), "break break break")=> "(break (break (break)))"
((v=v"1.14",), "break continue") => "(break (continue))"
((v=v"1.14",), "break break continue") => "(break (break (continue)))"
# Extended break should error on older versions
((v=v"1.13",), "break x") => "(break (error) x)"
((v=v"1.13",), "break break") => "(break (error) (break))"
((v=v"1.13",), "break continue") => "(break (error) (continue))"
# module/baremodule
"module A end" => "(module A (block))"
"baremodule A end" => "(module-bare A (block))"
Expand Down
17 changes: 17 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ New language features
- `ᵅ` (U+U+1D45), `ᵋ` (U+1D4B), `ᶲ` (U+1DB2), `˱` (U+02F1), `˲` (U+02F2), and `ₔ` (U+2094) can now also be used as
operator suffixes, accessible as `\^alpha`, `\^epsilon`, `\^ltphi`, `\_<`, `\_>`, and `\_schwa` at the REPL
([#60285]).
- Extended `break` syntax for loops (requires syntax version 1.14):
- Valued `break`: `break value` makes the loop expression return `value` instead of `nothing`.
- Multi-level `break`: `break break` exits two nested loops; additional `break`s exit more levels.
- `break continue`: exits the current loop and continues the enclosing loop's next iteration.
These forms can be combined, e.g., `break break value` or `break break continue`.
- Loop `then` clause (requires syntax version 1.14): `for`/`while` loops can now include a `then` block
that executes only when the loop completes without `break`. The `then` clause's value becomes the
loop's return value when no `break` occurs, making it easy to express search patterns:
```julia
result = for i in collection
if found(i)
break i # returns i
end
then
default_value # returned if no break
end
```

Language changes
----------------
Expand Down
77 changes: 77 additions & 0 deletions base/essentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,83 @@ macro goto(name::Symbol)
return esc(Expr(:symbolicgoto, name))
end

# Extended break syntax: labeled loops (Julia 1.14+)

"""
@label name loop

Labels a `for` or `while` loop with the symbolic name `name`, allowing targeted
break/continue via `@goto name break` or `@goto name continue`.

!!! compat "Julia 1.14"
Labeled loops require Julia 1.14 or later.

# Example
```julia
@label outer for i in 1:10
for j in 1:10
if i * j > 50
@goto outer break (i, j) # break outer loop with value
end
end
end
```
"""
macro label(name::Symbol, loop)
if !(loop isa Expr && loop.head in (:for, :while))
throw(ArgumentError("@label must be followed by a for or while loop"))
end
return esc(Expr(:labeled_loop, name, loop))
end

"""
@goto name break [value]
@goto name continue

Break from or continue a labeled loop.

!!! compat "Julia 1.14"
Labeled loop break/continue requires Julia 1.14 or later.

# Examples
```julia
@goto outer break # break from @label outer loop
@goto outer break x # break with value x
@goto outer continue # continue the @label outer loop
```
"""
macro goto(name::Symbol, action::Symbol)
if action === :break
return esc(Expr(:labeled_break, name, nothing))
elseif action === :continue
return esc(Expr(:labeled_continue, name))
else
throw(ArgumentError("@goto label must be followed by break or continue"))
end
end

macro goto(name::Symbol, action::Symbol, value)
if action !== :break
throw(ArgumentError("@goto label continue does not accept a value"))
end
return esc(Expr(:labeled_break, name, value))
end

# Handle @goto label break value when parsed as @goto label (break value)
macro goto(name::Symbol, break_expr::Expr)
if break_expr.head === :break
if isempty(break_expr.args)
return esc(Expr(:labeled_break, name, nothing))
else
return esc(Expr(:labeled_break, name, break_expr.args[1]))
end
elseif break_expr.head === :continue
return esc(Expr(:labeled_continue, name))
else
throw(ArgumentError("@goto label must be followed by break or continue"))
end
end

# linear indexing
function getindex(A::Array, i::Int)
@_noub_if_noinbounds_meta
Expand Down
Loading