diff --git a/JuliaLowering/src/JuliaLowering.jl b/JuliaLowering/src/JuliaLowering.jl index e5e004ceef281..778c72b671307 100644 --- a/JuliaLowering/src/JuliaLowering.jl +++ b/JuliaLowering/src/JuliaLowering.jl @@ -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, diff --git a/JuliaLowering/src/desugaring.jl b/JuliaLowering/src/desugaring.jl index f42754cb16d48..8ad4fc2c3e2c4 100644 --- a/JuliaLowering/src/desugaring.jl +++ b/JuliaLowering/src/desugaring.jl @@ -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" @@ -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) diff --git a/JuliaLowering/src/kinds.jl b/JuliaLowering/src/kinds.jl index bfd7ba44b5fa5..176ebd14b6d69 100644 --- a/JuliaLowering/src/kinds.jl +++ b/JuliaLowering/src/kinds.jl @@ -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" diff --git a/JuliaLowering/src/linear_ir.jl b/JuliaLowering/src/linear_ir.jl index 56be369d5e5e9..ba4a78f9fa45a 100644 --- a/JuliaLowering/src/linear_ir.jl +++ b/JuliaLowering/src/linear_ir.jl @@ -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} @@ -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 @@ -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) @@ -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) @@ -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) @@ -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)) @@ -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 @@ -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 diff --git a/JuliaLowering/src/scope_analysis.jl b/JuliaLowering/src/scope_analysis.jl index 9c64ec8c9698d..d6f24855c259c 100644 --- a/JuliaLowering/src/scope_analysis.jl +++ b/JuliaLowering/src/scope_analysis.jl @@ -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) @@ -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" @@ -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 @@ -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" @@ -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 diff --git a/JuliaLowering/src/syntax_macros.jl b/JuliaLowering/src/syntax_macros.jl index f9b588dcfd846..6b116fc56868e 100644 --- a/JuliaLowering/src/syntax_macros.jl +++ b/JuliaLowering/src/syntax_macros.jl @@ -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" diff --git a/JuliaSyntax/src/julia/parser.jl b/JuliaSyntax/src/julia/parser.jl index 15e9ade682673..aa644e1947eec 100644 --- a/JuliaSyntax/src/julia/parser.jl +++ b/JuliaSyntax/src/julia/parser.jl @@ -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)) diff --git a/JuliaSyntax/test/parser.jl b/JuliaSyntax/test/parser.jl index a4057ba81a42a..6a5b3c74e211e 100644 --- a/JuliaSyntax/test/parser.jl +++ b/JuliaSyntax/test/parser.jl @@ -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))" diff --git a/JuliaSyntax/test/test_utils.jl b/JuliaSyntax/test/test_utils.jl index 2ad1ecef7a53c..e2a4f478bb3de 100644 --- a/JuliaSyntax/test/test_utils.jl +++ b/JuliaSyntax/test/test_utils.jl @@ -65,9 +65,24 @@ function remove_macro_linenums!(ex) return ex end -function remove_all_linenums!(ex) +function remove_module_versions!(ex) + # In v1.14+, JuliaSyntax adds a version as the first argument to module expressions. + # Remove it for comparison with flisp output. + if Meta.isexpr(ex, :module) && length(ex.args) >= 1 && ex.args[1] isa VersionNumber + deleteat!(ex.args, 1) + end + if ex isa Expr + for arg in ex.args + remove_module_versions!(arg) + end + end + return ex +end + +function remove_all_linenums_and_modvers!(ex) JuliaSyntax.remove_linenums!(ex) remove_macro_linenums!(ex) + remove_module_versions!(ex) end function kw_to_eq(ex) @@ -97,7 +112,7 @@ function triple_string_roughly_equal(fl_str, str) end function exprs_equal_no_linenum(fl_ex, ex) - remove_all_linenums!(deepcopy(ex)) == remove_all_linenums!(deepcopy(fl_ex)) + remove_all_linenums_and_modvers!(deepcopy(ex)) == remove_all_linenums_and_modvers!(deepcopy(fl_ex)) end function is_eventually_call(ex) @@ -172,6 +187,10 @@ function exprs_roughly_equal(fl_ex, ex) # The flisp parser adds an extra block around `w` in the following case # f(::g(z) = w) = 1 fl_args[2] = fl_args[2].args[2] + elseif h == :module && length(args) == length(fl_args) + 1 && args[1] isa VersionNumber + # In v1.14+, JuliaSyntax adds a version as the first argument to module expressions. + # Skip the version when comparing. + args = args[2:end] end if length(fl_args) != length(args) return false @@ -210,7 +229,7 @@ function parsers_agree_on_file(text, filename; exprs_equal=exprs_equal_no_linenu return true end try - stream = ParseStream(text; version=v"1.13") + stream = ParseStream(text; version=v"1.14") parse!(stream) ex = build_tree(Expr, stream, filename=filename) return !JuliaSyntax.any_error(stream) && exprs_equal(fl_ex, ex) diff --git a/NEWS.md b/NEWS.md index 2b144a05e9d3b..42d2ca4e35a8d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -11,6 +11,9 @@ 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]). + - The `@label` macro can now create labeled blocks that can be exited early with `break name [value]`. Use + `@label name expr` for named blocks or `@label _ expr` for anonymous blocks. The `continue` statement also + supports labels with `continue name` to continue a labeled loop ([#60481]). Language changes ---------------- diff --git a/base/arrayshow.jl b/base/arrayshow.jl index 903fb4f6b68bc..0973779b986c0 100644 --- a/base/arrayshow.jl +++ b/base/arrayshow.jl @@ -290,55 +290,56 @@ function _show_nd(io::IO, @nospecialize(a::AbstractArray), print_matrix::Functio reached_last_d = false for I in Is idxs = I.I - if limit - for i = 1:nd - ii = idxs[i] - ind = tailinds[i] - if length(ind) > 10 - all_first = true - for d = 1:i-1 - if idxs[d] != first(tailinds[d]) - all_first = false - break + @label _ begin + if limit + for i = 1:nd + ii = idxs[i] + ind = tailinds[i] + if length(ind) > 10 + all_first = true + for d = 1:i-1 + if idxs[d] != first(tailinds[d]) + all_first = false + break + end end - end - if ii == ind[firstindex(ind)+3] && all_first - for j=i+1:nd - szj = length(axs[j+2]) - indj = tailinds[j] - if szj>10 && first(indj)+2 < idxs[j] <= last(indj)-3 - @goto skip + if ii == ind[firstindex(ind)+3] && all_first + for j=i+1:nd + szj = length(axs[j+2]) + indj = tailinds[j] + if szj>10 && first(indj)+2 < idxs[j] <= last(indj)-3 + break _ + end end + print(io, ";"^(i+2)) + print(io, " \u2026 ") + show_full && print(io, "\n\n") + break _ + end + if ind[firstindex(ind)+2] < ii <= ind[end-3] + break _ end - print(io, ";"^(i+2)) - print(io, " \u2026 ") - show_full && print(io, "\n\n") - @goto skip - end - if ind[firstindex(ind)+2] < ii <= ind[end-3] - @goto skip end end end - end - if show_full - _show_nd_label(io, a, idxs) - end - slice = view(a, axs[1], axs[2], idxs...) - if show_full - print_matrix(io, slice) - print(io, idxs == map(last,tailinds) ? "" : "\n\n") - else - idxdiff = lastidxs .- idxs .< 0 - if any(idxdiff) - lastchangeindex = 2 + findlast(idxdiff) - print(io, ";"^lastchangeindex) - lastchangeindex == ndims(a) && (reached_last_d = true) - print(io, " ") + if show_full + _show_nd_label(io, a, idxs) + end + slice = view(a, axs[1], axs[2], idxs...) + if show_full + print_matrix(io, slice) + print(io, idxs == map(last,tailinds) ? "" : "\n\n") + else + idxdiff = lastidxs .- idxs .< 0 + if any(idxdiff) + lastchangeindex = 2 + findlast(idxdiff) + print(io, ";"^lastchangeindex) + lastchangeindex == ndims(a) && (reached_last_d = true) + print(io, " ") + end + print_matrix(io, slice) end - print_matrix(io, slice) end - @label skip lastidxs = idxs end if !show_full diff --git a/base/essentials.jl b/base/essentials.jl index 797c247949147..3e32245af4980 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -941,11 +941,49 @@ end Labels a statement with the symbolic label `name`. The label marks the end-point of an unconditional jump with [`@goto name`](@ref). + + @label _ expr + @label name expr + +Creates a labeled block that can be exited early with `break _ value` or `break name value`. +The block evaluates to `value` if a `break` statement is executed, otherwise it evaluates to +the result of `expr`. Use `@label _ expr` for anonymous blocks (break with `break _`) or +`@label name expr` for named blocks (break with `break name`). + +# Example +```julia +result = @label myblock begin + for i in 1:10 + if i > 5 + break myblock i * 2 # exits with value 12 + end + end + 0 # default value if no break +end +``` """ macro label(name::Symbol) return esc(Expr(:symboliclabel, name)) end +macro label(name::Symbol, body) + # If body is a syntactic loop, wrap its body in a continue block + # This allows `continue name` to work by breaking to `name#cont` + if body isa Expr && (body.head === :for || body.head === :while) + cont_name = Symbol(string(name, "#cont")) + if body.head === :for + loop_body = body.args[2] + wrapped_body = Expr(:symbolicblock, cont_name, loop_body) + body = Expr(:for, body.args[1], wrapped_body) + else # while + loop_body = body.args[2] + wrapped_body = Expr(:symbolicblock, cont_name, loop_body) + body = Expr(:while, body.args[1], wrapped_body) + end + end + return esc(Expr(:symbolicblock, name, body)) +end + """ @goto name diff --git a/base/loading.jl b/base/loading.jl index e578c14a059be..2cb51d41c2095 100644 --- a/base/loading.jl +++ b/base/loading.jl @@ -481,56 +481,48 @@ function locate_package_env(pkg::PkgId, stopenv::Union{String, Nothing}=nothing) specenv = get(cache.located, (pkg, stopenv), missing) specenv === missing || return specenv end - spec = nothing - env′ = nothing - syntax_version = VERSION - if pkg.uuid === nothing - # The project we're looking for does not have a Project.toml (n.b. - present - # `Project.toml` without UUID gets a path-based dummy UUID). It must have - # come from an implicit manifest environment, so go through those only. - # N.B.: Implicitly loaded packages do not participate in syntax versioning. - for env in load_path() - project_file = env_project_file(env) - (project_file isa Bool && project_file) || continue - found = implicit_manifest_pkgid(env, pkg.name) - if found !== nothing && found.uuid === nothing - @assert found.name == pkg.name - spec = implicit_manifest_uuid_load_spec(env, pkg) - env′ = env - @goto done - end - if !(loading_extension || precompiling_extension) - stopenv == env && @goto done - end - end - else - for env in load_path() - spec = manifest_uuid_load_spec(env, pkg) - # missing is used as a sentinel to stop looking further down in envs - if spec === missing - is_stdlib(pkg) && @goto stdlib_fallback - spec = nothing - @goto done + (env′, spec) = @label _ begin + if pkg.uuid === nothing + # The project we're looking for does not have a Project.toml (n.b. - present + # `Project.toml` without UUID gets a path-based dummy UUID). It must have + # come from an implicit manifest environment, so go through those only. + # N.B.: Implicitly loaded packages do not participate in syntax versioning. + for env in load_path() + project_file = env_project_file(env) + (project_file isa Bool && project_file) || continue + found = implicit_manifest_pkgid(env, pkg.name) + if found !== nothing && found.uuid === nothing + @assert found.name == pkg.name + break _ (env, implicit_manifest_uuid_load_spec(env, pkg)) + end + if !(loading_extension || precompiling_extension) + stopenv == env && break _ (nothing, nothing) + end end - if spec !== nothing - env′ = env - @goto done + else + for env in load_path() + spec = manifest_uuid_load_spec(env, pkg) + # missing is used as a sentinel to stop looking further down in envs + if spec === missing + is_stdlib(pkg) && break + break _ (nothing, nothing) + end + if spec !== nothing + break _ (env, spec) + end + if !(loading_extension || precompiling_extension) + stopenv == env && break + end end - if !(loading_extension || precompiling_extension) - stopenv == env && break + # Allow loading of stdlibs if the name/uuid are given + # e.g. if they have been explicitly added to the project/manifest + mbyspec = manifest_uuid_load_spec(Sys.STDLIB, pkg) + if mbyspec isa PkgLoadSpec + break _ (Sys.STDLIB, mbyspec) end end - @label stdlib_fallback - # Allow loading of stdlibs if the name/uuid are given - # e.g. if they have been explicitly added to the project/manifest - mbyspec = manifest_uuid_load_spec(Sys.STDLIB, pkg) - if mbyspec isa PkgLoadSpec - spec = mbyspec - env′ = Sys.STDLIB - @goto done - end + (nothing, nothing) end - @label done if spec !== nothing && !isfile_casesensitive(spec.path) spec = nothing end @@ -1177,42 +1169,43 @@ function explicit_manifest_deps_get(project_file::String, where::PkgId, name::St # a table of entries (deps = {"DepA" = "6ea...", "DepB" = "55d..."} deps = get(entry, "deps", nothing)::Union{Vector{String}, Dict{String, Any}, Nothing} local dep::Union{Nothing, PkgId} - if UUID(uuid) === where.uuid - dep = dep_stanza_get(deps, name) - - # We found `where` in this environment, but it did not have a deps entry for - # `name`. This is likely because the dependency was modified without a corresponding - # change to dependency's Project or our Manifest. Return a sentinel here indicating - # that we know the package, but do not know its UUID. The caller will terminate the - # search and provide an appropriate error to the user. - dep === nothing && return PkgId(name) - else - # Check if we're trying to load into an extension of this package - extensions = get(entry, "extensions", nothing) - if extensions !== nothing - if haskey(extensions, where.name) && where.uuid == uuid5(UUID(uuid), where.name) - if name == dep_name - # Extension loads its base package - return PkgId(UUID(uuid), name) - end - exts = extensions[where.name]::Union{String, Vector{String}} - # Extensions are allowed to load: - # 1. Any ordinary dep of the parent package - # 2. Any weakdep of the parent package declared as an extension trigger - for deps′ in (ext_may_load_weakdep(exts, name) ? - (get(entry, "weakdeps", nothing)::Union{Vector{String}, Dict{String, Any}, Nothing}, deps) : - (deps,)) - dep = dep_stanza_get(deps′, name) - dep === nothing && continue - @goto have_dep + @label _ begin + if UUID(uuid) === where.uuid + dep = dep_stanza_get(deps, name) + + # We found `where` in this environment, but it did not have a deps entry for + # `name`. This is likely because the dependency was modified without a corresponding + # change to dependency's Project or our Manifest. Return a sentinel here indicating + # that we know the package, but do not know its UUID. The caller will terminate the + # search and provide an appropriate error to the user. + dep === nothing && return PkgId(name) + else + # Check if we're trying to load into an extension of this package + extensions = get(entry, "extensions", nothing) + if extensions !== nothing + if haskey(extensions, where.name) && where.uuid == uuid5(UUID(uuid), where.name) + if name == dep_name + # Extension loads its base package + return PkgId(UUID(uuid), name) + end + exts = extensions[where.name]::Union{String, Vector{String}} + # Extensions are allowed to load: + # 1. Any ordinary dep of the parent package + # 2. Any weakdep of the parent package declared as an extension trigger + for deps′ in (ext_may_load_weakdep(exts, name) ? + (get(entry, "weakdeps", nothing)::Union{Vector{String}, Dict{String, Any}, Nothing}, deps) : + (deps,)) + dep = dep_stanza_get(deps′, name) + dep === nothing && continue + break _ + end + return PkgId(name) end - return PkgId(name) end + continue end - continue end - @label have_dep dep.uuid !== nothing && return dep # We have the dep, but it did not specify a UUID. In this case, @@ -2039,14 +2032,14 @@ function compilecache_freshest_path(pkg::PkgId; end end for build_id in try_build_ids - for path_to_try in cachepaths + @label next_path for path_to_try in cachepaths staledeps = stale_cachefile(pkg, build_id, sourcespec, path_to_try; ignore_loaded, requested_flags=flags) if staledeps === true continue end staledeps, _, _ = staledeps::Tuple{Vector{Any}, Union{Nothing, String}, UInt128} # finish checking staledeps module graph - for dep in staledeps + @label next_dep for dep in staledeps dep isa Module && continue modspec, modkey, modbuild_id = dep::Tuple{PkgLoadSpec, PkgId, UInt128} modpaths = get(() -> find_all_in_cache_path(modkey), cachepath_cache, modkey) @@ -2056,10 +2049,9 @@ function compilecache_freshest_path(pkg::PkgId; stale_cache, stale_cache_key) continue end - @goto check_next_dep + continue next_dep end - @goto check_next_path - @label check_next_dep + continue next_path end try # update timestamp of precompilation file so that it is the first to be tried by code loading @@ -2069,7 +2061,6 @@ function compilecache_freshest_path(pkg::PkgId; ex isa IOError || rethrow() end return path_to_try - @label check_next_path end end end @@ -2221,7 +2212,7 @@ end end end for build_id in try_build_ids - for path_to_try in paths::Vector{String} + @label next_path for path_to_try in paths::Vector{String} staledeps = stale_cachefile(pkg, build_id, sourcespec, path_to_try; reasons, stalecheck) if staledeps === true continue @@ -2248,7 +2239,7 @@ end continue else @debug "Rejecting cache file $path_to_try because module $modkey got loaded at a different version than expected." - @goto check_next_path + continue next_path end continue elseif dep === nothing @@ -2258,7 +2249,7 @@ end i = 0 end end - for i in reverse(eachindex(staledeps)) + @label next_dep for i in reverse(eachindex(staledeps)) dep = staledeps[i] dep isa Module && continue modspec, modkey, modbuild_id = dep::Tuple{PkgLoadSpec, PkgId, UInt128} @@ -2274,11 +2265,10 @@ end end modstaledeps, modocachepath, _ = modstaledeps::Tuple{Vector{Any}, Union{Nothing, String}, UInt128} staledeps[i] = (modspec, modkey, modbuild_id, modpath_to_try, modstaledeps, modocachepath) - @goto check_next_dep + continue next_dep end @debug "Rejecting cache file $path_to_try because required dependency $modkey with build ID $(UUID(modbuild_id)) is missing from the cache." - @goto check_next_path - @label check_next_dep + continue next_path end M = maybe_loaded_precompile(pkg, newbuild_id) if isa(M, Module) @@ -2302,7 +2292,7 @@ end dep = _include_from_serialized(modkey, modcachepath, modocachepath, modstaledeps; register = stalecheck) if !isa(dep, Module) @debug "Rejecting cache file $path_to_try because required dependency $modkey failed to load from cache file for $modcachepath." exception=dep - @goto check_next_path + continue next_path else startedloading = i + 1 end_loading(modkey, dep) @@ -2316,7 +2306,6 @@ end end isa(restored, Module) && return restored @debug "Deserialization checks failed while attempting to load cache from $path_to_try" exception=restored - @label check_next_path finally # cancel all start_loading locks that were taken but not fulfilled before failing for i in startedloading:length(staledeps) @@ -2667,19 +2656,20 @@ function find_unsuitable_manifests_versions() manifest_file isa String || continue # no manifest file m = parsed_toml(manifest_file) man_julia_version = get(m, "julia_version", nothing) - man_julia_version isa String || @goto mark - man_julia_version = VersionNumber(man_julia_version) - thispatch(man_julia_version) != thispatch(VERSION) && @goto mark - isempty(man_julia_version.prerelease) != isempty(VERSION.prerelease) && @goto mark - isempty(man_julia_version.prerelease) && continue - man_julia_version.prerelease[1] != VERSION.prerelease[1] && @goto mark - if VERSION.prerelease[1] == "DEV" - # manifests don't store the 2nd part of prerelease, so cannot check further - # so treat them specially in the warning - push!(dev_manifests, manifest_file) - end - continue - @label mark + @label _ begin + man_julia_version isa String || break _ + man_julia_version = VersionNumber(man_julia_version) + thispatch(man_julia_version) != thispatch(VERSION) && break _ + isempty(man_julia_version.prerelease) != isempty(VERSION.prerelease) && break _ + isempty(man_julia_version.prerelease) && continue + man_julia_version.prerelease[1] != VERSION.prerelease[1] && break _ + if VERSION.prerelease[1] == "DEV" + # manifests don't store the 2nd part of prerelease, so cannot check further + # so treat them specially in the warning + push!(dev_manifests, manifest_file) + end + continue + end push!(unsuitable_manifests, string(manifest_file, " (v", man_julia_version, ")")) end return unsuitable_manifests, dev_manifests diff --git a/base/partr.jl b/base/partr.jl index d488330f0c87e..417fa6c6d1eec 100644 --- a/base/partr.jl +++ b/base/partr.jl @@ -171,6 +171,7 @@ end function multiq_deletemin() local rn1::UInt32 + local heap, task tid = Threads.threadid() tp = ccall(:jl_threadpoolid, Int8, (Int16,), tid-1) + 1 @@ -179,38 +180,40 @@ function multiq_deletemin() end tpheaps = heaps[tp] - @label retry - GC.safepoint() - heap_p = UInt32(length(tpheaps)) - for i = UInt32(0):heap_p - if i == heap_p - return nothing - end - rn1 = cong(heap_p) - rn2 = cong(heap_p) - prio1 = tpheaps[rn1].priority - prio2 = tpheaps[rn2].priority - if prio1 > prio2 - prio1 = prio2 - rn1 = rn2 - elseif prio1 == prio2 && prio1 == typemax(UInt16) - continue - end - if trylock(tpheaps[rn1].lock) - if prio1 == tpheaps[rn1].priority - break + while true + GC.safepoint() + heap_p = UInt32(length(tpheaps)) + for i = UInt32(0):heap_p + if i == heap_p + return nothing + end + rn1 = cong(heap_p) + rn2 = cong(heap_p) + prio1 = tpheaps[rn1].priority + prio2 = tpheaps[rn2].priority + if prio1 > prio2 + prio1 = prio2 + rn1 = rn2 + elseif prio1 == prio2 && prio1 == typemax(UInt16) + continue + end + if trylock(tpheaps[rn1].lock) + if prio1 == tpheaps[rn1].priority + break + end + unlock(tpheaps[rn1].lock) end - unlock(tpheaps[rn1].lock) end - end - @assert @isdefined(rn1) "Assertion to tell the compiler about the definedness of this variable" + @assert @isdefined(rn1) "Assertion to tell the compiler about the definedness of this variable" - heap = tpheaps[rn1] - task = heap.tasks[1] - if ccall(:jl_set_task_tid, Cint, (Any, Cint), task, tid-1) == 0 - unlock(heap.lock) - @goto retry + heap = tpheaps[rn1] + task = heap.tasks[1] + if ccall(:jl_set_task_tid, Cint, (Any, Cint), task, tid-1) == 0 + unlock(heap.lock) + continue + end + break end ntasks = heap.ntasks @atomic :monotonic heap.ntasks = ntasks - Int32(1) diff --git a/base/strings/annotated_io.jl b/base/strings/annotated_io.jl index 519c6ebb7799d..3223e927b695d 100644 --- a/base/strings/annotated_io.jl +++ b/base/strings/annotated_io.jl @@ -165,28 +165,26 @@ This is implemented so that one can say write an `AnnotatedString` to an new annotation for each character. """ function _insert_annotations!(annots::Vector{RegionAnnotation}, newannots::Vector{RegionAnnotation}, offset::Int = 0) - run = 0 - if !isempty(annots) && last(last(annots).region) == offset - for i in reverse(axes(newannots, 1)) - annot = newannots[i] - first(annot.region) == 1 || continue - i <= length(annots) || continue - if annot.label == last(annots).label && annot.value == last(annots).value - valid_run = true - for runlen in 1:i + run = @label _ begin + if !isempty(annots) && last(last(annots).region) == offset + for i in reverse(axes(newannots, 1)) + annot = newannots[i] + first(annot.region) == 1 || continue + i <= length(annots) || continue + annot.label == last(annots).label || continue + annot.value == last(annots).value || continue + all(1:i) do runlen new = newannots[begin+runlen-1] old = annots[end-i+runlen] - if last(old.region) != offset || first(new.region) != 1 || old.label != new.label || old.value != new.value - valid_run = false - break - end - end - if valid_run - run = i - break - end + !(last(old.region) != offset || + first(new.region) != 1 || + old.label != new.label || + old.value != new.value) + end || continue + break _ i end end + break _ 0 end for runindex in 0:run-1 old_index = lastindex(annots) - run + 1 + runindex diff --git a/base/strings/string.jl b/base/strings/string.jl index ab139a89af9ff..66f5ef1340fcc 100644 --- a/base/strings/string.jl +++ b/base/strings/string.jl @@ -461,24 +461,25 @@ end # duck-type s so that external UTF-8 string packages like StringViews can hook in function iterate_continued(s, i::Int, u::UInt32) - u < 0xc0000000 && (i += 1; @goto ret) - n = ncodeunits(s) - # first continuation byte - (i += 1) > n && @goto ret - @inbounds b = codeunit(s, i) - b & 0xc0 == 0x80 || @goto ret - u |= UInt32(b) << 16 - # second continuation byte - ((i += 1) > n) | (u < 0xe0000000) && @goto ret - @inbounds b = codeunit(s, i) - b & 0xc0 == 0x80 || @goto ret - u |= UInt32(b) << 8 - # third continuation byte - ((i += 1) > n) | (u < 0xf0000000) && @goto ret - @inbounds b = codeunit(s, i) - b & 0xc0 == 0x80 || @goto ret - u |= UInt32(b); i += 1 -@label ret + @label _ begin + u < 0xc0000000 && (i += 1; break _) + n = ncodeunits(s) + # first continuation byte + (i += 1) > n && break _ + @inbounds b = codeunit(s, i) + b & 0xc0 == 0x80 || break _ + u |= UInt32(b) << 16 + # second continuation byte + ((i += 1) > n) | (u < 0xe0000000) && break _ + @inbounds b = codeunit(s, i) + b & 0xc0 == 0x80 || break _ + u |= UInt32(b) << 8 + # third continuation byte + ((i += 1) > n) | (u < 0xf0000000) && break _ + @inbounds b = codeunit(s, i) + b & 0xc0 == 0x80 || break _ + u |= UInt32(b); i += 1 + end return reinterpret(Char, u), i end @@ -491,28 +492,29 @@ end # duck-type s so that external UTF-8 string packages like StringViews can hook in function getindex_continued(s, i::Int, u::UInt32) - if u < 0xc0000000 - # called from `getindex` which checks bounds - @inbounds isvalid(s, i) && @goto ret - string_index_err(s, i) + @label _ begin + if u < 0xc0000000 + # called from `getindex` which checks bounds + @inbounds isvalid(s, i) && break _ + string_index_err(s, i) + end + n = ncodeunits(s) + + (i += 1) > n && break _ + @inbounds b = codeunit(s, i) # cont byte 1 + b & 0xc0 == 0x80 || break _ + u |= UInt32(b) << 16 + + ((i += 1) > n) | (u < 0xe0000000) && break _ + @inbounds b = codeunit(s, i) # cont byte 2 + b & 0xc0 == 0x80 || break _ + u |= UInt32(b) << 8 + + ((i += 1) > n) | (u < 0xf0000000) && break _ + @inbounds b = codeunit(s, i) # cont byte 3 + b & 0xc0 == 0x80 || break _ + u |= UInt32(b) end - n = ncodeunits(s) - - (i += 1) > n && @goto ret - @inbounds b = codeunit(s, i) # cont byte 1 - b & 0xc0 == 0x80 || @goto ret - u |= UInt32(b) << 16 - - ((i += 1) > n) | (u < 0xe0000000) && @goto ret - @inbounds b = codeunit(s, i) # cont byte 2 - b & 0xc0 == 0x80 || @goto ret - u |= UInt32(b) << 8 - - ((i += 1) > n) | (u < 0xf0000000) && @goto ret - @inbounds b = codeunit(s, i) # cont byte 3 - b & 0xc0 == 0x80 || @goto ret - u |= UInt32(b) -@label ret return reinterpret(Char, u) end diff --git a/src/julia-parser.scm b/src/julia-parser.scm index 1a11494b5c8e3..6abd31b429d46 100644 --- a/src/julia-parser.scm +++ b/src/julia-parser.scm @@ -1597,13 +1597,60 @@ (if (or (eqv? t #\newline) (closing-token? t)) (list 'return '(null)) (list 'return (parse-eq s))))) - ((break continue) + ((continue) (let ((t (peek-token s))) - (if (or (eof-object? t) - (and (eq? t 'end) (not end-symbol)) - (memv t '(#\newline #\; #\) :))) - (list word) - (error (string "unexpected \"" t "\" after " word))))) + (cond ((or (eof-object? t) + (and (eq? t 'end) (not end-symbol)) + (memv t '(#\newline #\; #\)))) + ;; continue with no arguments + (list word)) + ((and range-colon-enabled (eq? t ':)) + ;; Could be :label or ternary. Take : and check if immediately followed by identifier. + (take-token s) + (let ((nxt (peek-token s))) + (if (or (closing-token? nxt) (newline? nxt) (ts:space? s)) + ;; Space after : or closer - this is ternary, put back : + (begin (ts:put-back! s ': #t) ;; had space before : + (list word)) + ;; No space after :, parse as atom (label validated in lowering) + (begin + (ts:put-back! s ': #f) ;; put back : with no preceding space + (list word (parse-atom s)))))) + (else + ;; Parse label as atom (validated in lowering) + (list word (parse-atom s)))))) + + ((break) + (let ((t (peek-token s))) + (define (parse-break-value lbl) + (let ((t2 (peek-token s))) + (if (or (eof-object? t2) + (and (eq? t2 'end) (not end-symbol)) + (memv t2 '(#\newline #\; #\) :))) + ;; break label + (list word lbl) + ;; break label value + (list word lbl (parse-eq s))))) + (cond ((or (eof-object? t) + (and (eq? t 'end) (not end-symbol)) + (memv t '(#\newline #\; #\)))) + ;; break with no arguments + (list word)) + ((and range-colon-enabled (eq? t ':)) + ;; Could be :label or ternary. Take : and check if immediately followed by identifier. + (take-token s) + (let ((nxt (peek-token s))) + (if (or (closing-token? nxt) (newline? nxt) (ts:space? s)) + ;; Space after : or closer - this is ternary, put back : + (begin (ts:put-back! s ': #t) ;; had space before : + (list word)) + ;; No space after :, parse as atom (label validated in lowering) + (begin + (ts:put-back! s ': #f) ;; put back : with no preceding space + (parse-break-value (parse-atom s)))))) + (else + ;; Parse label as atom (validated in lowering) + (parse-break-value (parse-atom s)))))) ((module baremodule) (let* ((name (parse-unary-prefix s)) diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index c6e34d9db79fb..d7160ee6c8d90 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -2862,10 +2862,42 @@ 'break (lambda (e) (if (pair? (cdr e)) - e + ;; break with label - validate and unwrap quote/inert if needed + ;; Note: QuoteNode becomes (inert sym) when converted from Julia to flisp + (let* ((label-expr (cadr e)) + (label (cond ((symbol? label-expr) label-expr) ;; plain identifier (including _) + ((and (pair? label-expr) + (memq (car label-expr) '(quote inert)) + (symbol? (cadr label-expr))) + (cadr label-expr)) + (else (error "invalid break label: expected identifier"))))) + (if (length> e 2) + ;; (break label val) -> expand val + `(break ,label ,(expand-forms (caddr e))) + `(break ,label))) '(break loop-exit))) - 'continue (lambda (e) '(break loop-cont)) + 'continue + (lambda (e) + (if (pair? (cdr e)) + ;; continue with label - validate and unwrap quote/inert if needed + ;; Note: QuoteNode becomes (inert sym) when converted from Julia to flisp + (let* ((label-expr (cadr e)) + (name (cond ((symbol? label-expr) label-expr) ;; plain identifier (including _) + ((and (pair? label-expr) + (memq (car label-expr) '(quote inert)) + (symbol? (cadr label-expr))) + (cadr label-expr)) + (else (error "invalid continue label: expected identifier"))))) + ;; (continue name) -> (break name#cont) + `(break ,(symbol (string name "#cont")))) + '(break loop-cont))) + + 'symbolicblock + (lambda (e) + ;; (symbolicblock name body) -> expand body + ;; The @label macro inserts the continue block for loops, so we just expand the body + `(symbolicblock ,(cadr e) ,(expand-forms (caddr e)))) 'for (lambda (e) @@ -3259,7 +3291,12 @@ ;; returns lambdas in the form (lambda (args...) (locals...) body) (define (resolve-scopes- e scope (sp '()) (loc #f)) - (cond ((symbol? e) + (cond ((and (pair? e) (eq? (car e) 'break)) + ;; break may have a value that needs scope resolution + (if (length> e 2) + `(break ,(cadr e) ,(resolve-scopes- (caddr e) scope sp loc)) + e)) + ((symbol? e) (let ((val (and scope (get (scope:table scope) e #f)))) (cond (val (car val)) ((underscore-symbol? e) e) @@ -3428,6 +3465,9 @@ ((eq? (car e) 'break-block) `(break-block ,(cadr e) ;; ignore type symbol of break-block expression ,(resolve-scopes- (caddr e) scope '() loc))) ;; body of break-block expression + ((eq? (car e) 'symbolicblock) + `(symbolicblock ,(cadr e) ;; label name of symbolic block + ,(resolve-scopes- (caddr e) scope '() loc))) ;; body of symbolic block ((eq? (car e) 'with-static-parameters) `(with-static-parameters ,(resolve-scopes- (cadr e) scope (cddr e) loc) @@ -3472,6 +3512,7 @@ ((symbol? e) (put! tab e #t)) ((and (pair? e) (memq (car e) '(global globalref))) tab) ((and (pair? e) (eq? (car e) 'break-block)) (free-vars- (caddr e) tab)) + ((and (pair? e) (eq? (car e) 'symbolicblock)) (free-vars- (caddr e) tab)) ((and (pair? e) (eq? (car e) 'with-static-parameters)) (free-vars- (cadr e) tab)) ((or (atom? e) (quoted? e)) tab) ((eq? (car e) 'lambda) @@ -4015,6 +4056,8 @@ f(x) = yt(x) (eager-any visit (cdr e))) ((eq? (car e) 'break-block) (visit (caddr e))) + ((eq? (car e) 'symbolicblock) + (visit (caddr e))) ((eq? (car e) 'return) (begin0 (visit (cadr e)) (kill))) @@ -4167,7 +4210,12 @@ f(x) = yt(x) ((atom? e) e) (else (case (car e) - ((quote top core globalref thismodule thisfunction lineinfo line break inert module toplevel null true false meta) e) + ((quote top core globalref thismodule thisfunction lineinfo line inert module toplevel null true false meta) e) + ((break) + ;; break may have a value that needs closure conversion + (if (length> e 2) + `(break ,(cadr e) ,(cl-convert (caddr e) fname lam namemap defined toplevel interp opaq toplevel-pure parsed-method-stack globals locals)) + e)) ((toplevel_pure) ;; Used to wrap the Expr returned from generation functions: do not ;; generate top-level side effects for this Expr (declare_global). @@ -4990,11 +5038,49 @@ f(x) = yt(x) #f #f) (mark-label endl)) (if value (compile '(null) break-labels value tail))) + ((symbolicblock) + ;; Labeled block/loop that can be exited with `break name value` + (let* ((name (cadr e)) + (body (caddr e)) + (endl (make-label)) + (need-value (or value tail)) + ;; Create result-var if value is needed (for break name val support) + (result-var (if need-value (new-mutable-var name) #f))) + ;; Register the symbolic block to prevent mixing with @goto + (if (has? label-nesting name) + (error (string "label \"" name "\" defined multiple times"))) + (put! label-nesting name 'symbolicblock) + ;; Initialize result-var to nothing (for break without value case) + (if result-var + (emit `(= ,result-var (null)))) + ;; Compile body with this block in break-labels + (let ((body-val (compile body + (cons (list name endl handler-token-stack catch-token-stack result-var) + break-labels) + need-value #f))) + ;; If body produces a value and we haven't broken, assign it to result-var + (if (and result-var body-val) + (emit `(= ,result-var ,body-val)))) + (mark-label endl) + ;; Return result-var if value is needed + (cond (tail (emit-return tail result-var)) + (value result-var) + (else #f)))) ((break) (let ((labl (assq (cadr e) break-labels))) (if (not labl) (error "break or continue outside loop") - (emit-break labl)))) + (begin + ;; Check if this is a valued break (break name val) + (if (length> e 2) + ;; symbolicblock entries have 5 elements, regular break-block entries have 4 + (let ((result-var (and (length> labl 4) (list-ref labl 4)))) + (if (not result-var) + (error (string "break with value not allowed for label \"" (cadr e) "\"")) + (let ((val (compile (caddr e) break-labels #t #f))) + (emit `(= ,result-var ,val)))))) + (emit-break labl) + #f)))) ((label symboliclabel) (if (eq? (car e) 'symboliclabel) (if (has? label-nesting (cadr e)) @@ -5008,6 +5094,9 @@ f(x) = yt(x) (emit-return tail '(null)) (if value (error "misplaced label"))))) ((symbolicgoto) + ;; Check if target is a symbolicblock (not allowed) + (if (eq? (get label-nesting (cadr e) #f) 'symbolicblock) + (error (string "cannot use @goto to jump to @label block \"" (cadr e) "\""))) (let* ((m (get label-map (cadr e) #f)) (m (or m (let ((l (make-label))) (put! label-map (cadr e) l) @@ -5272,6 +5361,9 @@ f(x) = yt(x) (let ((target-nesting (get label-nesting lab #f))) (if (not target-nesting) (error (string "label \"" lab "\" referenced but not defined"))) + ;; Check if target is a symbolicblock (cannot use @goto to jump to @label block) + (if (eq? target-nesting 'symbolicblock) + (error (string "cannot use @goto to jump to @label block \"" lab "\""))) (let ((target-handler-tokens (car target-nesting)) (target-catch-tokens (cadr target-nesting))) (let ((plist (pop-handler-list src-handler-tokens target-handler-tokens lab))) diff --git a/src/macroexpand.scm b/src/macroexpand.scm index 2990d98eefe6e..74fa437e7c44b 100644 --- a/src/macroexpand.scm +++ b/src/macroexpand.scm @@ -429,6 +429,9 @@ ((macrocall) e) ; invalid syntax anyways, so just act like it's quoted. ((symboliclabel) e) ((symbolicgoto) e) + ((symbolicblock) + ;; recursively expand the body of a symbolic block + `(symbolicblock ,(cadr e) ,(resolve-expansion-vars- (caddr e) env m lno parent-scope inarg))) ((struct) `(struct ,(cadr e) ,(resolve-expansion-vars- (caddr e) env m lno parent-scope inarg) ,(map (lambda (x) @@ -674,6 +677,27 @@ (newlabel (if havelabel havelabel (named-gensy s)))) (if (not havelabel) (put! relabels s newlabel)) `(,(car e) ,newlabel))) + ((eq? (car e) 'symbolicblock) + ;; rename label and recurse into body + (let* ((s (cadr e)) + (havelabel (if (or (null? parent-scope) (not (symbol? s))) s (get relabels s #f))) + (newlabel (if havelabel havelabel (named-gensy s)))) + (if (not havelabel) (put! relabels s newlabel)) + `(symbolicblock ,newlabel ,(rename-symbolic-labels- (caddr e) relabels parent-scope)))) + ((and (eq? (car e) 'break) (pair? (cdr e)) (symbol? (cadr e))) + ;; rename break label if it exists in relabels + (let* ((s (cadr e)) + (newlabel (if (null? parent-scope) s (get relabels s s)))) + (if (length> e 2) + ;; break label val + `(break ,newlabel ,(rename-symbolic-labels- (caddr e) relabels parent-scope)) + ;; break label + `(break ,newlabel)))) + ((and (eq? (car e) 'continue) (pair? (cdr e)) (symbol? (cadr e))) + ;; rename continue label if it exists in relabels + (let* ((s (cadr e)) + (newlabel (if (null? parent-scope) s (get relabels s s)))) + `(continue ,newlabel))) (else (cons (car e) (map (lambda (x) (rename-symbolic-labels- x relabels parent-scope)) diff --git a/stdlib/Dates/src/parse.jl b/stdlib/Dates/src/parse.jl index 3730e8877339e..4f4d9f40e4c6e 100644 --- a/stdlib/Dates/src/parse.jl +++ b/stdlib/Dates/src/parse.jl @@ -216,85 +216,85 @@ function Base.parse(::Type{DateTime}, s::AbstractString, df::typeof(ISODateTimeF local dy dm = dd = Int64(1) th = tm = ts = tms = Int64(0) + @label error begin + @label done begin + # Optional sign + let val = tryparsenext_sign(s, i, end_pos) + if val !== nothing + coefficient, i = val + end + end - # Optional sign - let val = tryparsenext_sign(s, i, end_pos) - if val !== nothing - coefficient, i = val - end - end + let val = tryparsenext_base10(s, i, end_pos, 1) + val === nothing && break error + dy, i = val + i > end_pos && break done + end - let val = tryparsenext_base10(s, i, end_pos, 1) - val === nothing && @goto error - dy, i = val - i > end_pos && @goto done - end + c, i = iterate(s, i)::Tuple{Char, Int} + c != '-' && break error + i > end_pos && break done - c, i = iterate(s, i)::Tuple{Char, Int} - c != '-' && @goto error - i > end_pos && @goto done + let val = tryparsenext_base10(s, i, end_pos, 1, 2) + val === nothing && break error + dm, i = val + i > end_pos && break done + end - let val = tryparsenext_base10(s, i, end_pos, 1, 2) - val === nothing && @goto error - dm, i = val - i > end_pos && @goto done - end + c, i = iterate(s, i)::Tuple{Char, Int} + c != '-' && break error + i > end_pos && break done - c, i = iterate(s, i)::Tuple{Char, Int} - c != '-' && @goto error - i > end_pos && @goto done + let val = tryparsenext_base10(s, i, end_pos, 1, 2) + val === nothing && break error + dd, i = val + i > end_pos && break done + end - let val = tryparsenext_base10(s, i, end_pos, 1, 2) - val === nothing && @goto error - dd, i = val - i > end_pos && @goto done - end + c, i = iterate(s, i)::Tuple{Char, Int} + c != 'T' && break error + i > end_pos && break done - c, i = iterate(s, i)::Tuple{Char, Int} - c != 'T' && @goto error - i > end_pos && @goto done + let val = tryparsenext_base10(s, i, end_pos, 1, 2) + val === nothing && break error + th, i = val + i > end_pos && break done + end - let val = tryparsenext_base10(s, i, end_pos, 1, 2) - val === nothing && @goto error - th, i = val - i > end_pos && @goto done - end + c, i = iterate(s, i)::Tuple{Char, Int} + c != ':' && break error + i > end_pos && break done - c, i = iterate(s, i)::Tuple{Char, Int} - c != ':' && @goto error - i > end_pos && @goto done + let val = tryparsenext_base10(s, i, end_pos, 1, 2) + val === nothing && break error + tm, i = val + i > end_pos && break done + end - let val = tryparsenext_base10(s, i, end_pos, 1, 2) - val === nothing && @goto error - tm, i = val - i > end_pos && @goto done - end + c, i = iterate(s, i)::Tuple{Char, Int} + c != ':' && break error + i > end_pos && break done - c, i = iterate(s, i)::Tuple{Char, Int} - c != ':' && @goto error - i > end_pos && @goto done + let val = tryparsenext_base10(s, i, end_pos, 1, 2) + val === nothing && break error + ts, i = val + i > end_pos && break done + end - let val = tryparsenext_base10(s, i, end_pos, 1, 2) - val === nothing && @goto error - ts, i = val - i > end_pos && @goto done - end + c, i = iterate(s, i)::Tuple{Char, Int} + c != '.' && break error + i > end_pos && break done - c, i = iterate(s, i)::Tuple{Char, Int} - c != '.' && @goto error - i > end_pos && @goto done + let val = tryparsenext_base10(s, i, end_pos, 1, 3) + val === nothing && break error + tms, j = val + tms *= 10 ^ (3 - (j - i)) + j > end_pos || break error + end + end - let val = tryparsenext_base10(s, i, end_pos, 1, 3) - val === nothing && @goto error - tms, j = val - tms *= 10 ^ (3 - (j - i)) - j > end_pos || @goto error + return DateTime(dy * coefficient, dm, dd, th, tm, ts, tms) end - - @label done - return DateTime(dy * coefficient, dm, dd, th, tm, ts, tms) - - @label error throw(ArgumentError("Invalid DateTime string")) end diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index a4cc0bb0e94b0..0f2fd10f047a0 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -1625,19 +1625,20 @@ function setup_interface( linfos = repl.last_shown_line_infos str = String(take!(LineEdit.buffer(s))) n = tryparse(Int, str) - n === nothing && @goto writeback - if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[") - @goto writeback - end - try - InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2]) - catch ex - ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow() - @info "edit failed" _exception=ex + @label writeback begin + n === nothing && break writeback + if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[") + break writeback + end + try + InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2]) + catch ex + ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow() + @info "edit failed" _exception=ex + end + LineEdit.refresh_line(s) + return end - LineEdit.refresh_line(s) - return - @label writeback write(LineEdit.buffer(s), str) return end, diff --git a/stdlib/Test/src/Test.jl b/stdlib/Test/src/Test.jl index 5d7d8a692aa5f..949cca3c60db6 100644 --- a/stdlib/Test/src/Test.jl +++ b/stdlib/Test/src/Test.jl @@ -443,14 +443,15 @@ so that e.g. `@test a ≈ b atol=ε` means `@test ≈(a, b, atol=ε)`. test_expr!(m, ex) = ex function test_expr!(m, ex, kws...) - ex isa Expr && ex.head === :call || @goto fail - for kw in kws - kw isa Expr && kw.head === :(=) || @goto fail - kw.head = :kw - push!(ex.args, kw) + @label fail begin + ex isa Expr && ex.head === :call || break fail + for kw in kws + kw isa Expr && kw.head === :(=) || break fail + kw.head = :kw + push!(ex.args, kw) + end + return ex end - return ex -@label fail error("invalid test macro call: $m $ex $(join(kws," "))") end diff --git a/test/goto.jl b/test/goto.jl index e069058f38d52..37a5061baaf42 100644 --- a/test/goto.jl +++ b/test/goto.jl @@ -168,3 +168,374 @@ function foo28077() return s end @test foo28077() == 55 + +# Block break tests (@label name expr with break name val) + +# Basic block break with value +function block_break_test1() + @label myblock begin + for i in 1:10 + if i > 5 + break myblock i * 2 + end + end + 0 + end +end +@test block_break_test1() == 12 + +# Block break without value (returns nothing) +function block_break_test2() + result = @label myblock begin + for i in 1:10 + if i > 5 + break myblock + end + end + 42 + end + result +end +@test block_break_test2() === nothing + +# Block break from nested loops +function block_break_test3() + @label outer begin + for i in 1:5 + for j in 1:5 + if i * j > 10 + local result = (i, j) + break outer result + end + end + end + (0, 0) + end +end +@test block_break_test3() == (3, 4) + +# Block break with no break executed (returns body value) +function block_break_test4() + @label myblock begin + x = 1 + 2 + x * 3 + end +end +@test block_break_test4() == 9 + +# Block break in tail position +function block_break_test5(n) + @label myblock begin + if n > 0 + break myblock n * 2 + end + -1 + end +end +@test block_break_test5(5) == 10 +@test block_break_test5(-1) == -1 + +# Error: cannot use @goto to jump to @label block +@test Expr(:error, "cannot use @goto to jump to @label block \"myblock\"") == + Meta.lower(@__MODULE__, quote + function block_break_test_error1() + @goto myblock + @label myblock begin + 42 + end + end + end) + +# Break with value DOES work for labeled loops (returns the value) +function block_break_value_from_loop() + @label loop_exit while true + break loop_exit 42 + end +end +@test block_break_value_from_loop() == 42 + +# Error: duplicate label (symbolicblock and symboliclabel) +@test Expr(:error, "label \"foo\" defined multiple times") == + Meta.lower(@__MODULE__, quote + function block_break_test_error3() + @label foo begin + 42 + end + @label foo + end + end) + +# Nested block breaks +function block_break_nested() + @label outer begin + x = @label inner begin + if true + break inner 10 + end + 0 + end + x + 5 + end +end +@test block_break_nested() == 15 + +# Block break with computed value +function block_break_computed(arr) + @label search begin + for (i, v) in enumerate(arr) + if v > 100 + break search i => v + end + end + nothing + end +end +@test block_break_computed([1, 50, 150, 200]) == (3 => 150) +@test block_break_computed([1, 2, 3]) === nothing + +# Labeled continue tests +function labeled_continue_test1() + result = Int[] + @label outer for i in 1:5 + for j in 1:5 + if j > 2 + continue outer # skip to next i + end + push!(result, i * 10 + j) + end + end + result +end +@test labeled_continue_test1() == [11, 12, 21, 22, 31, 32, 41, 42, 51, 52] + +# Labeled break from outer loop +function labeled_break_loop_test() + result = Int[] + @label outer for i in 1:5 + for j in 1:5 + if i == 3 && j == 2 + break outer # exit both loops + end + push!(result, i * 10 + j) + end + end + result +end +@test labeled_break_loop_test() == [11, 12, 13, 14, 15, 21, 22, 23, 24, 25, 31] + +# Labeled break with value from loop +function labeled_break_value_test() + @label outer for i in 1:10 + for j in 1:10 + if i * j > 50 + local result = (i, j, i * j) + break outer result + end + end + end +end +@test labeled_break_value_test() == (6, 9, 54) + +# While loop with labeled continue +function while_labeled_continue() + result = Int[] + i = 0 + @label outer while i < 5 + i += 1 + j = 0 + while j < 5 + j += 1 + if j > 2 + continue outer + end + push!(result, i * 10 + j) + end + end + result +end +@test while_labeled_continue() == [11, 12, 21, 22, 31, 32, 41, 42, 51, 52] + +# Nested labeled blocks with loops +function nested_labeled_loops() + @label outer for i in 1:3 + x = @label inner for j in 1:3 + if j == 2 + break inner j * 100 + end + end + if x > 100 + break outer x + i + end + end +end +@test nested_labeled_loops() == 201 + +# Ternary operator disambiguation: `cond ? break : val` should work +# The space after `:` indicates ternary, not labeled break +function ternary_break_test() + for i in 1:10 + x = i > 5 ? break : i * 2 + @test x == i * 2 + end +end +ternary_break_test() + +function ternary_continue_test() + result = Int[] + for i in 1:10 + i > 5 ? continue : push!(result, i) + end + result +end +@test ternary_continue_test() == [1, 2, 3, 4, 5] + +# Also test labeled break/continue in ternary still works +function ternary_labeled_break() + @label outer for i in 1:5 + for j in 1:5 + # labeled break in ternary condition + i == 3 && j == 2 ? break outer : nothing + end + end + 42 +end +@test ternary_labeled_break() == 42 + +function ternary_labeled_continue() + result = Int[] + @label outer for i in 1:5 + for j in 1:5 + # labeled continue in ternary condition + j > 2 ? continue outer : push!(result, i * 10 + j) + end + end + result +end +@test ternary_labeled_continue() == [11, 12, 21, 22, 31, 32, 41, 42, 51, 52] + +# Tests for combined labeled and ordinary break/continue +# Ensure that ordinary break/continue still work inside labeled loops + +# Ordinary continue inside a labeled loop +function combined_ordinary_continue() + result = Int[] + @label outer for i in 1:5 + if i == 3 + continue # ordinary continue - should skip to next i + end + push!(result, i) + end + result +end +@test combined_ordinary_continue() == [1, 2, 4, 5] + +# Ordinary break inside a labeled loop +function combined_ordinary_break() + result = Int[] + @label outer for i in 1:5 + if i == 3 + break # ordinary break - should exit the loop + end + push!(result, i) + end + result +end +@test combined_ordinary_break() == [1, 2] + +# Both labeled and ordinary continue in the same loop +function combined_labeled_and_ordinary_continue() + result = Int[] + @label outer for i in 1:4 + for j in 1:4 + if j == 1 + continue # ordinary continue - skip to next j + end + if j == 2 + continue outer # labeled continue - skip to next i + end + push!(result, i * 10 + j) + end + push!(result, i * 100) # should not be reached due to continue outer at j==2 + end + result +end +@test combined_labeled_and_ordinary_continue() == Int64[] + +# Both labeled and ordinary break in the same nested loop +function combined_labeled_and_ordinary_break() + result = Int[] + @label outer for i in 1:4 + for j in 1:4 + push!(result, i * 10 + j) + if j == 2 && i == 1 + break # ordinary break - exit inner loop only + end + if j == 1 && i == 2 + break outer # labeled break - exit outer loop + end + end + push!(result, i * 100) + end + result +end +# i=1: j=1->11, j=2->12, break inner, 100 +# i=2: j=1->21, break outer +@test combined_labeled_and_ordinary_break() == [11, 12, 100, 21] + +# Multiple nested loops with mixed break/continue +function combined_triple_nested() + result = Int[] + @label outer for i in 1:3 + @label middle for j in 1:3 + for k in 1:3 + if k == 2 + continue # skip to next k + end + if k == 3 && j == 1 + continue middle # skip to next j + end + if k == 3 && j == 2 + continue outer # skip to next i + end + push!(result, i * 100 + j * 10 + k) + end + push!(result, -(i * 10 + j)) # middle loop epilogue + end + push!(result, i * 1000) # outer loop epilogue + end + result +end +# i=1, j=1: k=1->111, k=2 skipped, k=3 continue middle +# i=1, j=2: k=1->121, k=2 skipped, k=3 continue outer +# i=2, j=1: k=1->211, k=2 skipped, k=3 continue middle +# i=2, j=2: k=1->221, k=2 skipped, k=3 continue outer +# i=3, j=1: k=1->311, k=2 skipped, k=3 continue middle +# i=3, j=2: k=1->321, k=2 skipped, k=3 continue outer +@test combined_triple_nested() == [111, 121, 211, 221, 311, 321] + +# Unlabeled block inside labeled loop - break should exit the inner block, not the loop +function unlabeled_block_in_labeled_loop() + result = Int[] + @label outer for i in 1:3 + x = @label _ begin + if i == 2 + break _ 100 # break out of unlabeled block with value + end + i * 10 + end + push!(result, x) + if i == 2 && x == 100 + continue # ordinary continue - should work + end + push!(result, x + 1) + end + result +end +@test unlabeled_block_in_labeled_loop() == [10, 11, 100, 30, 31] + +# Nested `_` labels should error - `_` doesn't get special treatment +@test_throws "label \"_\" defined multiple times" @eval @label _ begin + @label _ begin + break _ 42 + end +end