Skip to content
Merged
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
1 change: 1 addition & 0 deletions JuliaLowering/src/JuliaLowering.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ using .JuliaSyntax: highlight, Kind, @KSet_str, is_leaf, children, numchildren,
head, kind, flags, has_flags, filename, first_byte, last_byte, byte_range,
sourcefile, source_location, span, sourcetext, is_literal, is_infix_op_call,
is_postfix_op_call, @isexpr, SyntaxHead, is_syntactic_operator,
is_contextual_keyword,
SyntaxGraph, SyntaxTree, SyntaxList, NodeId, SourceRef, SourceAttrType,
ensure_attributes, ensure_attributes!, delete_attributes, new_id!, hasattr,
setattr, setattr!, syntax_graph, is_compatible_graph,
Expand Down
39 changes: 37 additions & 2 deletions JuliaLowering/src/desugaring.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4449,10 +4449,40 @@ function expand_forms_2(ctx::DesugaringContext, ex::SyntaxTree, docs=nothing)
elseif k == K"="
expand_assignment(ctx, ex)
elseif k == K"break"
numchildren(ex) > 0 ? ex :
nc = numchildren(ex)
if nc == 0
@ast ctx ex [K"break" "loop_exit"::K"symbolic_label"]
else
@chk nc <= 2 (ex, "Too many arguments to break")
label = ex[1]
label_kind = kind(label)
# Convert Symbol (from Expr conversion) to symbolic_label
if label_kind == K"Symbol"
label = @ast ctx label label.name_val::K"symbolic_label"
elseif !(label_kind == K"Identifier" || label_kind == K"symbolic_label" ||
is_contextual_keyword(label_kind))
throw(LoweringError(label, "Invalid break label: expected identifier"))
end
if nc == 2
@ast ctx ex [K"break" label expand_forms_2(ctx, ex[2])]
else
@ast ctx ex [K"break" label]
end
end
elseif k == K"continue"
@ast ctx ex [K"break" "loop_cont"::K"symbolic_label"]
nc = numchildren(ex)
if nc == 0
@ast ctx ex [K"break" "loop_cont"::K"symbolic_label"]
else
@chk nc == 1 (ex, "Too many arguments to continue")
label = ex[1]
label_kind = kind(label)
if !(label_kind == K"Identifier" || label_kind == K"Placeholder" ||
label_kind == K"Symbol" || is_contextual_keyword(label_kind))
throw(LoweringError(label, "Invalid continue label: expected identifier"))
end
@ast ctx ex [K"break" string(label.name_val, "#cont")::K"symbolic_label"]
end
elseif k == K"comparison"
expand_forms_2(ctx, expand_compare_chain(ctx, ex))
elseif k == K"doc"
Expand Down Expand Up @@ -4613,6 +4643,11 @@ function expand_forms_2(ctx::DesugaringContext, ex::SyntaxTree, docs=nothing)
]
elseif k == K"inert" || k == K"inert_syntaxtree"
ex
elseif k == K"symbolic_block"
# @label name body -> (symbolic_block name expanded_body)
# The @label macro inserts the continue block for loops, so we just expand the body
@chk numchildren(ex) == 2
@ast ctx ex [K"symbolic_block" ex[1] expand_forms_2(ctx, ex[2])]
elseif k == K"gc_preserve"
s = ssavar(ctx, ex)
r = ssavar(ctx, ex)
Expand Down
2 changes: 2 additions & 0 deletions JuliaLowering/src/kinds.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ function _register_kinds()
"symbolic_label"
# Goto named label
"symbolic_goto"
# Labeled block for `@label name expr` (block break)
"symbolic_block"
# Internal initializer for struct types, for inner constructors/functions
"new"
"splatnew"
Expand Down
60 changes: 54 additions & 6 deletions JuliaLowering/src/linear_ir.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ struct JumpTarget{Attrs}
label::SyntaxTree{Attrs}
handler_token_stack::SyntaxList{Attrs, Vector{NodeId}}
catch_token_stack::SyntaxList{Attrs, Vector{NodeId}}
result_var::Union{SyntaxTree{Attrs}, Nothing} # for symbolic_block valued breaks
end

function JumpTarget(label::SyntaxTree{Attrs}, ctx) where {Attrs}
JumpTarget{Attrs}(label, copy(ctx.handler_token_stack), copy(ctx.catch_token_stack))
function JumpTarget(label::SyntaxTree{Attrs}, ctx, result_var=nothing) where {Attrs}
JumpTarget{Attrs}(label, copy(ctx.handler_token_stack), copy(ctx.catch_token_stack), result_var)
end

struct JumpOrigin{Attrs}
Expand Down Expand Up @@ -79,6 +80,7 @@ struct LinearIRContext{Attrs} <: AbstractLoweringContext
finally_handlers::Vector{FinallyHandler{Attrs}}
symbolic_jump_targets::Dict{String,JumpTarget{Attrs}}
symbolic_jump_origins::Vector{JumpOrigin{Attrs}}
symbolic_block_labels::Set{String} # labels that are symbolic blocks (not allowed as @goto targets)
meta::Dict{Symbol, Any}
mod::Module
end
Expand All @@ -91,7 +93,7 @@ function LinearIRContext(ctx, is_toplevel_thunk, lambda_bindings, return_type)
is_toplevel_thunk, lambda_bindings, Dict{IdTag,IdTag}(), rett,
Dict{String,JumpTarget{Attrs}}(), SyntaxList(ctx), SyntaxList(ctx),
Vector{FinallyHandler{Attrs}}(), Dict{String,JumpTarget{Attrs}}(),
Vector{JumpOrigin{Attrs}}(), Dict{Symbol, Any}(), ctx.mod)
Vector{JumpOrigin{Attrs}}(), Set{String}(), Dict{Symbol, Any}(), ctx.mod)
end

function current_lambda_bindings(ctx::LinearIRContext)
Expand Down Expand Up @@ -309,6 +311,14 @@ function emit_break(ctx, ex)
ty = name == "loop_exit" ? "break" : "continue"
throw(LoweringError(ex, "$ty must be used inside a `while` or `for` loop"))
end
# Handle valued break (break name val)
if numchildren(ex) >= 2
if isnothing(target.result_var)
throw(LoweringError(ex, "break with value not allowed for label `$name`"))
end
val = compile(ctx, ex[2], true, false)
emit_assignment(ctx, ex, target.result_var, val)
end
if !isempty(ctx.finally_handlers)
handler = last(ctx.finally_handlers)
if length(target.handler_token_stack) < length(handler.target.handler_token_stack)
Expand Down Expand Up @@ -683,7 +693,9 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos)
end_label = make_label(ctx, ex)
name = ex[1].name_val
outer_target = get(ctx.break_targets, name, nothing)
ctx.break_targets[name] = JumpTarget(end_label, ctx)
# Inherit result_var from outer symbolicblock if present
outer_result_var = isnothing(outer_target) ? nothing : outer_target.result_var
ctx.break_targets[name] = JumpTarget(end_label, ctx, outer_result_var)
compile(ctx, ex[2], false, false)
if isnothing(outer_target)
delete!(ctx.break_targets, name)
Expand All @@ -694,12 +706,44 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos)
if needs_value
compile(ctx, nothing_(ctx, ex), needs_value, in_tail_pos)
end
elseif k == K"symbolic_block"
name = ex[1].name_val
if haskey(ctx.symbolic_jump_targets, name) || name in ctx.symbolic_block_labels
throw(LoweringError(ex, "Label `$name` defined multiple times"))
end
push!(ctx.symbolic_block_labels, name)
end_label = make_label(ctx, ex)
result_var = if needs_value || in_tail_pos
rv = new_local_binding(ctx, ex, "$(name)_result")
emit_assignment(ctx, ex, rv, nothing_(ctx, ex))
rv
else
nothing
end
outer_target = get(ctx.break_targets, name, nothing)
ctx.break_targets[name] = JumpTarget(end_label, ctx, result_var)
body_val = compile(ctx, ex[2], !isnothing(result_var), false)
if !isnothing(result_var) && !isnothing(body_val)
emit_assignment(ctx, ex, result_var, body_val)
end
if isnothing(outer_target)
delete!(ctx.break_targets, name)
else
ctx.break_targets[name] = outer_target
end
emit(ctx, end_label)
if in_tail_pos
emit_return(ctx, ex, result_var)
nothing
else
result_var
end
elseif k == K"break"
emit_break(ctx, ex)
elseif k == K"symbolic_label"
label = emit_label(ctx, ex)
name = ex.name_val
if haskey(ctx.symbolic_jump_targets, name)
if haskey(ctx.symbolic_jump_targets, name) || name in ctx.symbolic_block_labels
throw(LoweringError(ex, "Label `$name` defined multiple times"))
end
push!(ctx.symbolic_jump_targets, name=>JumpTarget(label, ctx))
Expand Down Expand Up @@ -950,6 +994,10 @@ function compile_body(ctx::LinearIRContext, ex)
name = origin.goto.name_val
target = get(ctx.symbolic_jump_targets, name, nothing)
if isnothing(target)
# Check if it's a symbolic block label
if name in ctx.symbolic_block_labels
throw(LoweringError(origin.goto, "cannot use @goto to jump to @label block `$name`"))
end
throw(LoweringError(origin.goto, "label `$name` referenced but not defined"))
end
i = origin.index
Expand Down Expand Up @@ -1172,7 +1220,7 @@ loops, etc) to gotos and exception handling to enter/leave. We also convert
Vector{FinallyHandler{Attrs}}(),
Dict{String, JumpTarget{Attrs}}(),
Vector{JumpOrigin{Attrs}}(),
Dict{Symbol, Any}(), ctx.mod)
Set{String}(), Dict{Symbol, Any}(), ctx.mod)
res = compile_lambda(_ctx, reparent(_ctx, ex))
_ctx, res
end
21 changes: 21 additions & 0 deletions JuliaLowering/src/scope_analysis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ function _find_scope_decls!(ctx, scope, ex)
k === K"constdecl" && numchildren(ex) == 2)
_find_scope_decls!(ctx, scope, ex[2])
end
elseif k === K"symbolic_block"
# Only recurse into the body (second child), not the label name (first child)
_find_scope_decls!(ctx, scope, ex[2])
elseif k === K"break" && numchildren(ex) >= 2
# For break with value, only recurse into the value expression (second child), not the label
_find_scope_decls!(ctx, scope, ex[2])
elseif needs_resolution(ex) && !(k === K"scope_block" || k === K"lambda")
for e in children(ex)
_find_scope_decls!(ctx, scope, e)
Expand Down Expand Up @@ -343,6 +349,10 @@ function _resolve_scopes(ctx, ex::SyntaxTree,
ex
elseif k == K"softscope"
newleaf(ctx, ex, K"TOMBSTONE")
elseif k == K"break" && numchildren(ex) >= 2
# For break with value (break label value), process the value expression but not the label
# This must come BEFORE !needs_resolution check since K"break" is in is_quoted
@ast ctx ex [K"break" ex[1] _resolve_scopes(ctx, ex[2], scope)]
elseif !needs_resolution(ex)
ex
elseif k == K"local"
Expand Down Expand Up @@ -507,6 +517,9 @@ function _resolve_scopes(ctx, ex::SyntaxTree,
@assert numchildren(ex) === 2
assignment_kind = bk == :global ? K"constdecl" : K"="
@ast ctx ex _resolve_scopes(ctx, [assignment_kind ex[1] ex[2]], scope)
elseif k == K"symbolic_block"
# Only recurse into the body (second child), not the label name (first child)
@ast ctx ex [K"symbolic_block" ex[1] _resolve_scopes(ctx, ex[2], scope)]
else
mapchildren(e->_resolve_scopes(ctx, e, scope), ctx, ex)
end
Expand Down Expand Up @@ -600,6 +613,11 @@ function analyze_variables!(ctx, ex)
@assert b.kind === :global || b.is_ssa || haskey(ctx.lambda_bindings.locals_capt, b.id)
elseif k == K"Identifier"
@assert false
elseif k == K"break" && numchildren(ex) >= 2
# For break with value, only analyze the value expression (second child), not the label
# This must come BEFORE !needs_resolution check since K"break" is in is_quoted
analyze_variables!(ctx, ex[2])
return
elseif !needs_resolution(ex)
return
elseif k == K"static_eval"
Expand Down Expand Up @@ -676,6 +694,9 @@ function analyze_variables!(ctx, ex)
ctx.graph, ctx.bindings, ctx.mod, ctx.scopes, lambda_bindings,
ctx.method_def_stack, ctx.closure_bindings)
foreach(e->analyze_variables!(ctx2, e), ex[3:end]) # body & return type
elseif k == K"symbolic_block"
# Only analyze the body (second child), not the label name (first child)
analyze_variables!(ctx, ex[2])
else
foreach(e->analyze_variables!(ctx, e), children(ex))
end
Expand Down
29 changes: 29 additions & 0 deletions JuliaLowering/src/syntax_macros.jl
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,35 @@ function Base.var"@label"(__context__::MacroContext, ex)
@ast __context__ ex ex=>K"symbolic_label"
end

function Base.var"@label"(__context__::MacroContext, name, body)
# Handle `@label _ body` (anonymous) or `@label name body` (named)
k = kind(name)
if k == K"Identifier"
# `@label _ body` or `@label name body` - plain identifier
elseif is_contextual_keyword(k)
# Contextual keyword used as label name (e.g., `@label outer body`)
else
throw(MacroExpansionError(name, "Expected identifier for block label"))
end
# If body is a syntactic loop, wrap its body in a continue block
# This allows `continue name` to work by breaking to `name#cont`
body_kind = kind(body)
if body_kind == K"for" || body_kind == K"while"
cont_name = string(name.name_val, "#cont")
loop_body = body[2]
wrapped_body = @ast __context__ loop_body [K"symbolic_block"
cont_name::K"Identifier"
loop_body
]
if body_kind == K"for"
body = @ast __context__ body [K"for" body[1] wrapped_body]
else # while
body = @ast __context__ body [K"while" body[1] wrapped_body]
end
end
@ast __context__ __context__.macrocall [K"symbolic_block" name body]
end

function Base.var"@goto"(__context__::MacroContext, ex)
@chk kind(ex) == K"Identifier"
@ast __context__ ex ex=>K"symbolic_goto"
Expand Down
59 changes: 52 additions & 7 deletions JuliaSyntax/src/julia/parser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2058,15 +2058,60 @@ function parse_resword(ps::ParseState)
parse_eq(ps)
end
emit(ps, mark, K"return")
elseif word in KSet"break continue"
# break ==> (break)
# continue ==> (continue)
elseif word == K"continue"
# continue ==> (continue)
# continue _ ==> (continue _) [1.14+]
# continue label ==> (continue label) [1.14+]
bump(ps, TRIVIA_FLAG)
emit(ps, mark, word)
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))")
if k in KSet"NewlineWs ; ) EndMarker" || (k == K"end" && !ps.end_symbol)
# continue with no arguments
emit(ps, mark, K"continue")
elseif ps.range_colon_enabled && k == K":"
# Ternary case: `cond ? continue : x`
emit(ps, mark, K"continue")
elseif k == K"Identifier" || is_contextual_keyword(k)
# continue label - plain identifier or contextual keyword as label
bump(ps)
emit(ps, mark, K"continue")
min_supported_version(v"1.14", ps, mark, "labeled `continue`")
else
# Error: unexpected token after continue
emit(ps, mark, K"continue")
end
elseif word == K"break"
# break ==> (break)
# break _ ==> (break _) [1.14+]
# break _ val ==> (break _ val) [1.14+]
# break label ==> (break label) [1.14+]
# break label val ==> (break label val) [1.14+]
bump(ps, TRIVIA_FLAG)
function parse_break_value(ps, mark)
k2 = peek(ps)
if k2 in KSet"NewlineWs ; ) : EndMarker" || (k2 == K"end" && !ps.end_symbol)
# break label
emit(ps, mark, K"break")
else
# break label value
parse_eq(ps)
emit(ps, mark, K"break")
end
min_supported_version(v"1.14", ps, mark, "labeled `break`")
end
k = peek(ps)
if k in KSet"NewlineWs ; ) EndMarker" || (k == K"end" && !ps.end_symbol)
# break with no arguments
emit(ps, mark, K"break")
elseif ps.range_colon_enabled && k == K":"
# Ternary case: `cond ? break : x`
emit(ps, mark, K"break")
elseif k == K"Identifier" || is_contextual_keyword(k)
# break label [value] - plain identifier or contextual keyword as label
bump(ps)
parse_break_value(ps, mark)
else
# Error: unexpected token after break
emit(ps, mark, K"break")
end
elseif word in KSet"module baremodule"
# module A end ==> (module A (block))
Expand Down
7 changes: 7 additions & 0 deletions JuliaSyntax/test/parser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,13 @@ tests = [
# break/continue
"break" => "(break)"
"continue" => "(continue)"
# break/continue with labels (plain identifiers only, requires 1.14+)
((v=v"1.14",), "break _") => "(break _)"
((v=v"1.14",), "break _ x") => "(break _ x)"
((v=v"1.14",), "break label") => "(break label)"
((v=v"1.14",), "break label x") => "(break label x)"
((v=v"1.14",), "continue _") => "(continue _)"
((v=v"1.14",), "continue label") => "(continue label)"
# module/baremodule
"module A end" => "(module A (block))"
"baremodule A end" => "(module-bare A (block))"
Expand Down
Loading