diff --git a/AGENTS.md b/AGENTS.md index d81defa16f136..6c6205fd0d021 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,10 @@ If you made changes to the runtime (any files in `src/`), you will need to rebui julia. Run `make -j` to rebuild julia. This process may take up to 10 minutes depending on your changes. +If you made changes to Scheme files (e.g. `src/julia-syntax.scm`, `src/julia-parser.scm`), +you can test them quickly with `make -C src` which rebuilds just the flisp bootstrap. +After that, run `make -j` to rebuild the full system with the changes. + After making changes, run static analysis checks: - First run `make -C src install-analysis-deps` to initialize dependencies (only needed once the first time). - Run `make -C src analyze- --output-sync -j8` (replace `` with the basename of any C or C++ file you modified, excluding headers). diff --git a/JuliaSyntax/src/integration/expr.jl b/JuliaSyntax/src/integration/expr.jl index c5ecc5365ce88..c7a8e205b755e 100644 --- a/JuliaSyntax/src/integration/expr.jl +++ b/JuliaSyntax/src/integration/expr.jl @@ -610,6 +610,11 @@ end elseif k == K"juxtapose" retexpr.head = :call pushfirst!(args, :*) + elseif k == K"match" + # Change head to match? when EXCEPT_FLAG is set + if has_flags(nodehead, EXCEPT_FLAG) + retexpr.head = Symbol("match?") + end elseif k == K"struct" @assert args[2].head == :block orig_fields = args[2].args diff --git a/JuliaSyntax/src/julia/julia_parse_stream.jl b/JuliaSyntax/src/julia/julia_parse_stream.jl index 87ad038699a77..9bd147872ebdb 100644 --- a/JuliaSyntax/src/julia/julia_parse_stream.jl +++ b/JuliaSyntax/src/julia/julia_parse_stream.jl @@ -58,6 +58,11 @@ Set for K"module" when it's not bare (`module`, not `baremodule`) """ const BARE_MODULE_FLAG = RawFlags(1<<8) +""" +Set for K"match" when it's an exception-catching match (`match?`, not `match`) +""" +const EXCEPT_FLAG = RawFlags(1<<8) + # Flags holding the dimension of an nrow or other UInt8 not held in the source # TODO: Given this is only used for nrow/ncat, we could actually use all the flags? const NUMERIC_FLAGS = RawFlags(RawFlags(0xff)<<8) diff --git a/JuliaSyntax/src/julia/kinds.jl b/JuliaSyntax/src/julia/kinds.jl index 07b005e07ed9f..0fb2a9026682e 100644 --- a/JuliaSyntax/src/julia/kinds.jl +++ b/JuliaSyntax/src/julia/kinds.jl @@ -229,6 +229,7 @@ register_kinds!(JuliaSyntax, 0, [ "try" "using" "while" + "match" "BEGIN_BLOCK_CONTINUATION_KEYWORDS" "catch" "finally" @@ -1055,6 +1056,12 @@ register_kinds!(JuliaSyntax, 0, [ "macro_name" # Container for a single statement/atom plus any trivia and errors "wrapper" + # Pattern matching + "matcharm" + "match-assign" + "guard" + # Declared exceptions + "postfix-?" "END_SYNTAX_KINDS" # Special tokens diff --git a/JuliaSyntax/src/julia/parser.jl b/JuliaSyntax/src/julia/parser.jl index 75c806caaa60a..47d685791a189 100644 --- a/JuliaSyntax/src/julia/parser.jl +++ b/JuliaSyntax/src/julia/parser.jl @@ -21,24 +21,27 @@ struct ParseState whitespace_newline::Bool # Enable parsing `where` with high precedence where_enabled::Bool + # Parsing a match pattern - disables `->` as anonymous function syntax + match_pattern::Bool end # Normal context function ParseState(stream::ParseStream) - ParseState(stream, true, false, false, false, false, true) + ParseState(stream, true, false, false, false, false, true, false) end function ParseState(ps::ParseState; range_colon_enabled=nothing, space_sensitive=nothing, for_generator=nothing, end_symbol=nothing, whitespace_newline=nothing, - where_enabled=nothing) + where_enabled=nothing, match_pattern=nothing) ParseState(ps.stream, range_colon_enabled === nothing ? ps.range_colon_enabled : range_colon_enabled, space_sensitive === nothing ? ps.space_sensitive : space_sensitive, for_generator === nothing ? ps.for_generator : for_generator, end_symbol === nothing ? ps.end_symbol : end_symbol, whitespace_newline === nothing ? ps.whitespace_newline : whitespace_newline, - where_enabled === nothing ? ps.where_enabled : where_enabled) + where_enabled === nothing ? ps.where_enabled : where_enabled, + match_pattern === nothing ? ps.match_pattern : match_pattern) end # Functions to change parse state @@ -50,7 +53,12 @@ function normal_context(ps::ParseState) where_enabled=true, for_generator=false, end_symbol=false, - whitespace_newline=false) + whitespace_newline=false, + match_pattern=false) +end + +function with_match_pattern(ps::ParseState) + ParseState(ps, match_pattern=true) end function with_space_sensitive(ps::ParseState) @@ -253,12 +261,73 @@ function is_reserved_word(k) is_keyword(k) && !is_contextual_keyword(k) end +# Check if the current token is the identifier "match" +function is_match_identifier(ps::ParseState) + t = peek_full_token(ps.stream) + kind(t) == K"Identifier" || return false + r = byte_range(t) + length(r) != 5 && return false + buf = unsafe_textbuf(ps.stream) + buf[r[1]] == UInt8('m') && buf[r[2]] == UInt8('a') && + buf[r[3]] == UInt8('t') && buf[r[4]] == UInt8('c') && + buf[r[5]] == UInt8('h') +end + # Return true if the next word (or word pair) is reserved, introducing a # syntactic structure. function peek_initial_reserved_words(ps::ParseState) k = peek(ps) if is_initial_reserved_word(ps, k) return true + elseif is_match_identifier(ps) + # `match` is only a reserved word when followed by an expression that + # could be a valid scrutinee. When followed by certain tokens it's + # treated as an identifier for backwards compatibility: + # match(x, y) - function call (no whitespace before `(`) + # match[i] - indexing (no whitespace before `[`) + # foo(match) - match as argument + # match = 1 - assignment + # match::T - type assertion + # for match in x - loop variable + # match.field - field access + # match => v - pair + # match === x - comparison (binary operators) + # function match end - function definition (match followed by end) + t2 = peek_token(ps, 2) + k2 = kind(t2) + # Not a match statement if followed by closing tokens, assignment, newline, etc. + # A newline after match means it's a standalone expression (identifier) + if k2 in KSet") ] } , ; end EndMarker NewlineWs" + return false + end + # Not a match statement if followed by `(` or `[` without whitespace + if k2 in KSet"( [" && !preceding_whitespace(t2) + return false + end + # Special case: `match?` (exception-catching match) - ? without whitespace + # is part of the keyword, so match IS a reserved word + if k2 == K"?" && !preceding_whitespace(t2) + return true + end + # Not a match statement if followed by operators that can only be binary + # (not unary). Unary operators like `+`, `-`, `!`, `~` can start an expression. + if is_operator(k2) + # These can be unary prefix operators, so they CAN start an expression + # <: >: + - ! ~ ¬ √ ∛ ∜ ⋆ ± ∓ $ & : + # Note: `:` is for quoting symbols like `:foo` + # Note: `::` is NOT included - it's a type annotation operator + if k2 in KSet"<: >: + - ! ~ ¬ √ ∛ ∜ ⋆ ± ∓ $ & :" + return true + end + # All other operators are binary-only, so match is not a keyword + return false + end + # Assignment, field access, pair, iteration keywords, type annotation + if k2 in KSet"= . => :: in ∈" + return false + end + # Otherwise, match is a keyword + return true elseif is_contextual_keyword(k) k2 = peek(ps, 2, skip_newlines=false) return (k == K"mutable" && k2 == K"struct") || @@ -1411,7 +1480,8 @@ function parse_decl_with_initial_ex(ps::ParseState, mark) parse_where(ps, parse_call) emit(ps, mark, K"::", INFIX_FLAG) end - if peek(ps) == K"->" + # In match pattern context, -> is a delimiter, not anonymous function syntax + if peek(ps) == K"->" && !ps.match_pattern kb = peek_behind(ps).kind if kb == K"tuple" # (x,y) -> z @@ -1769,6 +1839,11 @@ function parse_call_chain(ps::ParseState, mark, is_macrocall=false) # f'ᵀ ==> (call-post f 'ᵀ) bump(ps, remap_kind=K"Identifier") emit(ps, mark, K"call", POSTFIX_OP_FLAG) + elseif k == K"?" && !preceding_whitespace(t) + # Postfix ? for declared exceptions - exception forwarding/unwrapping + # expr? ==> (postfix-? expr) + bump(ps, TRIVIA_FLAG) + emit(ps, mark, K"postfix-?") elseif k == K"{" processing_macro_name = maybe_parsed_macro_name( ps, processing_macro_name, last_identifier_orig_kind, mark) @@ -2046,6 +2121,8 @@ function parse_resword(ps::ParseState) emit(ps, mark, K"primitive") elseif word == K"try" parse_try(ps) + elseif word == K"Identifier" && is_match_identifier(ps) + parse_match(ps) elseif word == K"return" bump(ps, TRIVIA_FLAG) k = peek(ps) @@ -2412,6 +2489,128 @@ function parse_catch(ps::ParseState) emit(ps, mark, K"catch") end +# Parse match expression +# +# match x +# 1 -> :one +# 2 -> :two +# _ -> :other +# end +# +# ==> (match x (matcharm 1 :one) (matcharm 2 :two) (matcharm _ :other)) +# +# match (a, b) +# (1, x) | (x, 1) -> x + 1 +# _ -> 0 +# end +# +# ==> (match (tuple a b) (matcharm (call-i (tuple 1 x) | (tuple x 1)) (call-i x + 1)) (matcharm _ 0)) +# +# With if guards: +# match val +# (a, b) if sin(b) == 0. -> a +# (a, b) -> b +# end +# +# ==> (match val (matcharm (guard (tuple a b) (call-i sin(b) == 0.)) a) (matcharm (tuple a b) b)) +# +# Inline match-destructuring: +# match (a, b) = val +# +# ==> (match-assign (tuple a b) val) +function parse_match(ps::ParseState) + mark = position(ps) + bump(ps, TRIVIA_FLAG) # consume 'match' + + # Check for ? immediately after match (no whitespace) for exception-catching mode + is_exception_mode = false + if peek(ps) == K"?" && !preceding_whitespace(peek_token(ps)) + bump(ps, TRIVIA_FLAG) # consume '?' as trivia + is_exception_mode = true + end + + # Parse the first expression (could be scrutinee or pattern for inline destructuring) + # Use parse_comma which doesn't include assignment, so we can detect = for match-assign + let ps = with_match_pattern(ps) + parse_comma(ps) + end + # Check for inline match-destructuring: match pattern = expr + if peek(ps) == K"=" + bump(ps, TRIVIA_FLAG) # consume '=' + parse_eq(ps) # parse the value expression + emit(ps, mark, K"match-assign") + return + end + # Otherwise, this is a full match statement - the first expression is the scrutinee + # Expect newline or semicolon after scrutinee + k = peek(ps) + if k in KSet"NewlineWs ;" + bump(ps, TRIVIA_FLAG) + elseif k != K"end" + recover(is_closer_or_newline, ps, TRIVIA_FLAG, + error="expected newline or `;` after match expression") + end + # Parse match arms until 'end' + n_arms = 0 + while peek(ps) != K"end" && peek(ps) != K"EndMarker" + bump_trivia(ps) + if peek(ps) in KSet"end EndMarker" + break + end + arm_mark = position(ps) + # Parse pattern with match_pattern context (disables -> as anon function) + let ps = with_match_pattern(ps) + parse_eq(ps) + end + # Check for if guard: pattern if condition -> body + if peek(ps) == K"if" + guard_mark = position(ps) + bump(ps, TRIVIA_FLAG) # consume 'if' + # Parse guard condition (up to ->) + let ps = with_match_pattern(ps) + parse_cond(ps) + end + # Emit guard node wrapping pattern and condition + emit(ps, arm_mark, K"guard") + end + # Expect and consume -> + if peek(ps) == K"->" + bump(ps, TRIVIA_FLAG) + else + recover((ps, k) -> k in KSet"-> NewlineWs end", ps, TRIVIA_FLAG, + error="expected `->` in match arm") + if peek(ps) == K"->" + bump(ps, TRIVIA_FLAG) + end + end + # Parse body expression + k = peek(ps) + if k in KSet"NewlineWs ;" || k == K"end" + # Empty body - emit nothing, body is implicitly nothing + bump_invisible(ps, K"Placeholder") + else + parse_eq(ps) + end + emit(ps, arm_mark, K"matcharm") + n_arms += 1 + # Consume trailing newline/semicolon + k = peek(ps) + if k in KSet"NewlineWs ;" + bump(ps, TRIVIA_FLAG) + end + end + if n_arms == 0 + bump_invisible(ps, K"error", TRIVIA_FLAG, + error="match requires at least one arm") + end + bump_closing_token(ps, K"end") + if is_exception_mode + emit(ps, mark, K"match", EXCEPT_FLAG) + else + emit(ps, mark, K"match") + end +end + # flisp: parse-do function parse_do(ps::ParseState) mark = position(ps) diff --git a/JuliaSyntax/test/parser.jl b/JuliaSyntax/test/parser.jl index 2b3e106e1dae4..e2dfe3638109f 100644 --- a/JuliaSyntax/test/parser.jl +++ b/JuliaSyntax/test/parser.jl @@ -697,6 +697,31 @@ tests = [ "try x finally y catch e z end" => "(try (block x) (finally (block y)) (catch e (block z)))" "try x end" => "(try (block x) (error-t))" ], + JuliaSyntax.parse_match => [ + # Basic match + "match x\n 1 -> :one\n 2 -> :two\n _ -> :other\n end" => + "(match x (matcharm 1 (quote-: one)) (matcharm 2 (quote-: two)) (matcharm _ (quote-: other)))" + # Single arm + "match x; _ -> 0 end" => "(match x (matcharm _ 0))" + # Alternation with | + "match x\n (1, a) | (a, 1) -> a\n end" => + "(match x (matcharm (call-i (tuple-p 1 a) | (tuple-p a 1)) a))" + # Type constraint + "match x\n n::Int -> n * 2\n end" => + "(match x (matcharm (::-i n Int) (call-i n * 2)))" + # Value escaping with $ + "match x\n \$zero -> :zero\n n -> n\n end" => + "(match x (matcharm (\$ zero) (quote-: zero)) (matcharm n n))" + # Complex scrutinee + "match f(x, y)\n a -> a\n end" => + "(match (call f x y) (matcharm a a))" + # Empty body + "match x\n 1 ->\n 2 -> :two\n end" => + "(match x (matcharm 1 □) (matcharm 2 (quote-: two)))" + # Call pattern + "match x\n Some(y) -> y\n None() -> 0\n end" => + "(match x (matcharm (call Some y) y) (matcharm (call None) 0))" + ], JuliaSyntax.parse_imports => [ "import A as B: x" => "(import (: (error (as (importpath A) B)) (importpath x)))" "import A, y" => "(import (importpath A) (importpath y))" diff --git a/NEWS.md b/NEWS.md index bd72e673728f0..813bd509d9500 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,19 @@ Julia v1.14 Release Notes New language features --------------------- + - New `match` expression for pattern matching. The `match` statement allows matching values against + patterns with support for literals, wildcards (`_`), variable capture, type constraints (`::T`), + tuple destructuring, alternation (`|`), value escaping (`$`), and constructor patterns. + A `MatchError` is thrown if no pattern matches. + ```julia + match x + 0 -> "zero" + n::Int -> "integer: $n" + (a, b) -> "tuple: $a, $b" + _ -> "other" + end + ``` + - It is now possible to control which version of the Julia syntax will be used to parse a package by setting the `compat.julia` or `syntax.julia_version` key in Project.toml. This feature is similar to the notion of "editions" in other language ecosystems and will allow non-breaking evolution of Julia syntax in future versions. @@ -27,6 +40,9 @@ Build system changes New library functions --------------------- + - New `MatchError` exception type, thrown when a `match` expression fails to find a matching pattern. + - New `@match` macro providing pattern matching via a `begin...end` block syntax. + New library features -------------------- diff --git a/base/Base.jl b/base/Base.jl index 57d9915239fdf..c8297dcc1504e 100644 --- a/base/Base.jl +++ b/base/Base.jl @@ -90,6 +90,11 @@ include("reinterpretarray.jl") # Some type include("some.jl") +# Pattern matching runtime support +include("match.jl") + +# Declared exceptions runtime support is in Base_compiler.jl (except.jl) + include("dict.jl") include("set.jl") diff --git a/base/Base_compiler.jl b/base/Base_compiler.jl index 781ad5c69d512..5ff6c2054e6c1 100644 --- a/base/Base_compiler.jl +++ b/base/Base_compiler.jl @@ -311,6 +311,9 @@ include("traits.jl") include("range.jl") include("error.jl") +# Declared exceptions runtime support (needed early for type annotations) +include("except.jl") + # core numeric operations & types ==(x, y) = x === y include("bool.jl") diff --git a/base/Enums.jl b/base/Enums.jl index d4094945853ec..5f9c711574fff 100644 --- a/base/Enums.jl +++ b/base/Enums.jl @@ -147,15 +147,21 @@ macro enum(T::Union{Symbol,Expr}, syms...) end basetype = Int32 typename = T - if isa(T, Expr) && T.head === :(::) && length(T.args) == 2 && isa(T.args[1], Symbol) - typename = T.args[1] - basetype = Core.eval(__module__, T.args[2]) - if !isa(basetype, DataType) || !(basetype <: Integer) || !isbitstype(basetype) - throw(ArgumentError( - LazyString("invalid base type for Enum ", typename, ", ", T, "=::", basetype, "; base type must be an integer primitive type"))) + match T + T::Expr -> begin + if T.head === :(::) && length(T.args) == 2 && isa(T.args[1], Symbol) + typename = T.args[1] + basetype = Core.eval(__module__, T.args[2]) + if !isa(basetype, DataType) || !(basetype <: Integer) || !isbitstype(basetype) + throw(ArgumentError( + LazyString("invalid base type for Enum ", typename, ", ", T, "=::", basetype, "; base type must be an integer primitive type"))) + end + else + throw(ArgumentError(LazyString("invalid type expression for enum ", T))) + end end - elseif !isa(T, Symbol) - throw(ArgumentError(LazyString("invalid type expression for enum ", T))) + T::Symbol -> nothing + _ -> throw(ArgumentError(LazyString("invalid type expression for enum ", T))) end values = Vector{basetype}() seen = Set{Symbol}() @@ -168,22 +174,27 @@ macro enum(T::Union{Symbol,Expr}, syms...) end for s in syms s isa LineNumberNode && continue - if isa(s, Symbol) - if i == typemin(basetype) && !isempty(values) - throw(ArgumentError(LazyString("overflow in value \"", s, "\" of Enum ", typename))) + match s + s::Symbol -> begin + if i == typemin(basetype) && !isempty(values) + throw(ArgumentError(LazyString("overflow in value \"", s, "\" of Enum ", typename))) + end end - elseif isa(s, Expr) && - (s.head === :(=) || s.head === :kw) && - length(s.args) == 2 && isa(s.args[1], Symbol) - i = Core.eval(__module__, s.args[2]) # allow exprs, e.g. uint128"1" - if !isa(i, Integer) - throw(ArgumentError(LazyString("invalid value for Enum ", typename, ", ", s, "; values must be integers"))) + s::Expr -> begin + if (s.head === :(=) || s.head === :kw) && + length(s.args) == 2 && isa(s.args[1], Symbol) + i = Core.eval(__module__, s.args[2]) # allow exprs, e.g. uint128"1" + if !isa(i, Integer) + throw(ArgumentError(LazyString("invalid value for Enum ", typename, ", ", s, "; values must be integers"))) + end + i = convert(basetype, i) + s = s.args[1] + hasexpr = true + else + throw(ArgumentError(LazyString("invalid argument for Enum ", typename, ": ", s))) + end end - i = convert(basetype, i) - s = s.args[1] - hasexpr = true - else - throw(ArgumentError(LazyString("invalid argument for Enum ", typename, ": ", s))) + _ -> throw(ArgumentError(LazyString("invalid argument for Enum ", typename, ": ", s))) end s = s::Symbol if !Base.isidentifier(s) diff --git a/base/accumulate.jl b/base/accumulate.jl index c155ecfb4f75f..ef5e311c19338 100644 --- a/base/accumulate.jl +++ b/base/accumulate.jl @@ -22,9 +22,9 @@ function _accumulate_pairwise!(op::Op, c::AbstractVector{T}, v::AbstractVector, return s_ end -function accumulate_pairwise!(op::Op, result::AbstractVector, v::AbstractVector) where Op +function accumulate_pairwise!(op::Op, result::AbstractVector, v::AbstractVector)::Except{typeof(result), DimensionMismatch} where Op li = LinearIndices(v) - li != LinearIndices(result) && throw(DimensionMismatch("input and output array sizes and indices must match")) + li != LinearIndices(result) && throw(DimensionMismatch("input and output array sizes and indices must match"))? n = length(li) n == 0 && return result i1 = first(li) @@ -278,7 +278,7 @@ julia> accumulate(+, fill(1, 2, 5), dims=2, init=100.0) 101.0 102.0 103.0 104.0 105.0 ``` """ -function accumulate(op, A; dims::Union{Nothing,Integer}=nothing, kw...) +function accumulate(op, A; dims::Union{Nothing,Integer}=nothing, kw...)::AnyExcept{ArgumentError} if dims === nothing && !(A isa AbstractVector) # This branch takes care of the cases not handled by `_accumulate!`. return collect(Iterators.accumulate(op, A; kw...)) @@ -286,7 +286,7 @@ function accumulate(op, A; dims::Union{Nothing,Integer}=nothing, kw...) nt = values(kw) if !(isempty(kw) || keys(nt) === (:init,)) - throw(ArgumentError("accumulate does not support the keyword arguments $(setdiff(keys(nt), (:init,)))")) + throw(ArgumentError("accumulate does not support the keyword arguments $(setdiff(keys(nt), (:init,)))"))? end out = similar(A, _accumulate_promote_op(op, A; kw...)) @@ -344,19 +344,19 @@ julia> accumulate!(*, B, A, dims=2, init=10) 40 200 1200 ``` """ -function accumulate!(op, B, A; dims::Union{Integer, Nothing} = nothing, kw...) +function accumulate!(op, B, A; dims::Union{Integer, Nothing} = nothing, kw...)::AnyExcept{ArgumentError} nt = values(kw) if isempty(kw) _accumulate!(op, B, A, dims, nothing) elseif keys(kw) === (:init,) _accumulate!(op, B, A, dims, Some(nt.init)) else - throw(ArgumentError("accumulate! does not support the keyword arguments $(setdiff(keys(nt), (:init,)))")) + throw(ArgumentError("accumulate! does not support the keyword arguments $(setdiff(keys(nt), (:init,)))"))? end end -function _accumulate!(op, B, A, dims::Nothing, init::Union{Nothing, Some}) - throw(ArgumentError("Keyword argument dims must be provided for multidimensional arrays")) +function _accumulate!(op, B, A, dims::Nothing, init::Union{Nothing, Some})::AnyExcept{ArgumentError} + throw(ArgumentError("Keyword argument dims must be provided for multidimensional arrays"))? end function _accumulate!(op, B, A::AbstractVector, dims::Nothing, init::Nothing) @@ -371,10 +371,10 @@ function _accumulate!(op, B, A::AbstractVector, dims::Nothing, init::Some) _accumulate1!(op, B, v1, A, 1) end -function _accumulate!(op, B, A, dims::Integer, init::Union{Nothing, Some}) - dims > 0 || throw(ArgumentError("dims must be a positive integer")) +function _accumulate!(op, B, A, dims::Integer, init::Union{Nothing, Some})::Except{typeof(B), Union{ArgumentError, DimensionMismatch}} + dims > 0 || throw(ArgumentError("dims must be a positive integer"))? inds_t = axes(A) - axes(B) == inds_t || throw(DimensionMismatch("shape of B must match A")) + axes(B) == inds_t || throw(DimensionMismatch("shape of B must match A"))? dims > ndims(A) && return copyto!(B, A) isempty(inds_t[dims]) && return B if dims == 1 @@ -433,10 +433,10 @@ end B end -function _accumulate1!(op, B, v1, A::AbstractVector, dim::Integer) - dim > 0 || throw(ArgumentError("dim must be a positive integer")) +function _accumulate1!(op, B, v1, A::AbstractVector, dim::Integer)::Except{typeof(B), Union{ArgumentError, DimensionMismatch}} + dim > 0 || throw(ArgumentError("dim must be a positive integer"))? inds = LinearIndices(A) - inds == LinearIndices(B) || throw(DimensionMismatch("LinearIndices of A and B don't match")) + inds == LinearIndices(B) || throw(DimensionMismatch("LinearIndices of A and B don't match"))? dim > 1 && return copyto!(B, A) (i1, state) = iterate(inds)::NTuple{2,Any} # We checked earlier that A isn't empty cur_val = v1 diff --git a/base/bitarray.jl b/base/bitarray.jl index 9770fe0a336c5..9ea89414a5026 100644 --- a/base/bitarray.jl +++ b/base/bitarray.jl @@ -1918,13 +1918,13 @@ end # BitArray I/O write(s::IO, B::BitArray) = write(s, B.chunks) -function read!(s::IO, B::BitArray) +function read!(s::IO, B::BitArray)::Except{BitArray, DimensionMismatch} n = length(B) Bc = B.chunks nc = length(read!(s, Bc)) if length(Bc) > 0 && Bc[end] & _msk_end(n) ≠ Bc[end] Bc[end] &= _msk_end(n) # ensure that the BitArray is not broken - throw(DimensionMismatch("read mismatch, found non-zero bits after BitArray length")) + throw(DimensionMismatch("read mismatch, found non-zero bits after BitArray length"))? end return B end diff --git a/base/error.jl b/base/error.jl index 149a929966361..fa66bb30e1b75 100644 --- a/base/error.jl +++ b/base/error.jl @@ -321,3 +321,4 @@ function retry(f; delays=ExponentialBackOff(), check=nothing) return f(args...; kwargs...) end end + diff --git a/base/errorshow.jl b/base/errorshow.jl index 33edb4cee92a4..c56c974c8b7ef 100644 --- a/base/errorshow.jl +++ b/base/errorshow.jl @@ -82,12 +82,10 @@ function showerror(io::IO, ex::TypeError) elseif ex.func === :var"dict key" print(io, "$(limitrepr(ex.got)) is not a valid key for type $(ex.expected)") else - if isvarargtype(ex.got) - targs = (ex.got,) - elseif isa(ex.got, Type) - targs = ("Type{", ex.got, "}") - else - targs = ("a value of type $(typeof(ex.got))",) + targs = match ex.got + g if isvarargtype(g) -> (g,) + g::Type -> ("Type{", g, "}") + g -> ("a value of type $(typeof(g))",) end if ex.context == "" ctx = "in $(ex.func)" diff --git a/base/essentials.jl b/base/essentials.jl index 797c247949147..49c14f3336f80 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -1288,15 +1288,16 @@ macro world(sym, world) if world == :∞ world = Expr(:call, get_world_counter) end - if isa(sym, Symbol) - return :($(_resolve_in_world)($(esc(world)), $(QuoteNode(GlobalRef(__module__, sym))))) - elseif isa(sym, GlobalRef) - return :($(_resolve_in_world)($(esc(world)), $(QuoteNode(sym)))) - elseif isa(sym, Expr) && sym.head === :(.) && - length(sym.args) == 2 && isa(sym.args[2], QuoteNode) && isa(sym.args[2].value, Symbol) - return :($(_resolve_in_world)($(esc(world)), $(GlobalRef)($(esc(sym.args[1])), $(sym.args[2])))) - else - error("`@world` requires a symbol or GlobalRef") + match sym + sym::Symbol -> + :($(_resolve_in_world)($(esc(world)), $(QuoteNode(GlobalRef(__module__, sym))))) + sym::GlobalRef -> + :($(_resolve_in_world)($(esc(world)), $(QuoteNode(sym)))) + sym::Expr if sym.head === :(.) && + length(sym.args) == 2 && isa(sym.args[2], QuoteNode) && isa(sym.args[2].value, Symbol) -> + :($(_resolve_in_world)($(esc(world)), $(GlobalRef)($(esc(sym.args[1])), $(sym.args[2])))) + _ -> + error("`@world` requires a symbol or GlobalRef") end end diff --git a/base/except.jl b/base/except.jl new file mode 100644 index 0000000000000..f836cb5364d7e --- /dev/null +++ b/base/except.jl @@ -0,0 +1,162 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +# Declared exceptions runtime support + +""" + Except{T, E} + +A wrapper type for values that may be either a successful result of type `T` +or an exception of type `E`. Similar to Rust's `Result`. + +Functions can declare their exception types using `Except` as a return type annotation: + +```julia +function getindex(a::Vector{T}, i::Int)::Except{T, BoundsError} + # ... +end +``` + +For multiple exception types, use `Union`: +```julia +function foo()::Except{Int, Union{KeyError, BoundsError}} + # ... +end +``` + +See also: [`AnyExcept`](@ref), [`unwrap`](@ref), [`is_exception`](@ref) +""" +mutable struct Except{T, E} + _value::T + _exception::Union{E, Nothing} + + # Success constructor - value only + function Except{T,E}(val) where {T, E} + e = new{T,E}() + e._value = val + e._exception = nothing + return e + end + + # Internal constructor for exception case + function Except{T,E}(::Nothing, exc::E) where {T, E} + e = new{T,E}() + e._exception = exc + return e + end +end + +""" + AnyExcept{E} + +Convenience alias for `Except{Any, E}`. Use when you want to declare exception types +without constraining the return type. + +```julia +function may_fail()::AnyExcept{IOError} + # ... +end +``` +""" +const AnyExcept{E} = Except{Any, E} + +""" + except_value(::Type{Except{T,E}}, val) where {T, E} + +Create an `Except` containing a successful value. +""" +except_value(::Type{Except{T,E}}, val) where {T, E} = Except{T,E}(val) + +""" + except_exception(::Type{Except{T,E}}, exc::E) where {T, E} + +Create an `Except` containing an exception. +""" +except_exception(::Type{Except{T,E}}, exc::E) where {T, E} = Except{T,E}(nothing, exc) + +""" + is_exception(e::Except) -> Bool + +Return `true` if `e` contains an exception, `false` if it contains a value. +""" +is_exception(e::Except) = e._exception !== nothing + +""" + unwrap(e::Except) + +Extract the value from an `Except`. If `e` contains an exception, throw it. + +# Examples +```julia +e = Except{Int, BoundsError}(42) +unwrap(e) # returns 42 + +e_err = except_exception(Except{Int, BoundsError}, BoundsError([1,2], 5)) +unwrap(e_err) # throws BoundsError +``` +""" +function unwrap(e::Except) + if is_exception(e) + throw(e._exception) + end + return e._value +end + +""" + get_exception(e::Except) + +Return the exception contained in `e`, or `nothing` if `e` contains a value. +""" +get_exception(e::Except) = e._exception + +""" + forward_or_unwrap(e::Except{T,E}) where {T,E} + +For the postfix `?` operator: if `e` contains an exception, return a new `Except` +with that exception (for forwarding). Otherwise, return the unwrapped value. +""" +function forward_or_unwrap(e::Except{T,E}) where {T,E} + if is_exception(e) + return except_exception(Except{T,E}, e._exception) + end + return e._value +end + +# For non-Except values, just return them (identity) +forward_or_unwrap(x) = x + +""" + ExceptRaw + +Sentinel type used to dispatch to the raw (non-unwrapping) version of a function +that returns `Except`. Used internally by the `?` and `match?` operators. +""" +struct ExceptRaw end + +""" + except_raw + +Singleton instance of `ExceptRaw` used to call the raw version of Except-returning functions. +""" +const except_raw = ExceptRaw() + +# Convert a value to Except (wrap as success) +function convert(::Type{Except{T,E}}, val) where {T, E} + return Except{T,E}(convert(T, val)) +end + +# Convert between Except types when the exception is compatible +function convert(::Type{Except{T,E}}, e::Except{T2,E2}) where {T, E, T2, E2} + if is_exception(e) + exc = e._exception + if exc isa E + return Except{T,E}(nothing, exc) + else + # Exception type mismatch - this shouldn't happen with proper usage + throw(TypeError(:convert, "exception type", E, exc)) + end + else + return Except{T,E}(convert(T, e._value)) + end +end + +# show method is defined in show.jl diff --git a/base/experimental.jl b/base/experimental.jl index 2deb3bc76af6c..9b8bbb14d46c1 100644 --- a/base/experimental.jl +++ b/base/experimental.jl @@ -834,10 +834,9 @@ information. In particular, `(@VERSION).syntax` provides the syntax version used function var"@VERSION"(__source__::Union{LineNumberNode, Core.MacroSource}, __module__::Module) # This macro has special handling in the parser, which puts the current syntax # version into __source__. - if isa(__source__, LineNumberNode) - return :((; syntax = v"1.13", runtime = VERSION)) - else - return :((; syntax = $(__source__.syntax_ver), runtime = VERSION)) + match __source__ + ::LineNumberNode -> :((; syntax = v"1.13", runtime = VERSION)) + src::Core.MacroSource -> :((; syntax = $(src.syntax_ver), runtime = VERSION)) end end diff --git a/base/exports.jl b/base/exports.jl index 4111bbbd157ff..5173642fde8c2 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -88,6 +88,9 @@ export Dict, Dims, Enum, + Except, + AnyExcept, + ExceptRaw, ExponentialBackOff, IndexCartesian, IndexLinear, @@ -177,12 +180,21 @@ export EOFError, InvalidStateException, KeyError, + MatchError, MissingException, ProcessFailedException, TaskFailedException, SystemError, StringIndexError, +# Declared exceptions + is_exception, + unwrap, + get_exception, + except_value, + except_exception, + except_raw, + # Global constants and variables ARGS, C_NULL, @@ -779,6 +791,7 @@ export skipmissing, @something, something, + @match, isnothing, nonmissingtype, diff --git a/base/expr.jl b/base/expr.jl index eaa04aa0a0226..b5d636d17d5f5 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -67,14 +67,12 @@ end # copy parts of an IR that the compiler mutates # (this is not a general-purpose copy for an Expr AST) function copy_exprs(@nospecialize(x)) - if isa(x, Expr) - return copy(x) - elseif isa(x, PhiNode) - return copy(x) - elseif isa(x, PhiCNode) - return copy(x) + match x + ::Expr -> copy(x) + ::PhiNode -> copy(x) + ::PhiCNode -> copy(x) + _ -> x end - return x end copy_exprargs(x::Array{Any,1}) = Any[copy_exprs(@inbounds x[i]) for i in eachindex(x)] diff --git a/base/libdl.jl b/base/libdl.jl index 024c88ecf2a16..706c49cb4e09e 100644 --- a/base/libdl.jl +++ b/base/libdl.jl @@ -56,8 +56,8 @@ Look up a symbol from a shared library handle, return callable function pointer If the symbol cannot be found, this method throws an error, unless the keyword argument `throw_error` is set to `false`, in which case this method returns `nothing`. """ -function dlsym(hnd::Ptr, s::Union{Symbol,AbstractString}; throw_error::Bool = true) - hnd == C_NULL && throw(ArgumentError("NULL library handle")) +function dlsym(hnd::Ptr, s::Union{Symbol,AbstractString}; throw_error::Bool = true)::AnyExcept{ArgumentError} + hnd == C_NULL && throw(ArgumentError("NULL library handle"))? val = Ref(Ptr{Cvoid}(0)) symbol_found = ccall(:jl_dlsym, Cint, (Ptr{Cvoid}, Cstring, Ref{Ptr{Cvoid}}, Cint, Cint), @@ -524,7 +524,7 @@ function dlopen(ll::LazyLibrary, flags::Integer = ll.flags; kwargs...) return handle end -dlopen(x::Any) = throw(TypeError(:dlopen, "", Union{Symbol,String,LazyLibrary}, x)) +dlopen(x::Any)::AnyExcept{TypeError} = throw(TypeError(:dlopen, "", Union{Symbol,String,LazyLibrary}, x))? dlsym(ll::LazyLibrary, args...; kwargs...) = dlsym(dlopen(ll), args...; kwargs...) dlpath(ll::LazyLibrary) = dlpath(dlopen(ll)) end # module Libdl diff --git a/base/match.jl b/base/match.jl new file mode 100644 index 0000000000000..83b38b15cfcc3 --- /dev/null +++ b/base/match.jl @@ -0,0 +1,303 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +# Pattern matching runtime support + +""" + MatchError <: Exception + +Exception thrown when a `match` expression fails to match any pattern. + +# Examples +```julia +match x + 1 -> "one" + 2 -> "two" +end # throws MatchError if x is not 1 or 2 +``` +""" +struct MatchError <: Exception + value::Any +end + +function showerror(io::IO, e::MatchError) + print(io, "MatchError: no pattern matched value ") + show(io, e.value) +end + +# Abstract base type for all matchers +abstract type Matcher end + +# Wildcard matcher: matches anything, captures nothing +struct Wildcard <: Matcher end + +function match(::Wildcard, @nospecialize(value)) + return Dict{Symbol,Any}() +end + +# Literal matcher: matches by == +struct Literal <: Matcher + value::Any +end + +function match(m::Literal, @nospecialize(value)) + m.value == value ? Dict{Symbol,Any}() : nothing +end + +# Capture matcher: captures value under a name +struct Capture <: Matcher + name::Symbol +end + +function match(m::Capture, @nospecialize(value)) + return Dict{Symbol,Any}(m.name => value) +end + +# Type matcher: matches by isa +struct TypeMatcher <: Matcher + type::Type +end + +function match(m::TypeMatcher, @nospecialize(value)) + value isa m.type ? Dict{Symbol,Any}() : nothing +end + +# Typed capture: captures with type constraint +struct TypedCapture <: Matcher + name::Symbol + type::Type +end + +function match(m::TypedCapture, @nospecialize(value)) + value isa m.type ? Dict{Symbol,Any}(m.name => value) : nothing +end + +# Tuple matcher: structural match for tuples +struct TupleMatcher <: Matcher + matchers::Vector{Matcher} +end + +TupleMatcher(matchers::Matcher...) = TupleMatcher(collect(Matcher, matchers)) + +function match(m::TupleMatcher, @nospecialize(value)) + value isa Tuple || return nothing + length(value) == length(m.matchers) || return nothing + bindings = Dict{Symbol,Any}() + for (matcher, v) in zip(m.matchers, value) + result = match(matcher, v) + result === nothing && return nothing + merge!(bindings, result) + end + return bindings +end + +# Alternation matcher: tries each pattern, returns first match +struct Alternation <: Matcher + matchers::Vector{Matcher} +end + +Alternation(matchers::Matcher...) = Alternation(collect(Matcher, matchers)) + +function match(m::Alternation, @nospecialize(value)) + for matcher in m.matchers + result = match(matcher, value) + result !== nothing && return result + end + return nothing +end + +# Call matcher: matches constructor patterns like Some(x) +struct CallMatcher <: Matcher + f::Any # The constructor/type to match + matchers::Vector{Matcher} +end + +CallMatcher(f, matchers::Matcher...) = CallMatcher(f, collect(Matcher, matchers)) + +function match(m::CallMatcher, @nospecialize(value)) + # For types, check if value is an instance + if m.f isa Type + value isa m.f || return nothing + # Match against fields + nfields(value) == length(m.matchers) || return nothing + bindings = Dict{Symbol,Any}() + for (i, matcher) in enumerate(m.matchers) + result = match(matcher, getfield(value, i)) + result === nothing && return nothing + merge!(bindings, result) + end + return bindings + else + # For non-type constructors, we can't deconstruct + # This is for future extensibility + return nothing + end +end + +# @match macro - provides macro-level pattern matching +""" + @match value begin + pattern1 -> result1 + pattern2 -> result2 + _ -> default + end + +Pattern matching macro. Evaluates `value` and matches it against each pattern +in order. Returns the result of the first matching pattern's expression. + +Throws `MatchError` if no pattern matches. + +# Patterns +- Literals: `1`, `"hello"`, `:symbol` +- Wildcard: `_` matches anything +- Variables: `x` captures the matched value +- Type assertions: `x::Int` captures with type check, `::Int` just checks type +- Tuples: `(a, b, c)` destructures tuples +- Alternation: `p1 | p2` matches if either pattern matches +- Value escaping: `\$x` matches against the value of variable `x` + +# Examples +```julia +result = @match x begin + 0 -> "zero" + n::Int -> "integer: \$n" + (a, b) -> "tuple: \$a, \$b" + _ -> "something else" +end +``` +""" +macro match(value, body) + # Transform the body from begin...end block with -> expressions + # into a match expression + if !(body isa Expr && body.head === :block) + error("@match body must be a begin...end block") + end + + arms = Expr[] + for ex in body.args + ex isa LineNumberNode && continue + if ex isa Expr && ex.head === :-> + push!(arms, ex) + else + error("@match arms must be `pattern -> result` expressions") + end + end + + if isempty(arms) + error("@match requires at least one arm") + end + + # Build nested if/else structure + scrutinee = gensym("value") + result = :(throw(MatchError($scrutinee))) + + for arm in reverse(arms) + pattern = arm.args[1] + body_expr = arm.args[2] + cond, bindings = pattern_to_condition(pattern, scrutinee) + + if isempty(bindings) + result = Expr(:if, cond, body_expr, result) + else + # Create a let block for the bindings + let_bindings = [Expr(:(=), name, val) for (name, val) in bindings] + body_with_bindings = Expr(:let, Expr(:block, let_bindings...), body_expr) + result = Expr(:if, cond, body_with_bindings, result) + end + end + + return esc(Expr(:let, Expr(:(=), scrutinee, value), result)) +end + +# Convert a pattern to a condition expression and bindings +function pattern_to_condition(pattern, scrutinee) + if pattern === :_ + # Wildcard + return true, Pair{Symbol,Any}[] + elseif pattern isa Symbol + # Variable capture + return true, [pattern => scrutinee] + elseif pattern isa Union{Number, String, Char, Bool} + # Literal + return :($scrutinee == $pattern), Pair{Symbol,Any}[] + elseif pattern isa QuoteNode + # Quoted symbol + return :($scrutinee === $(pattern)), Pair{Symbol,Any}[] + elseif pattern isa Expr + if pattern.head === :$ + # Escaped value + return :($scrutinee == $(pattern.args[1])), Pair{Symbol,Any}[] + elseif pattern.head === :(::) + if length(pattern.args) == 1 + # ::T - just type check + T = pattern.args[1] + return :($scrutinee isa $T), Pair{Symbol,Any}[] + else + # x::T - capture with type check + name = pattern.args[1] + T = pattern.args[2] + return :($scrutinee isa $T), [name => scrutinee] + end + elseif pattern.head === :tuple + # Tuple destructuring + n = length(pattern.args) + conds = [:($scrutinee isa Tuple), :(length($scrutinee) == $n)] + bindings = Pair{Symbol,Any}[] + for (i, p) in enumerate(pattern.args) + elem_var = gensym("elem") + elem_cond, elem_bindings = pattern_to_condition(p, elem_var) + if elem_cond !== true + push!(conds, :($elem_var = $scrutinee[$i]; $elem_cond)) + else + for (name, _) in elem_bindings + push!(bindings, name => :($scrutinee[$i])) + end + continue + end + for (name, val) in elem_bindings + if val === elem_var + push!(bindings, name => :($scrutinee[$i])) + else + push!(bindings, name => val) + end + end + end + cond = foldl((a, b) -> :($a && $b), conds; init=true) + return cond, bindings + elseif pattern.head === :call && pattern.args[1] === :| + # Alternation + conds = Any[] + all_bindings = Pair{Symbol,Any}[] + for p in pattern.args[2:end] + c, b = pattern_to_condition(p, scrutinee) + push!(conds, c) + append!(all_bindings, b) + end + cond = foldl((a, b) -> :($a || $b), conds) + # For alternation, bindings are tricky - for now just use first set + return cond, unique(first, all_bindings) + elseif pattern.head === :call + # Constructor pattern + f = pattern.args[1] + conds = [:($scrutinee isa $f)] + bindings = Pair{Symbol,Any}[] + for (i, p) in enumerate(pattern.args[2:end]) + field_var = gensym("field") + field_cond, field_bindings = pattern_to_condition(p, field_var) + if field_cond !== true + push!(conds, :($field_var = getfield($scrutinee, $i); $field_cond)) + end + for (name, val) in field_bindings + if val === field_var + push!(bindings, name => :(getfield($scrutinee, $i))) + else + push!(bindings, name => val) + end + end + end + cond = foldl((a, b) -> :($a && $b), conds; init=true) + return cond, bindings + end + end + # Default: treat as literal comparison + return :($scrutinee == $pattern), Pair{Symbol,Any}[] +end diff --git a/base/meta.jl b/base/meta.jl index 1f5f49b72575f..1519245dbe812 100644 --- a/base/meta.jl +++ b/base/meta.jl @@ -461,68 +461,62 @@ function _partially_inline!(@nospecialize(x), slot_replacements::Vector{Any}, @nospecialize(type_signature), static_param_values::Vector{Any}, slot_offset::Int, statement_offset::Int, boundscheck::Symbol) - if isa(x, Core.SSAValue) - return Core.SSAValue(x.id + statement_offset) - end - if isa(x, Core.GotoNode) - return Core.GotoNode(x.label + statement_offset) - end - if isa(x, Core.SlotNumber) - id = x.id - if 1 <= id <= length(slot_replacements) - return slot_replacements[id] + match x + x::Core.SSAValue -> Core.SSAValue(x.id + statement_offset) + x::Core.GotoNode -> Core.GotoNode(x.label + statement_offset) + x::Core.SlotNumber -> begin + id = x.id + if 1 <= id <= length(slot_replacements) + return slot_replacements[id] + end + Core.SlotNumber(id + slot_offset) end - return Core.SlotNumber(id + slot_offset) - end - if isa(x, Core.NewvarNode) - return Core.NewvarNode(_partially_inline!(x.slot, slot_replacements, type_signature, + x::Core.NewvarNode -> Core.NewvarNode(_partially_inline!(x.slot, slot_replacements, type_signature, static_param_values, slot_offset, statement_offset, boundscheck)) - end - if isa(x, Core.PhiNode) - partially_inline!(x.values, slot_replacements, type_signature, static_param_values, - slot_offset, statement_offset, boundscheck) - x.edges .+= slot_offset - return x - end - if isa(x, Core.UpsilonNode) - if !isdefined(x, :val) - return x + x::Core.PhiNode -> begin + partially_inline!(x.values, slot_replacements, type_signature, static_param_values, + slot_offset, statement_offset, boundscheck) + x.edges .+= slot_offset + x end - return Core.UpsilonNode( - _partially_inline!(x.val, slot_replacements, type_signature, static_param_values, - slot_offset, statement_offset, boundscheck), - ) - end - if isa(x, Core.PhiCNode) - _partially_inline!(x.values, slot_replacements, type_signature, static_param_values, - slot_offset, statement_offset, boundscheck) - end - if isa(x, Core.ReturnNode) - # Unreachable doesn't have val defined - if !isdefined(x, :val) - return x - else - return Core.ReturnNode( - _partially_inline!(x.val, slot_replacements, type_signature, static_param_values, - slot_offset, statement_offset, boundscheck), - ) - end - end - if isa(x, Core.GotoIfNot) - return Core.GotoIfNot( + x::Core.UpsilonNode -> begin + if !isdefined(x, :val) + return x + end + Core.UpsilonNode( + _partially_inline!(x.val, slot_replacements, type_signature, static_param_values, + slot_offset, statement_offset, boundscheck), + ) + end + x::Core.PhiCNode -> begin + _partially_inline!(x.values, slot_replacements, type_signature, static_param_values, + slot_offset, statement_offset, boundscheck) + x + end + x::Core.ReturnNode -> begin + # Unreachable doesn't have val defined + if !isdefined(x, :val) + return x + else + Core.ReturnNode( + _partially_inline!(x.val, slot_replacements, type_signature, static_param_values, + slot_offset, statement_offset, boundscheck), + ) + end + end + x::Core.GotoIfNot -> Core.GotoIfNot( _partially_inline!(x.cond, slot_replacements, type_signature, static_param_values, slot_offset, statement_offset, boundscheck), x.dest + statement_offset, ) - end - if isa(x, Core.EnterNode) - if x.catch_dest == 0 - return x + x::Core.EnterNode -> begin + if x.catch_dest == 0 + return x + end + Core.EnterNode(x, x.catch_dest + statement_offset) end - return Core.EnterNode(x, x.catch_dest + statement_offset) - end - if isa(x, Expr) + x::Expr -> begin head = x.head if head === :static_parameter if isassigned(static_param_values, x.args[1]) @@ -597,8 +591,10 @@ function _partially_inline!(@nospecialize(x), slot_replacements::Vector{Any}, partially_inline!(x.args, slot_replacements, type_signature, static_param_values, slot_offset, statement_offset, boundscheck) end + x + end + _ -> x end - return x end _instantiate_type_in_env(x, spsig, spvals) = ccall(:jl_instantiate_type_in_env, Any, (Any, Any, Ptr{Any}), x, spsig, spvals) diff --git a/base/options.jl b/base/options.jl index 0255a8f2dc642..70d48b16d7837 100644 --- a/base/options.jl +++ b/base/options.jl @@ -101,10 +101,10 @@ function show(io::IO, opt::JLOptions) nfields = length(fields) for (i, f) in enumerate(fields) v = getfield(opt, i) - if isa(v, Ptr{UInt8}) - v = (v != C_NULL) ? unsafe_string(v) : "" - elseif isa(v, Ptr{Ptr{UInt8}}) - v = unsafe_load_commands(v) + v = match v + v::Ptr{UInt8} -> (v != C_NULL) ? unsafe_string(v) : "" + v::Ptr{Ptr{UInt8}} -> unsafe_load_commands(v) + v -> v end print(io, f, " = ", repr(v), i < nfields ? ", " : "") end diff --git a/base/reducedim.jl b/base/reducedim.jl index 703babc9bc56f..7c4d89aa39c8c 100644 --- a/base/reducedim.jl +++ b/base/reducedim.jl @@ -5,13 +5,13 @@ # for reductions that expand 0 dims to 1 reduced_index(i::OneTo{T}) where {T} = OneTo(one(T)) reduced_index(i::Union{Slice, IdentityUnitRange}) = oftype(i, first(i):first(i)) -reduced_index(i::AbstractUnitRange) = +reduced_index(i::AbstractUnitRange)::AnyExcept{ArgumentError} = throw(ArgumentError( """ No method is implemented for reducing index range of type $(typeof(i)). Please implement reduced_index for this index type or report this as an issue. """ - )) + ))? reduced_indices(a::AbstractArrayOrBroadcasted, region) = reduced_indices(axes(a), region) # for reductions that keep 0 dims as 0 @@ -27,10 +27,10 @@ function reduced_indices0(axs::Indices{N}, region) where N ntuple(d -> d in region && !isempty(axs[d]) ? reduced_index(axs[d]) : axs[d], Val(N)) end -function _check_valid_region(region) +function _check_valid_region(region)::AnyExcept{ArgumentError} for d in region - isa(d, Integer) || throw(ArgumentError("reduced dimension(s) must be integers")) - Int(d) < 1 && throw(ArgumentError("region dimension(s) must be ≥ 1, got $d")) + isa(d, Integer) || throw(ArgumentError("reduced dimension(s) must be integers"))? + Int(d) < 1 && throw(ArgumentError("region dimension(s) must be ≥ 1, got $d"))? end end @@ -203,7 +203,7 @@ end has_fast_linear_indexing(a::AbstractArrayOrBroadcasted) = IndexStyle(a) === IndexLinear() has_fast_linear_indexing(a::AbstractVector) = true -function check_reducedims(R, A) +function check_reducedims(R, A)::Except{Int, DimensionMismatch} # Check whether R has compatible dimensions w.r.t. A for reduction # # It returns an integer value (useful for choosing implementation) @@ -212,7 +212,7 @@ function check_reducedims(R, A) # it will be size(A, 1) or size(A, 1) * size(A, 2). # - Otherwise, e.g. sum(A, dims=2) or sum(A, dims=(1,3)), it returns 0. # - ndims(R) <= ndims(A) || throw(DimensionMismatch("cannot reduce $(ndims(A))-dimensional array to $(ndims(R)) dimensions")) + ndims(R) <= ndims(A) || throw(DimensionMismatch("cannot reduce $(ndims(A))-dimensional array to $(ndims(R)) dimensions"))? lsiz = 1 had_nonreduc = false for i = 1:ndims(A) @@ -227,7 +227,7 @@ function check_reducedims(R, A) end end else - Ri == Ai || throw(DimensionMismatch("reduction on array with indices $(axes(A)) with output with indices $(axes(R))")) + Ri == Ai || throw(DimensionMismatch("reduction on array with indices $(axes(A)) with output with indices $(axes(R))"))? had_nonreduc = true end end @@ -1016,11 +1016,11 @@ end ##### findmin & findmax ##### # The initial values of Rval are not used if the corresponding indices in Rind are 0. # -function findminmax!(f, op, Rval, Rind, A::AbstractArray{T,N}) where {T,N} +function findminmax!(f, op, Rval, Rind, A::AbstractArray{T,N})::AnyExcept{DimensionMismatch} where {T,N} (isempty(Rval) || isempty(A)) && return Rval, Rind lsiz = check_reducedims(Rval, A) for i = 1:N - axes(Rval, i) == axes(Rind, i) || throw(DimensionMismatch("Find-reduction: outputs must have the same indices")) + axes(Rval, i) == axes(Rind, i) || throw(DimensionMismatch("Find-reduction: outputs must have the same indices"))? end # If we're reducing along dimension 1, for efficiency we can make use of a temporary. # Otherwise, keep the result in Rval/Rind so that we traverse A in storage order. @@ -1125,11 +1125,11 @@ julia> findmin(abs2, A, dims=2) """ findmin(f, A::AbstractArray; dims::D=:) where {D} = _findmin(f, A, dims) -function _findmin(f, A, region::D) where {D} +function _findmin(f, A, region::D)::AnyExcept{Union{ArgumentError, DimensionMismatch}} where {D} ri = reduced_indices0(A, region) if isempty(A) if prod(map(length, reduced_indices(A, region))) != 0 - throw(ArgumentError("collection slices must be non-empty")) + throw(ArgumentError("collection slices must be non-empty"))? end similar(A, promote_op(f, eltype(A)), ri), zeros(eltype(keys(A)), ri) else @@ -1198,11 +1198,11 @@ julia> findmax(abs2, A, dims=2) """ findmax(f, A::AbstractArray; dims::D=:) where {D} = _findmax(f, A, dims) -function _findmax(f, A, region::D) where {D} +function _findmax(f, A, region::D)::AnyExcept{Union{ArgumentError, DimensionMismatch}} where {D} ri = reduced_indices0(A, region) if isempty(A) if prod(map(length, reduced_indices(A, region))) != 0 - throw(ArgumentError("collection slices must be non-empty")) + throw(ArgumentError("collection slices must be non-empty"))? end similar(A, promote_op(f, eltype(A)), ri), zeros(eltype(keys(A)), ri) else diff --git a/base/reflection.jl b/base/reflection.jl index 03e1d4c71e393..4a67a19fe3d6b 100644 --- a/base/reflection.jl +++ b/base/reflection.jl @@ -1147,14 +1147,11 @@ function bodyfunction(basemethod::Method) elseif fsym.mod === Core && fsym.name === :_apply_iterate fsym = callexpr.args[3] end - if isa(fsym, Symbol) - return getfield(fmod, fsym)::Function - elseif isa(fsym, GlobalRef) - return getfield(fsym.mod, fsym.name)::Function - elseif isa(fsym, Core.SSAValue) - fsym = ast.code[fsym.id] - else - return nothing + match fsym + fsym::Symbol -> return getfield(fmod, fsym)::Function + fsym::GlobalRef -> return getfield(fsym.mod, fsym.name)::Function + fsym::Core.SSAValue -> (fsym = ast.code[fsym.id]) + _ -> return nothing end elseif isa(fsym, Core.SSAValue) fsym = ast.code[fsym.id] diff --git a/base/runtime_internals.jl b/base/runtime_internals.jl index ab6d086cdbd72..231f0f82a6f84 100644 --- a/base/runtime_internals.jl +++ b/base/runtime_internals.jl @@ -955,13 +955,12 @@ If `T` is not a type, then return `false`. """ function ismutationfree(@nospecialize(t)) t = unwrap_unionall(t) - if isa(t, DataType) - return datatype_ismutationfree(t) - elseif isa(t, Union) - return ismutationfree(t.a) && ismutationfree(t.b) + # TypeVar, etc. returns false + match t + t::DataType -> datatype_ismutationfree(t) + t::Union -> ismutationfree(t.a) && ismutationfree(t.b) + _ -> false end - # TypeVar, etc. - return false end datatype_isidentityfree(dt::DataType) = (@_total_meta; (dt.flags & 0x0200) == 0x0200) @@ -975,13 +974,12 @@ If `T` is not a type, then return `false`. """ function isidentityfree(@nospecialize(t)) t = unwrap_unionall(t) - if isa(t, DataType) - return datatype_isidentityfree(t) - elseif isa(t, Union) - return isidentityfree(t.a) && isidentityfree(t.b) + # TypeVar, etc. returns false + match t + t::DataType -> datatype_isidentityfree(t) + t::Union -> isidentityfree(t.a) && isidentityfree(t.b) + _ -> false end - # TypeVar, etc. - return false end """ @@ -1578,11 +1576,12 @@ end unionlen(@nospecialize(x)) = x isa Union ? unionlen(x.a) + unionlen(x.b) : 1 function _uniontypes(@nospecialize(x), ts::Array{Any,1}) - if x isa Union - _uniontypes(x.a, ts) - _uniontypes(x.b, ts) - else - push!(ts, x) + match x + x::Union -> begin + _uniontypes(x.a, ts) + _uniontypes(x.b, ts) + end + _ -> push!(ts, x) end return ts end diff --git a/base/scopedvalues.jl b/base/scopedvalues.jl index 989663d59ab76..be4a2ace8925c 100644 --- a/base/scopedvalues.jl +++ b/base/scopedvalues.jl @@ -224,11 +224,12 @@ function Base.getindex(val::AbstractScopedValue{T})::T where T end function Base.show(io::IO, val::AbstractScopedValue) - if isa(val, ScopedValue) - print(io, ScopedValue) - print(io, '{', eltype(val), '}') - else - print(io, typeof(val)) + match val + val::ScopedValue -> begin + print(io, ScopedValue) + print(io, '{', eltype(val), '}') + end + _ -> print(io, typeof(val)) end print(io, '(') v = get(val) diff --git a/base/show.jl b/base/show.jl index cb71376fedc68..1ee96dd2154b7 100644 --- a/base/show.jl +++ b/base/show.jl @@ -52,19 +52,21 @@ function show(io::IO, ::MIME"text/plain", f::Function) get(io, :compact, false)::Bool && return show(io, f) ft = typeof(f) name = ft.name.singletonname - if isa(f, Core.IntrinsicFunction) - print(io, f) - id = Core.Intrinsics.bitcast(Int32, f) - print(io, " (intrinsic function #$id)") - elseif isa(f, Core.Builtin) - print(io, name, " (built-in function)") - else - n = length(methods(f)) - m = n==1 ? "method" : "methods" - sname = string(name) - ns = (_isself(ft) || '#' in sname) ? sname : string("(::", ft, ")") - what = startswith(ns, '@') ? "macro" : "generic function" - print(io, ns, " (", what, " with $n $m)") + match f + f::Core.IntrinsicFunction -> begin + print(io, f) + id = Core.Intrinsics.bitcast(Int32, f) + print(io, " (intrinsic function #$id)") + end + ::Core.Builtin -> print(io, name, " (built-in function)") + _ -> begin + n = length(methods(f)) + m = n==1 ? "method" : "methods" + sname = string(name) + ns = (_isself(ft) || '#' in sname) ? sname : string("(::", ft, ")") + what = startswith(ns, '@') ? "macro" : "generic function" + print(io, ns, " (", what, " with $n $m)") + end end end @@ -277,10 +279,10 @@ function show(io::IO, ::MIME"text/plain", opt::JLOptions) nfields = length(fields) for (i, f) in enumerate(fields) v = getfield(opt, i) - if isa(v, Ptr{UInt8}) - v = (v != C_NULL) ? unsafe_string(v) : "" - elseif isa(v, Ptr{Ptr{UInt8}}) - v = unsafe_load_commands(v) + v = match v + v::Ptr{UInt8} -> (v != C_NULL) ? unsafe_string(v) : "" + v::Ptr{Ptr{UInt8}} -> unsafe_load_commands(v) + v -> v end println(io, " ", f, " = ", repr(v), i < nfields ? "," : "") end @@ -597,11 +599,10 @@ io_has_tvar_name(io::IO, name::Symbol, @nospecialize(x)) = false modulesof!(s::Set{Module}, x::TypeVar) = modulesof!(s, x.ub) function modulesof!(s::Set{Module}, x::Type) x = unwrap_unionall(x) - if x isa DataType - push!(s, parentmodule(x)) - elseif x isa Union - modulesof!(s, x.a) - modulesof!(s, x.b) + match x + x::DataType -> push!(s, parentmodule(x)) + x::Union -> (modulesof!(s, x.a); modulesof!(s, x.b)) + _ -> nothing end s end @@ -982,16 +983,22 @@ function _show_type(io::IO, @nospecialize(x::Type)) return elseif get(io, :compact, true)::Bool && show_typealias(io, x) return - elseif x isa DataType - show_datatype(io, x) - return - elseif x isa Union - if get(io, :compact, true)::Bool && show_unionaliases(io, x) + end + + match x + x::DataType -> begin + show_datatype(io, x) return end - print(io, "Union") - show_delim_array(io, uniontypes(x), '{', ',', '}', false) - return + x::Union -> begin + if get(io, :compact, true)::Bool && show_unionaliases(io, x) + return + end + print(io, "Union") + show_delim_array(io, uniontypes(x), '{', ',', '}', false) + return + end + x::UnionAll -> nothing end x = x::UnionAll @@ -1379,38 +1386,42 @@ show(io::IO, mi::Core.MethodInstance) = show_mi(io, mi) function show(io::IO, codeinst::Core.CodeInstance) print(io, "CodeInstance for ") def = codeinst.def - if isa(def, Core.ABIOverride) - show_mi(io, def.def) - print(io, " (ABI Overridden)") - else - show_mi(io, def::MethodInstance) + match def + def::Core.ABIOverride -> begin + show_mi(io, def.def) + print(io, " (ABI Overridden)") + end + def::MethodInstance -> show_mi(io, def) end end function show_mi(io::IO, mi::Core.MethodInstance, from_stackframe::Bool=false) def = mi.def - if isa(def, Method) - if isdefined(def, :generator) && mi === def.generator - print(io, "MethodInstance generator for ") - show(io, def) - else - print(io, "MethodInstance for ") - show_tuple_as_call(io, def.name, mi.specTypes; qualified=true) + match def + def::Method -> begin + if isdefined(def, :generator) && mi === def.generator + print(io, "MethodInstance generator for ") + show(io, def) + else + print(io, "MethodInstance for ") + show_tuple_as_call(io, def.name, mi.specTypes; qualified=true) + end end - else - print(io, "Toplevel MethodInstance thunk") - # `thunk` is not very much information to go on. If this - # MethodInstance is part of a stacktrace, it gets location info - # added by other means. But if it isn't, then we should try - # to print a little more identifying information. - if !from_stackframe && isdefined(mi, :cache) - ci = mi.cache - if ci.owner === :uninferred - di = ci.inferred.debuginfo - file, line = IRShow.debuginfo_firstline(di) - file = string(file) - line = isempty(file) || line < 0 ? "" : "$file:$line" - print(io, " from ", def, " starting at ", line) + _ -> begin + print(io, "Toplevel MethodInstance thunk") + # `thunk` is not very much information to go on. If this + # MethodInstance is part of a stacktrace, it gets location info + # added by other means. But if it isn't, then we should try + # to print a little more identifying information. + if !from_stackframe && isdefined(mi, :cache) + ci = mi.cache + if ci.owner === :uninferred + di = ci.inferred.debuginfo + file, line = IRShow.debuginfo_firstline(di) + file = string(file) + line = isempty(file) || line < 0 ? "" : "$file:$line" + print(io, " from ", def, " starting at ", line) + end end end end @@ -1761,44 +1772,56 @@ end function show_unquoted(io::IO, ex::SlotNumber, ::Int, ::Int) slotid = ex.id slotnames = get(io, :SOURCE_SLOTNAMES, false) - if isa(slotnames, Vector{String}) && slotid ≤ length(slotnames) - print(io, slotnames[slotid]) - else - print(io, "_", slotid) + match slotnames + slotnames::Vector{String} -> begin + if slotid ≤ length(slotnames) + print(io, slotnames[slotid]) + else + print(io, "_", slotid) + end + end + _ -> print(io, "_", slotid) end end function show_unquoted(io::IO, ex::QuoteNode, indent::Int, prec::Int) - if isa(ex.value, Symbol) - show_unquoted_quote_expr(io, ex.value, indent, prec, 0) - else - print(io, "\$(QuoteNode(") - # QuoteNode does not allows for interpolation, so if ex.value is an - # Expr it should be shown with quote_level equal to zero. - # Calling show(io, ex.value) like this implicitly enforce that. - show(io, ex.value) - print(io, "))") + match ex.value + value::Symbol -> show_unquoted_quote_expr(io, value, indent, prec, 0) + _ -> begin + print(io, "\$(QuoteNode(") + # QuoteNode does not allows for interpolation, so if ex.value is an + # Expr it should be shown with quote_level equal to zero. + # Calling show(io, ex.value) like this implicitly enforce that. + show(io, ex.value) + print(io, "))") + end end end function show_unquoted_quote_expr(io::IO, @nospecialize(value), indent::Int, prec::Int, quote_level::Int) - if isa(value, Symbol) - sym = value::Symbol - if value in quoted_syms - print(io, ":(", sym, ")") - else - if isidentifier(sym) || (_isoperator(sym) && sym !== Symbol("'")) - print(io, ":", sym) + match value + sym::Symbol -> begin + if value in quoted_syms + print(io, ":(", sym, ")") else - print(io, "Symbol(", repr(String(sym)), ")") + if isidentifier(sym) || (_isoperator(sym) && sym !== Symbol("'")) + print(io, ":", sym) + else + print(io, "Symbol(", repr(String(sym)), ")") + end end end - else - if isa(value,Expr) && value.head === :block - value = value::Expr - show_block(IOContext(io, beginsym=>false), "quote", value, indent, quote_level) - print(io, "end") - else + value::Expr -> begin + if value.head === :block + show_block(IOContext(io, beginsym=>false), "quote", value, indent, quote_level) + print(io, "end") + else + print(io, ":(") + show_unquoted(io, value, indent+2, -1, quote_level) # +2 for `:(` + print(io, ")") + end + end + _ -> begin print(io, ":(") show_unquoted(io, value, indent+2, -1, quote_level) # +2 for `:(` print(io, ")") @@ -2853,10 +2876,9 @@ function dump(io::IOContext, @nospecialize(x), n::Int, indent) return end T = typeof(x) - if isa(x, Function) - print(io, x, " (function of type ", T, ")") - else - print(io, T) + match x + x::Function -> print(io, x, " (function of type ", T, ")") + _ -> print(io, T) end nf = nfields(x) if nf > 0 @@ -3389,3 +3411,16 @@ function show(io::IO, ei::Core.EvalInto) show_default(io, ei) end end + +# Except type display +function show(io::IO, e::Except{T,E}) where {T,E} + if is_exception(e) + print(io, "Except{", T, ", ", E, "}(exception: ") + show(io, e._exception) + print(io, ")") + else + print(io, "Except{", T, ", ", E, "}(") + show(io, e._value) + print(io, ")") + end +end diff --git a/base/strings/annotated.jl b/base/strings/annotated.jl index 89cba6db42c8d..20c365b833754 100644 --- a/base/strings/annotated.jl +++ b/base/strings/annotated.jl @@ -248,28 +248,31 @@ function annotatedstring(xs...) annotations = Vector{RegionAnnotation}() for x in xs size = filesize(s.io) - if x isa AnnotatedString - for annot in x.annotations - push!(annotations, setindex(annot, annot.region .+ size, :region)) + match x + x::AnnotatedString -> begin + for annot in x.annotations + push!(annotations, setindex(annot, annot.region .+ size, :region)) + end + print(s, x.string) end - print(s, x.string) - elseif x isa SubString{<:AnnotatedString} - for annot in x.string.annotations - start, stop = first(annot.region), last(annot.region) - if start <= x.offset + x.ncodeunits && stop > x.offset - rstart = size + max(0, start - x.offset - 1) + 1 - rstop = size + min(stop, x.offset + x.ncodeunits) - x.offset - push!(annotations, setindex(annot, rstart:rstop, :region)) + x::SubString{<:AnnotatedString} -> begin + for annot in x.string.annotations + start, stop = first(annot.region), last(annot.region) + if start <= x.offset + x.ncodeunits && stop > x.offset + rstart = size + max(0, start - x.offset - 1) + 1 + rstop = size + min(stop, x.offset + x.ncodeunits) - x.offset + push!(annotations, setindex(annot, rstart:rstop, :region)) + end end + print(s, SubString(x.string.string, x.offset, x.ncodeunits, Val(:noshift))) end - print(s, SubString(x.string.string, x.offset, x.ncodeunits, Val(:noshift))) - elseif x isa AnnotatedChar - for annot in x.annotations - push!(annotations, (region=1+size:1+size, annot...)) + x::AnnotatedChar -> begin + for annot in x.annotations + push!(annotations, (region=1+size:1+size, annot...)) + end + print(s, x.char) end - print(s, x.char) - else - print(s, x) + _ -> print(s, x) end end str = takestring!(buf) diff --git a/base/strings/io.jl b/base/strings/io.jl index 1e2e41f1bca0c..3c9a9f1741252 100644 --- a/base/strings/io.jl +++ b/base/strings/io.jl @@ -112,20 +112,14 @@ function sprint(f::Function, args...; context=nothing, sizehint::Integer=0) end function _str_sizehint(x) - if x isa Float64 - return 20 - elseif x isa Float32 - return 12 - elseif x isa String || x isa SubString{String} - return sizeof(x) - elseif x isa Char - return ncodeunits(x) - elseif x isa UInt64 || x isa UInt32 - return ndigits(x) - elseif x isa Int64 || x isa Int32 - return ndigits(x) + (x < zero(x)) - else - return 8 + match x + ::Float64 -> 20 + ::Float32 -> 12 + x::Union{String, SubString{String}} -> sizeof(x) + x::Char -> ncodeunits(x) + x::Union{UInt64, UInt32} -> ndigits(x) + x::Union{Int64, Int32} -> ndigits(x) + (x < zero(x)) + _ -> 8 end end diff --git a/base/toml_parser.jl b/base/toml_parser.jl index f07f25eeddf25..2aace1a1f11cb 100644 --- a/base/toml_parser.jl +++ b/base/toml_parser.jl @@ -371,11 +371,10 @@ end @inline function accept(l::Parser, f::Union{Function, Char})::Bool c = peek(l) c == EOF_CHAR && return false - ok = false - if isa(f, Function) - ok = f(c) - elseif isa(f, Char) - ok = c === f + ok = match f + f::Function -> f(c) + f::Char -> c === f + _ -> false end ok && eat_char(l) return ok diff --git a/base/tuple.jl b/base/tuple.jl index 3cdf78fa4d135..70c7d081da79a 100644 --- a/base/tuple.jl +++ b/base/tuple.jl @@ -427,18 +427,18 @@ fill_to_length(t::Tuple{}, val, ::Val{2}) = (val, val) function tuple_type_tail(T::Type) @_foldable_meta # TODO: this method is wrong (and not :foldable) - if isa(T, UnionAll) - return UnionAll(T.var, tuple_type_tail(T.body)) - elseif isa(T, Union) - return Union{tuple_type_tail(T.a), tuple_type_tail(T.b)} - else - T.name === Tuple.name || throw(MethodError(tuple_type_tail, (T,))) - if isvatuple(T) && length(T.parameters) == 1 - va = unwrap_unionall(T.parameters[1])::Core.TypeofVararg - (isdefined(va, :N) && isa(va.N, Int)) || return T - return Tuple{Vararg{va.T, va.N-1}} + match T + T::UnionAll -> UnionAll(T.var, tuple_type_tail(T.body)) + T::Union -> Union{tuple_type_tail(T.a), tuple_type_tail(T.b)} + T -> begin + T.name === Tuple.name || throw(MethodError(tuple_type_tail, (T,))) + if isvatuple(T) && length(T.parameters) == 1 + va = unwrap_unionall(T.parameters[1])::Core.TypeofVararg + (isdefined(va, :N) && isa(va.N, Int)) || return T + return Tuple{Vararg{va.T, va.N-1}} + end + return Tuple{argtail(T.parameters...)...} end - return Tuple{argtail(T.parameters...)...} end end diff --git a/doc/src/manual/control-flow.md b/doc/src/manual/control-flow.md index 04bb1ea621f33..40a6a71ae9a49 100644 --- a/doc/src/manual/control-flow.md +++ b/doc/src/manual/control-flow.md @@ -4,7 +4,8 @@ Julia provides a variety of control flow constructs: * [Compound Expressions](@ref man-compound-expressions): `begin` and `;`. * [Conditional Evaluation](@ref man-conditional-evaluation): `if`-`elseif`-`else` and `?:` (ternary operator). - * [Short-Circuit Evaluation](@ref): logical operators `&&` (“and”) and `||` (“or”), and also chained comparisons. + * [Pattern Matching](@ref man-pattern-matching): `match` expressions for structural matching. + * [Short-Circuit Evaluation](@ref): logical operators `&&` ("and") and `||` ("or"), and also chained comparisons. * [Repeated Evaluation: Loops](@ref man-loops): `while` and `for`. * [Exception Handling](@ref): `try`-`catch`, [`error`](@ref) and [`throw`](@ref). * [Tasks (aka Coroutines)](@ref man-tasks): [`yieldto`](@ref). @@ -245,6 +246,151 @@ no "no" ``` +## [Pattern Matching](@id man-pattern-matching) + +Pattern matching provides a way to match values against patterns and extract components. Julia's +`match` expression evaluates an expression and compares it against a series of patterns, executing +the body of the first matching pattern. + +```julia +match value + pattern1 -> result1 + pattern2 -> result2 + _ -> default +end +``` + +Here's a simple example: + +```julia +function describe(x) + match x + 0 -> "zero" + 1 -> "one" + _ -> "something else" + end +end +``` + +### Pattern Types + +The following patterns are supported: + + * **Literals**: Numbers, strings, and other values match by equality. + ```julia + match x + 42 -> "the answer" + "hello" -> "greeting" + _ -> "unknown" + end + ``` + + * **Wildcard**: The underscore `_` matches any value without binding it. + ```julia + match (a, b) + (1, _) -> "starts with one" + (_, 2) -> "ends with two" + _ -> "other" + end + ``` + + * **Variable Capture**: A plain identifier captures the matched value. + ```julia + match x + n -> "got $n" + end + ``` + + * **Type Constraints**: Use `::T` for type checking, and `name::T` for typed capture. + ```julia + match x + n::Int -> "integer: $n" + s::String -> "string: $s" + ::Nothing -> "nothing" + _ -> "other type" + end + ``` + + * **Tuple Destructuring**: Match and destructure tuples. + ```julia + match point + (0, 0) -> "origin" + (x, 0) -> "on x-axis at $x" + (0, y) -> "on y-axis at $y" + (x, y) -> "at ($x, $y)" + end + ``` + + * **Alternation**: Use `|` to match multiple patterns with the same body. + ```julia + match x + 1 | 2 | 3 -> "small" + _ -> "large" + end + ``` + + * **Value Escaping**: Use `$expr` to match against the value of an expression rather than + treating it as a pattern. + ```julia + expected = 42 + match x + $expected -> "matched expected" + _ -> "different" + end + ``` + + * **Constructor Patterns**: Match against struct constructors. + ```julia + match opt + Some(x) -> "got $x" + nothing -> "nothing" + end + ``` + +### Return Behavior + +Using `return` inside a `match` arm exits the `match` expression, not the enclosing function: + +```julia +function example(x) + result = match x + n::Int -> return n * 2 # exits match, not example() + _ -> return 0 + end + println("result: $result") + result + 1 +end +``` + +### MatchError + +If no pattern matches, a [`MatchError`](@ref) is thrown: + +```julia +match 5 + 1 -> "one" + 2 -> "two" +end +# throws MatchError: no pattern matched value 5 +``` + +Always include a wildcard `_` pattern as the last arm if a match failure is not desired. + +### The @match Macro + +The [`@match`](@ref) macro provides an alternative syntax using a `begin...end` block: + +```julia +@match value begin + pattern1 -> result1 + pattern2 -> result2 + _ -> default +end +``` + +This form is useful when you want pattern matching without the native syntax, or for +meta-programming purposes. + ## Short-Circuit Evaluation The `&&` and `||` operators in Julia correspond to logical “and” and “or” operations, respectively, diff --git a/src/julia-parser.scm b/src/julia-parser.scm index 1a11494b5c8e3..ac509fc7d19c7 100644 --- a/src/julia-parser.scm +++ b/src/julia-parser.scm @@ -1136,8 +1136,76 @@ (let ((nxt (peek-token s))) (parse-call-with-initial-ex s (parse-unary-prefix s) nxt))) +;; Parse a match pattern - an expression that stops at -> +;; Patterns may include guards: `pattern if cond ->` +;; Returns the pattern, possibly wrapped in (guard pattern cond) +;; +;; The challenge: -> is consumed by parse-decl-with-initial-ex which is called +;; deep in the expression parsing chain. We need to parse expressions without +;; going through that path. We use parse-call which handles function calls, +;; indexing, etc. but doesn't consume ->. +(define (parse-match-pattern s) + (define (parse-pattern-expr s) + ;; Parse an expression suitable for a match pattern, stopping at -> + ;; This is like parse-call but handles operators that don't include -> + (let ((nxt (peek-token s))) + (parse-call-with-initial-ex s (parse-unary-prefix s) nxt))) + + (let loop ((parts '())) + (let* ((part (parse-pattern-expr s)) + (t (peek-token s))) + (cond + ;; Guard condition: pattern if cond -> ... + ((eq? t 'if) + (take-token s) + (let ((cond-expr (parse-pattern-expr s))) + (let ((combined (if (null? parts) + part + `(tuple ,@(reverse (cons part parts)))))) + `(guard ,combined ,cond-expr)))) + ;; Tuple continuation with comma + ((eqv? t #\,) + (take-token s) + (loop (cons part parts))) + ;; End of pattern (at ->) + (else + (if (null? parts) + part + `(tuple ,@(reverse (cons part parts))))))))) + +;; Check if 'match' should be treated as a keyword based on next token context +;; Called AFTER 'match' has already been consumed, so we just peek at what comes next. +;; Returns #t if match should be parsed as a keyword, #f if as an identifier +(define (match-is-keyword? s) + ;; IMPORTANT: Must call peek-token FIRST to ensure the next token is read + ;; and the space info is updated. ts:space? returns stale info if called + ;; before peek-token when the last token was just consumed. + (let* ((t2 (peek-token s)) + (spc (ts:space? s))) + (cond + ;; Newline after match means it's a standalone identifier (like `return match`) + ((newline? t2) #f) + ;; Closing tokens indicate match is an identifier + ((closing-token? t2) #f) + ;; Type annotation, assignment, field access, pair, iteration keywords + ((memq t2 '(|::| = |.| => in |∈|)) #f) + ;; Call or indexing without space means function call (match(x)) + ((and (memv t2 '(#\( #\[)) (not spc)) #f) + ;; Special case: match? (exception-catching match) - ? without whitespace + ;; is part of the keyword, so match IS a reserved word + ((and (eq? t2 '?) (not spc)) #t) + ;; Operators that can't start an expression mean match is identifier + ((and (or (operator? t2) (memq t2 '(=== !== ≡ ≢))) + (not (unary-op? t2))) + #f) + ;; Otherwise, treat as keyword + (else #t)))) + (define (parse-call-with-initial-ex s ex tok) - (if (or (initial-reserved-word? tok) (memq tok '(mutable primitive abstract))) + (if (or (initial-reserved-word? tok) + (memq tok '(mutable primitive abstract)) + ;; 'match' is a contextual keyword - only a keyword if followed by expression + (and (eq? tok 'match) (match-is-keyword? s))) (parse-resword s ex) (parse-call-chain s ex #f))) @@ -1278,6 +1346,14 @@ (list t ex) (list 'call t ex))))) ex)) + ((?) + ;; Postfix ? for declared exceptions - exception forwarding/unwrapping + ;; expr? ==> (postfix-? expr) + (if (not (ts:space? s)) + (begin + (take-token s) + (loop (list 'postfix-? ex))) + ex)) ((|.'|) (error "the \".'\" operator is discontinued")) ((#\{ ) (disallow-space s ex t) @@ -1593,6 +1669,43 @@ finalb eb))) (else (expect-end-error nxt 'try)))))) + + ((match) + ;; Check for ? immediately after match (no whitespace) for exception-catching mode + (let* ((is-except (and (eq? (peek-token s) '?) (not (ts:space? s)))) + (_ (if is-except (take-token s))) ;; consume ? as trivia + (scrutinee (parse-eq s))) + ;; Expect newline or semicolon after scrutinee + (if (not (memv (peek-token s) '(#\newline #\;))) + (if (not (eq? (peek-token s) 'end)) + (error "expected newline or semicolon after match expression"))) + (take-lineendings s) + ;; Parse match arms + (let loop ((arms '())) + (let ((t (peek-token s))) + (cond + ((eq? t 'end) + (take-token s) + (if (null? arms) + (error "match requires at least one arm")) + ;; Emit match or match? depending on exception mode + (if is-except + `(match? ,scrutinee ,@(reverse arms)) + `(match ,scrutinee ,@(reverse arms)))) + (else + ;; Parse pattern (stopping at ->) + (let* ((pattern (parse-match-pattern s)) + (arrow (require-token s))) + (if (not (eq? arrow '->)) + (error "expected '->' in match arm")) + (take-token s) + ;; Parse body + (let ((body (if (memv (peek-token s) '(#\newline #\;)) + '(null) + (parse-eq s)))) + (take-lineendings s) + (loop (cons `(matcharm ,pattern ,body) arms)))))))))) + ((return) (let ((t (peek-token s))) (if (or (eqv? t #\newline) (closing-token? t)) (list 'return '(null)) diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index c9b1fd551e359..4febfff3b294d 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -746,6 +746,20 @@ ;; definitions without keyword arguments are passed to method-def-expr-, ;; which handles optional positional arguments by adding the needed small ;; boilerplate definitions. +;; Check if a return type expression is Except{...} or AnyExcept{...} +(define (except-return-type? rett) + (and (pair? rett) + (eq? (car rett) 'curly) + (pair? (cdr rett)) + (let ((type-name (cadr rett))) + (or (eq? type-name 'Except) + (eq? type-name 'AnyExcept) + (and (pair? type-name) + (or (eq? (car type-name) 'top) + (eq? (car type-name) 'globalref) + (eq? (car type-name) 'outerref)) + (memq (cadr type-name) '(Except AnyExcept))))))) + (define (method-def-expr name sparams argl body rett) (let ((argl (throw-unassigned-kw-args (remove-empty-parameters argl)))) (if (has-parameters? argl) @@ -753,7 +767,77 @@ (begin (check-kw-args (cdar argl)) (keywords-method-def-expr name sparams argl body rett)) ;; no keywords - (method-def-expr- name sparams argl body rett)))) + (if (except-return-type? rett) + ;; Except return type - generate inner (raw) and outer (unwrapping) methods + (except-method-def-expr name sparams argl body rett) + (method-def-expr- name sparams argl body rett))))) + +;; Generate two methods for Except-returning functions: +;; 1. Inner method with ExceptRaw first arg - returns raw Except +;; 2. Outer method (normal signature) - calls inner and unwraps +;; Helper to extract arg name for calling, handling UNUSED substitution +;; Returns (new-arg . arg-name-for-call) +;; arg-counter-cell is a cons cell used as mutable counter: (car arg-counter-cell) is the count +(define (except-process-arg a arg-counter-cell) + (define (gen-name!) + (let ((n (+ 1 (car arg-counter-cell)))) + (set-car! arg-counter-cell n) + (symbol (string "#exc-arg" n "#")))) + (cond ((symbol? a) + (if (eq? a UNUSED) + ;; Generate name for bare UNUSED arg + (let ((gn (gen-name!))) + (cons gn gn)) + (cons a a))) + ((and (pair? a) (eq? (car a) '|::|)) + (cond ((length= a 2) + ;; Unnamed arg (:: Type) - generate a name + (let ((gn (gen-name!))) + (cons `(|::| ,gn ,(cadr a)) gn))) + ((eq? (cadr a) UNUSED) + ;; Already-processed unnamed arg (:: #unused# Type) - generate a real name + (let ((gn (gen-name!))) + (cons `(|::| ,gn ,(caddr a)) gn))) + (else + ;; Named arg (:: x Type) - use existing name + (cons a (cadr a))))) + ((and (pair? a) (eq? (car a) '...)) + ;; Vararg - process inner and wrap result + (let* ((inner (cadr a)) + (inner-result (except-process-arg inner arg-counter-cell)) + (new-inner (car inner-result)) + (inner-name (cdr inner-result))) + (cons `(... ,new-inner) `(... ,inner-name)))) + (else + ;; Fallback - this shouldn't happen with valid arglists + (let ((gn (gen-name!))) + (cons a gn))))) + +(define (except-method-def-expr name sparams argl body rett) + (let* (;; Get the function arg (first arg is #self#) + (self-arg (car argl)) + (rest-args (cdr argl)) + ;; For both methods: need to handle unnamed args (those with UNUSED as name) + ;; fix-arglist has already converted (:: Type) to (:: #unused# Type) + ;; We need to generate actual names for these to use in the outer method's call + (arg-counter-cell (cons 0 '())) ;; mutable counter as cons cell + (processed-args (map (lambda (a) (except-process-arg a arg-counter-cell)) rest-args)) + (named-rest-args (map car processed-args)) + (arg-names (map cdr processed-args)) + ;; Inner method arg list: add ExceptRaw as first positional arg after #self# + (raw-arg '(|::| |#except-raw#| (top ExceptRaw))) + (inner-argl (cons self-arg (cons raw-arg named-rest-args))) + ;; Outer method uses the same named args + (outer-argl (cons self-arg named-rest-args)) + (outer-body `(block + (return (call (top unwrap) + (call |#self#| (top except_raw) ,@arg-names)))))) + ;; Generate both inner and outer method definitions + `(block + ;; Inner method (with ExceptRaw) - executes the actual body + ,(method-def-expr- name sparams inner-argl body rett) + ;; Outer method (normal signature) - calls inner and unwraps + ,(method-def-expr- name sparams outer-argl outer-body '(core Any))))) (define (struct-def-expr name params super fields mut) (receive @@ -1439,6 +1523,258 @@ (else (error "invalid \"try\" form"))))) +;; Expand match expression +;; (match scrutinee (matcharm pattern1 body1) (matcharm pattern2 body2) ...) +;; Expands to nested if/else with pattern matching calls +(define (expand-match e) + (let ((scrutinee-var (make-ssavalue)) + (scrutinee-expr (cadr e)) + (arms (cddr e))) + (expand-forms + `(block + (= ,scrutinee-var ,scrutinee-expr) + ,(expand-match-arms scrutinee-var arms))))) + +;; Expand the arms of a match expression +(define (expand-match-arms scrutinee-var arms) + (if (null? arms) + ;; No match - throw MatchError + `(call (top throw) (call (top MatchError) ,scrutinee-var)) + (let* ((arm (car arms)) + (pattern (cadr arm)) + (body (caddr arm))) + (expand-match-arm scrutinee-var pattern body (cdr arms))))) + +;; Expand a single match arm +;; If pattern is (guard pat cond), we match pat first, then check cond +(define (expand-match-arm scrutinee-var pattern body remaining-arms) + (if (and (pair? pattern) (eq? (car pattern) 'guard)) + ;; Pattern with guard: (guard actual-pattern guard-condition) + (expand-match-arm-with-guard scrutinee-var (cadr pattern) (caddr pattern) body remaining-arms) + ;; Pattern without guard + (expand-match-arm-simple scrutinee-var pattern body remaining-arms))) + +;; Expand match arm without guard +(define (expand-match-arm-simple scrutinee-var pattern body remaining-arms) + (let ((bindings-var (make-ssavalue)) + (captures (collect-match-captures pattern))) + `(block + (= ,bindings-var (call (top match) ,(pattern-to-matcher pattern) ,scrutinee-var)) + (if (call (core ===) ,bindings-var (null)) + ,(expand-match-arms scrutinee-var remaining-arms) + ,(if (null? captures) + body + `(scope-block + (block + ,@(map (lambda (name) + `(= ,name (call (top getindex) ,bindings-var (inert ,name)))) + captures) + ,body))))))) + +;; Expand match arm with guard +(define (expand-match-arm-with-guard scrutinee-var pattern guard-cond body remaining-arms) + (let ((bindings-var (make-ssavalue)) + (captures (collect-match-captures pattern))) + `(block + (= ,bindings-var (call (top match) ,(pattern-to-matcher pattern) ,scrutinee-var)) + (if (call (core ===) ,bindings-var (null)) + ,(expand-match-arms scrutinee-var remaining-arms) + ,(if (null? captures) + `(if ,guard-cond + ,body + ,(expand-match-arms scrutinee-var remaining-arms)) + `(scope-block + (block + ,@(map (lambda (name) + `(= ,name (call (top getindex) ,bindings-var (inert ,name)))) + captures) + (if ,guard-cond + ,body + ,(expand-match-arms scrutinee-var remaining-arms))))))))) + +;; Expand inline match-destructuring: (match-assign pattern value) +(define (expand-match-assign e) + (let* ((pattern (cadr e)) + (value-expr (caddr e)) + (value-var (make-ssavalue)) + (bindings-var (make-ssavalue)) + (captures (collect-match-captures pattern))) + (expand-forms + `(block + (= ,value-var ,value-expr) + (= ,bindings-var (call (top match) ,(pattern-to-matcher pattern) ,value-var)) + (if (call (core ===) ,bindings-var (null)) + (call (top throw) (call (top MatchError) ,value-var)) + (block + ,@(map (lambda (name) + `(= ,name (call (top getindex) ,bindings-var (inert ,name)))) + captures) + ,value-var)))))) + +;; Convert a pattern AST to a matcher expression +(define (pattern-to-matcher pattern) + (cond + ;; Wildcard: _ matches anything + ((underscore-symbol? pattern) + `(call (top Wildcard))) + ;; Literal values + ((or (number? pattern) (string? pattern)) + `(call (top Literal) ,pattern)) + ;; Boolean literals + ((eq? pattern 'true) `(call (top Literal) (true))) + ((eq? pattern 'false) `(call (top Literal) (false))) + ;; Symbol: capture variable + ((symbol? pattern) + `(call (top Capture) (inert ,pattern))) + ;; Quote/inert: literal symbol + ((and (pair? pattern) (memq (car pattern) '(quote inert))) + `(call (top Literal) ,pattern)) + ;; Escaped value: $expr + ((and (pair? pattern) (eq? (car pattern) '$)) + `(call (top Literal) ,(cadr pattern))) + ;; Type assertion: x::T or ::T + ((and (pair? pattern) (eq? (car pattern) '|::|)) + (if (length= pattern 2) + ;; ::T - just type check + `(call (top TypeMatcher) ,(cadr pattern)) + ;; x::T - capture with type check + `(call (top TypedCapture) (inert ,(cadr pattern)) ,(caddr pattern)))) + ;; Tuple pattern: (tuple a b c) + ((and (pair? pattern) (eq? (car pattern) 'tuple)) + `(call (top TupleMatcher) ,@(map pattern-to-matcher (cdr pattern)))) + ;; Alternation: (call | p1 p2) + ((and (pair? pattern) (eq? (car pattern) 'call) (eq? (cadr pattern) '|\||)) + `(call (top Alternation) ,@(map pattern-to-matcher (cddr pattern)))) + ;; Call pattern: Some(x) -> (call Some x) + ((and (pair? pattern) (eq? (car pattern) 'call)) + `(call (top CallMatcher) ,(cadr pattern) ,@(map pattern-to-matcher (cddr pattern)))) + (else + (error (string "unsupported match pattern: " (deparse pattern)))))) + +;; Collect capture variable names from a pattern +(define (collect-match-captures pattern) + (cond + ((underscore-symbol? pattern) '()) + ((symbol? pattern) (list pattern)) + ((and (pair? pattern) (eq? (car pattern) '$)) '()) + ((and (pair? pattern) (memq (car pattern) '(quote inert))) '()) + ((and (pair? pattern) (eq? (car pattern) '|::|)) + (if (length= pattern 2) + '() + (if (symbol? (cadr pattern)) + (list (cadr pattern)) + '()))) + ((and (pair? pattern) (eq? (car pattern) 'tuple)) + (delete-duplicates (apply append (map collect-match-captures (cdr pattern))))) + ((and (pair? pattern) (eq? (car pattern) 'call)) + (if (eq? (cadr pattern) '|\||) + ;; Alternation - captures from all branches should be the same + (delete-duplicates (apply append (map collect-match-captures (cddr pattern)))) + ;; Call pattern - captures from arguments + (delete-duplicates (apply append (map collect-match-captures (cddr pattern)))))) + (else '()))) + +;; Expand postfix ? for exception forwarding: (postfix-? expr) +;; - throw(ex)? transforms to except_exception wrapping +;; - f(args...)? transforms to f(except_raw, args...) to call inner method +;; - other expr? transforms to forward_or_unwrap(expr) +(define (expand-postfix-question e) + (let ((expr (cadr e))) + (cond + ;; throw(ex)? - wrap exception in Except instead of throwing + ((and (pair? expr) (eq? (car expr) 'call) + (or (eq? (cadr expr) 'throw) + (and (pair? (cadr expr)) (eq? (car (cadr expr)) 'top) + (eq? (cadr (cadr expr)) 'throw)))) + (let ((exc-expr (caddr expr))) + (expand-forms + `(call (top except_exception) + (curly (top Except) (core Any) (top Exception)) + ,exc-expr)))) + ;; f(args...)? - call inner method with except_raw + ;; BUT: don't transform type constructor calls (where func is curly or a type) + ((and (pair? expr) (eq? (car expr) 'call) + ;; Don't transform if func is a curly expression (type constructor) + (not (and (pair? (cadr expr)) (eq? (car (cadr expr)) 'curly)))) + (let ((func (cadr expr)) + (args (cddr expr))) + (expand-forms + `(call (top forward_or_unwrap) + (call ,func (top except_raw) ,@args))))) + ;; Other expressions - just forward or unwrap + (else + (expand-forms + `(call (top forward_or_unwrap) ,expr)))))) + +;; Expand exception-catching match: (match? scrutinee arms...) +;; Similar to match but works on Except values +;; If scrutinee is a function call, insert except_raw to get raw Except +(define (expand-match-except e) + (let* ((scrutinee-var (make-ssavalue)) + (scrutinee-expr (cadr e)) + (arms (cddr e)) + ;; Transform function calls to use except_raw + (raw-scrutinee (if (and (pair? scrutinee-expr) (eq? (car scrutinee-expr) 'call)) + (let ((func (cadr scrutinee-expr)) + (args (cddr scrutinee-expr))) + `(call ,func (top except_raw) ,@args)) + scrutinee-expr))) + (expand-forms + `(block + (= ,scrutinee-var ,raw-scrutinee) + ,(expand-match-except-check scrutinee-var arms))))) + +;; Check if value is an Except with exception, then match +(define (expand-match-except-check scrutinee-var arms) + `(if (call (top is_exception) ,scrutinee-var) + ,(expand-match-except-arms scrutinee-var arms) + (call (top unwrap) ,scrutinee-var))) + +;; Expand exception match arms +(define (expand-match-except-arms scrutinee-var arms) + (if (null? arms) + ;; No match - re-throw by unwrapping + `(call (top unwrap) ,scrutinee-var) + (let* ((arm (car arms)) + (pattern (cadr arm)) + (body (caddr arm))) + (expand-match-except-arm scrutinee-var pattern body (cdr arms))))) + +;; Expand a single exception match arm +;; Pattern is typically ::ExceptionType +(define (expand-match-except-arm scrutinee-var pattern body remaining-arms) + (let ((exc-var (make-ssavalue))) + `(block + (= ,exc-var (call (top get_exception) ,scrutinee-var)) + ,(cond + ;; Type pattern: ::ErrorType + ((and (pair? pattern) (eq? (car pattern) '|::|) (length= pattern 2)) + (let ((exc-type (cadr pattern))) + `(if (call (core isa) ,exc-var ,exc-type) + ,body + ,(expand-match-except-arms scrutinee-var remaining-arms)))) + ;; Typed capture: e::ErrorType + ((and (pair? pattern) (eq? (car pattern) '|::|) (length= pattern 3)) + (let ((capture-name (cadr pattern)) + (exc-type (caddr pattern))) + `(if (call (core isa) ,exc-var ,exc-type) + (scope-block + (block + (= ,capture-name ,exc-var) + ,body)) + ,(expand-match-except-arms scrutinee-var remaining-arms)))) + ;; Simple capture: e (matches any exception) + ((symbol? pattern) + `(scope-block + (block + (= ,pattern ,exc-var) + ,body))) + ;; Wildcard: _ (matches any exception) + ((underscore-symbol? pattern) + body) + (else + (error (string "unsupported exception pattern: " (deparse pattern)))))))) + (define (expand-unionall-def name type-ex (const? #t)) (if (and (pair? name) (eq? (car name) 'curly)) @@ -2584,6 +2920,11 @@ 'macro expand-macro-def 'struct expand-struct-def 'try expand-try + 'match expand-match + 'match-assign expand-match-assign + 'match? expand-match-except + 'matcharm (lambda (e) (error "matcharm outside match")) + 'postfix-? expand-postfix-question 'lambda (lambda (e) diff --git a/test/choosetests.jl b/test/choosetests.jl index fcdddcc89a98c..aa22c8d09474e 100644 --- a/test/choosetests.jl +++ b/test/choosetests.jl @@ -21,7 +21,7 @@ const TESTNAMES = [ "combinatorics", "sysinfo", "env", "rounding", "ranges", "mod2pi", "euler", "show", "client", "terminfo", "errorshow", "sets", "goto", "llvmcall", "llvmcall2", "ryu", - "some", "meta", "stacktraces", "docs", "gc", + "some", "match", "meta", "stacktraces", "docs", "gc", "misc", "threads", "stress", "binaryplatforms","stdlib_dependencies", "atexit", "enums", "cmdlineargs", "int", "interpreter", "checked", "bitset", "floatfuncs", "precompile", "relocatedepot", diff --git a/test/except.jl b/test/except.jl new file mode 100644 index 0000000000000..0572c56cdcda0 --- /dev/null +++ b/test/except.jl @@ -0,0 +1,123 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +# Tests for declared exceptions (Except type, postfix ?, match?) + +@testset "Except type" begin + # Success case + e = Except{Int, BoundsError}(42) + @test !is_exception(e) + @test unwrap(e) == 42 + @test get_exception(e) === nothing + + # Exception case + ex = Base.except_exception(Except{Int, BoundsError}, BoundsError([1,2,3], 5)) + @test is_exception(ex) + @test get_exception(ex) isa BoundsError + @test_throws BoundsError unwrap(ex) +end + +@testset "AnyExcept alias" begin + e = AnyExcept{KeyError}("test value") + @test typeof(e) == Except{Any, KeyError} + @test !is_exception(e) + @test unwrap(e) == "test value" +end + +@testset "except_value and except_exception" begin + # except_value creates success + e1 = Base.except_value(Except{Int, KeyError}, 42) + @test !is_exception(e1) + @test unwrap(e1) == 42 + + # except_exception creates exception + e2 = Base.except_exception(Except{Int, KeyError}, KeyError(:foo)) + @test is_exception(e2) + @test get_exception(e2) isa KeyError +end + +@testset "postfix ? operator" begin + # Success case - unwraps value + e1 = Except{Int, BoundsError}(42) + @test e1? == 42 + + # Exception case - forwards exception (returns Except) + e2 = Base.except_exception(Except{Int, BoundsError}, BoundsError([1,2], 3)) + result = e2? + @test result isa Except + @test is_exception(result) +end + +@testset "match? basic" begin + # Success case returns unwrapped value + e1 = Except{Int, BoundsError}(42) + result = match? e1 + ::BoundsError -> "bounds" + _ -> "other" + end + @test result == 42 + + # Exception case matches pattern + e2 = Base.except_exception(Except{Int, BoundsError}, BoundsError([1,2], 3)) + result = match? e2 + ::BoundsError -> "bounds" + _ -> "other" + end + @test result == "bounds" +end + +@testset "match? multiple exception types" begin + e_bounds = Base.except_exception(Except{Int, Exception}, BoundsError([1,2], 3)) + e_key = Base.except_exception(Except{Int, Exception}, KeyError(:foo)) + e_other = Base.except_exception(Except{Int, Exception}, ErrorException("test")) + + function classify(e) + match? e + ::BoundsError -> "bounds" + ::KeyError -> "key" + _ -> "other" + end + end + + @test classify(e_bounds) == "bounds" + @test classify(e_key) == "key" + @test classify(e_other) == "other" +end + +@testset "match? with capture" begin + e = Base.except_exception(Except{Int, BoundsError}, BoundsError([1,2,3], 5)) + + # Capture exception value + result = match? e + err::BoundsError -> (err.a, err.i) + _ -> nothing + end + @test result == ([1,2,3], 5) +end + +@testset "match? wildcard" begin + e = Base.except_exception(Except{Int, ErrorException}, ErrorException("test")) + + result = match? e + _ -> "caught any" + end + @test result == "caught any" +end + +@testset "match? no match rethrows" begin + e = Base.except_exception(Except{Int, ErrorException}, ErrorException("test")) + + # Only BoundsError arm - should rethrow ErrorException + @test_throws ErrorException match? e + ::BoundsError -> "bounds" + end +end + +@testset "Except show" begin + e1 = Except{Int, BoundsError}(42) + @test contains(sprint(show, e1), "42") + @test contains(sprint(show, e1), "Except") + + e2 = Base.except_exception(Except{Int, BoundsError}, BoundsError([1], 2)) + @test contains(sprint(show, e2), "exception") + @test contains(sprint(show, e2), "BoundsError") +end diff --git a/test/match.jl b/test/match.jl new file mode 100644 index 0000000000000..5d18f037f238c --- /dev/null +++ b/test/match.jl @@ -0,0 +1,258 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +# Tests for the match statement and @match macro + +@testset "MatchError" begin + @test MatchError <: Exception + e = MatchError(42) + @test e.value == 42 + @test sprint(showerror, e) == "MatchError: no pattern matched value 42" +end + +@testset "Wildcard matcher" begin + m = Base.Wildcard() + @test Base.match(m, 1) == Dict{Symbol,Any}() + @test Base.match(m, "hello") == Dict{Symbol,Any}() + @test Base.match(m, nothing) == Dict{Symbol,Any}() +end + +@testset "Literal matcher" begin + m = Base.Literal(42) + @test Base.match(m, 42) == Dict{Symbol,Any}() + @test Base.match(m, 42.0) == Dict{Symbol,Any}() # 42.0 == 42 + @test Base.match(m, 43) === nothing + @test Base.match(m, "42") === nothing + + # String literals + m_str = Base.Literal("hello") + @test Base.match(m_str, "hello") == Dict{Symbol,Any}() + @test Base.match(m_str, "world") === nothing +end + +@testset "Capture matcher" begin + m = Base.Capture(:x) + @test Base.match(m, 42) == Dict{Symbol,Any}(:x => 42) + @test Base.match(m, "hello") == Dict{Symbol,Any}(:x => "hello") +end + +@testset "TypeMatcher" begin + m = Base.TypeMatcher(Int) + @test Base.match(m, 42) == Dict{Symbol,Any}() + @test Base.match(m, 42.0) === nothing + @test Base.match(m, "42") === nothing + + m_num = Base.TypeMatcher(Number) + @test Base.match(m_num, 42) == Dict{Symbol,Any}() + @test Base.match(m_num, 3.14) == Dict{Symbol,Any}() + @test Base.match(m_num, "42") === nothing +end + +@testset "TypedCapture matcher" begin + m = Base.TypedCapture(:x, Int) + @test Base.match(m, 42) == Dict{Symbol,Any}(:x => 42) + @test Base.match(m, 42.0) === nothing + + m_num = Base.TypedCapture(:n, Number) + @test Base.match(m_num, 42) == Dict{Symbol,Any}(:n => 42) + @test Base.match(m_num, 3.14) == Dict{Symbol,Any}(:n => 3.14) + @test Base.match(m_num, "42") === nothing +end + +@testset "TupleMatcher" begin + m = Base.TupleMatcher(Base.Capture(:a), Base.Capture(:b)) + @test Base.match(m, (1, 2)) == Dict{Symbol,Any}(:a => 1, :b => 2) + @test Base.match(m, (1, 2, 3)) === nothing # wrong length + @test Base.match(m, [1, 2]) === nothing # not a tuple + @test Base.match(m, 1) === nothing # not a tuple + + # Nested tuple + m_nested = Base.TupleMatcher( + Base.TupleMatcher(Base.Capture(:a), Base.Capture(:b)), + Base.Capture(:c) + ) + @test Base.match(m_nested, ((1, 2), 3)) == Dict{Symbol,Any}(:a => 1, :b => 2, :c => 3) + + # With literals + m_lit = Base.TupleMatcher(Base.Literal(1), Base.Capture(:x)) + @test Base.match(m_lit, (1, 42)) == Dict{Symbol,Any}(:x => 42) + @test Base.match(m_lit, (2, 42)) === nothing +end + +@testset "Alternation matcher" begin + m = Base.Alternation(Base.Literal(1), Base.Literal(2)) + @test Base.match(m, 1) == Dict{Symbol,Any}() + @test Base.match(m, 2) == Dict{Symbol,Any}() + @test Base.match(m, 3) === nothing + + # With captures + m_cap = Base.Alternation( + Base.TupleMatcher(Base.Literal(:a), Base.Capture(:x)), + Base.TupleMatcher(Base.Literal(:b), Base.Capture(:x)) + ) + @test Base.match(m_cap, (:a, 1)) == Dict{Symbol,Any}(:x => 1) + @test Base.match(m_cap, (:b, 2)) == Dict{Symbol,Any}(:x => 2) + @test Base.match(m_cap, (:c, 3)) === nothing +end + +@testset "CallMatcher" begin + # Test with Some type + m = Base.CallMatcher(Some, Base.Capture(:x)) + @test Base.match(m, Some(42)) == Dict{Symbol,Any}(:x => 42) + @test Base.match(m, 42) === nothing + @test Base.match(m, nothing) === nothing + + # With literal + m_lit = Base.CallMatcher(Some, Base.Literal(1)) + @test Base.match(m_lit, Some(1)) == Dict{Symbol,Any}() + @test Base.match(m_lit, Some(2)) === nothing +end + +@testset "@match macro basics" begin + # Literal matching + @test (@match 1 begin + 1 -> "one" + 2 -> "two" + _ -> "other" + end) == "one" + + @test (@match 2 begin + 1 -> "one" + 2 -> "two" + _ -> "other" + end) == "two" + + @test (@match 3 begin + 1 -> "one" + 2 -> "two" + _ -> "other" + end) == "other" +end + +@testset "@match variable capture" begin + result = @match 42 begin + x -> x + 1 + end + @test result == 43 + + result = @match (1, 2) begin + (a, b) -> a + b + end + @test result == 3 +end + +@testset "@match type constraints" begin + test_type_match(x) = @match x begin + n::Int -> "int: $n" + s::String -> "string: $s" + _ -> "other" + end + + @test test_type_match(42) == "int: 42" + @test test_type_match("hello") == "string: hello" + @test test_type_match(3.14) == "other" +end + +@testset "@match no match throws MatchError" begin + @test_throws MatchError @match 3 begin + 1 -> "one" + 2 -> "two" + end +end + +@testset "@match value escaping" begin + expected = 42 + result = @match 42 begin + $expected -> "matched" + _ -> "not matched" + end + @test result == "matched" + + result = @match 43 begin + $expected -> "matched" + _ -> "not matched" + end + @test result == "not matched" +end + +@testset "@match tuple destructuring" begin + result = @match (1, (2, 3)) begin + (a, (b, c)) -> a + b + c + end + @test result == 6 + + result = @match (1, 2, 3) begin + (1, x, y) -> x + y + _ -> 0 + end + @test result == 5 +end + +@testset "match statement if guards" begin + # Basic guard + result = match 5 + n if n > 0 -> "positive" + n if n < 0 -> "negative" + _ -> "zero" + end + @test result == "positive" + + result = match -3 + n if n > 0 -> "positive" + n if n < 0 -> "negative" + _ -> "zero" + end + @test result == "negative" + + result = match 0 + n if n > 0 -> "positive" + n if n < 0 -> "negative" + _ -> "zero" + end + @test result == "zero" + + # Guard with tuple destructuring + result = match (3, 4) + (a, b) if a + b > 5 -> "sum > 5" + (a, b) -> "sum <= 5" + end + @test result == "sum > 5" + + result = match (1, 2) + (a, b) if a + b > 5 -> "sum > 5" + (a, b) -> "sum <= 5" + end + @test result == "sum <= 5" + + # Guard that fails, falling through to next arm + result = match 10 + x if x < 5 -> "small" + x if x < 15 -> "medium" + _ -> "large" + end + @test result == "medium" +end + +@testset "match statement inline destructuring" begin + # Basic inline match-destructuring + match (x, y) = (10, 20) + @test x == 10 + @test y == 20 + + # Nested tuple destructuring + match (a, (b, c)) = (1, (2, 3)) + @test a == 1 + @test b == 2 + @test c == 3 + + # MatchError when pattern doesn't match + @test_throws MatchError match (p, q) = (1, 2, 3) + + # With type constraint + match (m::Int, n::Int) = (5, 6) + @test m == 5 + @test n == 6 + + # Type constraint mismatch throws MatchError + @test_throws MatchError match (r::String, s) = (1, 2) +end +