diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ed6bb0a..b091b56 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -18,8 +18,10 @@ jobs: fail-fast: false matrix: version: - - '1.6' - - '1' + - '1.4' + - '1.5' + - '1.8.2' + - '1.9' - 'nightly' os: - ubuntu-latest diff --git a/.gitignore b/.gitignore index 87b968c..2ae8951 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.jl.*.cov +*.jl.*.mem *.jl.cov *.jl.mem Manifest.toml @@ -6,3 +7,6 @@ Manifest.toml /docs/Manifest.toml /tmp/* .vscode +.DS_Store +._* +tmp diff --git a/CHANGES.md b/CHANGES.md index c864153..2be4eba 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +v2.0.0 / 2023-08-02 +=================== + + * Complete overhaul of implementation (taken from Rematch2.jl) + * Code generation via an optimized decision automaton. + * Requires Julia 1.4 + * Note incompatibility: drops support for multidimensional arrays. See `README.md`. v0.4.0 / 2017-07-11 ================== @@ -97,7 +104,7 @@ v0.0.3 / 2014-03-02 * Improve code generation for testing constant values. * Update exports, remove @fmatch, rename _fmatch -> fmatch * Fix matrix matching, update contains->in usage - + * Doc format updates * Fixes for ReadTheDocs/sphinx diff --git a/LICENSE.md b/LICENSE.md index e7ab291..3031001 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,23 +1,24 @@ +# Match.jl - Advanced Pattern Matching for Julia + The Match.jl package is licensed under the MIT Expat License: -> Copyright (c) 2013: Kevin Squire. -> -> Permission is hereby granted, free of charge, to any person obtaining -> a copy of this software and associated documentation files (the -> "Software"), to deal in the Software without restriction, including -> without limitation the rights to use, copy, modify, merge, publish, -> distribute, sublicense, and/or sell copies of the Software, and to -> permit persons to whom the Software is furnished to do so, subject to -> the following conditions: -> -> The above copyright notice and this permission notice shall be -> included in all copies or substantial portions of the Software. -> -> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -> CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Copyright (c) 2013-2023: Kevin Squire, RelationalAI, Inc, and contributors. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +“Software”), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject +to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Project.toml b/Project.toml index 63d1c98..ece9bb6 100644 --- a/Project.toml +++ b/Project.toml @@ -1,10 +1,16 @@ name = "Match" uuid = "7eb4fadd-790c-5f42-8a69-bfa0b872bfbf" -version = "1.2.0" -author = "Kevin Squire " +version = "2.0.0" +authors = ["Neal Gafter ", "Kevin Squire "] + +[deps] +MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" [compat] -julia = "0.7, 1" +MacroTools = "0.4, 0.5" +OrderedCollections = "1" +julia = "1.4" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/README.md b/README.md index e0e358c..415dfb2 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,9 @@ Features: * Matching against almost any data type with a first-match policy -* Deep matching within data types and matrices +* Deep matching within data types, tuples, and vectors * Variable binding within matches - -For alternatives to `Match`, check out - -* toivoh's [`PatternDispatch.jl`](https://github.com/toivoh/PatternDispatch.jl) for a more Julia-like function dispatch on patterns. - +* Produces a decision automaton to avoid repeated tests between patterns. ## Installation Use the Julia package manager. Within Julia, do: @@ -24,7 +20,9 @@ Pkg.add("Match") ## Usage -The package provides one macro, `@match`, which can be used as: +The package provides two macros for pattern-matching: `@match` and `@ismatch`. +It is possible to supply variables inside patterns, which will be bound +to corresponding values. using Match @@ -35,9 +33,73 @@ The package provides one macro, `@match`, which can be used as: _ => default_result end -It is possible to supply variables inside pattern, which will be bound -to corresponding values. + if @ismatch value pattern + # Code that uses variables bound in the pattern + end See the [documentation](https://JuliaServices.github.io/Match.jl/stable/) for examples of this and other features. +## Patterns + +* `_` matches anything +* `x` (an identifier) matches anything, binds value to the variable `x` +* `T(x,y,z)` matches structs of type `T` with fields matching patterns `x,y,z` +* `T(y=1)` matches structs of type `T` whose `y` field equals `1` +* `[x,y,z]` matches `AbstractArray`s with 3 entries matching `x,y,z` +* `(x,y,z)` matches `Tuple`s with 3 entries matching `x,y,z` +* `[x,y...,z]` matches `AbstractArray`s with at least 2 entries, where `x` matches the first entry, `z` matches the last entry and `y` matches the remaining entries. +* `(x,y...,z)` matches `Tuple`s with at least 2 entries, where `x` matches the first entry, `z` matches the last entry and `y` matches the remaining entries. +* `::T` matches any subtype (`isa`) of type `T` +* `x::T` matches any subtype (`isa`) of T that also matches pattern `x` +* `x || y` matches values which match either pattern `x` or `y` (only variables which exist in both branches will be bound) +* `x && y` matches values which match both patterns `x` and `y` +* `x, if condition end` matches only if `condition` is true (`condition` may use any variables that occur earlier in the pattern eg `(x, y, z where x + y > z)`) +* `x where condition` An alternative form for `x, if condition end` +* Anything else is treated as a constant and tested for equality +* Expressions can be interpolated in as constants via standard interpolation syntax `\$(x)`. Interpolations may use previously bound variables. + +Patterns can be nested arbitrarily. + +Repeated variables only match if they are equal (`isequal`). For example `(x,x)` matches `(1,1)` but not `(1,2)`. + +## Early exit and failure + +Inside the result part of a case, you can cause the pattern to fail (as if the pattern did not match), or you can return a value early: + +```julia +@match value begin + pattern1 => begin + if some_failure_condition + @match_fail + end + if some_shortcut_condition + @match_return 1 + end + ... + 2 + end + ... +end +``` + +In this example, the result value when matching `pattern1` is a block that has two early exit conditions. +When `pattern1` matches but `some_failure_condition` is `true`, then the whole case is treated as not matching and the following cases are tried. +Otherwise, if `some_shortcut_condition` is `true`, then `1` is the result value for this case. +Otherwise `2` is the result. + +## Differences from previous versions of `Match.jl` + +* If no branches are matched, throws `MatchFailure` instead of returning nothing. +* Matching against a struct with the wrong number of fields produces an error instead of silently failing. +* Repeated variables require equality, ie `@match (1,2) begin (x,x) => :ok end` fails. +* We add a syntax for guards `x where x > 1` in addition to the existing `x, if x > 1 end`. +* Structs can be matched by field-names, allowing partial matches: `@match Foo(1,2) begin Foo(y=2) => :ok end` returns `:ok`. +* Patterns support interpolation, ie `let x=1; @match ($x,$(x+1)) = (1,2); end` is a match. +* We have dropped support for matching against multidimensional arrays - all array patterns use linear indexing. +* We no longer support the (undocumented) syntax `@match value pattern` which returned an array of the bindings of the pattern variables. +* Errors now identify a specific line in the user's program where the problem occurred. +* Previously bound variables may now be used in interpolations, ie `@match (x, $(x+2)) = (1, 3)` is a match. +* A pure type match (without another pattern) can be written as `::Type`. +* Types appearing in type patterns (`::Type`) and struct patterns (`Type(...)`) are bound at macro-expansion time in the context of the module containing the macro usage. As a consequence, you cannot use certain type expressions that would differ. For example, you cannot use a type parameter or a local variable containing a type. The generated code checks that the type is the same at evaluation time as it was at macro expansion time, and an error is thrown if they differ. If this rare incompatibility affects you, you can use `x where x isa Type` as a workaround. If the type is not defined at macro-expansion time, an error is issued. +* A warning is issued at macro-expansion time if a case cannot be reached because it is subsumed by prior cases. diff --git a/docs/make.jl b/docs/make.jl index 6a5efce..56aca05 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -5,7 +5,7 @@ DocMeta.setdocmeta!(Match, :DocTestSetup, :(using Match); recursive=true) makedocs(; modules=[Match], - authors="Kevin Squire, Neal Gafter and contributors", + authors="Neal Gafter , Kevin Squire , and contributors", repo="https://github.com/JuliaServices/Match.jl/blob/{commit}{path}#{line}", sitename="Match.jl", format=Documenter.HTML(; diff --git a/docs/src/index.md b/docs/src/index.md index 59877ab..97da058 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,10 +1,16 @@ -# Match.jl --- Advanced Pattern Matching for Julia +```@meta +CurrentModule = Match +``` + +# [Match.jl](https://github.com/JuliaServices/Match.jl) --- Advanced Pattern Matching for Julia -This package provides both simple and advanced pattern matching capabilities for Julia. Features include: +This package provides both simple and advanced pattern matching capabilities for Julia. +Features include: - Matching against almost any data type with a first-match policy -- Deep matching within data types and matrices +- Deep matching within data types, tuples, and vectors - Variable binding within matches +- Efficient code generation via a decision automaton. # Installation @@ -16,22 +22,46 @@ Pkg.add("Match") # Usage -The package provides one macro, @match, which can be used as: +## Simple-pattern `@ismatch` macro + +The `@ismatch` macro tests if a value patches a given pattern, returning +either `true` if it matches, or `false` if it does not. When the pattern matches, +the variables named in the pattern are bound and can be used. + +```julia-repl +julia> using Match + +julia> @ismatch (1, 2) (x, y) +true + +julia> x +1 + +julia> y +2 +``` + +## Multi-case `@match` macro + +The `@match` macro acts as a pattern-matching switch statement, +in which each case has a pattern and a result for when that pattern matches. +The first case that matches is the one that computes the result for the `@match`. ```julia using Match - @match item begin pattern1 => result1 - pattern2, if cond end => result2 + pattern2 where cond => result2 pattern3 || pattern4 => result3 _ => default_result end ``` -Patterns can be values, regular expressions, type checks or constructors, tuples, or arrays, including multidimensional arrays. It is possible to supply variables inside pattern, which will be bound to corresponding values. This and other features are best seen with examples. +Patterns can be values, regular expressions, type checks or constructors, tuples, or arrays. +It is possible to supply variables inside a pattern, which will be bound to corresponding values. +This and other features are best seen with examples. -## Match Values +### Match Values The easiest kind of matching to use is simply to match against values: @@ -43,17 +73,29 @@ The easiest kind of matching to use is simply to match against values: end ``` -## Match Types +Values can be computed expressions by using interpolation. That is how to use `@match` with `@enum`s: + +```julia +@enum Color Red Blue Greed +@match item begin + $Red => "Red" + $Blue => "Blue" + $Greed => "Greed is the color of money" + _ => "Something else..." +end +``` + +### Match Types Julia already does a great job of this with functions and multiple dispatch, and it is generally be better to use those mechanisms when possible. But it can be done here: ```julia julia> matchtype(item) = @match item begin - n::Int => println("Integers are awesome!") - str::String => println("Strings are the best") - m::Dict{Int, String} => println("Ints for Strings?") - d::Dict => println("A Dict! Looking up a word?") - _ => println("Something unexpected") + ::Int => println("Integers are awesome!") + ::String => println("Strings are the best") + ::Dict{Int, String} => println("Ints for Strings?") + ::Dict => println("A Dict! Looking up a word?") + _ => println("Something unexpected") end julia> matchtype(66) @@ -72,7 +114,7 @@ julia> matchtype(2.0) Something unexpected ``` -## Deep Matching of Composite Types +### Deep Matching of Composite Types One nice feature is the ability to match embedded types, as well as bind variables to components of those types: @@ -114,7 +156,7 @@ julia> personinfo(Person("Linus", "Pauling", "Unknown person!" ``` -## Alternatives and Guards +### Alternatives and Guards Alternatives allow a match against multiple patterns. @@ -122,14 +164,14 @@ Guards allow a conditional match. They are not a standard part of Julia yet, so ```julia function parse_arg(arg::String, value::Any=nothing) - @match (arg, value) begin - ("-l", lang) => println("Language set to $lang") - ("-o" || "--optim", n::Int), - if 0 < n <= 5 end => println("Optimization level set to $n") - ("-o" || "--optim", n::Int) => println("Illegal optimization level $(n)!") - ("-h" || "--help", nothing) => println("Help!") - bad => println("Unknown argument: $bad") - end + @match (arg, value) begin + ("-l", lang) => println("Language set to $lang") + ("-o" || "--optim", n::Int), + if 0 < n <= 5 end => println("Optimization level set to $n") + ("-o" || "--optim", n::Int) => println("Illegal optimization level $(n)!") + ("-h" || "--help", nothing) => println("Help!") + bad => println("Unknown argument: $bad") + end end julia> parse_arg("-l", "eng") @@ -157,7 +199,22 @@ julia> parse_arg("--help") Help! ``` -## Match Ranges +The alternative guard syntax `pattern where expression` can sometimes be easier to use. + +```julia +function parse_arg(arg::String, value::Any=nothing) + @match (arg, value) begin + ("-l", lang) => println("Language set to $lang") + ("-o" || "--optim", n::Int) where 0 < n <= 5 => + println("Optimization level set to $n") + ("-o" || "--optim", n::Int) => println("Illegal optimization level $(n)!") + ("-h" || "--help", nothing) => println("Help!") + bad => println("Unknown argument: $bad") + end +end +``` + +### Match Ranges Borrowing a nice idea from pattern matching in Rust, pattern matching against ranges is also supported: @@ -192,269 +249,117 @@ julia> num_match(3:10) "three to ten" ``` -## Regular Expressions +### Regular Expressions -Match.jl used to have complex regular expression handling, but it was implemented using `eval`, which is generally a bad idea and was the source of some undesirable behavior. +A regular expression can be used as a pattern, and will match any string that satisfies the pattern. -With some work, it may be possible to reimplement, but it's unclear if this is a good idea yet. +Match.jl used to have complex regular expression handling, permitting the capturing of matched subpatterns. +We are considering adding that back again. ## Deep Matching Against Arrays -Arrays are intrinsic components of Julia. Match allows deep matching against arrays. +Arrays are intrinsic components of Julia. Match allows deep matching against single-dimensional vectors. + +Match previously supported multidimensional arrays. If there is sufficient demand, we'll add support for that again. The following examples also demonstrate how Match can be used strictly for its extraction/binding capabilities, by only matching against one pattern. ### Extract first element, rest of vector ```julia -julia> @match([1:4], [a,b...]); +julia> @ismatch 1:4 [a,b...] +true julia> a 1 julia> b -3-element SubArray{Int64,1,Array{Int64,1},(Range1{Int64},)}: - 2 - 3 - 4 +2:4 ``` ### Match values at the beginning of a vector ```julia -julia> @match([1:5], [1,2,a...]) - 3-element SubArray{Int64,1,Array{Int64,1},(Range1{Int64},)}: - 3 - 4 - 5 -``` - -### Match and collect columns - -```julia -julia> @match([1 2 3; 4 5 6], [a b...]); - -julia> a -2-element SubArray{Int64,1,Array{Int64,2},(Range1{Int64},Int64)}: - 1 - 4 - -julia> b -2x2 SubArray{Int64,2,Array{Int64,2},(Range1{Int64},Range1{Int64})}: - 2 3 - 5 6 - -julia> @match([1 2 3; 4 5 6], [a b c]); - -julia> a -2-element SubArray{Int64,1,Array{Int64,2},(Range1{Int64},Int64)}: - 1 - 4 - -julia> b -2-element SubArray{Int64,1,Array{Int64,2},(Range1{Int64},Int64)}: - 2 - 5 - -julia> c -2-element SubArray{Int64,1,Array{Int64,2},(Range1{Int64},Int64)}: - 3 - 6 - -julia> @match([1 2 3; 4 5 6], [[1,4] a b]); +julia> @ismatch 1:5 [1,2,a...] +true julia> a -2-element SubArray{Int64,1,Array{Int64,2},(Range1{Int64},Int64)}: - 2 - 5 - -julia> b -2-element SubArray{Int64,1,Array{Int64,2},(Range1{Int64},Int64)}: - 3 - 6 +3:5 ``` -### Match and collect rows - -```julia -julia> @match([1 2 3; 4 5 6], [a, b]); - -julia> a -1x3 SubArray{Int64,2,Array{Int64,2},(Range1{Int64},Range1{Int64})}: - 1 2 3 - -julia> b -1x3 SubArray{Int64,2,Array{Int64,2},(Range1{Int64},Range1{Int64})}: - 4 5 6 +### Notes/Gotchas -julia> @match([1 2 3; 4 5 6; 7 8 9], [a, b...]); - -julia> a -1x3 SubArray{Int64,2,Array{Int64,2},(Range1{Int64},Range1{Int64})}: - 1 2 3 - -julia> b -2x3 SubArray{Int64,2,Array{Int64,2},(Range1{Int64},Range1{Int64})}: - 4 5 6 - 7 8 9 - -julia> @match([1 2 3; 4 5 6], [[1 2 3], a]) -1x3 SubArray{Int64,2,Array{Int64,2},(Range1{Int64},Range1{Int64})}: - 4 5 6 - -julia> @match([1 2 3; 4 5 6], [1 2 3; a]) -1x3 SubArray{Int64,2,Array{Int64,2},(Range1{Int64},Range1{Int64})}: - 4 5 6 - -julia> @match([1 2 3; 4 5 6; 7 8 9], [1 2 3; a...]) -2x3 SubArray{Int64,2,Array{Int64,2},(Range1{Int64},Range1{Int64})}: - 4 5 6 - 7 8 9 -``` - -### Match individual positions - -```julia -julia> @match([1 2; 3 4], [1 a; b c]); - -julia> a -2 - -julia> b -3 - -julia> c -4 - -julia> @match([1 2; 3 4], [1 a; b...]); - -julia> a -2 - -julia> b -1x2 SubArray{Int64,2,Array{Int64,2},(Range1{Int64},Range1{Int64})}: - 3 4 -``` - -### Match 3D arrays - -```julia -julia> m = reshape([1:8], (2,2,2)) -2x2x2 Array{Int64,3}: -[:, :, 1] = - 1 3 - 2 4 - -[:, :, 2] = - 5 7 - 6 8 - -julia> @match(m, [a b]); - -julia> a -2x2 SubArray{Int64,2,Array{Int64,3},(Range1{Int64},Range1{Int64},Int64)}: - 1 3 - 2 4 - -julia> b -2x2 SubArray{Int64,2,Array{Int64,3},(Range1{Int64},Range1{Int64},Int64)}: - 5 7 - 6 8 - -julia> @match(m, [[1 a; b c] d]); - -julia> a -3 - -julia> b -2 +There are a few useful things to be aware of when using Match. -julia> c -4 +- `if` guards need a comma and an \`end\`: -julia> d -2x2 SubArray{Int64,2,Array{Int64,3},(Range1{Int64},Range1{Int64},Int64)}: - 5 7 - 6 8 -``` +#### Bad -## Notes/Gotchas + julia> _iseven(a) = @match a begin + n::Int if n%2 == 0 end => println("$n is even") + m::Int => println("$m is odd") + end + ERROR: syntax: extra token "if" after end of expression -There are a few useful things to be aware of when using Match. + julia> _iseven(a) = @match a begin + n::Int, if n%2 == 0 => println("$n is even") + m::Int => println("$m is odd") + end + ERROR: syntax: invalid identifier name => -- Guards need a comma and an \`end\`: +#### Good - ## Bad + julia> _iseven(a) = @match a begin + n::Int, if n%2 == 0 end => println("$n is even") + m::Int => println("$m is odd") + end + # methods for generic function _iseven + _iseven(a) at none:1 - julia> _iseven(a) = @match a begin - n::Int if n%2 == 0 end => println("$n is even") - m::Int => println("$m is odd") - end - ERROR: syntax: extra token "if" after end of expression +It is sometimes easier to use the `where` syntax for guards: - julia> _iseven(a) = @match a begin - n::Int, if n%2 == 0 => println("$n is even") - m::Int => println("$m is odd") - end - ERROR: syntax: invalid identifier name => + julia> _iseven(a) = @match a begin + n::Int where n%2 == 0 => println("$n is even") + m::Int => println("$m is odd") + end + # methods for generic function _iseven + _iseven(a) at none:1 - ## Good +### `@match_return` macro - julia> _iseven(a) = @match a begin - n::Int, if n%2 == 0 end => println("$n is even") - m::Int => println("$m is odd") - end - # methods for generic function _iseven - _iseven(a) at none:1 + @match_return value -- Without a default match, the result is \`nothing\`: +Within the result value (to the right of the `=>`) part of a `@match` case, +you can use the `@match_return` macro to return a result early, before the end of the +block. This is useful if you have a shortcut for computing the result in some cases. +You can think of it as a `return` statement for the `@match` macro. - julia> test(a) = @match a begin - n::Int => "Integer" - m::FloatingPoint => "Float" - end +Use of this macro anywhere else will result in an error. - julia> test("Julia is great") +### `@match_fail` macros - julia> + @match_fail -- In Scala, \_ is a wildcard pattern which matches anything, and is not bound as a variable. +Inside the result part of a `@match` case, you can cause the case to fail as +if the corresponding pattern did not match. The `@match` statement will resume +attempting to match the following cases. This is useful if you want to write some +complex code that would be awkward to express as a guard. - In Match for Julia, \_ can be used as a wildcard, and will be bound to the last use if it is referenced in the result expression: +Use of this macro anywhere else will result in an error. - julia> test(a) = @match a begin - n::Int => "Integer" - _::FloatingPoint => "$_ is a Float" - (_,_) => "$_ is the second part of a tuple" - end +## single-case `@match` macro - julia> test(1.0) - "1.0 is a Float" + @match pattern = value - julia> test((1,2)) - "2 is the second part of a tuple" +Returns the value if it matches the pattern, and binds any pattern variables. +Otherwise, throws `MatchFailure`. -- Note that variables not referenced in the result expression will not be bound (e.g., `n` is never bound above). One small exception to this rule is that when "=>" is not used, "\_" will not be assigned. +## `ismatch` macro -- If you want to see the code generated for a macro, you can use \`macroexpand\`: + @ismatch value pattern - julia> macroexpand(:(@match(a, begin - n::Int => "Integer" - m::FloatingPoint => "Float" - end)) - quote # REPL[1], line 2: - if isa(a,Int) # /Users/kevin/.julia/v0.5/Match/src/matchmacro.jl, line 387: - "Integer" - else # /Users/kevin/.julia/v0.5/Match/src/matchmacro.jl, line 389: - begin # REPL[1], line 3: - if isa(a,FloatingPoint) # /Users/kevin/.julia/v0.5/Match/src/matchmacro.jl, line 387: - "Float" - else # /Users/kevin/.julia/v0.5/Match/src/matchmacro.jl, line 389: - nothing - end - end - end - end +Returns `true` if `value` matches `pattern`, `false` otherwise. When returning `true`, +binds the pattern variables in the enclosing scope. # Examples @@ -462,7 +367,7 @@ Here are a couple of additional examples. ## Mathematica-Inspired Sparse Array Constructor -[Contributed by @benkj](https://github.com/kmsquire/Match.jl/issues/29) +[Contributed by @benkj](https://github.com/JuliaServices/Match.jl/issues/29) > I've realized that `Match.jl` is perfect for creating in Julia an equivalent of [SparseArray](https://reference.wolfram.com/language/ref/SparseArray.html) which I find quite useful in Mathematica. > @@ -529,3 +434,16 @@ The following pages on pattern matching in scala provided inspiration for the li - - - + +The following paper on pattern-matching inspired the automaton approach to code generation: + +- + +# API Documentation + +```@index +``` + +```@autodocs +Modules = [Match] +``` diff --git a/src/Match.jl b/src/Match.jl index 7effab1..f083efc 100644 --- a/src/Match.jl +++ b/src/Match.jl @@ -1,13 +1,271 @@ module Match -using Base.Meta +export @match, MatchFailure, @match_return, @match_fail, @ismatch -export @match, @ismatch +using MacroTools: MacroTools, @capture +using Base.Iterators: reverse +using Base: ImmutableDict +using OrderedCollections: OrderedDict -include("matchutils.jl") -include("matchmacro.jl") +""" + @match pattern = value + @match value begin + pattern1 => result1 + pattern2 => result2 + ... + end + +Match a given value to a pattern or series of patterns. + +This macro has two forms. In the first form + + @match pattern = value + +Return the value if it matches the pattern, and bind any pattern variables. +Otherwise, throw `MatchFailure`. + +In the second form + + @match value begin + pattern1 => result1 + pattern2 => result2 + ... + end + +Return `result` for the first matching `pattern`. +If there are no matches, throw `MatchFailure`. + +To avoid a `MatchFailure` exception, write the `@match` to handle every possible input. +One way to do that is to add a final case with the wildcard pattern `_`. + +# See Also + +See also + +- `@match_fail` +- `@match_return` +- `@ismatch` + +# Patterns: + +The following syntactic forms can be used in patterns: + +* `_` matches anything +* `x` (an identifier) matches anything, binds value to the variable `x` +* `T(x,y,z)` matches structs of type `T` with fields matching patterns `x,y,z` +* `T(y=1)` matches structs of type `T` whose `y` field equals `1` +* `[x,y,z]` matches `AbstractArray`s with 3 entries matching `x,y,z` +* `(x,y,z)` matches `Tuple`s with 3 entries matching `x,y,z` +* `[x,y...,z]` matches `AbstractArray`s with at least 2 entries, where `x` matches the first entry, `z` matches the last entry and `y` matches the remaining entries. +* `(x,y...,z)` matches `Tuple`s with at least 2 entries, where `x` matches the first entry, `z` matches the last entry and `y` matches the remaining entries. +* `::T` matches any subtype (`isa`) of type `T` +* `x::T` matches any subtype (`isa`) of T that also matches pattern `x` +* `x || y` matches values which match either pattern `x` or `y` (only variables which exist in both branches will be bound) +* `x && y` matches values which match both patterns `x` and `y` +* `x, if condition end` matches only if `condition` is true (`condition` may use any variables that occur earlier in the pattern eg `(x, y, z where x + y > z)`) +* `x where condition` An alternative form for `x, if condition end` +* Anything else is treated as a constant and tested for equality +* Expressions can be interpolated in as constants via standard interpolation syntax `\$(x)`. Interpolations may use previously bound variables. + +Patterns can be nested arbitrarily. + +Repeated variables only match if they are equal (`isequal`). For example `(x,x)` matches `(1,1)` but not `(1,2)`. + +# Examples +```julia-repl +julia> value=(1, 2, 3, 4) +(1, 2, 3, 4) + +julia> @match (x, y..., z) = value +(1, 2, 3, 4) + +julia> x +1 + +julia> y +(2, 3) + +julia> z +4 + +julia> struct Foo + x::Int64 + y::String + end + +julia> f(x) = @match x begin + _::String => :string + [a,a,a] => (:all_the_same, a) + [a,bs...,c] => (:at_least_2, a, bs, c) + Foo(x, "foo") where x > 1 => :foo + end +f (generic function with 1 method) + +julia> f("foo") +:string + +julia> f([1,1,1]) +(:all_the_same, 1) + +julia> f([1,1]) +(:at_least_2, 1, Int64[], 1) + +julia> f([1,2,3,4]) +(:at_least_2, 1, [2, 3], 4) + +julia> f([1]) +ERROR: MatchFailure([1]) +... -## Uncomment for debugging -# export unapply, unapply_array, gen_match_expr, subslicedim, getvars, getvar, arg1isa, joinexprs, let_expr, array_type_of, isexpr +julia> f(Foo(2, "foo")) +:foo + +julia> f(Foo(0, "foo")) +ERROR: MatchFailure(Foo(0, "foo")) +... + +julia> f(Foo(2, "not a foo")) +ERROR: MatchFailure(Foo(2, "not a foo")) +... +``` +""" +macro match end + +""" + @match_return value + +Inside the result part of a @match case, you can return a given value early. + +# Examples +```julia-repl +julia> struct Vect + x + y + end + +julia> function norm(v) + @match v begin + Vect(x, y) => begin + if x==0 && y==0 + @match_return v + end + l = sqrt(x^2 + y^2) + Vect(x/l, y/l) + end + _ => v + end + end +norm (generic function with 1 method) + +julia> norm(Vect(2, 3)) +Vect(0.5547001962252291, 0.8320502943378437) + +julia> norm(Vect(0, 0)) +Vect(0, 0) +``` +""" +macro match_return end + +""" + @match_fail + +Inside the result part of a @match case, you can cause the pattern to fail (as if the pattern did not match). + +# Examples +```julia-repl +julia> struct Vect + x + y + end + +julia> function norm(v) + @match v begin + Vect(x, y) => begin + if x==0 && y==0 + @match_fail + end + l = sqrt(x^2 + y^2) + Vect(x/l, y/l) + end + _ => v + end + end +norm (generic function with 1 method) + +julia> norm(Vect(2, 3)) +Vect(0.5547001962252291, 0.8320502943378437) + +julia> norm(Vect(0, 0)) +Vect(0, 0) +``` +""" +macro match_fail end + +""" + @ismatch value pattern + +Return `true` if `value` matches `pattern`, `false` otherwise. When returning `true`, +binds the pattern variables in the enclosing scope. + +See also `@match` for the syntax of patterns + +# Examples + +```julia-repl +julia> struct Point + x + y + end + +julia> p = Point(0, 3) +Point(0, 3) + +julia> if @ismatch p Point(0, y) + println("On the y axis at y = ", y) + end +On the y axis at y = 3 +``` + +Guarded patterns ought not be used with `@ismatch`, as you can just use `&&` instead: + +```julia-repl +julia> if (@ismatch p Point(x, y)) && x < y + println("The point (", x, ", ", y, ") is in the upper left semiplane") + end +The point (0, 3) is in the upper left semiplane +``` +""" +macro ismatch end + +""" + MatchFailure(value) + +Construct an exception to be thrown when a value fails to +match a pattern in the `@match` macro. +""" +struct MatchFailure <: Exception + value +end + +# const fields only suppored >= Julia 1.8 +macro _const(x) + (VERSION >= v"1.8") ? Expr(:const, esc(x)) : esc(x) +end + +is_expr(@nospecialize(e), head::Symbol) = e isa Expr && e.head == head +is_expr(@nospecialize(e), head::Symbol, n::Int) = is_expr(e, head) && length(e.args) == n +is_case(@nospecialize(e)) = is_expr(e, :call, 3) && e.args[1] == :(=>) + +include("topological.jl") +include("immutable_vector.jl") +include("bound_pattern.jl") +include("binding.jl") +include("lowering.jl") +include("match_cases_simple.jl") +include("matchmacro.jl") +include("automaton.jl") +include("pretty.jl") +include("match_cases_opt.jl") +include("match_return.jl") end # module diff --git a/src/automaton.jl b/src/automaton.jl new file mode 100644 index 0000000..7c8b93e --- /dev/null +++ b/src/automaton.jl @@ -0,0 +1,131 @@ +abstract type AbstractAutomatonNode end + +# +# A node of the decision automaton (i.e. a point in the generated code), +# which is represented a set of partially matched cases. +# +mutable struct AutomatonNode <: AbstractAutomatonNode + # The status of the cases. Impossible cases, which are designated by a + # `false` `bound_pattern`, are removed from this array. Cases are always + # ordered by `case_number`. + @_const cases::ImmutableVector{BoundCase} + + # The selected action to take from this node: either + # - Nothing, before it has been computed, or + # - Case whose tests have all passed, or + # - A bound pattern to perform and then move on to the next node, or + # - An Expr to insert into the code when all else is exhausted + # (which throws MatchFailure) + action::Union{Nothing, BoundCase, BoundPattern, Expr} + + # The next node(s): + # - Nothing before being computed + # - Tuple{} if the action is a case which was matched or a MatchFailure + # - Tuple{AutomatonNode} if the action was a fetch pattern. It designates + # the node for code that follows the fetch. + # - Tuple{AutomatonNode, AutomatonNode} if the action is a test. These are the nodes + # to go to if the result of the test is true ([1]) or false ([2]). + next::Union{Nothing, Tuple{}, Tuple{AutomatonNode}, Tuple{AutomatonNode, AutomatonNode}} + + @_const _cached_hash::UInt64 + + function AutomatonNode(cases::Vector{BoundCase}) + cases = filter(case -> !(case.pattern isa BoundFalsePattern), cases) + for i in eachindex(cases) + if is_irrefutable(cases[i].pattern) + cases = cases[1:i] + break + end + end + new(ImmutableVector(cases), nothing, nothing, hash(cases, 0xc98a9a23c2d4d915)) + end +end +Base.hash(case::AutomatonNode, h::UInt64) = hash(case._cached_hash, h) +function Base.:(==)(a::AutomatonNode, b::AutomatonNode) + a === b || + a._cached_hash == b._cached_hash && + isequal(a.cases, b.cases) +end +function name(node::T, id::IdDict{T, Int}) where { T <: AbstractAutomatonNode } + "Node $(id[node])" +end +function successors(c::T)::Vector{T} where { T <: AbstractAutomatonNode } + @assert !(c.next isa Nothing) + collect(c.next) +end +function reachable_nodes(root::T)::Vector{T} where { T <: AbstractAutomatonNode } + topological_sort(successors, [root]) +end + +# We merge nodes with identical behavior, bottom-up, to minimize the size of +# the decision automaton. We define `hash` and `==` to take account of only what matters. +# Specifically, we ignore the `cases::ImmutableVector{BoundCase}` of `AutomatonNode`. +mutable struct DeduplicatedAutomatonNode <: AbstractAutomatonNode + # The selected action to take from this node: either + # - Case whose tests have all passed, or + # - A bound pattern to perform and then move on to the next node, or + # - An Expr to insert into the code when all else is exhausted + # (which throws MatchFailure) + @_const action::Union{BoundCase, BoundPattern, Expr} + + # The next code point(s): + # - Tuple{} if the action is a case which was matched or a MatchFailure + # - Tuple{DeduplicatedAutomatonNode} if the action was a fetch pattern. It designates + # the note to go to after the fetch. + # - Tuple{DeduplicatedAutomatonNode, DeduplicatedAutomatonNode} if the action is a + # test. These are the nodes to go to if the result of the test is true ([1]) or + # false ([2]). + @_const next::Union{Tuple{}, Tuple{DeduplicatedAutomatonNode}, Tuple{DeduplicatedAutomatonNode, DeduplicatedAutomatonNode}} + + @_const _cached_hash::UInt64 + function DeduplicatedAutomatonNode(action, next) + action isa BoundCase && @assert action.pattern isa BoundTruePattern + new(action, next, hash((action, next))) + end +end +Base.hash(node::DeduplicatedAutomatonNode, h::UInt64) = hash(node._cached_hash, h) +Base.hash(node::DeduplicatedAutomatonNode) = node._cached_hash +function Base.:(==)(a::DeduplicatedAutomatonNode, b::DeduplicatedAutomatonNode) + a === b || + a._cached_hash == b._cached_hash && + isequal(a.action, b.action) && + isequal(a.next, b.next) +end + +# +# Deduplicate a code point, given the deduplications of the downstream code points. +# Has the side-effect of adding a mapping to the dict. +# +function dedup!( + dict::Dict{DeduplicatedAutomatonNode, DeduplicatedAutomatonNode}, + node::AutomatonNode, + binder::BinderContext) + next = if node.next isa Tuple{} + node.next + elseif node.next isa Tuple{AutomatonNode} + (dedup!(dict, node.next[1], binder),) + elseif node.next isa Tuple{AutomatonNode, AutomatonNode} + t = dedup!(dict, node.next[1], binder) + f = dedup!(dict, node.next[2], binder) + (t, f) + else + error("Unknown next type: $(node.next)") + end + key = DeduplicatedAutomatonNode(node.action, next) + result = get!(dict, key, key) + result +end + +# +# Deduplicate the decision automaton by collapsing behaviorally identical nodes. +# +function deduplicate_automaton(entry::AutomatonNode, binder::BinderContext) + dedup_map = Dict{DeduplicatedAutomatonNode, DeduplicatedAutomatonNode}() + result = Vector{DeduplicatedAutomatonNode}() + top_down_nodes = reachable_nodes(entry) + for e in Iterators.reverse(top_down_nodes) + _ = dedup!(dedup_map, e, binder) + end + new_entry = dedup!(dedup_map, entry, binder) + return reachable_nodes(new_entry) +end diff --git a/src/binding.jl b/src/binding.jl new file mode 100644 index 0000000..bc6f9a0 --- /dev/null +++ b/src/binding.jl @@ -0,0 +1,584 @@ +# +# Persistent data that we use across different patterns, to ensure the same computations +# are always represented by the same synthetic variables. We use this during lowering +# and also during code generation, since it holds some of the context required during code +# generation (such as assertions and assignments) +# +struct BinderContext + # The module containing the pattern, in which types appearing in the + # pattern should be bound. + mod::Module + + # The variable that contains the original input. + input_variable::Symbol + + # The bindings to be used for each intermediate computations. This maps from the + # computation producing the value (or the pattern variable that needs a temp) + # to the symbol for the temp holding that value. + assignments::Dict{BoundFetchPattern, Symbol} + + # We track the type of each intermediate value. If we don't know, then `Any`. + types::Dict{Symbol, Type} + + # The set of type syntax forms that have asserted bindings in assertions + asserted_types::Vector{Any} + + # Assertions that should be executed at runtime before the automaton. + assertions::Vector{Any} + + # A dictionary used to intern AutomatonNode values in Match2Cases. + intern::Dict # {AutomatonNode, AutomatonNode} + + # A counter used to dispense unique integers to make prettier gensyms + num_gensyms::Ref{Int} + + function BinderContext(mod::Module) + new( + mod, + gensym("input_value"), + Dict{BoundFetchPattern, Symbol}(), + Dict{Symbol, Type}(), + Vector{Any}(), + Vector{Any}(), + Dict(), # {AutomatonNode, AutomatonNode}(), + Ref{Int}(0) + ) + end +end +function gensym(base::String, binder::BinderContext)::Symbol + s = gensym("$(base)_$(binder.num_gensyms[])") + binder.num_gensyms[] += 1 + s +end +gensym(base::String)::Symbol = Base.gensym(base) +function bind_type(location, T, input, binder) + # bind type at macro expansion time. It will be verified at runtime. + bound_type = nothing + try + bound_type = Core.eval(binder.mod, Expr(:block, location, T)) + catch ex + error("$(location.file):$(location.line): Could not bind `$T` as a type (due to `$ex`).") + end + + if !(bound_type isa Type) + error("$(location.file):$(location.line): Attempted to match non-type `$T` as a type.") + end + + bound_type +end + +function simple_name(s::Symbol) + simple_name(string(s)) +end +function simple_name(n::String) + @assert startswith(n, "##") + n1 = n[3:end] + last = findlast('#', n1) + isnothing(last) ? n1 : n1[1:prevind(n1, last)] +end + +# +# Generate a fresh synthetic variable whose name hints at its purpose. +# +function gentemp(p)::Symbol + error("not implemented: gentemp(::$(typeof(p)))") +end +function gentemp(p::BoundFetchFieldPattern)::Symbol + gensym(string(simple_name(p.input), ".", p.field_name)) +end +function gentemp(p::BoundFetchIndexPattern)::Symbol + gensym(string(simple_name(p.input), "[", p.index, "]")) +end +function gentemp(p::BoundFetchRangePattern)::Symbol + gensym(string(simple_name(p.input), "[", p.first_index, ":(length-", p.from_end, ")]")) +end +function gentemp(p::BoundFetchLengthPattern)::Symbol + gensym(string("length(", simple_name(p.input), ")")) +end + +# +# The following are special bindings used to handle the point where +# a disjunction merges when and two sides have different bindings. +# In dataflow-analysis terms, this is represented by a phi function. +# This is a synthetic variable to hold the value that should be used +# to hold the value after the merge point. +# +function get_temp(binder::BinderContext, p::BoundFetchPattern)::Symbol + temp = get!(binder.assignments, p) do; gentemp(p); end + if haskey(binder.types, temp) + binder.types[temp] = Union{p.type, binder.types[temp]} + else + binder.types[temp] = p.type + end + temp +end +function get_temp(binder::BinderContext, p::BoundFetchExpressionPattern)::Symbol + temp = get!(binder.assignments, p) do + if p.key isa Symbol + p.key + else + gensym("where", binder) + end + end + if haskey(binder.types, temp) + binder.types[temp] = Union{p.type, binder.types[temp]} + else + binder.types[temp] = p.type + end + temp +end + +# +# +# We restrict the struct pattern to require something that looks like +# a type name before the open paren. This improves the diagnostics +# for error cases like `(a + b)`, which produces an analogous Expr node +# but with `+` as the operator. +# +# +is_possible_type_name(t) = false +is_possible_type_name(t::Symbol) = Base.isidentifier(t) +function is_possible_type_name(t::Expr) + t.head == :. && + is_possible_type_name(t.args[1]) && + t.args[2] isa QuoteNode && + is_possible_type_name(t.args[2].value) || + t.head == :curly && + all(is_possible_type_name, t.args) +end + +function bind_pattern!( + location::LineNumberNode, + source::Any, + input::Symbol, + binder::BinderContext, + assigned::ImmutableDict{Symbol, Symbol}) + + if source == :_ + # wildcard pattern + pattern = BoundTruePattern(location, source) + + elseif !(source isa Expr || source isa Symbol) + # a constant, e.g. a regular expression, version number, raw string, etc. + pattern = BoundIsMatchTestPattern(input, BoundExpression(location, source), false) + + elseif is_expr(source, :macrocall) + # We permit custom string macros as long as they do not contain any unbound + # variables. We accomplish that simply by expanding the macro. Macros that + # interpolate, like lazy"", will fail because they produce a `call` rather + # than an object. Also, we permit users to define macros that expand to patterns. + while is_expr(source, :macrocall) + source = macroexpand(binder.mod, source; recursive = false) + end + (pattern, assigned) = bind_pattern!(location, source, input, binder, assigned) + + elseif is_expr(source, :$) + # an interpolation + interpolation = source.args[1] + bound_expression = bind_expression(location, interpolation, assigned) + pattern = BoundIsMatchTestPattern(input, bound_expression, false) + + elseif source isa Symbol + # variable pattern (just a symbol) + varsymbol::Symbol = source + if haskey(assigned, varsymbol) + # previously introduced variable. Get the symbol holding its value + var_value = assigned[varsymbol] + bound_expression = BoundExpression( + location, source, ImmutableDict{Symbol, Symbol}(varsymbol, var_value)) + pattern = BoundIsMatchTestPattern( + input, bound_expression, + true) # force an equality check + else + # this patterns assigns the variable. + assigned = ImmutableDict{Symbol, Symbol}(assigned, varsymbol, input) + pattern = BoundTruePattern(location, source) + end + + elseif is_expr(source, :(::), 1) + # ::type + T = source.args[1] + T, where_clause = split_where(T, location) + bound_type = bind_type(location, T, input, binder) + pattern = BoundTypeTestPattern(location, T, input, bound_type) + # Support `::T where ...` even though the where clause parses as + # part of the type. + pattern = join_where_clause(pattern, where_clause, location, binder, assigned) + + elseif is_expr(source, :(::), 2) + subpattern = source.args[1] + T = source.args[2] + T, where_clause = split_where(T, location) + bound_type = bind_type(location, T, input, binder) + pattern1 = BoundTypeTestPattern(location, T, input, bound_type) + pattern2, assigned = bind_pattern!(location, subpattern, input, binder, assigned) + pattern = BoundAndPattern(location, source, BoundPattern[pattern1, pattern2]) + # Support `::T where ...` even though the where clause parses as + # part of the type. + pattern = join_where_clause(pattern, where_clause, location, binder, assigned) + + elseif is_expr(source, :call) && is_possible_type_name(source.args[1]) + # struct pattern. + # TypeName(patterns...) + T = source.args[1] + subpatterns = source.args[2:length(source.args)] + len = length(subpatterns) + named_fields = [pat.args[1] for pat in subpatterns if is_expr(pat, :kw)] + named_count = length(named_fields) + if named_count != length(unique(named_fields)) + error("$(location.file):$(location.line): Pattern `$source` has duplicate " * + "named arguments $named_fields.") + elseif named_count != 0 && named_count != len + error("$(location.file):$(location.line): Pattern `$source` mixes named " * + "and positional arguments.") + end + + match_positionally = named_count == 0 + + # bind type at macro expansion time + pattern0, assigned = bind_pattern!(location, :(::($T)), input, binder, assigned) + bound_type = (pattern0::BoundTypeTestPattern).type + patterns = BoundPattern[pattern0] + field_names::Tuple = match_fieldnames(bound_type) + if match_positionally && len != length(field_names) + error("$(location.file):$(location.line): The type `$bound_type` has " * + "$(length(field_names)) fields but the pattern expects $len fields.") + end + + for i in 1:len + pat = subpatterns[i] + if match_positionally + field_name = field_names[i] + pattern_source = pat + else + @assert pat.head == :kw + field_name = pat.args[1] + pattern_source = pat.args[2] + if !(field_name in field_names) + error("$(location.file):$(location.line): Type `$bound_type` has " * + "no field `$field_name`.") + end + end + + field_type = nothing + if field_name == match_fieldnames(Symbol)[1] + # special case Symbol's hypothetical name field. + field_type = String + else + for (fname, ftype) in zip(Base.fieldnames(bound_type), Base.fieldtypes(bound_type)) + if fname == field_name + field_type = ftype + break + end + end + end + @assert field_type !== nothing + + fetch = BoundFetchFieldPattern(location, pattern_source, input, field_name, field_type) + field_temp = push_pattern!(patterns, binder, fetch) + bound_subpattern, assigned = bind_pattern!( + location, pattern_source, field_temp, binder, assigned) + push!(patterns, bound_subpattern) + end + + pattern = BoundAndPattern(location, source, patterns) + + elseif is_expr(source, :(&&), 2) + # conjunction: `(a && b)` where `a` and `b` are patterns. + subpattern1 = source.args[1] + subpattern2 = source.args[2] + bp1, assigned = bind_pattern!(location, subpattern1, input, binder, assigned) + bp2, assigned = bind_pattern!(location, subpattern2, input, binder, assigned) + pattern = BoundAndPattern(location, source, BoundPattern[bp1, bp2]) + + elseif is_expr(source, :call, 3) && source.args[1] == :& + # conjunction: `(a & b)` where `a` and `b` are patterns. + return bind_pattern!(location, Expr(:(&&), source.args[2], source.args[3]), input, binder, assigned) + + elseif is_expr(source, :(||), 2) + # disjunction: `(a || b)` where `a` and `b` are patterns. + subpattern1 = source.args[1] + subpattern2 = source.args[2] + bp1, assigned1 = bind_pattern!(location, subpattern1, input, binder, assigned) + bp2, assigned2 = bind_pattern!(location, subpattern2, input, binder, assigned) + + # compute the common assignments. + both = intersect(keys(assigned1), keys(assigned2)) + assigned = ImmutableDict{Symbol, Symbol}() + for key in both + v1 = assigned1[key] + v2 = assigned2[key] + if v1 == v2 + assigned = ImmutableDict{Symbol, Symbol}(assigned, key, v1) + else + # Every phi gets its own distinct variable. That ensures we do not + # share them between patterns. + temp = gensym(string("phi_", key), binder) + if v1 != temp + bound_expression = BoundExpression(location, v1, ImmutableDict{Symbol, Symbol}(key, v1)) + save = BoundFetchExpressionPattern(bound_expression, temp, Any) + bp1 = BoundAndPattern(location, source, BoundPattern[bp1, save]) + end + if v2 != temp + bound_expression = BoundExpression(location, v2, ImmutableDict{Symbol, Symbol}(key, v2)) + save = BoundFetchExpressionPattern(bound_expression, temp, Any) + bp2 = BoundAndPattern(location, source, BoundPattern[bp2, save]) + end + assigned = ImmutableDict{Symbol, Symbol}(assigned, key, temp) + end + end + pattern = BoundOrPattern(location, source, BoundPattern[bp1, bp2]) + + elseif is_expr(source, :call, 3) && source.args[1] == :| + # disjunction: `(a | b)` where `a` and `b` are patterns. + return bind_pattern!(location, Expr(:(||), source.args[2], source.args[3]), input, binder, assigned) + + elseif is_expr(source, :tuple) || is_expr(source, :vect) + # array or tuple + subpatterns = source.args + splat_count = count(s -> is_expr(s, :...), subpatterns) + if splat_count > 1 + error("$(location.file):$(location.line): More than one `...` in " * + "pattern `$source`.") + end + + # produce a check that the input is an array (or tuple) + patterns = BoundPattern[] + base = source.head == :vect ? AbstractArray : Tuple + pattern0 = BoundTypeTestPattern(location, base, input, base) + push!(patterns, pattern0) + len = length(subpatterns) + + # produce a check that the length of the input is sufficient + length_temp = push_pattern!(patterns, binder, + BoundFetchLengthPattern(location, source, input, Any)) + check_length = + if splat_count != 0 + BoundRelationalTestPattern( + location, source, length_temp, :>=, length(subpatterns)-1) + else + bound_expression = BoundExpression(location, length(subpatterns)) + BoundIsMatchTestPattern(length_temp, bound_expression, true) + end + push!(patterns, check_length) + + seen_splat = false + for (i, subpattern) in enumerate(subpatterns) + if is_expr(subpattern, :...) + @assert length(subpattern.args) == 1 + @assert !seen_splat + seen_splat = true + range_temp = push_pattern!(patterns, binder, + BoundFetchRangePattern(location, subpattern, input, i, len-i, Any)) + patterni, assigned = bind_pattern!( + location, subpattern.args[1], range_temp, binder, assigned) + push!(patterns, patterni) + else + index = seen_splat ? (i - len - 1) : i + index_temp = push_pattern!(patterns, binder, + BoundFetchIndexPattern(location, subpattern, input, index, Any)) + patterni, assigned = bind_pattern!( + location, subpattern, index_temp, binder, assigned) + push!(patterns, patterni) + end + end + pattern = BoundAndPattern(location, source, patterns) + + elseif is_expr(source, :where, 2) + # subpattern where guard + subpattern = source.args[1] + guard = source.args[2] + pattern0, assigned = bind_pattern!(location, subpattern, input, binder, assigned) + pattern1 = shred_where_clause(guard, false, location, binder, assigned) + pattern = BoundAndPattern(location, source, BoundPattern[pattern0, pattern1]) + + elseif is_expr(source, :call) && source.args[1] == :(:) && length(source.args) in 3:4 + # A range pattern. We depend on the Range API to make sense of it. + lower = source.args[2] + upper = source.args[3] + step = (length(source.args) == 4) ? source.args[4] : nothing + if upper isa Expr || upper isa Symbol || + lower isa Expr || lower isa Symbol || + step isa Expr || step isa Symbol + error("$(location.file):$(location.line): Non-constant range pattern: `$source`.") + end + pattern = BoundIsMatchTestPattern(input, BoundExpression(location, source), false) + + else + error("$(location.file):$(location.line): Unrecognized pattern syntax `$(pretty(source))`.") + end + + return (pattern, assigned) +end + +function push_pattern!(patterns::Vector{BoundPattern}, binder::BinderContext, pat::BoundFetchPattern) + push!(patterns, pat) + get_temp(binder, pat) +end + +function split_where(T, location) + type = T + where_clause = nothing + while is_expr(type, :where) + where_clause = (where_clause === nothing) ? type.args[2] : :($(type.args[2]) && $where_clause) + type = type.args[1] + end + + if !is_possible_type_name(type) + error("$(location.file):$(location.line): Invalid type name: `$type`.") + end + + return (type, where_clause) +end + +function join_where_clause(pattern, where_clause, location, binder, assigned) + if where_clause === nothing + return pattern + else + pattern1 = shred_where_clause(where_clause, false, location, binder, assigned) + return BoundAndPattern(location, where_clause, BoundPattern[pattern, pattern1]) + end +end + +""" + match_fieldnames(type::Type) + +Return a tuple containing the ordered list of the names (as Symbols) of fields that +can be matched either nominally or positionally. This list should exclude synthetic +fields that are produced by packages such as Mutts and AutoHashEqualsCached. This +function may be overridden by the client to hide fields that should not be matched. +""" +function match_fieldnames(type::Type) + Base.fieldnames(type) +end + +# For the purposes of pattern-matching, we pretend that `Symbol` has a single field. +const symbol_field_name = Symbol("«name(::Symbol)»") +match_fieldnames(::Type{Symbol}) = (symbol_field_name,) + +# +# Shred a `where` clause into its component parts, conjunct by conjunct. If necessary, +# we push negation operators down. This permits us to share the parts of a where clause +# between different rules. +# +function shred_where_clause( + guard::Any, + inverted::Bool, + location::LineNumberNode, + binder::BinderContext, + assigned::ImmutableDict{Symbol, Symbol})::BoundPattern + if @capture(guard, !g_) + return shred_where_clause(g, !inverted, location, binder, assigned) + elseif @capture(guard, g1_ && g2_) || @capture(guard, g1_ || g2_) + left = shred_where_clause(g1, inverted, location, binder, assigned) + right = shred_where_clause(g2, inverted, location, binder, assigned) + # DeMorgan's law: + # `!(a && b)` => `!a || !b` + # `!(a || b)` => `!a && !b` + result_type = (inverted == (guard.head == :&&)) ? BoundOrPattern : BoundAndPattern + return result_type(location, guard, BoundPattern[left, right]) + else + bound_expression = bind_expression(location, guard, assigned) + fetch = BoundFetchExpressionPattern(bound_expression, nothing, Any) + temp = get_temp(binder, fetch) + test = BoundWhereTestPattern(location, guard, temp, inverted) + return BoundAndPattern(location, guard, BoundPattern[fetch, test]) + end +end + +# +# getvars +# +# get all symbols in an expression +# +getvars(e) = Set{Symbol}() +getvars(e::Symbol) = startswith(string(e), '@') ? Set{Symbol}() : push!(Set{Symbol}(), e) +getvars(e::Expr) = getvars(is_expr(e, :call) ? e.args[2:end] : e.args) +getvars(es::AbstractArray) = union(Set{Symbol}(), [getvars(e) for e in es]...) + +# +# Produce a `BoundExpression` object for the given expression. This is used to +# determine the set of variable bindings that are used in the expression, and to +# simplify code generation. +# +function bind_expression(location::LineNumberNode, expr, assigned::ImmutableDict{Symbol, Symbol}) + if is_expr(expr, :(...)) + error("$(location.file):$(location.line): Splatting not supported in interpolation: `$expr`.") + end + + # determine the variables *actually* used in the expression + used = getvars(expr) + + # we sort the used variables by name so that we have a deterministic order + # that will make it more likely we can share the resulting expression. + used = sort(collect(intersect(keys(assigned), used))) + + assignments = Expr(:block) + new_assigned = ImmutableDict{Symbol, Symbol}() + for v in used + tmp = get(assigned, v, nothing) + @assert tmp !== nothing + push!(assignments.args, Expr(:(=), v, tmp)) + new_assigned = ImmutableDict(new_assigned, v => tmp) + end + + return BoundExpression(location, expr, new_assigned) +end + +is_empty_block(x) = is_expr(x, :block) && all(a -> a isa LineNumberNode, x.args) + +# +# Bind a case. +# +function bind_case( + case_number::Int, + location::LineNumberNode, + case, + predeclared_temps, + binder::BinderContext)::BoundCase + while true + # do some rewritings if needed + if is_expr(case, :macrocall) + # expand top-level macros only + case = macroexpand(binder.mod, case, recursive=false) + + elseif is_expr(case, :tuple, 2) && is_case(case.args[2]) && is_expr(case.args[2].args[2], :if, 2) + # rewrite `pattern, if guard end => result`, which parses as + # `pattern, (if guard end => result)` + # to `(pattern, if guard end) => result` + # so that the guard is part of the pattern. + pattern = case.args[1] + if_guard = case.args[2].args[2] + result = case.args[2].args[3] + case = :(($pattern, $if_guard) => $result) + + elseif is_case(case) + # rewrite `(pattern, if guard end) => result` + # to `(pattern where guard) => result` + pattern = case.args[2] + if is_expr(pattern, :tuple, 2) && is_expr(pattern.args[2], :if, 2) + if_guard = pattern.args[2] + if !is_empty_block(if_guard.args[2]) + error("$(location.file):$(location.line): Unrecognized @match guard syntax: `$if_guard`.") + end + pattern = pattern.args[1] + guard = if_guard.args[1] + result = case.args[3] + case = :(($pattern where $guard) => $result) + end + break + + else + error("$(location.file):$(location.line): Unrecognized @match case syntax: `$case`.") + end + end + + @assert is_case(case) + pattern = case.args[2] + result = case.args[3] + (pattern, result) = adjust_case_for_return_macro(binder.mod, location, pattern, result, predeclared_temps) + bound_pattern, assigned = bind_pattern!( + location, pattern, binder.input_variable, binder, ImmutableDict{Symbol, Symbol}()) + result_expression = bind_expression(location, result, assigned) + return BoundCase(case_number, location, pattern, bound_pattern, result_expression) +end diff --git a/src/bound_pattern.jl b/src/bound_pattern.jl new file mode 100644 index 0000000..f6f06d0 --- /dev/null +++ b/src/bound_pattern.jl @@ -0,0 +1,375 @@ +# Unfortunately, using a type alias instead of the written-out type tickles a Julia bug. +# See https://github.com/JuliaLang/julia/issues/50241 +# That bug is apparently fixed in Julia 1.10, but this package supports backward compat +# versions earlier than that. +# const Assigned = ImmutableDict{Symbol, Symbol} + +# We have a node for each pattern form. Some syntactic pattern forms are broken +# up into more primitive forms. For example, the pattern `s::String` is represented as +# an `AndPattern` that combines a `TypePattern` (for String) with a +# `BindVariablePattern` (to bind s). +abstract type BoundPattern end +loc(p::BoundPattern) = p.location +source(p::BoundPattern) = p.source + +# Patterns which fetch intermediate values so they may be reused later without +# being recomputed. Each one has the side-effect of assigning a computed +# value to a temporary variable. +abstract type BoundFetchPattern <: BoundPattern end + +# Patterns which test some boolean condition. +abstract type BoundTestPattern <: BoundPattern end + +# A pattern that always matches +struct BoundTruePattern <: BoundPattern + location::LineNumberNode + source::Any +end +Base.hash(::BoundTruePattern, h::UInt64) = hash(0x8cc17f34ef3bbb1d, h) +Base.:(==)(a::BoundTruePattern, b::BoundTruePattern) = true + +# A pattern that never matches +struct BoundFalsePattern <: BoundPattern + location::LineNumberNode + source::Any +end +Base.hash(::BoundFalsePattern, h::UInt64) = hash(0xeb817c7d6beb3bda, h) +Base.:(==)(a::BoundFalsePattern, b::BoundFalsePattern) = true + +function BoundBoolPattern(location::LineNumberNode, source::Any, b::Bool) + b ? BoundTruePattern(location, source) : BoundFalsePattern(location, source) +end + +# +# A data structure representing a user-written expression, which can occur in one of +# several syntactic contexts. The `source` field is the expression itself, and the +# `assignments` field is a dictionary mapping from variable names to the names of +# temporary variables that have been assigned to hold the values of those variables. +# The `assignments` field is used to rewrite the expression into a form that can be +# evaluated in the context of a pattern match. +# The `location` field is the location of the expression in the source code. +# +struct BoundExpression + location::LineNumberNode + source::Any + assignments::ImmutableDict{Symbol, Symbol} + function BoundExpression(location::LineNumberNode, + source::Any, + assignments::ImmutableDict{Symbol, Symbol} = ImmutableDict{Symbol, Symbol}()) + new(location, source, assignments) + end +end +function Base.hash(a::BoundExpression, h::UInt64) + hash((a.source, a.assignments, 0xdbf4fc8b24ef8890), h) +end +# TODO: we should compare source ignoring location nodes contained within it +function Base.:(==)(a::BoundExpression, b::BoundExpression) + isequal(a.source, b.source) && a.assignments == b.assignments +end +loc(p::BoundExpression) = p.location +source(p::BoundExpression) = p.source + +# A pattern like `1`, `$(expression)`, or `x` where `x` is already bound. +# Note that for a pattern variable `x` that is previously bound, `x` means +# the same thing as `$x` or `$(x)`. We test a constant pattern by applying +# `pattern_matches_value(pattern.value, input_value)` +struct BoundIsMatchTestPattern <: BoundTestPattern + input::Symbol + bound_expression::BoundExpression + # `force_equality` is to force the use of `isequal` instead of `ismatch` + # (for when pattern variables are reused in multiple places) + force_equality::Bool +end +function Base.hash(a::BoundIsMatchTestPattern, h::UInt64) + hash((a.input, a.bound_expression, a.force_equality, 0x7e92a644c831493f), h) +end +function Base.:(==)(a::BoundIsMatchTestPattern, b::BoundIsMatchTestPattern) + a.input == b.input && + a.force_equality == b.force_equality && + isequal(a.bound_expression, b.bound_expression) +end +loc(p::BoundIsMatchTestPattern) = loc(p.bound_expression) +source(p::BoundIsMatchTestPattern) = source(p.bound_expression) + +# A pattern that compares the input, which must be an Integer, using a Relational +# operator, to a given value. Used to ensure that list patterns match against a +# list of sufficient length. The input is on the left and the value is on the +# right of whatever relation is used. Currently only `>=` is supported. +struct BoundRelationalTestPattern <: BoundTestPattern + location::LineNumberNode + source::Any + input::Symbol + relation::Symbol # one of `:<`, `:<=`, `:>`, `:>=` + value::Int +end +function Base.hash(a::BoundRelationalTestPattern, h::UInt64) + hash((a.input, a.relation, a.value, 0xbfe66949d262f0e0), h) +end +function Base.:(==)(a::BoundRelationalTestPattern, b::BoundRelationalTestPattern) + a.input == b.input && a.relation == b.relation && a.value == b.value +end + +# A pattern that simply checks the given boolean variable +struct BoundWhereTestPattern <: BoundTestPattern + location::LineNumberNode + source::Any + input::Symbol # variable holding the evaluated where clause + inverted::Bool # If the sense of the test is inverted +end +function Base.hash(a::BoundWhereTestPattern, h::UInt64) + hash((a.input, a.inverted, 0x868a8076acbe0e12), h) +end +function Base.:(==)(a::BoundWhereTestPattern, b::BoundWhereTestPattern) + a.input == b.input && a.inverted == b.inverted +end + +# A pattern like ::Type which matches if the type matches. +struct BoundTypeTestPattern <: BoundTestPattern + location::LineNumberNode + source::Any + input::Symbol + type::Type # typically the type that source binds to +end +function Base.hash(a::BoundTypeTestPattern, h::UInt64) + hash((a.input, a.type, 0x92b4a01b9f8cb47b), h) +end +function Base.:(==)(a::BoundTypeTestPattern, b::BoundTypeTestPattern) + a.input == b.input && a.type == b.type +end + +# A pattern that matches if any disjunct matches +struct BoundOrPattern <: BoundPattern + location::LineNumberNode + source::Any + subpatterns::ImmutableVector{BoundPattern} + _cached_hash::UInt64 + function BoundOrPattern( + location::LineNumberNode, + source::Any, + subpatterns::Vector{BoundPattern}) + # We gather together (flatten) any nested and-patterns + gathered_subpatterns = mapreduce( + p -> if p isa BoundOrPattern + p.subpatterns + elseif p isa BoundFalsePattern + BoundPattern[] + else + BoundPattern[p] + end, + vcat, + subpatterns) + if any(p -> p isa BoundTruePattern, gathered_subpatterns) + return BoundTruePattern(location, source) + elseif isempty(gathered_subpatterns) + return BoundFalsePattern(location, source) + elseif length(gathered_subpatterns) == 1 + return gathered_subpatterns[1] + else + return new(location, source, gathered_subpatterns, + hash(subpatterns, 0x82d8dc51bf845b12)) + end + end +end +Base.hash(a::BoundOrPattern, h::UInt64) = hash(a._cached_hash, h) +function Base.:(==)(a::BoundOrPattern, b::BoundOrPattern) + a._cached_hash == b._cached_hash && a.subpatterns == b.subpatterns +end + +# A pattern that matches if all conjuncts match +struct BoundAndPattern <: BoundPattern + location::LineNumberNode + source::Any + subpatterns::Vector{BoundPattern} + _cached_hash::UInt64 + function BoundAndPattern( + location::LineNumberNode, + source::Any, + subpatterns::Vector{BoundPattern}) + # We gather together (flatten) any nested and-patterns + gathered_subpatterns = mapreduce( + p -> if p isa BoundAndPattern + p.subpatterns + elseif p isa BoundTruePattern + BoundPattern[] + else + BoundPattern[p] + end, + vcat, + subpatterns) + if any(p -> p isa BoundFalsePattern, gathered_subpatterns) + return BoundFalsePattern(location, source) + elseif isempty(gathered_subpatterns) + return BoundTruePattern(location, source) + elseif length(gathered_subpatterns) == 1 + return gathered_subpatterns[1] + else + return new(location, source, gathered_subpatterns, + hash(subpatterns, 0x9b7f2a204d994a1a)) + end + end +end +Base.hash(a::BoundAndPattern, h::UInt64) = hash(a._cached_hash, h) +function Base.:(==)(a::BoundAndPattern, b::BoundAndPattern) + a._cached_hash == b._cached_hash && a.subpatterns == b.subpatterns +end + +# Fetch a field of the input into into a fresh temporary synthetic variable. +# Used to decompose patterns that match subfields. Treated as always "true" +# for matching purposes, except it has the side effect of producing a temporary +# variable that can be used for further tests. That temporary may be reused across +# patterns when that makes sense. +struct BoundFetchFieldPattern <: BoundFetchPattern + location::LineNumberNode + source::Any + input::Symbol + field_name::Symbol + type::Type +end +# For the purposes of whether or not two fetches are the same: if they are fetching +# the same field name (from the same input), then yes. +function Base.hash(a::BoundFetchFieldPattern, h::UInt64) + hash((a.input, a.field_name, 0x0c5266ab2b5ed7f1), h) +end +function Base.:(==)(a::BoundFetchFieldPattern, b::BoundFetchFieldPattern) + a.input == b.input && a.field_name == b.field_name +end + +# Fetch a value at a given index of the input into a temporary. See +# `BoundFetchFieldPattern` for the general idea of how these are used. +# Negative indices index from the end of the input; `index==-1` accesses +# the last element. +struct BoundFetchIndexPattern <: BoundFetchPattern + location::LineNumberNode + source::Any + input::Symbol + # index value. If negative, it is from the end. `-1` accesses the last element + index::Int + type::Type +end +function Base.hash(a::BoundFetchIndexPattern, h::UInt64) + hash((a.input, a.index, 0x820a6d07cc13ac86), h) +end +function Base.:(==)(a::BoundFetchIndexPattern, b::BoundFetchIndexPattern) + a.input == b.input && a.index == b.index +end + +# Fetch a subsequence at a given range of the input into a temporary. +struct BoundFetchRangePattern <: BoundFetchPattern + location::LineNumberNode + source::Any + input::Symbol + first_index::Int # first index to include + from_end::Int # distance from the end for the last included index; 0 to include the last element + type::Type +end +function Base.hash(a::BoundFetchRangePattern, h::UInt64) + hash((a.input, a.first_index, a.from_end, 0x7aea7756428a1646), h) +end +function Base.:(==)(a::BoundFetchRangePattern, b::BoundFetchRangePattern) + a.input == b.input && a.first_index == b.first_index && a.from_end == b.from_end +end + +# Compute the length of the input (tuple or array) +struct BoundFetchLengthPattern <: BoundFetchPattern + location::LineNumberNode + source::Any + input::Symbol + type::Type +end +function Base.hash(a::BoundFetchLengthPattern, h::UInt64) + hash((a.input, 0xa7167fae5a24c457), h) +end +function Base.:(==)(a::BoundFetchLengthPattern, b::BoundFetchLengthPattern) + a.input == b.input +end + +# Preserve the value of the expression into a temp. Used +# (1) to force the binding on both sides of an or-pattern to be the same (a phi), and +# (2) to load the value of a `where` clause. +# +# The key, if provided, will be used as the temp. That is used to ensure every phi +# is assigned a distinct temp (even if it is the same variable binding that is being +# preserved). +struct BoundFetchExpressionPattern <: BoundFetchPattern + bound_expression::BoundExpression + key::Union{Nothing, Symbol} + type::Type +end +function Base.hash(a::BoundFetchExpressionPattern, h::UInt64) + hash((a.bound_expression, a.key, 0x53f0f6a137a891d8), h) +end +function Base.:(==)(a::BoundFetchExpressionPattern, b::BoundFetchExpressionPattern) + a.bound_expression == b.bound_expression && a.key == b.key +end +loc(p::BoundFetchExpressionPattern) = loc(p.bound_expression) +source(p::BoundFetchExpressionPattern) = source(p.bound_expression) + +# +# Pattern properties +# + +# Pattern might not be true +is_refutable(pattern::Union{BoundFetchPattern, BoundTruePattern}) = false +is_refutable(pattern::Union{BoundTestPattern, BoundFalsePattern}) = true +is_refutable(pattern::BoundAndPattern) = any(is_refutable, pattern.subpatterns) +is_refutable(pattern::BoundOrPattern) = all(is_refutable, pattern.subpatterns) + +# Pattern is definitely true +is_irrefutable(pattern::BoundPattern) = !is_refutable(pattern) + +# +# A data structure representing information about a single match case. +# Initially, it represents just what was in source. However, during construction of +# the decision automaton, the pattern represents only the remaining operations that +# must be performed to decide if the pattern matches. +# +struct BoundCase + # The index of the case, starting with 1 for the first => in the @match + case_number::Int + + # Its location for error reporting purposes + location::LineNumberNode + + # Its source for error reporting + pattern_source + + # The operations (or remaining operations) required to perform the match. + # In a node of the decision automaton, some operations may have already been done, + # and they are removed from the bound pattern in subsequent nodes of the automaton. + # When the bound pattern is simply `true`, the case's pattern has matched. + pattern::BoundPattern + + # The user's result expression for this case. + result_expression::BoundExpression + + _cached_hash::UInt64 + function BoundCase( + case_number::Int, + location::LineNumberNode, + pattern_source, + pattern::BoundPattern, + result_expression::BoundExpression) + _hash = hash((case_number, pattern, result_expression), 0x1cdd9657bfb1e645) + new(case_number, location, pattern_source, pattern, result_expression, _hash) + end +end +function with_pattern( + case::BoundCase, + new_pattern::BoundPattern) + return BoundCase( + case.case_number, + case.location, + case.pattern_source, + new_pattern, + case.result_expression) +end +function Base.hash(case::BoundCase, h::UInt64) + hash(case._cached_hash, h) +end +function Base.:(==)(a::BoundCase, b::BoundCase) + a._cached_hash == b._cached_hash && + isequal(a.case_number, b.case_number) && + isequal(a.pattern, b.pattern) && + isequal(a.result_expression, b.result_expression) +end +loc(case::BoundCase) = case.location diff --git a/src/immutable_vector.jl b/src/immutable_vector.jl new file mode 100644 index 0000000..d812211 --- /dev/null +++ b/src/immutable_vector.jl @@ -0,0 +1,20 @@ +# This data type makes it easier to avoid accidentally mutating a vector that was +# intended to be immutable. Use instead of `Vector{T}` when you intend it to be +# immutable. +struct ImmutableVector{T} <: AbstractVector{T} + _data::Vector{T} + _cached_hash::UInt64 + ImmutableVector{T}(data::Vector{T}) where {T} = new(copy(data), hash(data, 0xdc8f7e8a0e698fac)) +end +ImmutableVector(data::Vector{T}) where {T} = ImmutableVector{T}(data) +Base.size(a::ImmutableVector) = size(a._data) +Base.length(a::ImmutableVector) = length(a._data)::Int +Base.getindex(a::ImmutableVector{T}, i::Int) where {T} = a._data[i]::T +Base.getindex(a::ImmutableVector{T}, i::UnitRange{Int}) where {T} = ImmutableVector{T}(a._data[i]) +Base.eachindex(a::ImmutableVector) = 1:length(a) +Base.IndexStyle(::Type{<:ImmutableVector}) = IndexLinear() +Base.convert(::Type{ImmutableVector{T}}, x::Vector{T}) where {T} = ImmutableVector{T}(x) +Base.hash(a::ImmutableVector{T}, h::UInt64) where {T} = hash(a._cached_hash, h) +function Base.:(==)(a::ImmutableVector{T}, b::ImmutableVector{T}) where {T} + isequal(a._cached_hash, b._cached_hash) && isequal(a._data, b._data) +end diff --git a/src/lowering.jl b/src/lowering.jl new file mode 100644 index 0000000..8584c48 --- /dev/null +++ b/src/lowering.jl @@ -0,0 +1,102 @@ +# compute whether or not a constant pattern matches a value at runtime +pattern_matches_value(pattern, input) = isequal(pattern, input) +pattern_matches_value(r::AbstractRange, i) = i in r || isequal(i, r) +# For compat with Match.jl we permit a Regex to match an identical Regex by isequal +pattern_matches_value(r::Regex, s::AbstractString) = occursin(r, s) + +function assignments(assigned::ImmutableDict{Symbol, Symbol}) + # produce a list of assignments to be splatted into the caller + return (:($patvar = $resultsym) for (patvar, resultsym) in assigned) +end + +function code(e::BoundExpression) + value = Expr(:block, e.location, e.source) + assignments = Expr(:block, (:($k = $v) for (k, v) in e.assignments)...) + return Expr(:let, assignments, value) +end + +# return the code needed for a pattern. +code(bound_pattern::BoundTruePattern, binder::BinderContext) = true +function code(bound_pattern::BoundIsMatchTestPattern, binder::BinderContext) + func = bound_pattern.force_equality ? Base.isequal : (@__MODULE__).pattern_matches_value + :($func($(code(bound_pattern.bound_expression)), $(bound_pattern.input))) +end +function code(bound_pattern::BoundRelationalTestPattern, binder::BinderContext) + @assert bound_pattern.relation == :>= + :($(bound_pattern.relation)($(bound_pattern.input), $(bound_pattern.value))) +end +function code(bound_pattern::BoundWhereTestPattern, binder::BinderContext) + bound_pattern.inverted ? :(!$(bound_pattern.input)) : bound_pattern.input +end +function code(bound_pattern::BoundTypeTestPattern, binder::BinderContext) + # We assert that the type is invariant. Because this mutates binder.assertions, + # you must take the value of binder.assertions after all calls to the generated code. + if bound_pattern.source != bound_pattern.type && !(bound_pattern.source in binder.asserted_types) + test = :($(bound_pattern.type) == $(bound_pattern.source)) + thrown = :($throw($AssertionError($string($(string(bound_pattern.location.file)), + ":", $(bound_pattern.location.line), + ": The type syntax `::", $(string(bound_pattern.source)), "` bound to type ", + $string($(bound_pattern.type)), " at macro expansion time but ", + $(bound_pattern.source), " later.")))) + push!(binder.assertions, Expr(:block, bound_pattern.location, :($test || $thrown))) + push!(binder.asserted_types, bound_pattern.source) + end + :($(bound_pattern.input) isa $(bound_pattern.type)) +end +function code(bound_pattern::BoundOrPattern, binder::BinderContext) + :($(mapreduce(bp -> lower_pattern_to_boolean(bp, binder), + (a, b) -> :($a || $b), + bound_pattern.subpatterns))) +end +function code(bound_pattern::BoundAndPattern, binder::BinderContext) + :($(mapreduce(bp -> lower_pattern_to_boolean(bp, binder), + (a, b) -> :($a && $b), + bound_pattern.subpatterns))) +end +function code(bound_pattern::BoundFetchPattern, binder::BinderContext) + tempvar = get_temp(binder, bound_pattern) + :($tempvar = $(code(bound_pattern))) +end + +function code(bound_pattern::BoundFetchPattern) + location = loc(bound_pattern) + error("$(location.file):$(location.line): Internal error in Match: `code(::$(typeof(bound_pattern)))` not implemented.") +end +function code(bound_pattern::BoundFetchFieldPattern) + # As a special case, we pretend that `Symbol` has a field that contains + # the symbol's name. This is because we want to be able to match against it. + # But since there is no such field, we have to special-case it here. + if bound_pattern.field_name == match_fieldnames(Symbol)[1] + return :($string($(bound_pattern.input))) + end + :($getfield($(bound_pattern.input), $(QuoteNode(bound_pattern.field_name)))) +end +function code(bound_pattern::BoundFetchIndexPattern) + i = bound_pattern.index + if i < 0 + i = :($length($(bound_pattern.input)) + $(i + 1)) + end + :($getindex($(bound_pattern.input), $i)) +end +function code(bound_pattern::BoundFetchRangePattern) + index = :($(bound_pattern.first_index):(length($(bound_pattern.input)) - $(bound_pattern.from_end))) + :($getindex($(bound_pattern.input), $(index))) +end +function code(bound_pattern::BoundFetchLengthPattern) + :($length($(bound_pattern.input))) +end +function code(bound_pattern::BoundFetchExpressionPattern) + code(bound_pattern.bound_expression) +end + +# Return an expression that computes whether or not the pattern matches. +function lower_pattern_to_boolean(bound_pattern::BoundPattern, binder::BinderContext) + Expr(:block, loc(bound_pattern), code(bound_pattern, binder)) +end +function lower_pattern_to_boolean(bound_pattern::BoundFetchPattern, binder::BinderContext) + # since fetches are performed purely for their side-effects, and + # joined to the computations that require the fetched value using `and`, + # we return `true` as the boolean value whenever we perform one. + # (Fetches always succeed) + Expr(:block, loc(bound_pattern), code(bound_pattern, binder), true) +end diff --git a/src/match_cases_opt.jl b/src/match_cases_opt.jl new file mode 100644 index 0000000..424ca65 --- /dev/null +++ b/src/match_cases_opt.jl @@ -0,0 +1,484 @@ +# +# Build the decision automaton and return its entry point. +# +function build_automaton_core( + value, + source_cases::Vector{Any}, + location::LineNumberNode, + predeclared_temps, + binder::BinderContext)::AutomatonNode + cases = BoundCase[] + for case in source_cases + if case isa LineNumberNode + location = case + else + bound_case::BoundCase = bind_case(length(cases) + 1, location, case, predeclared_temps, binder) + bound_case = simplify(bound_case, binder) + push!(cases, bound_case) + end + end + + # Track the set of reachable cases (by index) + reachable = Set{Int}() + + # Make an entry point for the automaton + entry = AutomatonNode(cases) + + # If the value had a type annotation, then we can use that to + # narrow down the cases that we need to consider. + input_type = Any + if is_expr(value, :(::), 2) + type = value.args[2] + try + input_type = bind_type(location, type, binder.input_variable, binder) + catch + # If we don't understand the type annotation, then we'll just ignore it. + end + binder.types[binder.input_variable] = input_type + if input_type !== Any + filter = BoundTypeTestPattern(location, type, binder.input_variable, input_type) + entry = remove(filter, true, entry, binder) + end + end + binder.types[binder.input_variable] = input_type + + # Build the decision automaton with the given entry point + work_queue = Set{AutomatonNode}([entry]) + while !isempty(work_queue) + node = pop!(work_queue) + if node.action isa Nothing + set_next!(node, binder) + @assert node.action !== nothing + @assert node.next !== nothing + if node.action isa BoundCase + push!(reachable, node.action.case_number) + end + union!(work_queue, successors(node)) + end + end + + # Warn if there were any unreachable cases + for i in 1:length(cases) + if !(i in reachable) + case = cases[i] + loc = case.location + @warn("$(loc.file):$(loc.line): Case $(case.case_number): `$(case.pattern_source) =>` is not reachable.") + end + end + + entry +end + +# +# Generate all of the code given the entry point +# +function generate_code(top_down_nodes::Vector{DeduplicatedAutomatonNode}, @nospecialize(value), location::LineNumberNode, binder::BinderContext) + result_variable = gensym("match_result") + result_label = gensym("completed") + emit = Any[location, :($(binder.input_variable) = $value)] + + # put the first node last so it will be the first to be emitted + reverse!(top_down_nodes) + togen = top_down_nodes # now it's in the right order to consume from the end + + # Because we are producing code in a top-down topologically-sorted order, + # all of the `@goto`s that we emit are forward. So we are guaranteed to emit + # the `@goto` (and calling `label!`) before we hit the node where we need the `@label`. + labels = IdDict{DeduplicatedAutomatonNode, Symbol}() + function label!(code::DeduplicatedAutomatonNode) + get!(() -> gensym("label", binder), labels, code) + end + + while !isempty(togen) + node = pop!(togen) + if node in keys(labels) + push!(emit, :(@label $(labels[node]))) + end + action = node.action + if action isa BoundCase + # We've matched a pattern. + push!(emit, loc(action)) + push!(emit, :($result_variable = $(code(action.result_expression)))) + push!(emit, :(@goto $result_label)) + elseif action isa BoundFetchPattern + push!(emit, loc(action)) + push!(emit, code(action, binder)) + (next::DeduplicatedAutomatonNode,) = node.next + if last(togen) != next + # We need a `goto`, since it isn't the next thing we can fall into. + # This call to `label` sets up the label in the `labels` map to be + # produced when we emit the target node. + push!(emit, :(@goto $(label!(next)))) + end + elseif action isa BoundTestPattern + push!(emit, loc(action)) + next_true, next_false = node.next + push!(emit, :($(code(action, binder)) || @goto $(label!(next_false)))) + if last(togen) != next_true + # we need a `goto`, since it isn't the next thing we can fall into. + push!(emit, :(@goto $(label!(next_true)))) + end + elseif action isa Expr + push!(emit, action) + else + error("this node ($(typeof(action))) is believed unreachable") + end + end + + push!(emit, :(@label $result_label)) + push!(emit, result_variable) + Expr(:block, binder.assertions..., emit...) +end + +# +# Build the whole decision automaton from the syntax for the value and body +# +function build_automaton(location::LineNumberNode, mod::Module, @nospecialize(value), body) + if is_case(body) + # previous version of @match supports `@match(expr, pattern => value)` + body = Expr(:block, body) + end + if is_expr(body, :block) + source_cases = body.args + else + error("$(location.file):$(location.line): Unrecognized @match block syntax: `$body`.") + end + + binder = BinderContext(mod) + predeclared_temps = Any[] + entry = build_automaton_core(value, source_cases, location, predeclared_temps, binder) + return entry, predeclared_temps, binder +end + +# +# Build the whole decision automaton from the syntax for the value and body, +# optimize it, and return the resulting set of nodes along with the binder. +# +function build_deduplicated_automaton(location::LineNumberNode, mod::Module, value, body) + entry, predeclared_temps, binder = build_automaton(location::LineNumberNode, mod::Module, value, body) + top_down_nodes = deduplicate_automaton(entry, binder) + return top_down_nodes, predeclared_temps, binder +end + +# +# Compute and record the next action for the given node. +# +function set_next!(node::AutomatonNode, binder::BinderContext) + @assert node.action === nothing + @assert node.next === nothing + + action::Union{BoundCase, BoundPattern, Expr} = next_action(node, binder) + next::Union{Tuple{}, Tuple{AutomatonNode}, Tuple{AutomatonNode, AutomatonNode}} = + make_next(node, action, binder) + node.action = action + node.next = next + @assert !(node.next isa Nothing) +end + + +# +# Compute the next action for the given decision automaton node. We take the +# simple approach of just doing the next thing on the list of the first pattern +# that might match (the left-to-right "heusristic"). We might use different +# heuristics to do better, but not likely by more than a few percent except +# in machine-generated code. +# See https://www.cs.tufts.edu/~nr/cs257/archive/norman-ramsey/match.pdf for details. +# See https://gist.github.com/gafter/145db4a2282296bdaa08e0a0dcce9217 for an example +# of machine-generated pattern-matching code that can cause an explosion of generated +# code size. +# +function next_action( + node::AutomatonNode, + binder::BinderContext)::Union{BoundCase, BoundPattern, Expr} + if isempty(node.cases) + # cases have been exhausted. Return code to throw a match failure. + return :($throw($MatchFailure($(binder.input_variable)))) + end + first_case = node.cases[1] + if first_case.pattern isa BoundTruePattern + # case has been satisfied. Return it as our destination. + return first_case + end + return next_action(first_case.pattern) +end +next_action(pattern::BoundPattern) = pattern +function next_action(pattern::Union{BoundFalsePattern, BoundTruePattern}) + error("unreachable - a $(typeof(pattern)) cannot be the next action") +end +function next_action(pattern::Union{BoundAndPattern, BoundOrPattern}) + return next_action(pattern.subpatterns[1]) +end + +# +# Given an action, make the "next" result, which is the action or successor +# node of the decision automaton. +# +function make_next( + node::AutomatonNode, + action::Union{BoundCase, Expr}, + binder::BinderContext) + return () +end +function make_next( + node::AutomatonNode, + action::BoundPattern, + binder::BinderContext) + error("pattern cannot be the next action: $(typeof(action))") +end +function intern(node::AutomatonNode, binder::BinderContext) + get!(binder.intern, node, node) +end +function make_next( + node::AutomatonNode, + action::BoundFetchPattern, + binder::BinderContext)::Tuple{AutomatonNode} + succ = remove(action, node, binder) + succ = intern(succ, binder) + return (succ,) +end + +# When a test occurs, there are two subsequent nodes, depending on the outcome of the test. +function make_next( + node::AutomatonNode, + action::BoundTestPattern, + binder::BinderContext)::Tuple{AutomatonNode, AutomatonNode} + true_next = remove(action, true, node, binder) + false_next = remove(action, false, node, binder) + true_next = intern(true_next, binder) + false_next = intern(false_next, binder) + return (true_next, false_next) +end + +# The next code point is the same but without the action, since it has been done. +function remove(action::BoundFetchPattern, node::AutomatonNode, binder::BinderContext)::AutomatonNode + cases = map(c -> remove(action, c, binder), node.cases) + succ = AutomatonNode(cases) + + # If we know the type of the fetched value, we can assert that in downstream code. + bound_type = action.type + if bound_type !== Any + temp = get_temp(binder, action) + filter = BoundTypeTestPattern(loc(action), source(action), temp, bound_type) + succ = remove(filter, true, succ, binder) + end + succ +end +function remove(action::BoundTestPattern, sense::Bool, node::AutomatonNode, binder::BinderContext)::AutomatonNode + cases = map(c -> remove(action, sense, c, binder), node.cases) + return AutomatonNode(cases) +end + +function remove(action::BoundTestPattern, action_result::Bool, case::BoundCase, binder::BinderContext)::BoundCase + with_pattern(case, remove(action, action_result, case.pattern, binder)) +end +function remove(action::BoundFetchPattern, case::BoundCase, binder::BinderContext)::BoundCase + with_pattern(case, remove(action, case.pattern, binder)) +end + +# +# Remove the given action from a pattern. +# +remove(action::BoundFetchPattern, pattern::BoundPattern, binder::BinderContext)::BoundPattern = pattern +remove(action::BoundTestPattern, action_result::Bool, pattern::BoundPattern, binder::BinderContext)::BoundPattern = pattern +function remove(action::BoundFetchPattern, pattern::BoundFetchPattern, binder::BinderContext)::BoundPattern + return (action == pattern) ? BoundTruePattern(loc(pattern), source(pattern)) : pattern +end +function remove(action::BoundFetchPattern, pattern::Union{BoundAndPattern,BoundOrPattern}, binder::BinderContext)::BoundPattern + subpatterns = collect(BoundPattern, map(p -> remove(action, p, binder), pattern.subpatterns)) + return (typeof(pattern))(loc(pattern), source(pattern), subpatterns) +end +function remove(action::BoundTestPattern, action_result::Bool, pattern::BoundTestPattern, binder::BinderContext)::BoundPattern + return (action == pattern) ? BoundBoolPattern(loc(pattern), source(pattern), action_result) : pattern +end +function remove(action::BoundIsMatchTestPattern, action_result::Bool, pattern::BoundIsMatchTestPattern, binder::BinderContext)::BoundPattern + if action.input != pattern.input || action.force_equality != pattern.force_equality + return pattern + end + if isequal(action.bound_expression, pattern.bound_expression) + return BoundBoolPattern(loc(pattern), source(pattern), action_result) + end + + # As a special case, if the input variable is of type Bool, then we know that true and false + # are the only values it can hold. + type = get!(() -> Any, binder.types, action.input) + if type == Bool && action.bound_expression.source isa Bool && pattern.bound_expression.source isa Bool + @assert action.bound_expression.source != pattern.bound_expression.source # because we already checked for equality + # If the one succeeded, then the other one fails + return BoundBoolPattern(loc(pattern), source(pattern), !action_result) + end + + return pattern +end +function remove(action::BoundTestPattern, action_result::Bool, pattern::Union{BoundAndPattern,BoundOrPattern}, binder::BinderContext)::BoundPattern + subpatterns = collect(BoundPattern, map(p -> remove(action, action_result, p, binder), pattern.subpatterns)) + return (typeof(pattern))(loc(pattern), source(pattern), subpatterns) +end +function remove(action::BoundWhereTestPattern, action_result::Bool, pattern::BoundWhereTestPattern, binder::BinderContext)::BoundPattern + # Two where tests can be related by being the inverse of each other. + action.input == pattern.input || return pattern + replacement_value = (action.inverted == pattern.inverted) == action_result + return BoundBoolPattern(loc(pattern), source(pattern), replacement_value) +end +function remove(action::BoundTypeTestPattern, action_result::Bool, pattern::BoundTypeTestPattern, binder::BinderContext)::BoundPattern + # Knowing the result of one type test can give information about another. For + # example, if you know `x` is a `String`, then you know that it isn't an `Int`. + if (action == pattern) + return BoundBoolPattern(loc(pattern), source(pattern), action_result) + elseif action.input != pattern.input + return pattern + elseif action_result + # the type test succeeded. + if action.type <: pattern.type + return BoundTruePattern(loc(pattern), source(pattern)) + elseif pattern.type <: action.type + # we are asking about a narrower type - result unknown + return pattern + else + # Since Julia does not support multiple inheritance, if the two types + # are not related by inheritance, then no types that implement both will + # ever come into existence. + @assert typeintersect(pattern.type, action.type) == Base.Bottom + + # their intersection is empty, so it cannot be pattern.type + return BoundFalsePattern(loc(pattern), source(pattern)) + end + else + # the type test failed. + if action.type <: pattern.type + # we are asking about a wider type - result unknown + return pattern + elseif pattern.type <: action.type + # if it wasn't the wider type, then it won't be the narrower type + return BoundFalsePattern(loc(pattern), source(pattern)) + else + return pattern + end + end +end + +# +# Simplify a case by removing fetch operations whose results are not used. +# +function simplify(case::BoundCase, binder::BinderContext)::BoundCase + required_temps = Set(values(case.result_expression.assignments)) + simplified_pattern = simplify(case.pattern, required_temps, binder) + return with_pattern(case, simplified_pattern) +end + +# +# Simplify a pattern by removing fetch operations whose results are not used. +# +function simplify(pattern::BoundPattern, required_temps::Set{Symbol}, binder::BinderContext)::BoundPattern + push!(required_temps, pattern.input) + pattern +end +function simplify(pattern::Union{BoundTruePattern, BoundFalsePattern}, required_temps::Set{Symbol}, binder::BinderContext)::BoundPattern + pattern +end +function simplify(pattern::BoundFetchPattern, required_temps::Set{Symbol}, binder::BinderContext)::BoundPattern + temp = get_temp(binder, pattern) + if temp in required_temps + pop!(required_temps, temp) + push!(required_temps, pattern.input) + pattern + else + BoundTruePattern(loc(pattern), source(pattern)) + end +end +function simplify(pattern::BoundFetchExpressionPattern, required_temps::Set{Symbol}, binder::BinderContext)::BoundPattern + temp = get_temp(binder, pattern) + if temp in required_temps + pop!(required_temps, temp) + for (v, t) in pattern.bound_expression.assignments + push!(required_temps, t) + end + pattern + else + BoundTruePattern(loc(pattern), source(pattern)) + end +end +function simplify(pattern::BoundIsMatchTestPattern, required_temps::Set{Symbol}, binder::BinderContext)::BoundPattern + push!(required_temps, pattern.input) + for (v, t) in pattern.bound_expression.assignments + push!(required_temps, t) + end + pattern +end +function simplify(pattern::BoundAndPattern, required_temps::Set{Symbol}, binder::BinderContext)::BoundPattern + subpatterns = BoundPattern[] + for p in reverse(pattern.subpatterns) + simplified = simplify(p, required_temps, binder) + push!(subpatterns, simplified) + end + BoundAndPattern(loc(pattern), source(pattern), BoundPattern[reverse(subpatterns)...]) +end +function simplify(pattern::BoundOrPattern, required_temps::Set{Symbol}, binder::BinderContext)::BoundPattern + subpatterns = BoundPattern[] + new_required_temps = Set{Symbol}() + for p in reverse(pattern.subpatterns) + rt = copy(required_temps) + push!(subpatterns, simplify(p, rt, binder)) + union!(new_required_temps, rt) + end + empty!(required_temps) + union!(required_temps, new_required_temps) + BoundOrPattern(loc(pattern), source(pattern), BoundPattern[reverse(subpatterns)...]) +end + +# +# Some useful macros for testing and diagnosing the decision automaton. +# + +# Return the count of the number of nodes that would be generated by the match, +# but otherwise does not generate any code for the match. +macro match_count_nodes(value, body) + top_down_nodes, predeclared_temps, binder = build_deduplicated_automaton(__source__, __module__, value, body) + length(top_down_nodes) +end + +# Print the automaton (one line per node) to a given io channel +macro match_dump(io, value, body) + handle_match_dump(__source__, __module__, io, value, body) +end +# Print the automaton (one line per node) to stdout +macro match_dump(value, body) + handle_match_dump(__source__, __module__, stdout, value, body) +end +# Print the automaton (verbose) to a given io channel +macro match_dumpall(io, value, body) + handle_match_dump_verbose(__source__, __module__, io, value, body) +end +# Print the automaton (verbose) to stdout +macro match_dumpall(value, body) + handle_match_dump_verbose(__source__, __module__, stdout, value, body) +end + +function handle_match_dump(__source__, __module__, io, value, body) + top_down_nodes, predeclared_temps, binder = build_deduplicated_automaton(__source__, __module__, value, body) + esc(:($dumpall($io, $top_down_nodes, $binder, false))) +end + +function handle_match_dump_verbose(__source__, __module__, io, value, body) + entry, predeclared_temps, binder = build_automaton(__source__, __module__, value, body) + top_down_nodes = reachable_nodes(entry) + + esc(quote + # print the dump of the decision automaton before deduplication + $dumpall($io, $top_down_nodes, $binder, true) + # but return the count of deduplicated nodes. + $length($deduplicate_automaton($entry, $binder)) + end) +end + +# +# Implementation of `@match value begin ... end` +# +function handle_match_cases(location::LineNumberNode, mod::Module, value, body) + top_down_nodes, predeclared_temps, binder = build_deduplicated_automaton(location, mod, value, body) + result = generate_code(top_down_nodes, value, location, binder) + @assert is_expr(result, :block) + + # We use a `let` to ensure consistent closed scoping + result = Expr(:let, Expr(:block, predeclared_temps...), result) + esc(result) +end diff --git a/src/match_cases_simple.jl b/src/match_cases_simple.jl new file mode 100644 index 0000000..4b0857e --- /dev/null +++ b/src/match_cases_simple.jl @@ -0,0 +1,45 @@ +struct MatchCaseResult + location::LineNumberNode + matched_expression::Any + result_expression::Any +end + +function handle_match_cases_simple(location::LineNumberNode, mod::Module, value, match) + if is_case(match) + # previous version of @match supports `@match(expr, pattern => value)` + match = Expr(:block, location, match) + elseif !is_expr(match, :block) + error("$(location.file):$(location.line): Unrecognized @match block syntax: `$match`.") + end + + binder = BinderContext(mod) + input_variable::Symbol = binder.input_variable + cases = MatchCaseResult[] + predeclared_temps = Any[] + + for case in match.args + if case isa LineNumberNode + location = case + else + bound_case = bind_case(length(cases) + 1, location, case, predeclared_temps, binder) + matched = lower_pattern_to_boolean(bound_case.pattern, binder) + push!(cases, MatchCaseResult(location, matched, code(bound_case.result_expression))) + end + end + + # Fold the cases into a series of if-elseif-else statements + body = foldr(enumerate(cases); init = :($throw($MatchFailure($input_variable)))) do (i, case), tail + Expr(i == 1 ? :if : :elseif, case.matched_expression, case.result_expression, tail) + end + + declare_temps = Expr(:block, predeclared_temps...) + body = Expr(:block, + location, + binder.assertions..., + :($input_variable = $value), + body) + + # We use a `let` to ensure consistent closed scoping + body = Expr(:let, declare_temps, body) + esc(body) +end diff --git a/src/match_return.jl b/src/match_return.jl new file mode 100644 index 0000000..1a8d407 --- /dev/null +++ b/src/match_return.jl @@ -0,0 +1,117 @@ +""" + @match_fail + +This statement permits early-exit from the value of a @match case. +The programmer may write the value as a `begin ... end` and then, +within the value, the programmer may write + + @match_fail + +to cause the case to terminate as if its pattern had failed. +This permits cases to perform some computation before deciding if the +rule "*really*" matched. +""" +macro match_fail() + # These are rewritten during expansion of the `@match` macro, + # so the actual macro should not be used directly. + error("$(__source__.file):$(__source__.line): @match_fail may only be used within the value of a @match case.") +end + +""" + @match_return value + +This statement permits early-exit from the value of a @match case. +The programmer may write the value as a `begin ... end` and then, +within the value, the programmer may write + + @match_return value + +to terminate the value expression **early** with success, with the +given value. +""" +macro match_return(x) + # These are rewritten during expansion of the `@match` macro, + # so the actual macro should not be used. + error("$(__source__.file):$(__source__.line): @match_return may only be used within the value of a @match case.") +end + +# +# We implement @match_fail and @match_return as follows: +# +# Given a case (part of a @match) +# +# pattern => value +# +# in which the value part contains a use of one of these macros, we create +# two synthetic names: one for a `label`, and one for an intermediate `temp`. +# Then we rewrite `value` into `new_value` by replacing every occurrence of +# +# @match_return value +# +# with +# +# begin +# $temp = $value +# @goto $label +# end +# +# and every occurrence of +# +# @match_fail +# +# With +# +# @match_return $MatchFailure +# +# And then we replace the whole `pattern => value` with +# +# pattern where begin +# $temp = $value' +# @label $label +# $tmp !== $MatchFailure +# end => $temp +# +# Note that we are using the type `MatchFailure` as a sentinel value to indicate that the +# match failed. Therefore, don't use the @match_fail and @match_return macros for cases +# in which `MatchFailure` is a possible result. +# +function adjust_case_for_return_macro(__module__, location, pattern, result, predeclared_temps) + value = gensym("value") + label = gensym("label") + found_early_exit::Bool = false + function adjust_top(p) + is_expr(p, :macrocall) || return p + if length(p.args) == 3 && + (p.args[1] == :var"@match_return" || p.args[1] == Expr(:., Symbol(string(@__MODULE__)), QuoteNode(:var"@match_return"))) + # :(@match_return e) -> :($value = $e; @goto $label) + found_early_exit = true + return Expr(:block, p.args[2], :($value = $(p.args[3])), :(@goto $label)) + elseif length(p.args) == 2 && + (p.args[1] == :var"@match_fail" || p.args[1] == Expr(:., Symbol(string(@__MODULE__)), QuoteNode(:var"@match_fail"))) + # :(@match_fail) -> :($value = $MatchFaulure; @goto $label) + found_early_exit = true + return Expr(:block, p.args[2], :($value = $MatchFailure), :(@goto $label)) + elseif length(p.args) == 4 && + (p.args[1] == :var"@match" || p.args[1] == Expr(:., Symbol(string(@__MODULE__)), QuoteNode(:var"@match")) || + p.args[1] == :var"@match" || p.args[1] == Expr(:., Symbol(string(@__MODULE__)), QuoteNode(:var"@match"))) + # Nested uses of @match should be treated as independent + return macroexpand(__module__, p) + else + # It is possible for a macro to expand into @match_fail, so only expand one step. + return adjust_top(macroexpand(__module__, p; recursive = false)) + end + end + + rewritten_result = MacroTools.prewalk(adjust_top, result) + if found_early_exit + # Since we found an early exit, we need to predeclare the temp to ensure + # it is in scope both for where it is written and in the constructed where clause. + push!(predeclared_temps, value) + where_expr = Expr(:block, location, :($value = $rewritten_result), :(@label $label), :($value !== $MatchFailure)) + new_pattern = :($pattern where $where_expr) + new_result = value + (new_pattern, new_result) + else + (pattern, rewritten_result) + end +end diff --git a/src/matchmacro.jl b/src/matchmacro.jl index 796d884..b41efff 100644 --- a/src/matchmacro.jl +++ b/src/matchmacro.jl @@ -1,463 +1,130 @@ -### Match Expression Info - -const SymExpr = Union{Symbol,Expr,Bool} -const Assignment = Tuple{SymExpr,SymExpr} - -struct MatchExprInfo - tests::Vector{Expr} - guard_assignments::Vector{Assignment} - assignments::Vector{Assignment} - guards::Vector{Expr} - test_assign::Vector{Assignment} +# +# Implementation of `@match pattern = value` +# +function handle_match_eq(location::LineNumberNode, mod::Module, expr) + is_expr(expr, :(=), 2) || + error(string("Unrecognized @match syntax: ", expr)) + pattern = expr.args[1] + value = expr.args[2] + binder = BinderContext(mod) + input_variable::Symbol = binder.input_variable + (bound_pattern, assigned) = bind_pattern!( + location, pattern, input_variable, binder, ImmutableDict{Symbol, Symbol}()) + simplified_pattern = simplify(bound_pattern, Set{Symbol}(values(assigned)), binder) + matched = lower_pattern_to_boolean(simplified_pattern, binder) + q = Expr(:block, + location, + + # evaluate the assertions + binder.assertions..., + + # compute the input into a variable so we do not repeat its side-effects + :($input_variable = $value), + + # check that it matched the pattern; if not throw an exception + :($matched || $throw($MatchFailure($input_variable))), + + # assign to pattern variables in the enclosing scope + assignments(assigned)..., + + # finally, yield the input that was matched + input_variable + ) + esc(q) end -MatchExprInfo() = MatchExprInfo(Expr[], Assignment[], Assignment[], Expr[], Assignment[]) - -## unapply(val, expr, syms, guardsyms, valsyms, info) -## -## Generate code which matches val with expr, -## decomposing val or expr as needed. -## -## * Constant values are tested for equality. -## -## * Regex values are tested using ismatch() or -## match() when there are variables to extract -## -## * Variables assignments are created for symbols -## in expr which exist in syms -## -## * More complex expressions are handled specially -## (e.g., a || b allows matching against a or b) - -function unapply(val, sym::Symbol, syms, guardsyms, valsyms, info, array_checked::Bool=false) - -# # Symbol defined as a Regex (other Regex cases are handled below) -# if isdefined(current_module(),sym) && -# isa(eval(current_module(),sym), Regex) && -# !(sym in guardsyms) && !(sym in valsyms) -# push!(info.tests, :(Match.ismatch($sym, $val))) - -# Symbol in syms - if sym in syms - (sym in guardsyms) && push!(info.guard_assignments, (sym, val)) - (sym in valsyms) && push!(info.assignments, (sym, val)) - -# Constants - else - push!(info.tests, :(Match.ismatch($sym, $val))) - end - - info +# +# Implementation of `@ismatch value pattern` +# +function handle_ismatch(location::LineNumberNode, mod::Module, value, pattern) + binder = BinderContext(mod) + input_variable::Symbol = binder.input_variable + bound_pattern, assigned = bind_pattern!( + location, pattern, input_variable, binder, ImmutableDict{Symbol, Symbol}()) + simplified_pattern = simplify(bound_pattern, Set{Symbol}(values(assigned)), binder) + matched = lower_pattern_to_boolean(simplified_pattern, binder) + bindings = Expr(:block, assignments(assigned)..., true) + result = Expr(:block, + location, + + # evaluate the assertions + binder.assertions..., + + # compute the input into a variable so we do not repeat its side-effects + :($input_variable = $value), + + # check that it matched the pattern; if so assign pattern variables + :($matched && $bindings) + ) + esc(result) end -function unapply(val, expr::Expr, syms, guardsyms, valsyms, info, array_checked::Bool=false) - -# Match calls (or anything resembling a call) - if isexpr(expr, :call) - - # Match Type constructors - if length(syms) == 0 - push!(info.tests, :(Match.ismatch($expr, $val))) - info - else - # Assume this is a struct - typ = expr.args[1] - parms = expr.args[2:end] - - push!(info.tests, :(isa($val, $typ))) - # TODO: this verifies the that the number of fields is correct. - # We might want to force an error (e.g., by using an assert) instead. - push!(info.tests, :(length(fieldnames($typ)) == $(length(expr.args) - 1))) - - dotnums = Expr[:(getfield($val, $i)) for i in 1:length(expr.args) - 1] - - unapply(dotnums, parms, syms, guardsyms, valsyms, info, array_checked) - end - -# Tuple matching - elseif isexpr(expr, :tuple) - if isexpr(val, :tuple) - check_tuple_len(val, expr) - unapply(val.args, expr.args, syms, guardsyms, valsyms, info, array_checked) - else - push!(info.tests, :(isa($val, Tuple))) - push!(info.tests, check_tuple_len_expr(val, expr)) - unapply(val, expr.args, syms, guardsyms, valsyms, info, array_checked) - end - - info - - elseif isexpr(expr, :vcat) | isexpr(expr, :hcat) | isexpr(expr, :hvcat) | isexpr(expr, :cell1d) | isexpr(expr, :vect) - unapply_array(val, expr, syms, guardsyms, valsyms, info) - - elseif isexpr(expr, :row) - # pretend it's :hcat - unapply_array(val, Expr(:hcat, expr.args...), syms, guardsyms, valsyms, info) - -# Match a || b (i.e., match either expression) - elseif isexpr(expr, :(||)) - info1 = unapply(val, expr.args[1], syms, guardsyms, valsyms, MatchExprInfo(), array_checked) - info2 = unapply(val, expr.args[2], syms, guardsyms, valsyms, MatchExprInfo(), array_checked) - - ### info.test_assign - - # these are used to determine the assignment if the same variable is matched in both a and b - # they are set to false by default - g1 = gensym("test1") - g2 = gensym("test2") - - append!(info.test_assign, info1.test_assign) - append!(info.test_assign, info2.test_assign) - - if length(info1.assignments) > 0; push!(info.test_assign, (g1, false)); end - if length(info2.assignments) > 0; push!(info.test_assign, (g2, false)); end - - ### info.tests - - # assign g1, g2 during the test, if needed - expr1 = joinexprs(unique(info1.tests), :&&, :true) - expr2 = joinexprs(unique(info2.tests), :&&, :true) - if length(info1.assignments) > 0; expr1 = :($g1 = $expr1); end - if length(info2.assignments) > 0; expr2 = :($g2 = $expr2); end - - push!(info.tests, Expr(:(||), expr1, expr2)) - - ### info.assignments - - # fix up let assignments to determine which variables to match - vars1 = Dict(getvar(x) => (x, y) for (x, y) in info1.assignments) - vars2 = Dict(getvar(x) => (x, y) for (x, y) in info2.assignments) - - sharedvars = intersect(keys(vars1), keys(vars2)) - - for var in sharedvars - (expr1, val1) = vars1[var] - (expr2, val2) = vars2[var] - - # choose most specific variable typing - if expr1 == expr2 || !isexpr(expr2, :(::)) - condition_expr = expr1 - elseif !isexpr(expr1, :(::)) - condition_expr = expr2 - else - # here, both vars are typed, but with different types - # let the parser figure out the best typing - condition_expr = var - end - - push!(info.assignments, (condition_expr, :($g1 ? $val1 : $val2))) - end - - for (assignment_expr, assignment_val) in info1.assignments - vs = getvar(assignment_expr) - if !(vs in sharedvars) - # here and below, we assign to nothing - # so the type info is removed - # TODO: move it to $assignment_val??? - push!(info.assignments, (assignment_expr, :($g1 ? $assignment_val : nothing))) - end - end - - for (assignment_expr, assignment_val) in info2.assignments - vs = getvar(assignment_expr) - if !(vs in sharedvars) - push!(info.assignments, (assignment_expr, :($g2 ? $assignment_val : nothing))) - end - end - - ### info.guards - - # TODO: disallow guards from info1, info2? - append!(info.guards, info1.guards) - append!(info.guards, info2.guards) - - info - -# Match x::Type - elseif isexpr(expr, :(::)) && isa(expr.args[1], Symbol) - typ = expr.args[2] - sym = expr.args[1] - - push!(info.tests, :(isa($val, $typ))) - if sym in syms - sym in guardsyms && push!(info.guard_assignments, (expr, val)) - sym in valsyms && push!(info.assignments, (expr, val)) - end - - info - -# Regex strings (r"[a-z]*") - elseif isexpr(expr, :macrocall) && expr.args[1] == symbol("@r_str") - append!(info.tests, [:(isa($val, String)), :(Match.ismatch($expr, $val))]) - info - -# Other expressions: evaluate the expression and test for equality... - else - # TODO: test me! - push!(info.tests, :(Match.ismatch($expr, $val))) - info - end +# +# """ +# Usage: +# +# ``` +# @match pattern = value +# ``` +# +# If `value` matches `pattern`, bind variables and return `value`. +# Otherwise, throw `MatchFailure`. +# """ +# +macro match(expr) + handle_match_eq(__source__, __module__, expr) end -# Match symbols or complex type fields (e.g., foo.bar) representing a tuples - -function unapply(val::SymExpr, exprs::AbstractArray, syms, guardsyms, valsyms, - info, array_checked::Bool=false) - # if isa(val, Expr) && !isexpr(val, :(.)) - # error("unapply: Array expressions must be assigned to symbols or fields of a complex type (e.g., bar.foo)") - # end - - seen_dots = false - for i = 1:length(exprs) - if isexpr(exprs[i], :(...)) - if seen_dots #i < length(exprs) || ndims(exprs) > 1 - error("elipses (...) are only allowed once in an Array/Tuple pattern match.") #in the last position of - end - seen_dots = true - sym = array_type_of(exprs[i].args[1]) - unapply(:($val[$i:(end - $(length(exprs) - i))]), sym, syms, guardsyms, valsyms, info, array_checked) - elseif seen_dots - unapply(:($val[end - $(length(exprs) - i)]), exprs[i], syms, guardsyms, valsyms, info, array_checked) - else - unapply(:($val[$i]), exprs[i], syms, guardsyms, valsyms, info, array_checked) - end +""" +Usage: +``` + @__match__ value begin + pattern1 => result1 + pattern2 => result2 + ... end - - info +``` + +Return `result` for the first matching `pattern`. +If there are no matches, throw `MatchFailure`. +This uses a brute-force code gen strategy, essentially a series of if-else statements. +It is used for testing purposes, as a reference for correct semantics. +Because it is so simple, we have confidence about its correctness. +""" +macro __match__(value, cases) + handle_match_cases_simple(__source__, __module__, value, cases) end - -# Match arrays against arrays - -function unapply(vs::AbstractArray, es::AbstractArray, syms, guardsyms, valsyms, - info, array_checked::Bool=false) - if isexpr(es[1], :(...)) - sym = array_type_of(es[1].args[1]) - unapply(vs[1:end - (length(es) - 1)], sym, syms, guardsyms, valsyms, info, array_checked) - - elseif length(es) == length(vs) == 1 - unapply(vs[1], es[1], syms, guardsyms, valsyms, info, array_checked) - - elseif length(es) == length(vs) == 0 - info - - else - unapply(vs[1], es[1], syms, guardsyms, valsyms, info, array_checked) - unapply(view(vs, 2:length(vs)), view(es, 2:length(es)), syms, guardsyms, valsyms, info, array_checked) - end -end - -unapply(vals::Tuple, exprs::Tuple, syms, guardsyms, valsyms, - info, array_checked::Bool=false) = - unapply([vals...], [exprs...], syms, guardsyms, valsyms, info, array_checked) - -# fallback -function unapply(val, expr, _1, _2, _3, - info, array_checked::Bool=false) - push!(info.tests, :(Match.ismatch($expr, $val))) - info -end - - -# Match symbols or complex type fields (e.g., foo.bar) representing arrays - -function unapply_array(val, expr::Expr, syms, guardsyms, valsyms, info, array_checked::Bool=false) - - if isexpr(expr, :vcat) || isexpr(expr, :cell1d) || isexpr(expr, :vect) - dim = 1 - elseif isexpr(expr, :hcat) # || isexpr(expr, :hvcat) - # TODO: check hvcat... - dim = 2 - else - error("unapply_array() called on a non-array expression") - end - - sdim = :($dim + max(ndims($val) - 2, 0)) - - if !array_checked #!(isexpr(val, :call) && val.args[1] == :(Match.subslicedim)) - # if we recursively called this with subslicedim (below), - # don't do these checks - # TODO: if there are nested arrays in the match, these checks - # should actually be done! - # TODO: need to make this test more robust if we're only doing it once... - - #push!(info.tests, :(isa($val, AbstractArray))) - #push!(info.tests, check_dim_size_expr(val, sdim, expr)) - push!(info.tests, check_dim_size_expr(val, dim, expr)) - array_checked = true - end - - exprs = expr.args - seen_dots = false - if (isempty(getvars(exprs))) - # this array is all constant, so just see if it matches - push!(info.tests, :(all($val .== $expr))) - else - for i = 1:length(exprs) - if isexpr(exprs[i], :(...)) - if seen_dots # i < length(exprs) || ndims(exprs) > 1 - error("elipses (...) are only allowed once in an an Array pattern match.") #in the last position of - end - seen_dots = true - sym = array_type_of(exprs[i].args[1]) - j = length(exprs) - i - s = :(Match.slicedim($val, $dim, $i, $j)) - unapply(s, sym, syms, guardsyms, valsyms, info, array_checked) - elseif seen_dots - j = length(exprs) - i - s = :(Match.slicedim($val, $dim, $j, true)) - unapply(s, exprs[i], syms, guardsyms, valsyms, info, array_checked) - else - s = :(Match.slicedim($val, $dim, $i)) - unapply(s, exprs[i], syms, guardsyms, valsyms, info, array_checked) - end - end - - end - - info -end - -function ispair(m) - return isexpr(m, :call) && (m.args[1] == :(=>)) -end - -function rewrite_pair(m) - # The parsing of - # "expr, if a == b end => target" - # changed from - # (expr, if a == b end) => target # v0.6 - # to - # (expr, if a == b end => target) # v0.7 - # - # For now, we rewrite the expression to match v0.6 - # (In the future, we'll switch to using "where") - if isexpr(m, :tuple) && length(m.args) == 2 && ispair(m.args[2]) - target = m.args[2].args[3] - newtuple = Expr(:tuple, m.args[1], m.args[2].args[2]) - return Expr(:call, :(=>), newtuple, target) - end - return m -end - -function is_guarded_pair(m) - return ispair(m) && length(m.args) == 3 && isexpr(m.args[2], :tuple) && isexpr(m.args[2].args[2], :if) +# +# """ +# Usage: +# ``` +# @match value begin +# pattern1 => result1 +# pattern2 => result2 +# ... +# end +# ``` + +# Return `result` for the first matching `pattern`. +# If there are no matches, throw `MatchFailure`. +# """ +# +macro match(value, cases) + handle_match_cases(__source__, __module__, value, cases) end -function gen_match_expr(v, e, code, use_let::Bool=true) - e = rewrite_pair(e) - if ispair(e) - info = MatchExprInfo() - - (pattern, value) = e.args[2:3] - - # Extract guards - if is_guarded_pair(e) - guard = pattern.args[2].args[1] - pattern = pattern.args[1] - push!(info.guards, guard) - guardsyms = getvars(guard) - else - guardsyms = Symbol[] - end - - syms = getvars(pattern) - valsyms = getvars(value) - - info = unapply(v, pattern, syms, guardsyms, valsyms, info) - - # Create let statement for guards, and add it to tests - if length(info.guards) > 0 - guard_expr = joinexprs(info.guards, :&&) - - guard_assignment_exprs = Expr[:($expr = $val) for (expr, val) in info.guard_assignments] - - guard_tests = let_expr(guard_expr, guard_assignment_exprs) - - push!(info.tests, guard_tests) - end - - # filter and escape regular let assignments - # assignments = filter(assn->(sym = assn[1]; - # isa(sym, Symbol) && sym in syms || - # isexpr(sym, :(::)) && sym.args[1] in syms), - # info.assignments) - assignments = info.assignments - - if use_let - # Wrap value statement in let - let_assignments = Expr[:($expr = $val) for (expr, val) in assignments] - expr = let_expr(value, let_assignments) - else - esc_assignments = Expr[Expr(:(=), getvar(expr), val) for (expr, val) in assignments] - expr = Expr(:block, esc_assignments..., value) - end - - # Wrap expr in test expressions - if length(info.tests) == 0 - # no tests, exactly one match - expr - else - tests = joinexprs(info.tests, :&&) - - # Returned Expression - if expr == :true && code == :false - expr = tests - else - expr = :(if $tests - $expr - else - $code - end) - end - - test_assign = [:($expr = $val) for (expr, val) in info.test_assign] - - let_expr(expr, test_assign) - end - elseif isexpr(e, :line) || isa(e, LineNumberNode) - Expr(:block, e, code) - #code - elseif isa(e, Bool) - e - else - error("@match patterns must consist of :(=>) blocks") - end -end - -# The match macro -macro match(v, m) - code = :nothing - - if isexpr(m, :block) - for e in reverse(m.args) - code = gen_match_expr(v, e, code) - end - elseif ispair(m) - code = gen_match_expr(v, m, code) - else - code = :(error("Pattern does not match")) - vars = setdiff(getvars(m), [:_]) |> syms -> filter(x -> !startswith(string(x), "@"), syms) - if length(vars) == 0 - code = gen_match_expr(v, Expr(:call, :(=>), m, :true), code, false) - elseif length(vars) == 1 - code = gen_match_expr(v, Expr(:call, :(=>), m, vars[1]), code, false) - else - code = gen_match_expr(v, Expr(:call, :(=>), m, Expr(:tuple, vars...)), code, false) - end - end - - esc(code) -end - -# Function producing/showing the generated code -fmatch(v, m) = macroexpand(:(@match $v $m)) - -# The ismatch macro -macro ismatch(val, m) - code = gen_match_expr(val, Expr(:call, :(=>), m, :true), :false) - esc(code) +# +# """ +# Usage: +# ``` +# @ismatch value pattern +# ``` +# +# Return `true` if `value` matches `pattern`, `false` otherwise. When returning `true`, +# binds the pattern variables in the enclosing scope. +# """ +# +macro ismatch(value, pattern) + handle_ismatch(__source__, __module__, value, pattern) end - - -fismatch(val, m) = macroexpand(:(@ismatch $val $m)) diff --git a/src/matchutils.jl b/src/matchutils.jl deleted file mode 100644 index 0311365..0000000 --- a/src/matchutils.jl +++ /dev/null @@ -1,176 +0,0 @@ -### Utilities used by @match macro -# author: Kevin Squire (@kmsquire) - -# -# ismatch -# - -ismatch(r::AbstractRange, s) = s in r -ismatch(c::Char, s::Number) = false -ismatch(s::Number, c::Char) = false -ismatch(r::Regex, s::AbstractString) = occursin(r, s) -ismatch(r, s) = (r == s) - -# -# slicedim -# -# "view" version of slicedim - -function _slicedim(A::AbstractArray, d::Integer, i::Integer) - if (d < 1) | (d > ndims(A)) - throw(BoundsError()) - end - sz = size(A) - # Force 1x..x1 slices to extract the value - # Note that this is no longer a reference. - otherdims = [sz...] - splice!(otherdims, d) - if all(otherdims .== 1) - A[[ n == d ? i : 1 for n in 1:ndims(A) ]...] - else - view(A, [ n == d ? i : (1:sz[n]) for n in 1:ndims(A) ]...) - end -end - -function _slicedim(A::AbstractArray, d::Integer, i) - if (d < 1) | (d > ndims(A)) - throw(BoundsError()) - end - sz = size(A) - view(A, [ n == d ? i : (1:sz[n]) for n in 1:ndims(A) ]...) -end - -_slicedim(A::AbstractVector, d::Integer, i::Integer) = - (if (d < 0) | (d > 1); throw(BoundsError()) end; A[i]) - -_slicedim(A::AbstractVector, d::Integer, i) = - (if (d < 0) | (d > 1); throw(BoundsError()) end; view(A, i)) - -function slicedim(A::AbstractArray, s::Integer, i::Integer, from_end::Bool=false) - d = s + max(ndims(A) - 2, 0) - from_end && (i = size(A, d) - i) - _slicedim(A, d, i) -end - -function slicedim(A::AbstractArray, s::Integer, i::Integer, j::Integer) - d = s + max(ndims(A) - 2, 0) - j = size(A, d) - j # this is the distance from the end of the dim size - _slicedim(A, d, i:j) -end - -# -# getvars -# -# get all symbols in an expression - -getvars(e) = Symbol[] -getvars(e::Symbol) = startswith(string(e), '@') ? Symbol[] : Symbol[e] - -function getvars(e::Expr) - if isexpr(e, :call) - getvars(e.args[2:end]) - else - getvars(e.args) - end -end -getvars(es::AbstractArray) = unique!(vcat([getvars(e) for e in es]...)) - -# -# getvar -# -# get the symbol from a :(::) expression - -getvar(x::Expr) = isexpr(x, :(::)) ? x.args[1] : x -getvar(x::Symbol) = x - -# -# check_dim_size_expr -# -# generate an expression to check the size of a variable dimension against an array of expressions - -function check_dim_size_expr(val, dim, ex::Expr) - if length(ex.args) == 0 || !any([isexpr(e, :(...)) for e in ex.args]) - :(Match.checkdims($val, $dim, $(length(ex.args)))) - else - :(Match.checkdims2($val, $dim, $(length(ex.args)))) - end -end - -function checkdims(val::AbstractArray, dim, dimsize) - dim = dim + max(ndims(val) - 2, 0) - dim <= ndims(val) && size(val, dim) == dimsize -end - -checkdims(val, dim, dimsize) = false - -function checkdims2(val::AbstractArray, dim, dimsize) - dim = dim + max(ndims(val) - 2, 0) - dim <= ndims(val) && size(val, dim) >= dimsize - 1 -end - -checkdims2(val, dim, dimsize) = false - - -# -# check_tuple_len_expr -# -# generate an expression to check the length of a tuple variable against a tuple expression - -function check_tuple_len_expr(val, ex::Expr) - if length(ex.args) == 0 || !any([isexpr(e, :(...)) for e in ex.args]) - :(length($val) == $(length(ex.args))) - else - :(length($val) >= $(length(ex.args) - 1)) - end -end - -function check_tuple_len(val::Expr, ex::Expr) - if !isexpr(val, :tuple) || !isexpr(ex, :tuple) - false - elseif length(ex.args) == 0 || !any([isexpr(e, :(...)) for e in ex.args]) - length(val.args) == length(ex.args) - else - length(val.args) >= length(ex.args) - 1 - end -end - - -# -# joinexprs -# -# join an array of (e.g., true/false) expressions with an operator - -function joinexprs(exprs::AbstractArray, oper::Symbol, default=:nothing) - len = length(exprs) - - len == 0 ? default : - len == 1 ? exprs[1] : - Expr(oper, joinexprs(view(exprs, 1:(len - 1)), oper, default), exprs[end]) -end - - -# -# let_expr -# -# generate an optional let expression - -function let_expr(expr, assignments::AbstractArray) - length(assignments) == 0 && return expr - assignment_block = Expr(:block, assignments...) - return Expr(:let, assignment_block, expr) -end - -# -# array_type_of -# -# modify x::Type => x::AbstractArray{Type} - -function array_type_of(ex::Expr) - if isexpr(ex, :(::)) - :($(ex.args[1])::AbstractArray{$(ex.args[2])}) - else - ex - end -end - -array_type_of(sym::Symbol) = :($sym::AbstractArray) diff --git a/src/pretty.jl b/src/pretty.jl new file mode 100644 index 0000000..50a488c --- /dev/null +++ b/src/pretty.jl @@ -0,0 +1,190 @@ +# +# Pretty-printing utilities to simplify debugging of this package +# + +pretty(io::IO, x::Any) = print(io, x) +function pretty(x::Any) + io = IOBuffer() + pretty(io, x) + String(take!(io)) +end + +function dumpall(io::IO, all::Vector{T}, binder::BinderContext, long::Bool) where { T <: AbstractAutomatonNode } + # Make a map from each node to its index + id = IdDict{T, Int}(map(((i,s),) -> s => i, enumerate(all))...) + print(io, "Decision Automaton: ($(length(all)) nodes) input ") + pretty(io, binder.input_variable) + println(io) + for node in all + pretty(io, node, binder, id, long) + println(io) + end + println(io, "end # of automaton") + length(all) +end + +# Pretty-print either an AutomatonNode or a DeduplicatedAutomatonNode +function pretty( + io::IO, + node::T, + binder::BinderContext, + id::IdDict{T, Int}, + long::Bool = true) where { T <: AbstractAutomatonNode } + print(io, name(node, id)) + if long && hasfield(T, :cases) + println(io) + for case in node.cases + print(io, " ") + pretty(io, case, binder) + end + end + action = node.action + long && print(io, " ") + if action isa BoundCase + print(io, " MATCH ", action.case_number, " with value ") + pretty(io, action.result_expression) + elseif action isa BoundPattern + if action isa BoundTestPattern + print(io, " TEST ") + elseif action isa BoundFetchPattern + print(io, " FETCH ") + end + pretty(io, action, binder) + elseif action isa Expr + print(io, " FAIL ") + pretty(io, action) + end + next = node.next + if next isa Tuple{T} + fall_through = id[next[1]] == id[node] + 1 + if long || !fall_through + long && print(io, "\n ") + print(io, " NEXT: $(name(next[1], id))") + if id[next[1]] == id[node] + 1 + print(io, " (fall through)") + end + end + elseif next isa Tuple{T, T} + fall_through = id[next[1]] == id[node] + 1 + if long || !fall_through + long && print(io, "\n ") + print(io, " THEN: $(name(next[1], id))") + if id[next[1]] == id[node] + 1 + print(io, " (fall through)") + end + long && println(io) + end + long && print(io, " ") + print(io, " ELSE: $(name(next[2], id))") + end +end + +pretty(io::IO, p::BoundPattern, binder::BinderContext) = pretty(io, p) +function pretty(io::IO, p::BoundFetchPattern) + error("pretty-printing a BoundFetchPattern requires a BinderContext") +end +function pretty(io::IO, p::Union{BoundOrPattern, BoundAndPattern}, binder::BinderContext) + op = (p isa BoundOrPattern) ? "||" : "&&" + print(io, "(") + first = true + for sp in p.subpatterns + first || print(io, " ", op, " ") + first = false + pretty(io, sp, binder) + end + print(io, ")") +end +function pretty(io::IO, p::BoundFetchPattern, binder::BinderContext) + temp = get_temp(binder, p) + pretty(io, temp) + print(io, " := ") + pretty(io, p) +end +function pretty(io::IO, s::Symbol) + print(io, pretty_name(s)) +end +function pretty_name(s::Symbol) + s = string(s) + if startswith(s, "##") + string("«", simple_name(s), "»") + else + s + end +end +struct FrenchName; s::Symbol; end +Base.show(io::IO, x::FrenchName) = print(io, pretty_name(x.s)) +function pretty(io::IO, expr::Expr) + b = MacroTools.prewalk(MacroTools.rmlines, expr) + c = MacroTools.prewalk(MacroTools.unblock, b) + print(io, MacroTools.postwalk(c) do var + (var isa Symbol) ? Symbol(FrenchName(var)) : var + end) +end + +function pretty(io::IO, case::BoundCase, binder::BinderContext) + print(io, case.case_number, ": ") + pretty(io, case.pattern, binder) + print(io, " => ") + pretty(io, case.result_expression) + println(io) +end + +pretty(io::IO, ::BoundTruePattern) = print(io, "true") +pretty(io::IO, ::BoundFalsePattern) = print(io, "false") +function pretty(io::IO, e::BoundExpression) + if !isempty(e.assignments) + pretty(io, e.assignments) + print(io, " ") + end + pretty(io, e.source) +end +function pretty(io::IO, assignments::ImmutableDict{Symbol, Symbol}) + print(io, "[") + for (i, (k, v)) in enumerate(assignments) + i > 1 && print(io, ", ") + pretty(io, k) + print(io, " => ") + pretty(io, v) + end + print(io, "]") +end +function pretty(io::IO, p::BoundIsMatchTestPattern) + print(io, p.force_equality ? "isequal(" : "@ismatch(") + pretty(io, p.input) + print(io, ", ") + pretty(io, p.bound_expression) + print(io, ")") +end +function pretty(io::IO, p::BoundRelationalTestPattern) + pretty(io, p.input) + print(io, " ", p.relation, " ") + pretty(io, p.value) +end +function pretty(io::IO, p::BoundWhereTestPattern) + p.inverted && print(io, "!") + pretty(io, p.input) +end +function pretty(io::IO, p::BoundTypeTestPattern) + pretty(io, p.input) + print(io, " isa ", p.type) +end +function pretty(io::IO, p::BoundFetchFieldPattern) + pretty(io, p.input) + print(io, ".", p.field_name) +end +function pretty(io::IO, p::BoundFetchIndexPattern) + pretty(io, p.input) + print(io, "[", p.index, "]") +end +function pretty(io::IO, p::BoundFetchRangePattern) + pretty(io, p.input) + print(io, "[", p.first_index, ":(length(", pretty_name(p.input), ")-", p.from_end, ")]") +end +function pretty(io::IO, p::BoundFetchLengthPattern) + print(io, "length(") + pretty(io, p.input) + print(io, ")") +end +function pretty(io::IO, p::BoundFetchExpressionPattern) + pretty(io, p.bound_expression) +end diff --git a/src/topological.jl b/src/topological.jl new file mode 100644 index 0000000..5638dfc --- /dev/null +++ b/src/topological.jl @@ -0,0 +1,43 @@ +# Compute a topological ordering of a set of nodes reachable from the given +# roots by the given successor function. +function topological_sort(successors::Function, roots::AbstractVector{N}) where { N } + # Compute pred_counts, the number of predecessors of each node + pred_counts = OrderedDict{N, Int}() + counted = Set{N}() + to_count = Vector{N}(roots) + while !isempty(to_count) + node = pop!(to_count) + node in counted && continue + push!(counted, node) + get!(pred_counts, node, 0) + for succ::N in reverse(successors(node)) + push!(to_count, succ) + pred_counts[succ] = get(pred_counts, succ, 0) + 1 + end + end + + # Prepare a ready set of nodes to output that have no predecessors + ready = N[k for (k, v) in pred_counts if v == 0] + result = N[] + sizehint!(result, length(pred_counts)) + while !isempty(ready) + node::N = pop!(ready) + push!(result, node) + + # remove the node by decrementing the predecessor counts of its successors + for succ::N in reverse(successors(node)) + count = pred_counts[succ] + @assert count > 0 + count -= 1 + pred_counts[succ] = count + count == 0 && push!(ready, succ) + end + end + + # all of the nodes should have been output by now. Otherwise there was a cycle. + if length(pred_counts) != length(result) + error("graph had a cycle involving ", N[k for (k, v) in pred_counts if v != 0], ".") + end + + result +end diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 0000000..01c8096 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,5 @@ +[deps] +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +ReTest = "e0db7c4e-2690-44b9-bad6-7687da720f89" +Match = "7eb4fadd-790c-5f42-8a69-bfa0b872bfbf" +MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" diff --git a/test/coverage.jl b/test/coverage.jl new file mode 100644 index 0000000..428bab2 --- /dev/null +++ b/test/coverage.jl @@ -0,0 +1,553 @@ +# +# Additional tests to improve node coverage. +# These tests mainly exercise diagnostic code that isn't crucial to the +# normal operation of the package. +# + +abstract type A; end +struct B <: A; end +struct C <: A; end +struct D; x; end + +abstract type Abstract1; end +abstract type Abstract2; end + +struct TrashFetchPattern <: Match.BoundFetchPattern + location::LineNumberNode +end +struct TrashPattern <: Match.BoundPattern + location::LineNumberNode +end +Match.pretty(io::IO, ::TrashPattern) = print(io, "TrashPattern") +const T = 12 + +struct MyPair + x + y +end +Match.match_fieldnames(::Type{MyPair}) = error("May not @match MyPair") + +macro match_case(pattern, value) + return esc(:($pattern => $value)) +end + +expected=""" +Decision Automaton: (57 nodes) input «input_value» +Node 1 + 1: @ismatch(«input_value», 1) => 1 + 2: («input_value» isa Main.Rematch2Tests.Foo && «input_value.x» := «input_value».x && «input_value.y» := «input_value».y && isequal(«input_value.y», [x => «input_value.x»] x)) => 2 + 3: «input_value» isa Main.Rematch2Tests.D => 3 + 4: («input_value» isa AbstractArray && «length(input_value)» := length(«input_value») && «length(input_value)» >= 2 && «input_value[2:(length-1)]» := «input_value»[2:(length(«input_value»)-1)]) => [y => «input_value[2:(length-1)]»] y + 5: («input_value» isa Tuple && «length(input_value)» := length(«input_value») && «length(input_value)» >= 2 && «input_value[-1]» := «input_value»[-1] && «where_0» := e.q1 && «where_0») => [z => «input_value[-1]»] z + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + 7: («input_value» isa Main.Rematch2Tests.A && «where_1» := e.q2 && «where_1») => 7 + 8: («input_value» isa Main.Rematch2Tests.B && «where_2» := e.q3 && «where_2») => 8 + 9: («input_value» isa Main.Rematch2Tests.C && «where_3» := e.q4 && «where_3») => 9 + 10: («input_value» isa Main.Rematch2Tests.Foo && «input_value.x» := «input_value».x && «input_value.y» := «input_value».y && @ismatch(«input_value.y», 2) && «where_4» := [x => «input_value.x»] f1(x) && «where_4») => 1 + 11: («input_value» isa Main.Rematch2Tests.Foo && «input_value.x» := «input_value».x && @ismatch(«input_value.x», 1) && «input_value.y» := «input_value».y && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 2 + 12: («input_value» isa Main.Rematch2Tests.Foo && «input_value.x» := «input_value».x && «input_value.y» := «input_value».y && «where_4» := [x => «input_value.x»] f1(x) && «where_4» && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + TEST @ismatch(«input_value», 1) + THEN: Node 2 (fall through) + ELSE: Node 3 +Node 2 + 1: true => 1 + MATCH 1 with value 1 +Node 3 + 2: («input_value» isa Main.Rematch2Tests.Foo && «input_value.x» := «input_value».x && «input_value.y» := «input_value».y && isequal(«input_value.y», [x => «input_value.x»] x)) => 2 + 3: «input_value» isa Main.Rematch2Tests.D => 3 + 4: («input_value» isa AbstractArray && «length(input_value)» := length(«input_value») && «length(input_value)» >= 2 && «input_value[2:(length-1)]» := «input_value»[2:(length(«input_value»)-1)]) => [y => «input_value[2:(length-1)]»] y + 5: («input_value» isa Tuple && «length(input_value)» := length(«input_value») && «length(input_value)» >= 2 && «input_value[-1]» := «input_value»[-1] && «where_0» := e.q1 && «where_0») => [z => «input_value[-1]»] z + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + 7: («input_value» isa Main.Rematch2Tests.A && «where_1» := e.q2 && «where_1») => 7 + 8: («input_value» isa Main.Rematch2Tests.B && «where_2» := e.q3 && «where_2») => 8 + 9: («input_value» isa Main.Rematch2Tests.C && «where_3» := e.q4 && «where_3») => 9 + 10: («input_value» isa Main.Rematch2Tests.Foo && «input_value.x» := «input_value».x && «input_value.y» := «input_value».y && @ismatch(«input_value.y», 2) && «where_4» := [x => «input_value.x»] f1(x) && «where_4») => 1 + 11: («input_value» isa Main.Rematch2Tests.Foo && «input_value.x» := «input_value».x && @ismatch(«input_value.x», 1) && «input_value.y» := «input_value».y && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 2 + 12: («input_value» isa Main.Rematch2Tests.Foo && «input_value.x» := «input_value».x && «input_value.y» := «input_value».y && «where_4» := [x => «input_value.x»] f1(x) && «where_4» && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + TEST «input_value» isa Main.Rematch2Tests.Foo + THEN: Node 4 (fall through) + ELSE: Node 26 +Node 4 + 2: («input_value.x» := «input_value».x && «input_value.y» := «input_value».y && isequal(«input_value.y», [x => «input_value.x»] x)) => 2 + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + 10: («input_value.x» := «input_value».x && «input_value.y» := «input_value».y && @ismatch(«input_value.y», 2) && «where_4» := [x => «input_value.x»] f1(x) && «where_4») => 1 + 11: («input_value.x» := «input_value».x && @ismatch(«input_value.x», 1) && «input_value.y» := «input_value».y && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 2 + 12: («input_value.x» := «input_value».x && «input_value.y» := «input_value».y && «where_4» := [x => «input_value.x»] f1(x) && «where_4» && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + FETCH «input_value.x» := «input_value».x + NEXT: Node 5 (fall through) +Node 5 + 2: («input_value.y» := «input_value».y && isequal(«input_value.y», [x => «input_value.x»] x)) => 2 + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + 10: («input_value.y» := «input_value».y && @ismatch(«input_value.y», 2) && «where_4» := [x => «input_value.x»] f1(x) && «where_4») => 1 + 11: (@ismatch(«input_value.x», 1) && «input_value.y» := «input_value».y && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 2 + 12: («input_value.y» := «input_value».y && «where_4» := [x => «input_value.x»] f1(x) && «where_4» && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + FETCH «input_value.y» := «input_value».y + NEXT: Node 6 (fall through) +Node 6 + 2: isequal(«input_value.y», [x => «input_value.x»] x) => 2 + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + 10: (@ismatch(«input_value.y», 2) && «where_4» := [x => «input_value.x»] f1(x) && «where_4») => 1 + 11: (@ismatch(«input_value.x», 1) && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 2 + 12: («where_4» := [x => «input_value.x»] f1(x) && «where_4» && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + TEST isequal(«input_value.y», [x => «input_value.x»] x) + THEN: Node 7 (fall through) + ELSE: Node 8 +Node 7 + 2: true => 2 + MATCH 2 with value 2 +Node 8 + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + 10: (@ismatch(«input_value.y», 2) && «where_4» := [x => «input_value.x»] f1(x) && «where_4») => 1 + 11: (@ismatch(«input_value.x», 1) && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 2 + 12: («where_4» := [x => «input_value.x»] f1(x) && «where_4» && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + TEST @ismatch(«input_value», 6) + THEN: Node 44 + ELSE: Node 9 +Node 9 + 6: @ismatch(«input_value», 7) => 6 + 10: (@ismatch(«input_value.y», 2) && «where_4» := [x => «input_value.x»] f1(x) && «where_4») => 1 + 11: (@ismatch(«input_value.x», 1) && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 2 + 12: («where_4» := [x => «input_value.x»] f1(x) && «where_4» && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + TEST @ismatch(«input_value», 7) + THEN: Node 44 + ELSE: Node 10 +Node 10 + 10: (@ismatch(«input_value.y», 2) && «where_4» := [x => «input_value.x»] f1(x) && «where_4») => 1 + 11: (@ismatch(«input_value.x», 1) && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 2 + 12: («where_4» := [x => «input_value.x»] f1(x) && «where_4» && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + TEST @ismatch(«input_value.y», 2) + THEN: Node 11 (fall through) + ELSE: Node 17 +Node 11 + 10: («where_4» := [x => «input_value.x»] f1(x) && «where_4») => 1 + 11: (@ismatch(«input_value.x», 1) && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 2 + 12: («where_4» := [x => «input_value.x»] f1(x) && «where_4» && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + FETCH «where_4» := [x => «input_value.x»] f1(x) + NEXT: Node 12 (fall through) +Node 12 + 10: «where_4» => 1 + 11: (@ismatch(«input_value.x», 1) && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 2 + 12: («where_4» && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + TEST «where_4» + THEN: Node 13 (fall through) + ELSE: Node 14 +Node 13 + 10: true => 1 + MATCH 10 with value 1 +Node 14 + 11: (@ismatch(«input_value.x», 1) && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 2 + TEST @ismatch(«input_value.x», 1) + THEN: Node 15 (fall through) + ELSE: Node 57 +Node 15 + 11: («where_5» := [y => «input_value.y»] f2(y) && «where_5») => 2 + FETCH «where_5» := [y => «input_value.y»] f2(y) + NEXT: Node 16 (fall through) +Node 16 + 11: «where_5» => 2 + TEST «where_5» + THEN: Node 20 + ELSE: Node 57 +Node 17 + 11: (@ismatch(«input_value.x», 1) && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 2 + 12: («where_4» := [x => «input_value.x»] f1(x) && «where_4» && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + TEST @ismatch(«input_value.x», 1) + THEN: Node 18 (fall through) + ELSE: Node 21 +Node 18 + 11: («where_5» := [y => «input_value.y»] f2(y) && «where_5») => 2 + 12: («where_4» := [x => «input_value.x»] f1(x) && «where_4» && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + FETCH «where_5» := [y => «input_value.y»] f2(y) + NEXT: Node 19 (fall through) +Node 19 + 11: «where_5» => 2 + 12: («where_4» := [x => «input_value.x»] f1(x) && «where_4» && «where_5») => 3 + TEST «where_5» + THEN: Node 20 (fall through) + ELSE: Node 57 +Node 20 + 11: true => 2 + MATCH 11 with value 2 +Node 21 + 12: («where_4» := [x => «input_value.x»] f1(x) && «where_4» && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + FETCH «where_4» := [x => «input_value.x»] f1(x) + NEXT: Node 22 (fall through) +Node 22 + 12: («where_4» && «where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + TEST «where_4» + THEN: Node 23 (fall through) + ELSE: Node 57 +Node 23 + 12: («where_5» := [y => «input_value.y»] f2(y) && «where_5») => 3 + FETCH «where_5» := [y => «input_value.y»] f2(y) + NEXT: Node 24 (fall through) +Node 24 + 12: «where_5» => 3 + TEST «where_5» + THEN: Node 25 (fall through) + ELSE: Node 57 +Node 25 + 12: true => 3 + MATCH 12 with value 3 +Node 26 + 3: «input_value» isa Main.Rematch2Tests.D => 3 + 4: («input_value» isa AbstractArray && «length(input_value)» := length(«input_value») && «length(input_value)» >= 2 && «input_value[2:(length-1)]» := «input_value»[2:(length(«input_value»)-1)]) => [y => «input_value[2:(length-1)]»] y + 5: («input_value» isa Tuple && «length(input_value)» := length(«input_value») && «length(input_value)» >= 2 && «input_value[-1]» := «input_value»[-1] && «where_0» := e.q1 && «where_0») => [z => «input_value[-1]»] z + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + 7: («input_value» isa Main.Rematch2Tests.A && «where_1» := e.q2 && «where_1») => 7 + 8: («input_value» isa Main.Rematch2Tests.B && «where_2» := e.q3 && «where_2») => 8 + 9: («input_value» isa Main.Rematch2Tests.C && «where_3» := e.q4 && «where_3») => 9 + TEST «input_value» isa Main.Rematch2Tests.D + THEN: Node 27 (fall through) + ELSE: Node 28 +Node 27 + 3: true => 3 + MATCH 3 with value 3 +Node 28 + 4: («input_value» isa AbstractArray && «length(input_value)» := length(«input_value») && «length(input_value)» >= 2 && «input_value[2:(length-1)]» := «input_value»[2:(length(«input_value»)-1)]) => [y => «input_value[2:(length-1)]»] y + 5: («input_value» isa Tuple && «length(input_value)» := length(«input_value») && «length(input_value)» >= 2 && «input_value[-1]» := «input_value»[-1] && «where_0» := e.q1 && «where_0») => [z => «input_value[-1]»] z + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + 7: («input_value» isa Main.Rematch2Tests.A && «where_1» := e.q2 && «where_1») => 7 + 8: («input_value» isa Main.Rematch2Tests.B && «where_2» := e.q3 && «where_2») => 8 + 9: («input_value» isa Main.Rematch2Tests.C && «where_3» := e.q4 && «where_3») => 9 + TEST «input_value» isa AbstractArray + THEN: Node 29 (fall through) + ELSE: Node 33 +Node 29 + 4: («length(input_value)» := length(«input_value») && «length(input_value)» >= 2 && «input_value[2:(length-1)]» := «input_value»[2:(length(«input_value»)-1)]) => [y => «input_value[2:(length-1)]»] y + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + FETCH «length(input_value)» := length(«input_value») + NEXT: Node 30 (fall through) +Node 30 + 4: («length(input_value)» >= 2 && «input_value[2:(length-1)]» := «input_value»[2:(length(«input_value»)-1)]) => [y => «input_value[2:(length-1)]»] y + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + TEST «length(input_value)» >= 2 + THEN: Node 31 (fall through) + ELSE: Node 40 +Node 31 + 4: «input_value[2:(length-1)]» := «input_value»[2:(length(«input_value»)-1)] => [y => «input_value[2:(length-1)]»] y + FETCH «input_value[2:(length-1)]» := «input_value»[2:(length(«input_value»)-1)] + NEXT: Node 32 (fall through) +Node 32 + 4: true => [y => «input_value[2:(length-1)]»] y + MATCH 4 with value [y => «input_value[2:(length-1)]»] y +Node 33 + 5: («input_value» isa Tuple && «length(input_value)» := length(«input_value») && «length(input_value)» >= 2 && «input_value[-1]» := «input_value»[-1] && «where_0» := e.q1 && «where_0») => [z => «input_value[-1]»] z + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + 7: («input_value» isa Main.Rematch2Tests.A && «where_1» := e.q2 && «where_1») => 7 + 8: («input_value» isa Main.Rematch2Tests.B && «where_2» := e.q3 && «where_2») => 8 + 9: («input_value» isa Main.Rematch2Tests.C && «where_3» := e.q4 && «where_3») => 9 + TEST «input_value» isa Tuple + THEN: Node 34 (fall through) + ELSE: Node 42 +Node 34 + 5: («length(input_value)» := length(«input_value») && «length(input_value)» >= 2 && «input_value[-1]» := «input_value»[-1] && «where_0» := e.q1 && «where_0») => [z => «input_value[-1]»] z + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + FETCH «length(input_value)» := length(«input_value») + NEXT: Node 35 (fall through) +Node 35 + 5: («length(input_value)» >= 2 && «input_value[-1]» := «input_value»[-1] && «where_0» := e.q1 && «where_0») => [z => «input_value[-1]»] z + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + TEST «length(input_value)» >= 2 + THEN: Node 36 (fall through) + ELSE: Node 40 +Node 36 + 5: («input_value[-1]» := «input_value»[-1] && «where_0» := e.q1 && «where_0») => [z => «input_value[-1]»] z + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + FETCH «input_value[-1]» := «input_value»[-1] + NEXT: Node 37 (fall through) +Node 37 + 5: («where_0» := e.q1 && «where_0») => [z => «input_value[-1]»] z + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + FETCH «where_0» := e.q1 + NEXT: Node 38 (fall through) +Node 38 + 5: «where_0» => [z => «input_value[-1]»] z + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + TEST «where_0» + THEN: Node 39 (fall through) + ELSE: Node 40 +Node 39 + 5: true => [z => «input_value[-1]»] z + MATCH 5 with value [z => «input_value[-1]»] z +Node 40 + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + TEST @ismatch(«input_value», 6) + THEN: Node 44 + ELSE: Node 41 +Node 41 + 6: @ismatch(«input_value», 7) => 6 + TEST @ismatch(«input_value», 7) + THEN: Node 44 + ELSE: Node 57 +Node 42 + 6: (@ismatch(«input_value», 6) || @ismatch(«input_value», 7)) => 6 + 7: («input_value» isa Main.Rematch2Tests.A && «where_1» := e.q2 && «where_1») => 7 + 8: («input_value» isa Main.Rematch2Tests.B && «where_2» := e.q3 && «where_2») => 8 + 9: («input_value» isa Main.Rematch2Tests.C && «where_3» := e.q4 && «where_3») => 9 + TEST @ismatch(«input_value», 6) + THEN: Node 44 + ELSE: Node 43 +Node 43 + 6: @ismatch(«input_value», 7) => 6 + 7: («input_value» isa Main.Rematch2Tests.A && «where_1» := e.q2 && «where_1») => 7 + 8: («input_value» isa Main.Rematch2Tests.B && «where_2» := e.q3 && «where_2») => 8 + 9: («input_value» isa Main.Rematch2Tests.C && «where_3» := e.q4 && «where_3») => 9 + TEST @ismatch(«input_value», 7) + THEN: Node 44 (fall through) + ELSE: Node 45 +Node 44 + 6: true => 6 + MATCH 6 with value 6 +Node 45 + 7: («input_value» isa Main.Rematch2Tests.A && «where_1» := e.q2 && «where_1») => 7 + 8: («input_value» isa Main.Rematch2Tests.B && «where_2» := e.q3 && «where_2») => 8 + 9: («input_value» isa Main.Rematch2Tests.C && «where_3» := e.q4 && «where_3») => 9 + TEST «input_value» isa Main.Rematch2Tests.A + THEN: Node 46 (fall through) + ELSE: Node 57 +Node 46 + 7: («where_1» := e.q2 && «where_1») => 7 + 8: («input_value» isa Main.Rematch2Tests.B && «where_2» := e.q3 && «where_2») => 8 + 9: («input_value» isa Main.Rematch2Tests.C && «where_3» := e.q4 && «where_3») => 9 + FETCH «where_1» := e.q2 + NEXT: Node 47 (fall through) +Node 47 + 7: «where_1» => 7 + 8: («input_value» isa Main.Rematch2Tests.B && «where_2» := e.q3 && «where_2») => 8 + 9: («input_value» isa Main.Rematch2Tests.C && «where_3» := e.q4 && «where_3») => 9 + TEST «where_1» + THEN: Node 48 (fall through) + ELSE: Node 49 +Node 48 + 7: true => 7 + MATCH 7 with value 7 +Node 49 + 8: («input_value» isa Main.Rematch2Tests.B && «where_2» := e.q3 && «where_2») => 8 + 9: («input_value» isa Main.Rematch2Tests.C && «where_3» := e.q4 && «where_3») => 9 + TEST «input_value» isa Main.Rematch2Tests.B + THEN: Node 50 (fall through) + ELSE: Node 53 +Node 50 + 8: («where_2» := e.q3 && «where_2») => 8 + FETCH «where_2» := e.q3 + NEXT: Node 51 (fall through) +Node 51 + 8: «where_2» => 8 + TEST «where_2» + THEN: Node 52 (fall through) + ELSE: Node 57 +Node 52 + 8: true => 8 + MATCH 8 with value 8 +Node 53 + 9: («input_value» isa Main.Rematch2Tests.C && «where_3» := e.q4 && «where_3») => 9 + TEST «input_value» isa Main.Rematch2Tests.C + THEN: Node 54 (fall through) + ELSE: Node 57 +Node 54 + 9: («where_3» := e.q4 && «where_3») => 9 + FETCH «where_3» := e.q4 + NEXT: Node 55 (fall through) +Node 55 + 9: «where_3» => 9 + TEST «where_3» + THEN: Node 56 (fall through) + ELSE: Node 57 +Node 56 + 9: true => 9 + MATCH 9 with value 9 +Node 57 + FAIL (throw)((Match.MatchFailure)(var"«input_value»")) +end # of automaton +""" +@testset "Tests for node coverage" begin + @testset "exercise dumpall 1" begin + devnull = IOBuffer() + Match.@match_dumpall devnull e begin + 1 => 1 + Foo(x, x) => 2 + y::D => 3 + [x, y..., z] => y + (x, y..., z) where e.q1 => z + 6 || 7 => 6 + (x::A) where e.q2 => 7 + (x::B) where e.q3 => 8 + (x::C) where e.q4 => 9 + Foo(x, 2) where f1(x) => 1 + Foo(1, y) where f2(y) => 2 + Foo(x, y) where (f1(x) && f2(y)) => 3 + end + actual = String(take!(devnull)) + for (a, e) in zip(split(actual, '\n'), split(expected, '\n')) + @test a == e + end + end + + @testset "exercise dumpall 2" begin + devnull = IOBuffer() + Match.@match_dumpall devnull some_value begin + Foo(x, 2) where !f1(x) => 1 + Foo(1, y) where !f2(y) => 2 + Foo(x, y) where !(f1(x) || f2(y)) => 3 + _ => 5 + end + Match.@match_dump devnull some_value begin + Foo(x, 2) where !f1(x) => 1 + Foo(1, y) where !f2(y) => 2 + Foo(x, y) where !(f1(x) || f2(y)) => 3 + _ => 5 + end + end + + @testset "trigger some normally unreachable code 1" begin + @test_throws ErrorException Match.gentemp(:a) + + trash = TrashFetchPattern(LineNumberNode(@__LINE__, @__FILE__)) + binder = Match.BinderContext(@__MODULE__) + @test_throws ErrorException Match.code(trash) + @test_throws ErrorException Match.code(trash, binder) + @test_throws ErrorException Match.pretty(stdout, trash) + @test_throws LoadError @eval begin + x=123 + Match.@ismatch x (1:(2+3)) + end + end + + @testset "trigger some normally unreachable code 2" begin + # Simplification of patterns should not normally get into a position where it can + # discard an unneeded `BoundFetchExpressionPattern` because that pattern is + # produced at the same time as another node that consumes its result. However, + # the code is written to handle this case anyway. Here we force that code to + # be exercised and verify the correct result if it were to somehow happen. + location = LineNumberNode(@__LINE__, @__FILE__) + expr = Match.BoundExpression(location, 1) + pattern = Match.BoundFetchExpressionPattern(expr, nothing, Int) + binder = Match.BinderContext(@__MODULE__) + @test Match.simplify(pattern, Set{Symbol}(), binder) isa Match.BoundTruePattern + end + + @testset "trigger some normally unreachable code 3" begin + location = LineNumberNode(@__LINE__, @__FILE__) + action = TrashPattern(location) + result = Match.BoundExpression(location, 2) + node = Match.DeduplicatedAutomatonNode(action, ()) + binder = Match.BinderContext(@__MODULE__) + @test_throws ErrorException Match.generate_code([node], :x, location, binder) + end + + function f300(x::T) where { T } + # The implementation of @match can't bind `T` below - it finds the non-type `T` + # at module level - so it ignores it. + return @match x::T begin + _ => 1 + end + end + @test f300(1) == 1 + + @testset "Match.match_fieldnames(...) throwing" begin + let line = 0, file = @__FILE__ + try + line = (@__LINE__) + 2 + @eval @match MyPair(1, 2) begin + MyPair(1, 2) => 3 + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "May not @match MyPair" + end + end + end + + @testset "A macro that expands to a match case" begin + # macro match_case(pattern, value) + # return esc(:(x => value)) + # end + @test (@match :x begin + @match_case pattern 1 + end) == 1 + @test (@__match__ :x begin + @match_case pattern 1 + end) == 1 + end + + @testset "Some unlikely code" begin + @test (@match 1 begin + ::Int && ::Int => 1 + end) == 1 + + @test_throws LoadError @eval (@match true begin + x::Bool, if x; true; end => 1 + end) + end + + @testset "Some other normally unreachable code 1" begin + location = LineNumberNode(@__LINE__, @__FILE__) + f = Match.BoundFalsePattern(location, false) + @test_throws ErrorException Match.next_action(f) + + devnull = IOBuffer() + node = Match.AutomatonNode(Match.BoundCase[]) + pattern = TrashFetchPattern(location) + binder = Match.BinderContext(@__MODULE__) + @test_throws ErrorException Match.make_next(node, pattern, binder) + @test_throws ErrorException Match.pretty(devnull, pattern) + + f = Match.BoundFalsePattern(location, false) + @test Match.pretty(f) == "false" + @test f == f + + function pretty(node::T, binder) where { T } + io = IOBuffer() + numberer = IdDict{T, Int}() + numberer[node] = 0 + Match.pretty(io, node, binder, numberer, true) + String(take!(io)) + end + node = Match.AutomatonNode(Match.BoundCase[]) + @test pretty(node, binder) == "Node 0\n " + node.action = TrashPattern(location) + @test pretty(node, binder) == "Node 0\n TrashPattern" + pattern = TrashPattern(location) + @test_throws ErrorException Match.make_next(node, pattern, binder) + end + + @testset "Abstract types" begin + x = 2 + @test (@__match__ x begin + ::Abstract1 => 1 + ::Abstract2 => 2 + _ => 3 + end) == 3 + @test (@match x begin + ::Abstract1 => 1 + ::Abstract2 => 2 + _ => 3 + end) == 3 + end + + @testset "Nested use of @match" begin + @test (@match 1 begin + x => @match 2 begin + 2 => 3x + end + end) == 3 + end + + @testset "immutable vector" begin + iv = Match.ImmutableVector([1, 2, 3, 4, 5]) + @test iv[2:3] == Match.ImmutableVector([2, 3]) + h = hash(iv, UInt(0x12)) + @test h isa UInt + end + + @testset "Test the test macro @__match__" begin + @test (@__match__ 1 begin + 1 || 2 => 2 + end) == 2 + @test @__match__(1, 1 => 2) == 2 + end +end # @testset "Tests that add code coverage" diff --git a/test/match_return.jl b/test/match_return.jl new file mode 100644 index 0000000..4534616 --- /dev/null +++ b/test/match_return.jl @@ -0,0 +1,129 @@ +# Macro used in test below +macro user_match_fail() + quote + @match_fail + end +end + +# Macro used in test below +macro user_match_return(e) + esc(quote + @match_return($e) + end) +end + +@testset "@match_return tests" begin + +@testset "simple uses work correctly" begin + @test (@match Foo(1, 2) begin + Foo(x, 2) => begin + x + @match_fail + end + Foo(1, x) => begin + @match_return x + 12 + end + end) == 2 +end + +file = Symbol(@__FILE__) + +@testset "uses of early-exit macros outside @match produce errors 1" begin + let line = 0 + try + line = (@__LINE__) + 1 + @eval @match_return 2 + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: @match_return may only be used within the value of a @match case." + end + end +end + +@testset "uses of early-exit macros outside @match produce errors 2" begin + let line = 0 + try + line = (@__LINE__) + 1 + @eval @match_fail + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: @match_fail may only be used within the value of a @match case." + end + end +end + +@testset "uses of early-exit macros outside @match produce errors 3" begin + try + @eval @match_fail nothing + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa MethodError # wrong number of arguments to @match_fail + end +end + +@testset "nested uses do not interfere with each other" begin + @test (@match 1 begin + 1 => begin + t = @match 1 begin + 1 => begin + # yield from inner only + @match_return 1 + error() + end + end + # yield from outer only + @match_return t + 1 + error() + end + end) == 2 +end + +@testset "a macro may expand to @match_return or @match_fail" begin + @test (@match Foo(1, 2) begin + Foo(x, 2) => begin + x + @user_match_fail + end + Foo(1, x) => begin + @user_match_return x + 12 + end + end) == 2 +end + +@testset "a macro may use the long form 1" begin + @test (@match Foo(1, 2) begin + Foo(x, 2) => begin + x + Match.@match_fail + end + Foo(1, x) => begin + Match.@match_return x + 12 + end + end) == 2 +end + +@testset "a macro may use the long form 2" begin + @test (@match Foo(1, 2) begin + Foo(x, 2) => begin + x + @Match.match_fail + end + Foo(1, x) => begin + @Match.match_return x + 12 + end + end) == 2 +end + +end diff --git a/test/matchtests.jl b/test/matchtests.jl index 4a6b720..24d7a38 100644 --- a/test/matchtests.jl +++ b/test/matchtests.jl @@ -1,344 +1,333 @@ -using Test -using Match - -include("testtypes.jl") +@testset "tests from Match.jl" begin + +@testset "Type matching" begin + # Type matching + test1(item) = @match item begin + n::Int => "Integers are awesome!" + str::AbstractString => "Strings are the best" + m::Dict{Int,AbstractString} => "Ints for Strings?" + d::Dict => "A Dict! Looking up a word?" + _ => "Something unexpected" + end -import Base: show + d = Dict{Int,AbstractString}(1 => "a", 2 => "b") -# Type matching -test1(item) = @match item begin - n::Int => "Integers are awesome!" - str::AbstractString => "Strings are the best" - m::Dict{Int,AbstractString} => "Ints for Strings?" - d::Dict => "A Dict! Looking up a word?" - _ => "Something unexpected" + @test test1(66) == "Integers are awesome!" + @test test1("abc") == "Strings are the best" + @test test1(d) == "Ints for Strings?" + @test test1(Dict()) == "A Dict! Looking up a word?" + @test test1(2.0) == "Something unexpected" end -d = Dict{Int,AbstractString}(1 => "a", 2 => "b") - -@test test1(66) == "Integers are awesome!" -@test test1("abc") == "Strings are the best" -@test test1(d) == "Ints for Strings?" -@test test1(Dict()) == "A Dict! Looking up a word?" -@test test1(2.0) == "Something unexpected" - - -# Pattern extraction -# inspired by http://thecodegeneral.wordpress.com/2012/03/25/switch-statements-on-steroids-scala-pattern-matching/ - -# struct Address -# street::AbstractString -# city::AbstractString -# zip::AbstractString -# end - -# struct Person -# firstname::AbstractString -# lastname::AbstractString -# address::Address -# end +@testset "Pattern extraction" begin + # Pattern extraction + # inspired by http://thecodegeneral.wordpress.com/2012/03/25/switch-statements-on-steroids-scala-pattern-matching/ + + # struct Address + # street::AbstractString + # city::AbstractString + # zip::AbstractString + # end + + # struct Person + # firstname::AbstractString + # lastname::AbstractString + # address::Address + # end + + test2(person) = @match person begin + Person("Julia", lastname, _) => "Found Julia $lastname" + Person(firstname, "Julia", _) => "$firstname Julia was here!" + Person(firstname, lastname, Address(_, "Cambridge", zip)) => "$firstname $lastname lives in zip $zip" + _::Person => "Unknown person!" + end -test2(person) = @match person begin - Person("Julia", lastname, _) => "Found Julia $lastname" - Person(firstname, "Julia", _) => "$firstname Julia was here!" - Person(firstname, lastname, Address(_, "Cambridge", zip)) => "$firstname $lastname lives in zip $zip" - _::Person => "Unknown person!" + @test test2(Person("Julia", "Robinson", Address("450 Serra Mall", "Stanford", "94305"))) == "Found Julia Robinson" + @test test2(Person("Gaston", "Julia", Address("1 rue Victor Cousin", "Paris", "75005"))) == "Gaston Julia was here!" + @test test2(Person("Edwin", "Aldrin", Address("350 Memorial Dr", "Cambridge", "02139"))) == "Edwin Aldrin lives in zip 02139" + @test test2(Person("Linus", "Pauling", Address("1200 E California Blvd", "Pasadena", "91125"))) == "Unknown person!" # Really? end -@test test2(Person("Julia", "Robinson", Address("450 Serra Mall", "Stanford", "94305"))) == "Found Julia Robinson" -@test test2(Person("Gaston", "Julia", Address("1 rue Victor Cousin", "Paris", "75005"))) == "Gaston Julia was here!" -@test test2(Person("Edwin", "Aldrin", Address("350 Memorial Dr", "Cambridge", "02139"))) == "Edwin Aldrin lives in zip 02139" -@test test2(Person("Linus", "Pauling", Address("1200 E California Blvd", "Pasadena", "91125"))) == "Unknown person!" # Really? - - -# Guards, pattern extraction -# translated from Scala Case-classes http://docs.scala-lang.org/tutorials/tour/case-classes.html - -## -## Untyped lambda calculus definitions -## - -# abstract type Term end - -# struct Var <: Term -# name::AbstractString -# end +@testset "Guards, pattern extraction" begin + # Guards, pattern extraction + # translated from Scala Case-classes http://docs.scala-lang.org/tutorials/tour/case-classes.html -# struct Fun <: Term -# arg::AbstractString -# body::Term -# end + ## + ## Untyped lambda calculus definitions + ## -# struct App <: Term -# f::Term -# v::Term -# end + # abstract type Term end -# scala defines these automatically... -import Base.== + # struct Var <: Term + # name::AbstractString + # end -==(x::Var, y::Var) = x.name == y.name -==(x::Fun, y::Fun) = x.arg == y.arg && x.body == y.body -==(x::App, y::App) = x.f == y.f && x.v == y.v + # struct Fun <: Term + # arg::AbstractString + # body::Term + # end + # struct App <: Term + # f::Term + # v::Term + # end -# Not really the Julian way -function show(io::IO, term::Term) - @match term begin - Var(n) => print(io, n) - Fun(x, b) => begin - print(io, "^$x.") - show(io, b) - end - App(f, v) => begin - print(io, "(") - show(io, f) - print(io, " ") - show(io, v) - print(io, ")") + # Guard test is here + function is_identity_fun(term::Term) + @match term begin + Fun(x, Var(y)), if x == y end => true + _ => false end end -end -# Guard test is here -function is_identity_fun(term::Term) - @match term begin - Fun(x, Var(y)), if x == y end => true - _ => false - end -end + id = Fun("x", Var("x")) + t = Fun("x", Fun("y", App(Var("x"), Var("y")))) -id = Fun("x", Var("x")) -t = Fun("x", Fun("y", App(Var("x"), Var("y")))) - -let io = IOBuffer() - show(io, id) - @test String(take!(io)) == "^x.x" - show(io, t) - @test String(take!(io)) == "^x.^y.(x y)" - @test is_identity_fun(id) - @test !is_identity_fun(t) + let io = IOBuffer() + show(io, id) + @test String(take!(io)) == "^x.x" + show(io, t) + @test String(take!(io)) == "^x.^y.(x y)" + @test is_identity_fun(id) + @test !is_identity_fun(t) + end end -# Test single terms +@testset "single terms" begin + # Test single terms -myisodd(x::Int) = @match(x, i => i % 2 == 1) -@test filter(myisodd, 1:10) == filter(isodd, 1:10) == [1, 3, 5, 7, 9] + myisodd(x::Int) = @match(x, i => i % 2 == 1) + @test filter(myisodd, 1:10) == filter(isodd, 1:10) == [1, 3, 5, 7, 9] +end -# Alternatives, Guards +@testset "Alternatives, Guards" begin + # Alternatives, Guards -function parse_arg(arg::AbstractString, value::Any=nothing) - @match (arg, value) begin - ("-l", lang), if lang != nothing end => "Language set to $lang" - ("-o" || "--optim", n::Int), if 0 < n <= 5 end => "Optimization level set to $n" - ("-o" || "--optim", n::Int) => "Illegal optimization level $(n)!" - ("-h" || "--help", nothing) => "Help!" - bad => "Unknown argument: $bad" + function parse_arg(arg::AbstractString, value::Any=nothing) + @match (arg, value) begin + ("-l", lang), if lang != nothing end => "Language set to $lang" + ("-o" || "--optim", n::Int), if 0 < n <= 5 end => "Optimization level set to $n" + ("-o" || "--optim", n::Int) => "Illegal optimization level $(n)!" + ("-h" || "--help", nothing) => "Help!" + bad => "Unknown argument: $bad" + end end -end - -@test parse_arg("-l", "eng") == "Language set to eng" -@test parse_arg("-l") == "Unknown argument: (\"-l\", nothing)" -@test parse_arg("-o", 4) == "Optimization level set to 4" -@test parse_arg("--optim", 5) == "Optimization level set to 5" -@test parse_arg("-o", 0) == "Illegal optimization level 0!" -@test parse_arg("-o", 1.0) == "Unknown argument: (\"-o\", 1.0)" -@test parse_arg("-h") == parse_arg("--help") == "Help!" + @test parse_arg("-l", "eng") == "Language set to eng" + @test parse_arg("-l") == "Unknown argument: (\"-l\", nothing)" + @test parse_arg("-o", 4) == "Optimization level set to 4" + @test parse_arg("--optim", 5) == "Optimization level set to 5" + @test parse_arg("-o", 0) == "Illegal optimization level 0!" + @test parse_arg("-o", 1.0) == "Unknown argument: (\"-o\", 1.0)" + @test parse_arg("-h") == parse_arg("--help") == "Help!" +end +# # Regular Expressions +# +# We do not currently support complex regular expression patterns with subpatterns. +# If and when we do, the following tests might be useful. +# +@testset "Complex regular expression patterns are not supported" begin + # function regex_test(str) + # ## Defining these in the function doesn't work, because the macro + # ## (and related functions) don't have access to the local + # ## variables. -# TODO: Fix me! - -# Note: the following test only works because Ipv4Addr and EmailAddr -# are (module-level) globals! - -# const Ipv4Addr = r"(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})" -# const EmailAddr = r"\b([A-Z0-9._%+-]+)@([A-Z0-9.-]+\.[A-Z]{2,4})\b"i - -# function regex_test(str) -# ## Defining these in the function doesn't work, because the macro -# ## (and related functions) don't have access to the local -# ## variables. - -# # Ipv4Addr = r"(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})" -# # EmailAddr = r"\b([A-Z0-9._%+-]+)@([A-Z0-9.-]+\.[A-Z]{2,4})\b"i + # # Ipv4Addr = r"(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})" + # # EmailAddr = r"\b([A-Z0-9._%+-]+)@([A-Z0-9.-]+\.[A-Z]{2,4})\b"i -# @match str begin -# Ipv4Addr(_, _, octet3, _), if int(octet3) > 30 end => "IPv4 address with octet 3 > 30" -# Ipv4Addr() => "IPv4 address" + # @match str begin + # Ipv4Addr(_, _, octet3, _), if int(octet3) > 30 end => "IPv4 address with octet 3 > 30" + # Ipv4Addr() => "IPv4 address" -# EmailAddr(_,domain), if endswith(domain, "ucla.edu") end => "UCLA email address" -# EmailAddr => "Some email address" + # EmailAddr(_,domain), if endswith(domain, "ucla.edu") end => "UCLA email address" + # EmailAddr => "Some email address" -# r"MCM.*" => "In the twentieth century..." + # r"MCM.*" => "In the twentieth century..." -# _ => "No match" -# end -# end + # _ => "No match" + # end + # end -# @test regex_test("128.97.27.37") == "IPv4 address" -# @test regex_test("96.17.70.24") == "IPv4 address with octet 3 > 30" + # @test regex_test("128.97.27.37") == "IPv4 address" + # @test regex_test("96.17.70.24") == "IPv4 address with octet 3 > 30" -# @test regex_test("beej@cs.ucla.edu") == "UCLA email address" -# @test regex_test("beej@uchicago.edu") == "Some email address" + # @test regex_test("beej@cs.ucla.edu") == "UCLA email address" + # @test regex_test("beej@uchicago.edu") == "Some email address" -# @test regex_test("MCMLXXII") == "In the twentieth century..." -# @test regex_test("Open the pod bay doors, HAL.") == "No match" + # @test regex_test("MCMLXXII") == "In the twentieth century..." + # @test regex_test("Open the pod bay doors, HAL.") == "No match" +end +@testset "Pattern extraction from arrays" begin + # Pattern extraction from arrays -# Pattern extraction from arrays + # extract first, rest from array + # (b is a subarray of the original array) + @test @test_match([1:4;], [a, b...]) == (1, [2, 3, 4]) + @test @test_match([1:4;], [a..., b]) == ([1, 2, 3], 4) + @test @test_match([1:4;], [a, b..., c]) == (1, [2, 3], 4) -# extract first, rest from array -# (b is a subarray of the original array) -@test @match([1:4;], [a, b...]) == (1, [2, 3, 4]) -@test @match([1:4;], [a..., b]) == ([1, 2, 3], 4) -@test @match([1:4;], [a, b..., c]) == (1, [2, 3], 4) + # match particular values at the beginning of a vector + @test @test_match([1:10;], [1, 2, a...]) == [3:10;] + @test @test_match([1:10;], [1, a..., 9, 10]) == [2:8;] -# match particular values at the beginning of a vector -@test @match([1:10;], [1, 2, a...]) == [3:10;] -@test @match([1:10;], [1, a..., 9, 10]) == [2:8;] + # match / collect columns + # @test_broken @test_match([1 2 3; 4 5 6], [a b...]) == ([1, 4], [2 3; 5 6]) + # @test_broken @test_match([1 2 3; 4 5 6], [a... b]) == ([1 2; 4 5], [3, 6]) + # @test_broken @test_match([1 2 3; 4 5 6], [a b c]) == ([1, 4], [2, 5], [3, 6]) + # @test_broken @test_match([1 2 3; 4 5 6], [[1, 4] a b]) == ([2, 5], [3, 6]) -# match / collect columns -@test @match([1 2 3; 4 5 6], [a b...]) == ([1, 4], [2 3; 5 6]) -@test @match([1 2 3; 4 5 6], [a... b]) == ([1 2; 4 5], [3, 6]) -@test @match([1 2 3; 4 5 6], [a b c]) == ([1, 4], [2, 5], [3, 6]) -@test @match([1 2 3; 4 5 6], [[1, 4] a b]) == ([2, 5], [3, 6]) + # @test_broken @test_match([1 2 3 4; 5 6 7 8], [a b... c]) == ([1, 5], [2 3; 6 7], [4, 8]) -@test @match([1 2 3 4; 5 6 7 8], [a b... c]) == ([1, 5], [2 3; 6 7], [4, 8]) + # match / collect rows + @test_broken @test_match([1 2 3; 4 5 6], [a, b]) == ([1, 2, 3], [4, 5, 6]) + @test_broken @test_match([1 2 3; 4 5 6], [[1, 2, 3], a]) == [4, 5, 6] # TODO: don't match this + # @test_broken @test_match([1 2 3; 4 5 6], [1 2 3; a]) == [4,5,6] -# match / collect rows -@test @match([1 2 3; 4 5 6], [a, b]) == ([1, 2, 3], [4, 5, 6]) -@test @match([1 2 3; 4 5 6], [[1, 2, 3], a]) == [4, 5, 6] # TODO: don't match this -#@test @match([1 2 3; 4 5 6], [1 2 3; a]) == [4,5,6] + @test_broken @test_match([1 2 3; 4 5 6; 7 8 9], [a, b...]) == ([1, 2, 3], [4 5 6; 7 8 9]) + @test_broken @test_match([1 2 3; 4 5 6; 7 8 9], [a..., b]) == ([1 2 3; 4 5 6], [7, 8, 9]) + # @test_broken @test_match([1 2 3; 4 5 6; 7 8 9], [1 2 3; a...]) == [4 5 6; 7 8 9] -@test @match([1 2 3; 4 5 6; 7 8 9], [a, b...]) == ([1, 2, 3], [4 5 6; 7 8 9]) -@test @match([1 2 3; 4 5 6; 7 8 9], [a..., b]) == ([1 2 3; 4 5 6], [7, 8, 9]) -#@test @match([1 2 3; 4 5 6; 7 8 9], [1 2 3; a...]) == [4 5 6; 7 8 9] + @test_broken @test_match([1 2 3; 4 5 6; 7 8 9; 10 11 12], [a,b...,c]) == ([1,2,3], [4 5 6; 7 8 9], [10 11 12]) -#@test @match([1 2 3; 4 5 6; 7 8 9; 10 11 12], [a,b...,c]) == ([1,2,3], [4 5 6; 7 8 9], [10 11 12]) + # match invidual positions + # @test_broken @test_match([1 2; 3 4], [1 a; b c]) == (2,3,4) + # @test_broken @test_match([1 2; 3 4], [1 a; b...]) == (2,[3,4]) -# match invidual positions -#@test @match([1 2; 3 4], [1 a; b c]) == (2,3,4) -#@test @match([1 2; 3 4], [1 a; b...]) == (2,[3,4]) + # @test_broken @test_match([ 1 2 3 4 + # 5 6 7 8 + # 9 10 11 12 + # 13 14 15 16 + # 17 18 19 20 ], -# @test @match([ 1 2 3 4 -# 5 6 7 8 -# 9 10 11 12 -# 13 14 15 16 -# 17 18 19 20 ], -# -# [1 a... -# b... -# c... 15 16 -# d 18 19 20]) == ([2,3,4], [5 6 7 8; 9 10 11 12], [13,14], 17) + # [1 a... + # b... + # c... 15 16 + # d 18 19 20]) == ([2,3,4], [5 6 7 8; 9 10 11 12], [13,14], 17) -# match 3D arrays -m = reshape([1:8;], (2, 2, 2)) -@test @match(m, [a b]) == ([1 3; 2 4], [5 7; 6 8]) - -# match against an expression -function get_args(ex::Expr) - @match ex begin - Expr(:call, [:+, args...]) => args - _ => "None" - end + # match 3D arrays + m = reshape([1:8;], (2, 2, 2)) + # @test_broken @test_match(m, [a b]) == ([1 3; 2 4], [5 7; 6 8]) end -@test get_args(Expr(:call, :+, :x, 1)) == [:x, 1] - -# Zach Allaun's fizzbuzz (https://github.com/zachallaun/Match.jl#awesome-fizzbuzz) - -function fizzbuzz(range::AbstractRange) - io = IOBuffer() - for n in range - @match (n % 3, n % 5) begin - (0, 0) => print(io, "fizzbuzz ") - (0, _) => print(io, "fizz ") - (_, 0) => print(io, "buzz ") - (_, _) => print(io, n, ' ') +@testset "match against an expression" begin + # match against an expression + function get_args(ex::Expr) + @match ex begin + Expr(:call, [:+, args...]) => args + _ => "None" end end - String(take!(io)) -end - -@test fizzbuzz(1:15) == "1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz " -# Zach Allaun's "Balancing Red-Black Trees" (https://github.com/zachallaun/Match.jl#balancing-red-black-trees) - -abstract type RBTree end - -struct Leaf <: RBTree + @test get_args(Expr(:call, :+, :x, 1)) == [:x, 1] end -struct Red <: RBTree - value - left::RBTree - right::RBTree -end +@testset "fizzbuzz" begin + # Zach Allaun's fizzbuzz (https://github.com/zachallaun/Match.jl#awesome-fizzbuzz) + + function fizzbuzz(range::AbstractRange) + io = IOBuffer() + for n in range + @match (n % 3, n % 5) begin + (0, 0) => print(io, "fizzbuzz ") + (0, _) => print(io, "fizz ") + (_, 0) => print(io, "buzz ") + (_, _) => print(io, n, ' ') + end + end + String(take!(io)) + end -struct Black <: RBTree - value - left::RBTree - right::RBTree + @test fizzbuzz(1:15) == "1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz " end -function balance(tree::RBTree) - @match tree begin - (Black(z, Red(y, Red(x, a, b), c), d) - || Black(z, Red(x, a, Red(y, b, c)), d) - || Black(x, a, Red(z, Red(y, b, c), d)) - || Black(x, a, Red(y, b, Red(z, c, d)))) => Red(y, Black(x, a, b), - Black(z, c, d)) - tree => tree +@testset "Balancing Red-Black Trees" begin + # Zach Allaun's "Balancing Red-Black Trees" (https://github.com/zachallaun/Match.jl#balancing-red-black-trees) + + # abstract type RBTree end + + # struct Leaf <: RBTree + # end + + # struct Red <: RBTree + # value + # left::RBTree + # right::RBTree + # end + + # struct Black <: RBTree + # value + # left::RBTree + # right::RBTree + # end + + function balance(tree::RBTree) + @match tree begin + (Black(z, Red(y, Red(x, a, b), c), d) + || Black(z, Red(x, a, Red(y, b, c)), d) + || Black(x, a, Red(z, Red(y, b, c), d)) + || Black(x, a, Red(y, b, Red(z, c, d)))) => Red(y, Black(x, a, b), + Black(z, c, d)) + tree => tree + end end + + @test balance(Black(1, Red(2, Red(3, Leaf(), Leaf()), Leaf()), Leaf())) == + Red(2, Black(3, Leaf(), Leaf()), + Black(1, Leaf(), Leaf())) end -@test balance(Black(1, Red(2, Red(3, Leaf(), Leaf()), Leaf()), Leaf())) == - Red(2, Black(3, Leaf(), Leaf()), - Black(1, Leaf(), Leaf())) +@testset "num_match" begin + function num_match(n) + @match n begin + 0 => "zero" + 1 || 2 => "one or two" + 3:10 => "three to ten" + _ => "something else" + end + end + @test num_match(0) == "zero" + @test num_match(2) == "one or two" + @test num_match(4) == "three to ten" + @test num_match(12) == "something else" + @test num_match("hi") == "something else" + @test num_match('c') == "something else" + @test num_match(3:10) == "three to ten" +end -function num_match(n) - @match n begin - 0 => "zero" - 1 || 2 => "one or two" - 3:10 => "three to ten" - _ => "something else" +@testset "char_match" begin + function char_match(c) + @match c begin + 'A':'Z' => "uppercase" + 'a':'z' => "lowercase" + '0':'9' => "number" + _ => "other" + end end + + @test char_match('M') == "uppercase" + @test char_match('n') == "lowercase" + @test char_match('8') == "number" + @test char_match(' ') == "other" + @test char_match("8") == "other" + @test char_match(8) == "other" end -@test num_match(0) == "zero" -@test num_match(2) == "one or two" -@test num_match(4) == "three to ten" -@test num_match(12) == "something else" -@test num_match("hi") == "something else" -@test num_match('c') == "something else" - -function char_match(c) - @match c begin - 'A':'Z' => "uppercase" - 'a':'z' => "lowercase" - '0':'9' => "number" - _ => "other" +@testset "interpolation" begin + # Interpolation of matches in quoted expressions + test_interp(item) = @match item begin + [a, b] => :($a + $b) end + @test test_interp([1, 2]) == :(1 + 2) end -@test char_match('M') == "uppercase" -@test char_match('n') == "lowercase" -@test char_match('8') == "number" -@test char_match(' ') == "other" -@test char_match("8") == "other" -@test char_match(8) == "other" - -# Interpolation of matches in quoted expressions -test_interp(item) = @match item begin - [a, b] => :($a + $b) end -@test test_interp([1, 2]) == :(1 + 2) diff --git a/test/nontrivial.jl b/test/nontrivial.jl new file mode 100644 index 0000000..4f07eb8 --- /dev/null +++ b/test/nontrivial.jl @@ -0,0 +1,199 @@ + +abstract type Expression end +struct Add <: Expression + x::Expression + y::Expression +end +struct Sub <: Expression + x::Expression + y::Expression +end +struct Neg <: Expression + x::Expression +end +struct Mul <: Expression + x::Expression + y::Expression +end +struct Div <: Expression + x::Expression + y::Expression +end +struct Const <: Expression + value::Float64 +end +struct Variable <: Expression + name::Symbol +end + +macro simplify_top(n, mac) + top_name = Symbol("simplify_top", n) + simp_name = Symbol("simplify", n) + mac1 = copy(mac) + mac2 = copy(mac) + push!(mac1.args, quote + # identity elements + Add(Const(0.0), x) => x + Add(x, Const(0.0)) => x + Sub(x, Const(0.0)) => x + Mul(Const(1.0), x) => x + Mul(x, Const(1.0)) => x + Div(x, Const(1.0)) => x + Mul(Const(0.0) && x, _) => x + Mul(_, x && Const(0.0)) => x + Mul(Const(-0.0) && x, _) => x + Mul(_, x && Const(-0.0)) => x + # constant folding + Add(Const(x), Const(y)) => Const(x + y) + Sub(Const(x), Const(y)) => Const(x - y) + Neg(Const(x)) => Const(-x) + Mul(Const(x), Const(y)) => Const(x * y) + Div(Const(x), Const(y)) => Const(x / y) + # Algebraic Identities + Sub(x, x) => Const(0.0) + Neg(Neg(x)) => x + Sub(x, Neg(y)) => $top_name(Add(x, y)) + Add(x, Neg(y)) => $top_name(Sub(x, y)) + Add(Neg(x), y) => $top_name(Sub(y, x)) + Neg(Sub(x, y)) => $top_name(Sub(y, x)) + Add(x, x) => $top_name(Mul(x, Const(2.0))) + Add(x, Mul(Const(k), x))=> $top_name(Mul(x, Const(k + 1))) + Add(Mul(Const(k), x), x)=> $top_name(Mul(x, Const(k + 1))) + # Move constants to the left + Add(x, k::Const) => $top_name(Add(k, x)) + Mul(x, k::Const) => $top_name(Mul(k, x)) + # Move negations up the tree + Sub(Const(0.0), x) => Neg(x) + Sub(Const(-0.0), x) => Neg(x) + Sub(Neg(x), y) => $top_name(Neg($top_name(Add(x, y)))) + Mul(Neg(x), y) => $top_name(Neg($top_name(Mul(x, y)))) + Mul(x, Neg(y)) => $top_name(Neg($top_name(Mul(x, y)))) + x => x + end) + push!(mac2.args, quote + Add(x, y) => $top_name(Add($simp_name(x), $simp_name(y))) + Sub(x, y) => $top_name(Sub($simp_name(x), $simp_name(y))) + Mul(x, y) => $top_name(Mul($simp_name(x), $simp_name(y))) + Div(x, y) => $top_name(Div($simp_name(x), $simp_name(y))) + Neg(x) => $top_name(Neg($simp_name(x))) + x => x + end) + esc(quote + function $top_name(expr::Expression) + $mac1 + end + function $simp_name(expr::Expression) + $mac2 + end + end) +end + +# @simplify_top(0, Match.@match(expr)) +# @simplify_top(1, Rematch.@match(expr)) +@simplify_top(2, Match.@__match__(expr)) +@simplify_top(3, Match.@match(expr)) + +@testset "Check some complex cases" begin + + x = Variable(:x) + y = Variable(:y) + z = Variable(:z) + zero = Const(0.0) + one = Const(1.0) + + e1 = Add(zero, x) + e2 = Add(x, zero) + e3 = Sub(x, zero) + e4 = Mul(one, y) + e5 = Mul(y, one) + e6 = Div(z, one) + e7 = Add(Const(3), Const(4)) + e8 = Sub(Const(5), Const(6)) + e9 = Neg(e7) + e10 = Mul(e8, e9) + e11 = Div(e10, Add(one, one)) + e12 = Neg(Neg(e1)) + e13 = Sub(e2, Neg(e3)) + e14 = Add(e4, Neg(e5)) + e15 = Add(Neg(e6), e7) + e16 = Neg(Sub(e8, e9)) + e17 = Sub(Neg(e10), e11) + e18 = Add(Neg(e12), Neg(e13)) + e19 = Mul(Neg(e14), e15) + e20 = Mul(e16, Neg(e17)) + e21 = Sub(Neg(e18), e19) + e22 = Add(e20, Neg(e21)) + expr = e22 + + # The expected results of simplification + expected = Sub(Const(-63.0), Mul(Const(3.0), x)) + + @testset "Check some simple cases" begin + @test simplify_top2(Sub(x, Neg(y))) == Add(x, y) + @test simplify_top3(Sub(x, Neg(y))) == Add(x, y) + @test simplify_top2(Add(x, Neg(y))) == Sub(x, y) + @test simplify_top3(Add(x, Neg(y))) == Sub(x, y) + end + + # dump(expr) + # dump(simplify0(expr)) + + # @test simplify0(expr) == expected + # @test simplify1(expr) == expected + @test simplify2(expr) == expected + @test simplify3(expr) == expected + + # function performance_test(expr::Expression) + # # GC.gc() + # # println("===================== Match.@match") + # # @time for i in 1:2000000 + # # simplify0(expr) + # # end + # GC.gc() + # println("===================== Rematch.@match") + # @time for i in 1:2000000 + # simplify1(expr) + # end + # GC.gc() + # println("===================== Match.@__match__") + # @time for i in 1:2000000 + # simplify2(expr) + # end + # GC.gc() + # println("===================== Match.@match") + # @time for i in 1:2000000 + # simplify3(expr) + # end + # GC.gc() + # end + + # performance_test(expr) +end + +@testset "examples from Match.jl" begin + # matching expressions, example from Match.jl documentation and VideoIO.jl + # Code has been adapted due to https://github.com/JuliaServices/Match.jl/issues/32 + let + extract_name(x::Any) = Symbol(string(x)) + extract_name(x::Symbol) = x + extract_name(e::Expr) = @match e begin + Expr(:type, [[_, name], _...]) => name + Expr(:typealias, [[name, _], _...]) => name + Expr(:call, [name, _...]) => name + Expr(:function, [sig, _...]) => extract_name(sig) + Expr(:const, [assn, _...]) => extract_name(assn) + Expr(:(=), [fn, body, _...]) => extract_name(fn) + Expr(expr_type, _) => error("Can't extract name from ", + expr_type, " expression:\n", + " $e\n") + end + + @test extract_name(Expr(:type, [true, :name])) == :name + @test extract_name(Expr(:typealias, [:name, true])) == :name + @test extract_name(:(name(x))) == :name + @test extract_name(:(function name(x); end)) == :name + @test extract_name(:(const name = 12)) == :name + @test extract_name(:(name = 12)) == :name + @test extract_name(:(name(x) = x)) == :name + end +end diff --git a/test/rematch.jl b/test/rematch.jl new file mode 100644 index 0000000..9d35003 --- /dev/null +++ b/test/rematch.jl @@ -0,0 +1,547 @@ +# Note we do not use `@eval` to define a struct within a @testset +# because we need the types to be defined during macro expansion, +# which is earlier than evaluation. types are looked up during +# expansion of the @match macro. + +@testset "More @match tests" begin + +@testset "Assignments in the value do not leak out" begin + @match Foo(1, 2) begin + Foo(x, 2) => begin + new_variable = 3 + end + end + @test !(@isdefined x) + @test !(@isdefined new_variable) +end + +@testset "Assignments in a where clause do not leak to the rule's result" begin + @match Foo(1, 2) begin + Foo(x, 2) where begin + new_variable = 3 + true + end => begin + @test !(@isdefined new_variable) + 1 + end + end + @test !(@isdefined x) + @test !(@isdefined new_variable) +end + +@testset "A pure type pattern" begin + @test (@match ::Symbol = :test1) == :test1 + @test (@match ::String = "test2") == "test2" + @test_throws MatchFailure(:test1) @match ::String = :test1 + @test_throws MatchFailure("test2") @match ::Symbol = "test2" +end + +@testset "bound variables may be used in subsequent interpolations" begin + let x = nothing, y = nothing + @test (@match (x, y, $(x + 2)) = (1, 2, 3)) == (1, 2, 3) + @test x == 1 + @test y == 2 + end +end + +file = Symbol(@__FILE__) + +@testset "diagnostics produced are excellent" begin + + @testset "stack trace for MatchFailure" begin + let line = 0 + try + line = (@__LINE__) + 1 + @eval @match ::String = :test1 + @test false + catch e + @test e isa MatchFailure + @test e.value == :test1 + top = @where_thrown + @test top.file == file + @test top.line == line + end + end + end + + @testset "could not bind a type" begin + let line = 0 + try + line = (@__LINE__) + 2 + @eval @match Foo(1, 2) begin + ::Unknown => 1 + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: Could not bind `Unknown` as a type (due to `UndefVarError(:Unknown)`)." + end + end + end + + @testset "location of error for redundant field patterns 1" begin + let line = 0 + try + line = (@__LINE__) + 2 + @eval @match Foo(1, 2) begin + Foo(x = x1,x = x2) => (x1, x2) + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: Pattern `Foo(x = x1, x = x2)` has duplicate named arguments [:x, :x]." + end + end + end + + @testset "location of error for redundant field patterns 2" begin + let line = 0 + try + line = (@__LINE__) + 2 + @eval @match Foo(1, 2) begin + Foo(x = x1, x = x2) => 1 + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: Pattern `Foo(x = x1, x = x2)` has duplicate named arguments [:x, :x]." + end + end + end + + @testset "mix positional and named field patterns" begin + let line = 0 + try + line = (@__LINE__) + 2 + @eval @match Foo(1, 2) begin + Foo(x = x1, x2) => 1 + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: Pattern `Foo(x = x1, x2)` mixes named and positional arguments." + end + end + end + + @testset "wrong field count" begin + let line = 0 + try + line = (@__LINE__) + 2 + @eval @match Foo(1, 2) begin + Foo(x, y, z) => 1 + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: The type `$Foo` has 2 fields but the pattern expects 3 fields." + end + end + end + + @testset "field not found" begin + let line = 0 + try + line = (@__LINE__) + 2 + @eval @match Foo(1, 2) begin + Foo(z = 1) => 1 + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: Type `$Foo` has no field `z`." + end + end + end + + @testset "multiple splats" begin + let line = 0 + try + line = (@__LINE__) + 2 + @eval @match [1, 2, 3] begin + [x..., y, z...] => 1 + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: More than one `...` in pattern `[x..., y, z...]`." + end + end + end + + @testset "unrecognized pattern syntax" begin + let line = 0 + try + line = (@__LINE__) + 2 + @eval @match 1 begin + (x + y) => 1 + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: Unrecognized pattern syntax `x + y`." + end + end + end + + @testset "type binding changed 1" begin + let line = 0 + try + local String = Int64 + line = (@__LINE__) + 2 + @match 1 begin + ::String => 1 + end + @test false + catch e + @test e isa AssertionError + @test e.msg == "$file:$line: The type syntax `::String` bound to type String at macro expansion time but Int64 later." + end + end + end + + @testset "type binding changed 2" begin + let line = 0 + try + line = (@__LINE__) + 3 + function f(x::String) where { String } + @match x begin + ::String => 1 + end + end + f(Int64(1)) + @test false + catch e + @test e isa AssertionError + @test e.msg == "$file:$line: The type syntax `::String` bound to type String at macro expansion time but Int64 later." + end + end + end + + @testset "bad match case syntax" begin + let line = 0 + try + line = (@__LINE__) + 2 + @eval @match 1 begin + (2 + 2) = 4 + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test startswith(e.msg, "$file:$line: Unrecognized @match case syntax: `2 + 2 =") + end + end + end + +end + +# Tests inherited from Rematch below + +@testset "Match Struct by field names" begin + # match one struct field by name + let x = nothing + x1 = nothing + @test (@match Foo(1,2) begin + Foo(x=x1) => x1 + end) == 1 + @test x == nothing + @test x1 == nothing + end + + # match struct with mix of by-value and by-field name + let x1 = nothing + @test (@match Foo(1,2) begin + Foo(0,2) => nothing + Foo(x=x1) => x1 + end) == 1 + end + + # match multiple struct fields by name + let x1 = nothing, y1 = nothing + @test (@match Foo(1,2) begin + Foo(x=x1,y=y1) => (x1,y1) + end) == (1,2) + end + + # match struct field by name redundantly + let x1 = nothing, x2 = nothing + @test_throws LoadError (@eval @match Foo(1,2) begin + Foo(x=x1,x=x2) => (x1,x2) + end) + end + + # variables in patterns are local, and can match multiple positions + let z = 0 + @test z == 0 + @test (@match Foo(1,1) begin + Foo(x=z, y=z) => z # inner z matches both x and y + end) == 1 + @test z == 0 # no change to outer z + end + + # variable in a pattern can match multiple positions + @test_throws MatchFailure(Foo(1,2)) (@match Foo(1,2) begin + Foo(x=x1, y=x1) => true + end) +end + +@testset "non-struct Matches" begin + # throw MatchFailure if no matches + @test_throws MatchFailure(:this) @match :this begin + :that => :ok + end + + # match against symbols + @test (@match :this begin + :this => :ok + end) == :ok + + # treat macros as constants + @test (@match v"1.2.0" begin + v"1.2.0" => :ok + end) == :ok + + ### + ### We do not support `QuoteNode` or `Expr` in `@match` blocks like `Rematch.jl`. + ### There, they were treated as literals, but they could contain + ### interpolated expressions, which we would want to handle properly. + ### It would be nice to support some kind of pattern-matching on them. + ### + # QuoteNodes + # @test (@match :(:x) begin + # :(:x) => :ok + # end) == :ok + # @test (@match :(:x+:y) begin + # :(:x + :y) => :ok + # end) == :ok +end + +@testset "logical expressions with branches" begin + # disjunction + @test (@match (1,(2,3)) begin + (1, (x,:nope) || (2,x)) => x + end) == 3 + + # disjunction and repeated variables + @test (@match (1,(2,3), 3) begin + (1, (x,:nope) || (2,x), x) => x + end) == 3 + @test (@match (1,(2,3), 4) begin + (1, (x,:nope) || (2,x), x) => x + _ => :ok + end) == :ok + @test (@match (3,(2,3), 3) begin + (x, (x,:nope) || (2,x), 3) => x + end) == 3 + @test (@match (1,(2,3), 3) begin + (x, (x,:nope) || (2,x), 3) => x + _ => :ok + end) == :ok + @test (@match (3,(2,3), 3) begin + (x, (x,:nope) || (2,x), x) => x + end) == 3 + @test (@match (3,(2,3), 1) begin + (x, (x,:nope) || (2,x), x) => x + _ => :ok + end) == :ok + + # conjunction + @test (@match (1,(2,3)) begin + (1, a && (2,b)) => (a,b) + end) == ((2,3),3) + @test_throws MatchFailure((1,(2,3))) (@match (1,(2,3)) begin + (1, a && (1,b)) => (a,b) + end) == ((2,3),3) + + # only vars that exist in all branches can be accessed + @test_throws UndefVarError(:y) @match (1,(2,3)) begin + (1, (x,:nope) || (2,y)) => y + end +end + +@testset "Splats" begin + # splats + test0(x) = @match x begin + [a] => [a] + [a,b,c...] => [a,b,c] + (a,) => (a,) + (a...,b,c,d) => (a,b,c,d) + (a,b...,c) => (a,b,c) + _ => false + end + @test test0([1]) == [1] + @test test0([1,2]) == [1,2,[]] + @test test0([1,2,3]) == [1,2,[3]] + @test test0([1,2,3,4]) == [1,2,[3,4]] + @test test0((1,)) == (1,) + @test test0((1,2)) == (1, (), 2) + @test test0((1,2,3)) == ((), 1, 2, 3) + @test test0((1,2,3,4)) == ((1,), 2, 3, 4) + @test test0((1,2,3,4,5)) == ((1,2), 3, 4, 5) + + # no splats allowed in structs (would be nice, but need to implement getfield(struct, range)) + @test_throws LoadError @eval @match foo begin + Foo(x...) => :nope + end + + # at most one splat in tuples/arrays + @test_throws LoadError @eval @match [1,2,3] begin + [a...,b,c...] => :nope + end + @test_throws LoadError @eval @match [1,2,3] begin + (a...,b,c...) => :nope + end + + # inference for splats + infer1(x) = @match x begin + (a, b..., c) => a + end + @test @inferred(infer1((:ok,2,3,4))) == :ok + + infer2(x) = @match x begin + (a, b..., c) => c + end + + @test @inferred(infer2((1,2,3,:ok))) == :ok +end + +@testset "Inference in branches" begin + # inference in branches + infer3(foo) = @match foo begin + Foo(_,y::Symbol) => y + Foo(x::Symbol,_) => x + end + if VERSION >= v"1.6" + @test @inferred(infer3(Foo(1,:ok))) == :ok + end + infer4(foo) = @match foo begin + Foo(x,y::Symbol) => y + Foo(x::Symbol,y) => x + end + if VERSION >= v"1.6" + @test @inferred(infer4(Foo(1,:ok))) == :ok + end +end + +@testset "Nested Guards" begin + # nested guards can use earlier bindings + @test (@match [1,2] begin + [x, y where y > x] => (x,y) + end) == (1,2) + @test_throws MatchFailure([2,1]) @match [2,1] begin + [x, y where y > x] => (x,y) + end + + # nested guards can't use later bindings + @test_throws UndefVarError(:y) @match [2,1] begin + [x where y > x, y ] => (x,y) + end +end + +@testset "structs matching all fields" begin + # detect incorrect numbers of fields + @test_throws LoadError (@eval @match Foo(x) = Foo(1,2)) == (1,2) + @test_throws LoadError @eval @match Foo(x) = Foo(1,2) + @test_throws LoadError @eval @match Foo(x,y,z) = Foo(1,2) + + # ...even if the pattern is not reached + @test_throws LoadError (@eval @match Foo(1,2) begin + Foo(x,y) => :ok + Foo(x) => :nope + end) +end + +@testset "Miscellanea" begin + # match against fiddly symbols (https://github.com/JuliaServices/Match.jl/issues/32) + @test (@match :(@when a < b) begin + Expr(_, [Symbol("@when"), _, _]) => :ok + Expr(_, [other, _, _]) => other + end) == :ok + + # test repeated variables (https://github.com/JuliaServices/Match.jl/issues/27) + @test (@match (x,x) = (1,1)) == (1,1) + @test_throws MatchFailure((1,2)) @match (x,x) = (1,2) + + # match against single tuples (https://github.com/JuliaServices/Match.jl/issues/43) + @test (@match (:x,) begin + (:x,) => :ok + end) == :ok + + # match against empty structs (https://github.com/JuliaServices/Match.jl/issues/43) + e = (True(), 1) + @test (@match e begin + (True(), x) => x + end) == 1 + + # symbols are not interpreted as variables (https://github.com/JuliaServices/Match.jl/issues/45) + let x = 42 + @test (@match (:x,) begin + (:x,) => x + end) == 42 + end + + # allow & and | for conjunction/disjunction (https://github.com/RelationalAI-oss/Rematch.jl/issues/1) + @test (@match (1,(2,3)) begin + (1, (x,:nope) | (2,x)) => x + end) == 3 + @test (@match (1,(2,3)) begin + (1, a & (2,b)) => (a,b) + end) == ((2,3),3) + + @test_throws LoadError @eval @match a + b = x +end + +@testset "Interpolated Values" begin + # match against interpolated values + let outer = 2, b = nothing, c = nothing + @test (@match [1, $outer] = [1,2]) == [1,2] + @test (@match (1, $outer, b..., c) = (1,2,3,4,5)) == (1,2,3,4,5) + @test b == (3,4) + @test c == 5 + end + test_interp_pattern = let a=1, b=2, c=3, + arr=[10,20,30], tup=(100,200,300) + _t(x) = @match x begin + # scalars + [$a,$b,$c,out] => out + [fronts..., $a,$b,$c, back] => [fronts...,back] + # arrays & tuples + [fronts..., $arr, back] => [fronts...,back] + [fronts..., $tup, back] => [fronts...,back] + # complex expressions + [$(a+b+c), out] => out + # splatting existing values not supported + # [fronts..., $(arr...), back] => [fronts...,back] + end + end + # scalars + @test test_interp_pattern([1,2,3,4]) == 4 + @test test_interp_pattern([4,3,2,1, 1,2,3, 4]) == [4,3,2,1,4] + # arrays & tuples + @test test_interp_pattern([0,1, [10,20,30], 2]) == [0,1,2] + @test test_interp_pattern([0,1, (100,200,300), 2]) == [0,1,2] + # complex expressions + @test test_interp_pattern([6,1]) == 1 + # TODO: splatting existing values into pattern isn't suported + # @test_broken test_interp_pattern([0,1, 10,20,30, 2]) == [0,1,2] +end + +end diff --git a/test/rematch2.jl b/test/rematch2.jl new file mode 100644 index 0000000..76c4b1f --- /dev/null +++ b/test/rematch2.jl @@ -0,0 +1,658 @@ +# Note we do not use `@eval` to define types within a `@testset`` +# because we need the types to be defined during macro expansion, +# which is earlier than evaluation. types are looked up during +# expansion of the `@match` macro so we can use the known bindings +# of types to generate more efficient code. + +file = Symbol(@__FILE__) + +@enum Color Yellow Blue Greed + +macro casearm1(pattern, value) + esc(:($pattern => $value)) +end + +macro casearm2(pattern, value) + esc(:(@casearm1 $pattern $value)) +end + +macro check_is_identifier(x) + false +end +macro check_is_identifier(x::Symbol) + true +end + +@testset "Yet more @match tests" begin + +@testset "test the simplest form of regex matches, which are supported by Match.jl" begin + function is_ipv4_address(s) + @match s begin + r"(\d+)\.(\d+)\.(\d+)\.(\d+)" => true + _ => false + end + end + @test is_ipv4_address("192.168.0.5") + @test !is_ipv4_address("www.gafter.com") +end + +@testset "identical regex matching" begin + function func(x) + @match x begin + r"abc" => true + _ => false + end + end + # check that we are backward compatible in allowing a regex to match a regex. + @test func("abc") + @test func(r"abc") + @test !func(:abc) +end + +@testset "identical range matching" begin + function func(x) + @match x begin + 3:10 => true + _ => false + end + end + # check that we are backward compatible in allowing a range to match a range. + @test func(3) + @test func(10) + @test func(3:10) + @test !func(2:9) +end + +@testset "Check that `,if condition end` guards are parsed properly 1" begin + x = true + @test (@match 3 begin + ::Int, if x end => 1 + _ => 2 + end) == 1 + + x = false + @test (@match 3 begin + ::Int, if x end => 1 + _ => 2 + end) == 2 +end + +@testset "Check that `,if condition end` guards are parsed properly 2" begin + x = true + @test (@match 3 begin + (::Int, if x end) => 1 + _ => 2 + end) == 1 + + x = false + @test (@match 3 begin + (::Int, if x end) => 1 + _ => 2 + end) == 2 +end + +@testset "Check that `where` clauses are reparsed properly 1" begin + x = true + @test (@match 3 begin + ::Int where x => 1 + _ => 2 + end) == 1 + + x = false + @test (@match 3 begin + ::Int where x => 1 + _ => 2 + end) == 2 +end + +@testset "Check that `where` clauses are reparsed properly 2" begin + x = true + @test (@match 3 begin + a::Int where x => a + _ => 2 + end) == 3 + + x = false + @test (@match 3 begin + a::Int where x => a + _ => 2 + end) == 2 +end + +@testset "Check that `where` clauses are reparsed properly 3" begin + let line = 0 + try + line = (@__LINE__) + 2 + @eval @match Foo(1, 2) begin + (Foo where unbound)(1, 2) => 1 + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: Unrecognized pattern syntax `(Foo where unbound)(1, 2)`." + end + end +end + +@testset "Check that `where` clauses are reparsed properly 4" begin + for b1 in [false, true] + for b2 in [false, true] + @test (@match 3 begin + ::Int where b1 where b2 => 1 + _ => 2 + end) == ((b1 && b2) ? 1 : 2) + end + end +end + +@testset "Check that `where` clauses are reparsed properly 5" begin + for b1 in [false, true] + for b2 in [false, true] + @test (@match 3 begin + ::Int where b1 == b2 => 1 + _ => 2 + end) == ((b1 == b2) ? 1 : 2) + end + end +end + +@testset "Assignments in the value do not leak out" begin + @match Foo(1, 2) begin + Foo(x, 2) => begin + new_variable = 3 + end + end + @test !(@isdefined x) + @test !(@isdefined new_variable) +end + +@testset "Assignments in a where clause do not leak out" begin + @match Foo(1, 2) begin + Foo(x, 2) where begin + new_variable = 3 + true + end => begin + @test !(@isdefined new_variable) + end + end + @test !(@isdefined x) + @test !(@isdefined new_variable) +end + +@testset "A pure type pattern" begin + @test (@match ::Symbol = :test1) == :test1 + @test (@match ::String = "test2") == "test2" + @test_throws MatchFailure(:test1) @match ::String = :test1 + @test_throws MatchFailure("test2") @match ::Symbol = "test2" +end + +@testset "bound variables may be used in subsequent interpolations" begin + let x = nothing, y = nothing + @test (@match (x, y, $(x + 2)) = (1, 2, 3)) == (1, 2, 3) + @test x == 1 + @test y == 2 + end +end + +# +# To print the decision automaton shown in comments below, replace @match_count_nodes +# with @match_dump and run the test. To show the full details of how the decision +# automaton was computed, try @match_dumpall. +# + +@testset "test for decision automaton optimizations 1" begin + # Node 1 TEST «input_value» isa Foo ELSE: Node 5 («label_0») + # Node 2 FETCH «input_value.y» := «input_value».y + # Node 3 TEST «input_value.y» == 2 ELSE: Node 5 («label_0») + # Node 4 MATCH 1 with value 1 + # Node 5 («label_0») FAIL (throw)((Match.MatchFailure)(«input_value»)) + @test (Match.@match_count_nodes some_value begin + Foo(x, 2) => 1 + end) == 5 +end + +@testset "test for decision automaton optimizations 2" begin + # Node 1 TEST «input_value» isa Foo ELSE: Node 6 («label_0») + # Node 2 FETCH «input_value.y» := «input_value».y + # Node 3 TEST «input_value.y» == 2 ELSE: Node 5 («label_1») + # Node 4 MATCH 1 with value 1 + # Node 5 («label_1») MATCH 2 with value 2 + # Node 6 («label_0») MATCH 3 with value 4 + @test (Match.@match_count_nodes some_value begin + Foo(x, 2) => 1 + Foo(_, _) => 2 + _ => 4 + end) == 6 +end + +@testset "test for decision automaton optimizations 3" begin + # Node 1 TEST «input_value» isa Foo ELSE: Node 7 («label_0») + # Node 2 FETCH «input_value.x» := «input_value».x + # Node 3 FETCH «input_value.y» := «input_value».y + # Node 4 TEST «input_value.y» == 2 ELSE: Node 6 («label_1») + # Node 5 MATCH 1 with value (identity)(«input_value.x») + # Node 6 («label_1») MATCH 2 with value 2 + # Node 7 («label_0») FAIL (throw)((Match.MatchFailure)(«input_value»)) + @test (Match.@match_count_nodes some_value begin + Foo(x, 2) => x + Foo(_, _) => 2 + _ => 4 + end) == 7 +end + +@testset "test for decision automaton optimizations 4" begin + # Node 1 TEST «input_value» isa Foo ELSE: Node 7 («label_0») + # Node 2 FETCH «input_value.x» := «input_value».x + # Node 3 FETCH «input_value.y» := «input_value».y + # Node 4 TEST «input_value.y» == «input_value.x» ELSE: Node 6 («label_1») + # Node 5 MATCH 1 with value (identity)(«input_value.x») + # Node 6 («label_1») MATCH 2 with value 2 + # Node 7 («label_0») MATCH 3 with value 4 + @test (Match.@match_count_nodes some_value begin + Foo(x, x) => x + Foo(_, _) => 2 + _ => 4 + end) == 7 +end + +@testset "test for sharing where clause conjuncts" begin + # Node 1 TEST «input_value» isa Main.Rematch2Tests.Foo ELSE: Node 18 («label_2») + # Node 2 FETCH «input_value.x» := «input_value».x + # Node 3 FETCH «input_value.y» := «input_value».y + # Node 4 TEST «input_value.y» == 2 ELSE: Node 9 («label_5») + # Node 5 FETCH «where_0» := (f1)(«input_value.x») + # Node 6 TEST where «where_0» ELSE: Node 8 («label_4») + # Node 7 MATCH 1 with value 1 + # Node 8 («label_4») TEST «input_value.x» == 1 THEN: Node 10 ELSE: Node 18 («label_2») + # Node 9 («label_5») TEST «input_value.x» == 1 ELSE: Node 13 («label_3») + # Node 10 FETCH «where_1» := (f2)(«input_value.y») + # Node 11 TEST where «where_1» ELSE: Node 18 («label_2») + # Node 12 MATCH 2 with value 2 + # Node 13 («label_3») FETCH «where_0» := (f1)(«input_value.x») + # Node 14 TEST where «where_0» ELSE: Node 18 («label_2») + # Node 15 FETCH «where_1» := (f2)(«input_value.y») + # Node 16 TEST where «where_1» ELSE: Node 18 («label_2») + # Node 17 MATCH 3 with value 3 + # Node 18 («label_2») MATCH 4 with value 4 + @test (Match.@match_count_nodes some_value begin + Foo(x, 2) where f1(x) => 1 + Foo(1, y) where f2(y) => 2 + Foo(x, y) where (f1(x) && f2(y)) => 3 + _ => 4 + end) == 18 +end + +@testset "test for sharing where clause disjuncts" begin + # Node 1 TEST «input_value» isa Main.Rematch2Tests.Foo ELSE: Node 18 («label_2») + # Node 2 FETCH «input_value.x» := «input_value».x + # Node 3 FETCH «input_value.y» := «input_value».y + # Node 4 TEST «input_value.y» == 2 ELSE: Node 11 («label_3») + # Node 5 FETCH «where_0» := (f1)((identity)(«input_value.x»)) + # Node 6 TEST !«where_0» ELSE: Node 8 («label_5») + # Node 7 MATCH 1 with value 1 + # Node 8 («label_4») TEST «input_value.x» == 1 THEN: Node 10 ELSE: Node 18 («label_2») + # Node 9 («label_5») TEST «input_value.x» == 1 ELSE: Node 13 («label_3») + # Node 10 FETCH «where_1» := (f2)(«input_value.y») + # Node 11 TEST where !«where_1» ELSE: Node 18 («label_2») + # Node 12 MATCH 2 with value 2 + # Node 13 («label_3») FETCH «where_0» := (f1)(«input_value.x») + # Node 14 TEST where !«where_0» ELSE: Node 18 («label_2») + # Node 15 FETCH «where_1» := (f2)(«input_value.y») + # Node 16 TEST where !«where_1» ELSE: Node 18 («label_2») + # Node 17 MATCH 3 with value 3 + # Node 18 («label_2») MATCH 4 with value 5 + @test (Match.@match_count_nodes some_value begin + Foo(x, 2) where !f1(x) => 1 + Foo(1, y) where !f2(y) => 2 + Foo(x, y) where !(f1(x) || f2(y)) => 3 + _ => 5 + end) == 18 +end + +@testset "exercise the dumping code for coverage" begin + io = IOBuffer() + @test (Match.@match_dumpall io some_value begin + Foo(x, 2) where !f1(x) => 1 + Foo(1, y) where !f2(y) => 2 + Foo(x, y) where !(f1(x) || f2(y)) => 3 + _ => 5 + end) == 18 + @test (Match.@match_dump io some_value begin + Foo(x, 2) where !f1(x) => 1 + Foo(1, y) where !f2(y) => 2 + Foo(x, y) where !(f1(x) || f2(y)) => 3 + _ => 5 + end) == 18 +end + +@testset "test for correct semantics of complex where clauses" begin + function f1(a, b, c, d, e, f, g, h) + @match (a, b, c, d, e, f, g, h) begin + (a, b, c, d, e, f, g, h) where (!(!((!a || !b) && (c || !d)) || !(!e || f) && (g || h))) => 1 + (a, b, c, d, e, f, g, h) where (!((!a || b) && (c || d) || (e || !f) && (!g || !h))) => 2 + (a, b, c, d, e, f, g, h) where (!((a || b) && !(!c || !d) || !(!(!e || f) && !(g || !h)))) => 3 + (a, b, c, d, e, f, g, h) where (!(!(a || !b) && (!c || !d)) || !(!(e || !f) && (!g || h))) => 4 + (a, b, c, d, e, f, g, h) where (!(a || !b) && (!c || d) || (e || f) && !(!g || h)) => 5 + _ => 6 + end + end + function f2(a, b, c, d, e, f, g, h) + # For reference we use the brute-force implementation of pattern-matching that just + # performs the tests sequentially, like writing an if-elseif-else chain. + Match.@match (a, b, c, d, e, f, g, h) begin + (a, b, c, d, e, f, g, h) where (!(!((!a || !b) && (c || !d)) || !(!e || f) && (g || h))) => 1 + (a, b, c, d, e, f, g, h) where (!((!a || b) && (c || d) || (e || !f) && (!g || !h))) => 2 + (a, b, c, d, e, f, g, h) where (!((a || b) && !(!c || !d) || !(!(!e || f) && !(g || !h)))) => 3 + (a, b, c, d, e, f, g, h) where (!(!(a || !b) && (!c || !d)) || !(!(e || !f) && (!g || h))) => 4 + (a, b, c, d, e, f, g, h) where (!(a || !b) && (!c || d) || (e || f) && !(!g || h)) => 5 + _ => 6 + end + end + function f3(a, b, c, d, e, f, g, h) + @test f1(a, b, c, d, e, f, g, h) == f2(a, b, c, d, e, f, g, h) + end + for t in Iterators.product(([false, true] for a in 1:8)...,) + f3(t...) + end +end + +@testset "infer positional parameters from Match.match_fieldnames(T) 1" begin + # struct T207a + # x; y; z + # T207a(x, y) = new(x, y, x) + # end + # Match.match_fieldnames(::Type{T207a}) = (:x, :y) + r = @match T207a(1, 2) begin + T207a(x, y) => x + end + @test r == 1 + r = @match T207a(1, 2) begin + T207a(x, y) => y + end + @test r == 2 +end + +@testset "infer positional parameters from Match.match_fieldnames(T) 3" begin + # struct T207c + # x; y; z + # end + # T207c(x, y) = T207c(x, y, x) + # Match.match_fieldnames(::Type{T207c}) = (:x, :y) + r = @match T207c(1, 2) begin + T207c(x, y) => x + end + @test r == 1 + r = @match T207c(1, 2) begin + T207c(x, y) => y + end + @test r == 2 +end + +@testset "infer positional parameters from Match.match_fieldnames(T) 4" begin + # struct T207d + # x; z; y + # T207d(x, y) = new(x, 23, y) + # end + # Match.match_fieldnames(::Type{T207d}) = (:x, :y) + r = @match T207d(1, 2) begin + T207d(x, y) => x + end + @test r == 1 + r = @match T207d(1, 2) begin + T207d(x, y) => y + end + @test r == 2 +end + +@testset "diagnostics produced are excellent" begin + + @testset "infer positional parameters from Match.match_fieldnames(T) 2" begin + # struct T207b + # x; y; z + # T207b(x, y; z = x) = new(x, y, z) + # end + let line = 0 + try + line = (@__LINE__) + 2 + @eval @match T207b(1, 2) begin + T207b(x, y) => 1 + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: The type `$T207b` has 3 fields but the pattern expects 2 fields." + end + end + end + + @testset "attempt to match non-type 1" begin + let line = 0 + try + line = (@__LINE__) + 2 + @eval @match Foo(1, 2) begin + ::1 => 1 + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: Invalid type name: `1`." + end + end + end + + @testset "attempt to match non-type 2" begin + let line = 0 + try + line = (@__LINE__) + 2 + @eval @match Foo(1, 2) begin + ::Base => 1 + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: Attempted to match non-type `Base` as a type." + end + end + end + + @testset "bad match block syntax 1" begin + let line = 0 + try + line = (@__LINE__) + 1 + @eval @match a (b + c) + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: Unrecognized @match block syntax: `b + c`." + end + end + end + + @testset "bad match block syntax 2" begin + let line = 0 + try + line = (@__LINE__) + 1 + @eval @__match__ a (b + c) + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: Unrecognized @match block syntax: `b + c`." + end + end + end + + if VERSION >= v"1.8" + @testset "warn for unreachable cases" begin + let line = (@__LINE__) + 5 + @test_warn( + "$file:$line: Case 2: `Foo(1, 2) =>` is not reachable.", + @eval @match Foo(1, 2) begin + Foo(_, _) => 1 + Foo(1, 2) => 2 + end + ) + end + end + end + + @testset "assignment to pattern variables are permitted but act locally" begin + @test (@match 1 begin + x where begin + @test x == 1 + x = 12 + @test x == 12 + true + end => begin + @test x == 1 + x = 13 + @test x == 13 + 6 + end + end) == 6 + end + + if VERSION >= v"1.8" + @testset "type constraints on the input are observed" begin + let line = (@__LINE__) + 7 + @test_warn( + "$file:$line: Case 4: `_ =>` is not reachable.", + @eval @match identity(BoolPair(true, false))::BoolPair begin + BoolPair(true, _) => 1 + BoolPair(_, true) => 2 + BoolPair(false, false) => 3 + _ => 4 # unreachable + end + ) + end + end + end + + @testset "splatting interpolation is not supported" begin + let line = 0 + try + line = (@__LINE__) + 4 + Base.eval(@__MODULE__, @no_escape_quote begin + interp_values = [1, 2] + f(a) = @match a begin + [0, $(interp_values...), 3] => 1 + end + end) + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: Splatting not supported in interpolation: `interp_values...`." + end + end + end + + @testset "pattern variables are simple identifiers in a closed scope" begin + @match collect(1:5) begin + [x, y..., z] => begin + @test @check_is_identifier(x) + @test @check_is_identifier(y) + @test @check_is_identifier(z) + # test that pattern variable names are preserved in the code + @test string(:(x + z)) == "x + z" + @test x == 1 + @test y == [2, 3, 4] + @test z == 5 + x = 3 + @test x == 3 + q = 12 + end + end + @test !(@isdefined x) + @test !(@isdefined y) + @test !(@isdefined z) + @test !(@isdefined q) + end + + @testset "pattern variable names can be shadowed" begin + @match collect(1:5) begin + [x, y..., z] => begin + f(x) = x + 1 + @test f(x) == 2 + @test f(z) == 6 + @test x == 1 + end + end + @test !(@isdefined x) + end + + @testset "pattern variable names can be assigned (locally)" begin + z = "something" + q = "other" + @test (@match collect(1:5) begin + [x, y..., z] where begin + @test x == 1 + @test z == 5 + x = 55 + y = 2 + z = 100 + @test x == 55 + q = "changed" + true + end=> begin + @test x == 1 + @test z == 5 + @test @isdefined y + x + z + end + end) == 6 + @test !(@isdefined x) + @test !(@isdefined y) + @test z == "something" + @test q == "changed" + end + + @testset "disallow lazy strings in patterns due to their support of interpolation" begin + z=3 + @test_throws LoadError (@eval @match z begin + lazy"$(z)" => 1 + _ => 0 + end) + end +end + +@testset "ensure we use `isequal` and not `==`" begin + function f(v) + @match v begin + 0.0 => 1 + 1.0 => 4 + -0.0 => 2 + _ => 3 + end + end + @test f(0.0) == 1 + @test f(1.0) == 4 + @test f(-0.0) == 2 + @test f(2.0) == 3 +end + +@testset "ensure that enums work" begin + # @enum Color Yellow Blue Greed + function f(v) + @match v begin + $Greed => "Greed is the color of money." + _ => "other" + end + end + @test f(Greed) == "Greed is the color of money." + @test f(Yellow) == "other" +end + +end diff --git a/test/runtests.jl b/test/runtests.jl index eb528f4..625c3d9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,21 @@ -# Run Match tests +module Rematch2Tests -include(joinpath(dirname(@__FILE__), "matchtests.jl")) +using Match +using Match: topological_sort, @__match__ +using ReTest +using Random +using MacroTools: MacroTools + +include("testtypes.jl") +include("rematch.jl") +include("rematch2.jl") +include("coverage.jl") +include("nontrivial.jl") +include("topological.jl") +include("match_return.jl") +include("test_ismatch.jl") +include("matchtests.jl") + +retest(Rematch2Tests) + +end # module diff --git a/test/test_ismatch.jl b/test/test_ismatch.jl new file mode 100644 index 0000000..c5098d3 --- /dev/null +++ b/test/test_ismatch.jl @@ -0,0 +1,30 @@ +@testset "tests for the `@ismatch` macro" begin + + @testset "simple @ismatch cases 1" begin + + v = Foo(1, 2) + if @ismatch v Foo(x, y) + @test x == 1 + @test y == 2 + else + @test false + end + @test x == 1 + + end + + @testset "simple @ismatch cases 2" begin + + v = "something else" + if @ismatch v Foo(x, y) + @test false + else + @test !(@isdefined x) + @test !(@isdefined y) + end + @test !(@isdefined x) + @test !(@isdefined y) + + end + +end diff --git a/test/testtypes.jl b/test/testtypes.jl index 8dacb77..14b75ad 100644 --- a/test/testtypes.jl +++ b/test/testtypes.jl @@ -1,7 +1,47 @@ -# -# Address, Person types for deep match test -# +const get_current_exceptions = (VERSION >= v"1.7") ? current_exceptions : Base.catch_stack + +macro where_thrown() + quote + stack = $get_current_exceptions() + l = last(stack) + trace = stacktrace(l[2]) + trace[1] + end +end + +# Quote the syntax without interpolation. +macro no_escape_quote(x) + QuoteNode(x) +end + +struct True; end + +struct Foo + x + y +end + +########## + +abstract type RBTree end + +struct Leaf <: RBTree +end + +struct Red <: RBTree + value + left::RBTree + right::RBTree +end + +struct Black <: RBTree + value + left::RBTree + right::RBTree +end + +########## struct Address street::AbstractString @@ -15,9 +55,7 @@ struct Person address::Address end -# -# Untyped lambda calculus definitions -# +########## abstract type Term end @@ -34,3 +72,74 @@ struct App <: Term f::Term v::Term end + +Base.:(==)(x::Var, y::Var) = x.name == y.name +Base.:(==)(x::Fun, y::Fun) = x.arg == y.arg && x.body == y.body +Base.:(==)(x::App, y::App) = x.f == y.f && x.v == y.v + +# Not really the Julian way +function Base.show(io::IO, term::Term) + @match term begin + Var(n) => print(io, n) + Fun(x, b) => begin + print(io, "^$x.") + show(io, b) + end + App(f, v) => begin + print(io, "(") + show(io, f) + print(io, " ") + show(io, v) + print(io, ")") + end + end +end + +########## + +struct T207a + x; y; z + T207a(x, y) = new(x, y, x) +end +Match.match_fieldnames(::Type{T207a}) = (:x, :y) + +struct T207b + x; y; z + T207b(x, y; z = x) = new(x, y, z) +end + +struct T207c + x; y; z +end +T207c(x, y) = T207c(x, y, x) +Match.match_fieldnames(::Type{T207c}) = (:x, :y) + +struct T207d + x; z; y + T207d(x, y) = new(x, 23, y) +end +Match.match_fieldnames(::Type{T207d}) = (:x, :y) + +struct BoolPair + a::Bool + b::Bool +end + +# +# Match.jl used to support the undocumented syntax +# +# @match value pattern +# +# or +# +# @match(value, pattern) +# +# but this is no longer supported. The tests herein that used to use +# it now use this macro instead. +# +macro test_match(value, pattern) + names = unique(collect(Match.getvars(pattern))) + sort!(names) + result = (length(names) == 1) ? names[1] : Expr(:tuple, names...) + esc(Expr(:macrocall, Symbol("@match"), __source__, value, Expr(:call, :(=>), pattern, result))) +end diff --git a/test/topological.jl b/test/topological.jl new file mode 100644 index 0000000..9393dba --- /dev/null +++ b/test/topological.jl @@ -0,0 +1,43 @@ + +# Given an array of node descriptions, each of which is a node name, +# and the name of successor nodes, construct a graph. Return a successor +# function and a root node, which is the first node in the data. +function make_graph(data::Vector{Vector{T}}) where {T} + succ = Dict{T, Vector{T}}() + for node in data + succ[node[1]] = node[2:end] + end + successor(n) = get(succ, n, T[]) + return (successor, data[1][1]) +end + +@testset "topological_sort tests" begin + + @testset "Test topological_sort 1" begin + (succ, root) = make_graph([[1, 2, 3], [2, 3, 4]]) + @test topological_sort(succ, [root]) == [1, 2, 3, 4] + end + + @testset "Test topological_sort 2" begin + (succ, root) = make_graph([[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6], [5, 6, 7]]) + @test topological_sort(succ, [root]) == [1, 2, 3, 4, 5, 6, 7] + end + + @testset "Test topological_sort 3" begin + (succ, root) = make_graph([[5, 6, 7], [4, 5, 6], [3, 4, 5], [2, 3, 4], [1, 2, 3]]) + @test topological_sort(succ, [1]) == [1, 2, 3, 4, 5, 6, 7] + end + + @testset "Test topological_sort 4" begin + (succ, root) = make_graph([[5, 6, 7], [4, 5, 6], [3, 4, 5], [2, 3, 4], [1, 2, 3], [7, 3]]) + @test_throws ErrorException topological_sort(succ, [1]) + try + topological_sort(succ, [1]) + @test false + catch ex + @test ex isa ErrorException + @test ex.msg == "graph had a cycle involving [3, 4, 5, 6, 7]." + end + end + +end