From 5c348b16832d5a4ba063895992cef3b5dbd51b96 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Sat, 16 Nov 2024 10:37:01 +0100 Subject: [PATCH 01/20] Support named tuples --- src/binding.jl | 65 ++++++++++++++++++++++++++++++++++++++++++++++++- test/rematch.jl | 35 ++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/binding.jl b/src/binding.jl index 14a7e8f..f1a3f6a 100644 --- a/src/binding.jl +++ b/src/binding.jl @@ -352,9 +352,72 @@ function bind_pattern!( # 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, 1) && is_expr(source.args[1], :parameters) + # named tuples (; x=p, y=q; z) + parameters = source.args[1] + + conjuncts = BoundPattern[] + + # Check that all the named fields exist. + # We'd like to just test that input isa @NamedTuple($(field_names)...) + # But, NamedTuples are not covariant, so that test fails if the tuple elements are not of + # type Any. Instead, we just check that each field exists. + for param in parameters.args + if is_expr(param, :kw) + field_name = param.args[1] + elseif param isa Symbol + # (; x) + field_name = param + else + continue + end + pattern = shred_where_clause( + Expr(:call, :hasfield, Expr(:call, :typeof, input), QuoteNode(field_name)), + false, location, binder, assigned) + push!(conjuncts, pattern) + end + + # Check that all the fields exist. + # T = :( @NamedTuple($(field_names)...) ) + # bound_type = bind_type(location, T, input, binder) + # pattern = BoundTypeTestPattern(location, source, input, bound_type) + # push!(conjuncts, pattern) + + for param in parameters.args + if is_expr(param, :kw, 2) + # (; x = v) + field_name = param.args[1] + pattern_source = param.args[2] + elseif is_expr(param, :kw, 1) + # (; x) + field_name = param.args[1] + pattern_source = param.args[1] + elseif param isa Symbol + # (; x) + field_name = param + pattern_source = param + else + error("$(location.file):$(location.line): Unexpected named parameter " * + "`$param` in named tuple pattern `$source`.") + end + # TODO we should check that the field actually exists. + fetch = BoundFetchFieldPattern(location, pattern_source, input, field_name, Any) + field_temp = push_pattern!(conjuncts, binder, fetch) + bound_subpattern, assigned = bind_pattern!( + location, pattern_source, field_temp, binder, assigned) + push!(conjuncts, bound_subpattern) + end + + pattern = BoundAndPattern(location, source, conjuncts) + elseif is_expr(source, :tuple) || is_expr(source, :vect) # array or tuple subpatterns = source.args + + if any(arg -> is_expr(arg, :parameters), subpatterns) + error("$(location.file):$(location.line): Cannot mix named and positional parameters in pattern `$source`.") + end + splat_count = count(s -> is_expr(s, :...), subpatterns) if splat_count > 1 error("$(location.file):$(location.line): More than one `...` in " * @@ -410,7 +473,7 @@ function bind_pattern!( 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, :if, 2) # if expr end if !is_empty_block(source.args[2]) diff --git a/test/rematch.jl b/test/rematch.jl index 5b4b9b9..94f9fe0 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -474,6 +474,41 @@ end end) end +# match against named tuples +@testset "Named tuples" begin + @test (@match Foo(1,2) begin + (; x=1, y=2) => true + end) + + # Duplicate field names are allowed + @test (@match Foo(1,2) begin + (; x=1, x=(::Int)) => true + end) + + # Named tuples with `=` do not bind the field names + err = (VERSION < v"1.11-") ? UndefVarError(:x) : UndefVarError(:x, @__MODULE__) + @test_throws err (@match Foo(1,2) begin + (; x=1, y) => (x, y) + end) == (1, 2) + + @test (@match Foo(1,2) begin + (; x=1, y) => y + end) == 2 + + @test (@match Foo(1,2) begin + (; x, y) => (x, y) + end) == (1, 2) + + @test (@match Foo(1,2) begin + (; x) => x + end) == 1 + + @test (@match Foo(1,2) begin + (; x, y, z) => false + _ => true + end) +end + @testset "Miscellanea" begin # match against fiddly symbols (https://github.com/JuliaServices/Match.jl/issues/32) @test (@match :(@when a < b) begin From 6d5185f448cdc0ed1627cfd3613665680898761c Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Mon, 18 Nov 2024 10:08:10 +0100 Subject: [PATCH 02/20] refactoring --- src/binding.jl | 76 +++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/binding.jl b/src/binding.jl index f1a3f6a..d7972b5 100644 --- a/src/binding.jl +++ b/src/binding.jl @@ -354,53 +354,29 @@ function bind_pattern!( elseif is_expr(source, :tuple, 1) && is_expr(source.args[1], :parameters) # named tuples (; x=p, y=q; z) - parameters = source.args[1] + params = source.args[1] conjuncts = BoundPattern[] - # Check that all the named fields exist. - # We'd like to just test that input isa @NamedTuple($(field_names)...) - # But, NamedTuples are not covariant, so that test fails if the tuple elements are not of - # type Any. Instead, we just check that each field exists. - for param in parameters.args - if is_expr(param, :kw) - field_name = param.args[1] - elseif param isa Symbol - # (; x) - field_name = param - else - continue - end - pattern = shred_where_clause( - Expr(:call, :hasfield, Expr(:call, :typeof, input), QuoteNode(field_name)), - false, location, binder, assigned) - push!(conjuncts, pattern) - end - # Check that all the fields exist. # T = :( @NamedTuple($(field_names)...) ) # bound_type = bind_type(location, T, input, binder) # pattern = BoundTypeTestPattern(location, source, input, bound_type) # push!(conjuncts, pattern) - for param in parameters.args - if is_expr(param, :kw, 2) - # (; x = v) - field_name = param.args[1] - pattern_source = param.args[2] - elseif is_expr(param, :kw, 1) - # (; x) - field_name = param.args[1] - pattern_source = param.args[1] - elseif param isa Symbol - # (; x) - field_name = param - pattern_source = param - else - error("$(location.file):$(location.line): Unexpected named parameter " * - "`$param` in named tuple pattern `$source`.") - end - # TODO we should check that the field actually exists. + for param in params.args + (field_name, pattern_source) = parse_kw_param(param, location, source) + + # Check that the field exists. + # We'd like to just test that input isa @NamedTuple($(field_names)...) + # But, NamedTuples are not covariant, so that test fails if the tuple elements are not of + # type Any. + pattern = shred_where_clause( + Expr(:call, :hasfield, Expr(:call, :typeof, input), QuoteNode(field_name)), + false, location, binder, assigned) + push!(conjuncts, pattern) + + # Bind the field pattern. fetch = BoundFetchFieldPattern(location, pattern_source, input, field_name, Any) field_temp = push_pattern!(conjuncts, binder, fetch) bound_subpattern, assigned = bind_pattern!( @@ -506,6 +482,30 @@ function push_pattern!(patterns::Vector{BoundPattern}, binder::BinderContext, pa get_temp(binder, pat) end +function parse_kw_param(param, location, source) + if is_expr(param, :kw, 2) + # (; x = v) + field_name = param.args[1] + pattern_source = param.args[2] + elseif is_expr(param, :kw, 1) + # (; x) + field_name = param.args[1] + pattern_source = param.args[1] + elseif is_expr(param, :(::), 2) && param.args[1] isa Symbol + # (; x) + field_name = param.args[1] + pattern_source = Expr(:(::), param.args[2]) + elseif param isa Symbol + # (; x) + field_name = param + pattern_source = param + else + error("$(location.file):$(location.line): Unexpected named parameter " * + "`$param` in named tuple pattern `$source`.") + end + return (field_name, pattern_source) +end + function split_where(T, location) type = T where_clause = nothing From 78520fff225eb87ff200d5b10e62b47a6a025a24 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Mon, 18 Nov 2024 11:47:08 +0100 Subject: [PATCH 03/20] cleanup, readme, tests --- src/Match.jl | 3 +++ src/binding.jl | 14 ++++++-------- test/rematch.jl | 33 ++++++++++++++++++++++++++------- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/Match.jl b/src/Match.jl index f083efc..50625bf 100644 --- a/src/Match.jl +++ b/src/Match.jl @@ -54,6 +54,9 @@ The following syntactic forms can be used in patterns: * `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 values with fields `x,y,z` binding to variables `x,y,z`. +* `(;x=p)` matches values with field `x` matching pattern `p`. Also binds `x`. +* `(;x::T)` matches values with field `x` matching pattern `::T`. Also binds `x`. * `[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. diff --git a/src/binding.jl b/src/binding.jl index d7972b5..47aeca7 100644 --- a/src/binding.jl +++ b/src/binding.jl @@ -484,17 +484,15 @@ end function parse_kw_param(param, location, source) if is_expr(param, :kw, 2) - # (; x = v) + # (; x = p) field_name = param.args[1] - pattern_source = param.args[2] - elseif is_expr(param, :kw, 1) - # (; x) - field_name = param.args[1] - pattern_source = param.args[1] + # bind both the field name and pattern p + pattern_source = Expr(:(&&), param.args...) elseif is_expr(param, :(::), 2) && param.args[1] isa Symbol - # (; x) + # (; x::T) field_name = param.args[1] - pattern_source = Expr(:(::), param.args[2]) + # bind both the field name and pattern `::T` + pattern_source = param elseif param isa Symbol # (; x) field_name = param diff --git a/test/rematch.jl b/test/rematch.jl index 94f9fe0..457ffd8 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -476,20 +476,37 @@ end # match against named tuples @testset "Named tuples" begin + @test (@match (; x=1, y=2) begin + (; x=1, y=2) => true + end) + @test (@match Foo(1,2) begin (; x=1, y=2) => true end) - # Duplicate field names are allowed @test (@match Foo(1,2) begin - (; x=1, x=(::Int)) => true + (; y=2, x=1) => true + end) + + @test (@match Foo(1,2) begin + (; x=1) => true + end) + + @test (@match Foo(1,2) begin + (; x::Int) => true end) - # Named tuples with `=` do not bind the field names - err = (VERSION < v"1.11-") ? UndefVarError(:x) : UndefVarError(:x, @__MODULE__) - @test_throws err (@match Foo(1,2) begin + @test (@match Foo(1,2) begin + (; x) => true + end) + + @test (@match Foo(1,2) begin (; x=1, y) => (x, y) - end) == (1, 2) + end) == (1,2) + + @test (@match Foo(1,2) begin + (; x=z, y) => (x, y, z) + end) == (1,2,1) @test (@match Foo(1,2) begin (; x=1, y) => y @@ -504,7 +521,9 @@ end end) == 1 @test (@match Foo(1,2) begin - (; x, y, z) => false + (; x, y, z) => false # No field `z`. + (; x) => true + (; x, y) => true _ => true end) end From be7cdf245beef80ac4c7fe6dae170ba34d963e6a Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Mon, 18 Nov 2024 11:49:16 +0100 Subject: [PATCH 04/20] fix comment --- src/binding.jl | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/binding.jl b/src/binding.jl index 47aeca7..22a2182 100644 --- a/src/binding.jl +++ b/src/binding.jl @@ -358,19 +358,13 @@ function bind_pattern!( conjuncts = BoundPattern[] - # Check that all the fields exist. - # T = :( @NamedTuple($(field_names)...) ) - # bound_type = bind_type(location, T, input, binder) - # pattern = BoundTypeTestPattern(location, source, input, bound_type) - # push!(conjuncts, pattern) - for param in params.args (field_name, pattern_source) = parse_kw_param(param, location, source) # Check that the field exists. # We'd like to just test that input isa @NamedTuple($(field_names)...) - # But, NamedTuples are not covariant, so that test fails if the tuple elements are not of - # type Any. + # But, NamedTuples are not covariant, so that test would fail if the tuple element is + # not inferred as exactly Any. pattern = shred_where_clause( Expr(:call, :hasfield, Expr(:call, :typeof, input), QuoteNode(field_name)), false, location, binder, assigned) From 6617296132a9a3d74f91cfce4e469394071d54cc Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Mon, 18 Nov 2024 11:50:41 +0100 Subject: [PATCH 05/20] remove useless assert --- src/binding.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/binding.jl b/src/binding.jl index 22a2182..1d7eb2a 100644 --- a/src/binding.jl +++ b/src/binding.jl @@ -384,10 +384,6 @@ function bind_pattern!( # array or tuple subpatterns = source.args - if any(arg -> is_expr(arg, :parameters), subpatterns) - error("$(location.file):$(location.line): Cannot mix named and positional parameters in pattern `$source`.") - end - splat_count = count(s -> is_expr(s, :...), subpatterns) if splat_count > 1 error("$(location.file):$(location.line): More than one `...` in " * From 6f5fb013129a6604b83e57945d3d21057a01dd0f Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Mon, 18 Nov 2024 11:53:34 +0100 Subject: [PATCH 06/20] named tuples --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6b964a8..8b4045d 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ for examples of this and other features. * `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 values with fields `x,y,z` binding to variables `x,y,z`. +* `(;x=p)` matches values with field `x` matching pattern `p`. Also binds `x`. +* `(;x::T)` matches values with field `x` matching pattern `::T`. Also binds `x`. * `[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. From aaeb4b22d8fded87fc848579ac0cd934d0e328d5 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Mon, 18 Nov 2024 11:53:41 +0100 Subject: [PATCH 07/20] more cleanup --- src/binding.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/binding.jl b/src/binding.jl index 1d7eb2a..2bb95e0 100644 --- a/src/binding.jl +++ b/src/binding.jl @@ -359,7 +359,7 @@ function bind_pattern!( conjuncts = BoundPattern[] for param in params.args - (field_name, pattern_source) = parse_kw_param(param, location, source) + field_name, pattern_source = parse_kw_param(param, source) # Check that the field exists. # We'd like to just test that input isa @NamedTuple($(field_names)...) @@ -472,7 +472,7 @@ function push_pattern!(patterns::Vector{BoundPattern}, binder::BinderContext, pa get_temp(binder, pat) end -function parse_kw_param(param, location, source) +function parse_kw_param(param, source) if is_expr(param, :kw, 2) # (; x = p) field_name = param.args[1] From 8a2c3f405c8e0ebfcf511720449d9c2e9f3f9072 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Mon, 18 Nov 2024 11:54:18 +0100 Subject: [PATCH 08/20] small revert --- src/binding.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/binding.jl b/src/binding.jl index 2bb95e0..b09a7e2 100644 --- a/src/binding.jl +++ b/src/binding.jl @@ -383,7 +383,6 @@ function bind_pattern!( 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 " * From 53fb1be4bf601678ee3d4ac68bae3137be1b28bd Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Mon, 18 Nov 2024 11:57:07 +0100 Subject: [PATCH 09/20] comments --- test/rematch.jl | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/test/rematch.jl b/test/rematch.jl index 457ffd8..6063304 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -500,26 +500,22 @@ end (; x) => true end) + # Check that field names are bound. @test (@match Foo(1,2) begin - (; x=1, y) => (x, y) + (; x, y) => (x, y) + end) == (1, 2) + + # Check that field names are bound for `=` and `::` patterns too. + @test (@match Foo(1,2) begin + (; x=1, y::Int) => (x, y) end) == (1,2) + # Check that patterns after `=` also bind. @test (@match Foo(1,2) begin (; x=z, y) => (x, y, z) end) == (1,2,1) - @test (@match Foo(1,2) begin - (; x=1, y) => y - end) == 2 - - @test (@match Foo(1,2) begin - (; x, y) => (x, y) - end) == (1, 2) - - @test (@match Foo(1,2) begin - (; x) => x - end) == 1 - + # Check that we don't match if a field does not exist. @test (@match Foo(1,2) begin (; x, y, z) => false # No field `z`. (; x) => true From f9284e58394bc429bab2795cd8bdaa13dff6995c Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Mon, 18 Nov 2024 13:40:23 +0100 Subject: [PATCH 10/20] fix tests --- test/rematch.jl | 11 +++++++++-- test/rematch2.jl | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/test/rematch.jl b/test/rematch.jl index 6063304..92b4279 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -519,8 +519,15 @@ end @test (@match Foo(1,2) begin (; x, y, z) => false # No field `z`. (; x) => true - (; x, y) => true - _ => true + (; x, y) => false + _ => false + end) + + # Check that we don't match if a field does not exist. + @test (@match Foo(1,2) begin + (; x, y, z) => false # No field `z`. + (; x) => true + _ => false end) end diff --git a/test/rematch2.jl b/test/rematch2.jl index 7f814fe..9deb0b3 100644 --- a/test/rematch2.jl +++ b/test/rematch2.jl @@ -507,6 +507,20 @@ end end end + if VERSION >= v"1.8" + @testset "warn for unreachable cases with named tuples" begin + let line = (@__LINE__) + 5 + @test_warn( + "$file:$line: Case 2: `(; x, y) =>` is not reachable.", + @eval @match Foo(1, 2) begin + (; x) => 1 + (; x, y) => 2 + end + ) + end + end + end + @testset "assignment to pattern variables are permitted but act locally" begin @test (@match 1 begin x where begin From 149dd37e8d55cb39691fee60f46747996df51038 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Thu, 21 Nov 2024 16:35:34 +0100 Subject: [PATCH 11/20] readme cleanups --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8b4045d..155d1a8 100644 --- a/README.md +++ b/README.md @@ -42,13 +42,14 @@ for examples of this and other features. ## Patterns -* `_` matches anything -* `x` (an identifier) matches anything, binds value to the variable `x` +* `_` matches any value +* `x` (an identifier) matches any value and binds it to the variable `x` +* `1` (a literal value) matches that value * `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 values with fields `x,y,z` binding to variables `x,y,z`. -* `(;x=p)` matches values with field `x` matching pattern `p`. Also binds `x`. -* `(;x::T)` matches values with field `x` matching pattern `::T`. Also binds `x`. +* `T(y=p)` matches structs of type `T` whose `y` field matches pattern `p` +* `(;x,y,z)` matches values with fields `x,y,z` binding to variables `x,y,z` +* `(;x=p)` matches values with field `x` matching pattern `p`; also binds the field to `x` +* `(;x::T)` matches values with field `x` matching pattern `::T`; also binds the field to `x` * `[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. @@ -61,7 +62,7 @@ for examples of this and other features. * `x where condition` An alternative form for `x, if condition end` * `if condition end` A boolean computed pattern. `x && if condition end` is another way of writing `x where condition`. * 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. +* Expressions can be interpolated in as constants via standard interpolation syntax `$(x)`. Interpolations may use previously bound variables. Patterns can be nested arbitrarily. From 6c40e2ea388031b386b6262f1a6b4ad97c73c084 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Thu, 21 Nov 2024 16:36:30 +0100 Subject: [PATCH 12/20] cleanup docstring --- src/Match.jl | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Match.jl b/src/Match.jl index 50625bf..1c40952 100644 --- a/src/Match.jl +++ b/src/Match.jl @@ -50,13 +50,14 @@ See also The following syntactic forms can be used in patterns: -* `_` matches anything -* `x` (an identifier) matches anything, binds value to the variable `x` +* `_` matches any value +* `x` (an identifier) matches any value and binds it to the variable `x` +* `1` (a literal value) matches that value * `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 values with fields `x,y,z` binding to variables `x,y,z`. -* `(;x=p)` matches values with field `x` matching pattern `p`. Also binds `x`. -* `(;x::T)` matches values with field `x` matching pattern `::T`. Also binds `x`. +* `T(y=p)` matches structs of type `T` whose `y` field matches pattern `p` +* `(;x,y,z)` matches values with fields `x,y,z` binding to variables `x,y,z` +* `(;x=p)` matches values with field `x` matching pattern `p`; also binds the field to `x` +* `(;x::T)` matches values with field `x` matching pattern `::T`; also binds the field to `x` * `[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. @@ -67,6 +68,7 @@ The following syntactic forms can be used in patterns: * `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` +* `if condition end` A boolean computed pattern. `x && if condition end` is another way of writing `x where condition`. * 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. From 7044351be65f742a88a48e6a5dd1b0c9a6d5f365 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Thu, 21 Nov 2024 16:43:38 +0100 Subject: [PATCH 13/20] document literal, regex, range patterns --- README.md | 5 +++-- src/Match.jl | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 155d1a8..49cefb0 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,6 @@ for examples of this and other features. * `_` matches any value * `x` (an identifier) matches any value and binds it to the variable `x` -* `1` (a literal value) matches that value * `T(x,y,z)` matches structs of type `T` with fields matching patterns `x,y,z` * `T(y=p)` matches structs of type `T` whose `y` field matches pattern `p` * `(;x,y,z)` matches values with fields `x,y,z` binding to variables `x,y,z` @@ -61,7 +60,9 @@ for examples of this and other features. * `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` * `if condition end` A boolean computed pattern. `x && if condition end` is another way of writing `x where condition`. -* Anything else is treated as a constant and tested for equality +* `1` (a literal value) matches that value using `isequal` +* `r"[a-z]*"` (a regular expression) matches strings that match the regular expression +* `1:10` (a constant range) matches values in that range * Expressions can be interpolated in as constants via standard interpolation syntax `$(x)`. Interpolations may use previously bound variables. Patterns can be nested arbitrarily. diff --git a/src/Match.jl b/src/Match.jl index 1c40952..a1a4f0d 100644 --- a/src/Match.jl +++ b/src/Match.jl @@ -52,7 +52,6 @@ The following syntactic forms can be used in patterns: * `_` matches any value * `x` (an identifier) matches any value and binds it to the variable `x` -* `1` (a literal value) matches that value * `T(x,y,z)` matches structs of type `T` with fields matching patterns `x,y,z` * `T(y=p)` matches structs of type `T` whose `y` field matches pattern `p` * `(;x,y,z)` matches values with fields `x,y,z` binding to variables `x,y,z` @@ -69,7 +68,9 @@ The following syntactic forms can be used in patterns: * `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` * `if condition end` A boolean computed pattern. `x && if condition end` is another way of writing `x where condition`. -* Anything else is treated as a constant and tested for equality +* `1` (a literal value) matches that value using `isequal` +* `r"[a-z]*"` (a regular expression) matches strings that match the regular expression +* `1:10` (a constant range) matches values in that range * Expressions can be interpolated in as constants via standard interpolation syntax `\$(x)`. Interpolations may use previously bound variables. Patterns can be nested arbitrarily. From 39d4fc5d26056cf860346159c52598f5cc01fc74 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Sun, 24 Nov 2024 17:04:20 +0100 Subject: [PATCH 14/20] fix readme --- README.md | 2 +- src/Match.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 49cefb0..9094ee7 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ for examples of this and other features. * `T(x,y,z)` matches structs of type `T` with fields matching patterns `x,y,z` * `T(y=p)` matches structs of type `T` whose `y` field matches pattern `p` * `(;x,y,z)` matches values with fields `x,y,z` binding to variables `x,y,z` -* `(;x=p)` matches values with field `x` matching pattern `p`; also binds the field to `x` +* `(;x=p)` matches values with field `x` matching pattern `p`; does not bind `x`. * `(;x::T)` matches values with field `x` matching pattern `::T`; also binds the field to `x` * `[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` diff --git a/src/Match.jl b/src/Match.jl index a1a4f0d..89c490c 100644 --- a/src/Match.jl +++ b/src/Match.jl @@ -55,7 +55,7 @@ The following syntactic forms can be used in patterns: * `T(x,y,z)` matches structs of type `T` with fields matching patterns `x,y,z` * `T(y=p)` matches structs of type `T` whose `y` field matches pattern `p` * `(;x,y,z)` matches values with fields `x,y,z` binding to variables `x,y,z` -* `(;x=p)` matches values with field `x` matching pattern `p`; also binds the field to `x` +* `(;x=p)` matches values with field `x` matching pattern `p`; does not bind `x`. * `(;x::T)` matches values with field `x` matching pattern `::T`; also binds the field to `x` * `[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` From 80ce29bf6fc476f000c69ef24ad7c3809545b1aa Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Sun, 24 Nov 2024 17:04:28 +0100 Subject: [PATCH 15/20] do not bind field names --- src/binding.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/binding.jl b/src/binding.jl index b09a7e2..2a62fd1 100644 --- a/src/binding.jl +++ b/src/binding.jl @@ -475,15 +475,15 @@ function parse_kw_param(param, source) if is_expr(param, :kw, 2) # (; x = p) field_name = param.args[1] - # bind both the field name and pattern p - pattern_source = Expr(:(&&), param.args...) + # bind both the pattern p, but not the field name. + pattern_source = param.args[2] elseif is_expr(param, :(::), 2) && param.args[1] isa Symbol - # (; x::T) + # (; x::T) -- equivalent to (; x=(x::T)) field_name = param.args[1] # bind both the field name and pattern `::T` pattern_source = param elseif param isa Symbol - # (; x) + # (; x) -- equivalent to (; x=x) field_name = param pattern_source = param else From d4e13e9b42cec0c698cfc346b46e1067dd3a5323 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Sun, 24 Nov 2024 17:07:16 +0100 Subject: [PATCH 16/20] fix tests --- test/rematch.jl | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/test/rematch.jl b/test/rematch.jl index 92b4279..a018225 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -505,23 +505,21 @@ end (; x, y) => (x, y) end) == (1, 2) - # Check that field names are bound for `=` and `::` patterns too. + # Check that field names are bound for `::` patterns too. @test (@match Foo(1,2) begin - (; x=1, y::Int) => (x, y) + (; x::Int, y::Int) => (x, y) end) == (1,2) - # Check that patterns after `=` also bind. - @test (@match Foo(1,2) begin - (; x=z, y) => (x, y, z) - end) == (1,2,1) + # Check that field names are not bound for `=` patterns too. + err = (VERSION < v"1.11-") ? UndefVarError(:x) : UndefVarError(:x, @__MODULE__) + @test_throws err (@match Foo(1,2) begin + (; x=1, y) => (x, y) + end) == (1,2) - # Check that we don't match if a field does not exist. + # Check that patterns after `=` bind. @test (@match Foo(1,2) begin - (; x, y, z) => false # No field `z`. - (; x) => true - (; x, y) => false - _ => false - end) + (; x=z, y) => (y, z) + end) == (1,2,1) # Check that we don't match if a field does not exist. @test (@match Foo(1,2) begin From 2eec6d3c1597316ef813812ef0dbbdbb39955aa8 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Sun, 24 Nov 2024 17:10:04 +0100 Subject: [PATCH 17/20] more tests --- test/rematch.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/rematch.jl b/test/rematch.jl index a018225..5b57672 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -500,6 +500,12 @@ end (; x) => true end) + # Check deep matching. + @test (@match Foo(Foo(1,2),Foo(3,4)) begin + (; x=(; x=1, y=2), y=(; x=3, y=4)) => true + _ => false + end) + # Check that field names are bound. @test (@match Foo(1,2) begin (; x, y) => (x, y) @@ -519,7 +525,7 @@ end # Check that patterns after `=` bind. @test (@match Foo(1,2) begin (; x=z, y) => (y, z) - end) == (1,2,1) + end) == (2,1) # Check that we don't match if a field does not exist. @test (@match Foo(1,2) begin From aaac144ea316cbc1616c5992081715f3833bdfe3 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Tue, 17 Dec 2024 10:30:33 +0100 Subject: [PATCH 18/20] Fix syntax error in comment --- src/binding.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/binding.jl b/src/binding.jl index e552fd9..9a16227 100644 --- a/src/binding.jl +++ b/src/binding.jl @@ -387,7 +387,7 @@ function bind_pattern!( return bind_pattern!(location, Expr(:(||), source.args[2], source.args[3]), input, binder, assigned) elseif is_expr(source, :tuple, 1) && is_expr(source.args[1], :parameters) - # named tuples (; x=p, y=q; z) + # named tuples (; x=p, y=q, z) params = source.args[1] conjuncts = BoundPattern[] From 841f99eb56a99b865d69568ac4c056308c2106ee Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Tue, 17 Dec 2024 17:46:01 +0100 Subject: [PATCH 19/20] PR comments --- src/binding.jl | 6 +++--- test/coverage.jl | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/binding.jl b/src/binding.jl index e552fd9..395e716 100644 --- a/src/binding.jl +++ b/src/binding.jl @@ -387,13 +387,13 @@ function bind_pattern!( return bind_pattern!(location, Expr(:(||), source.args[2], source.args[3]), input, binder, assigned) elseif is_expr(source, :tuple, 1) && is_expr(source.args[1], :parameters) - # named tuples (; x=p, y=q; z) + # named tuples (; x=p, y=q, z) params = source.args[1] conjuncts = BoundPattern[] for param in params.args - field_name, pattern_source = parse_kw_param(param, source) + field_name, pattern_source = parse_kw_param(location, param, source) # Check that the field exists. # We'd like to just test that input isa @NamedTuple($(field_names)...) @@ -510,7 +510,7 @@ function push_pattern!(patterns::Vector{BoundPattern}, binder::BinderContext, pa get_temp(binder, pat) end -function parse_kw_param(param, source) +function parse_kw_param(location, param, source) if is_expr(param, :kw, 2) # (; x = p) field_name = param.args[1] diff --git a/test/coverage.jl b/test/coverage.jl index c00a300..89969eb 100644 --- a/test/coverage.jl +++ b/test/coverage.jl @@ -527,6 +527,23 @@ end # of automaton @test_throws ErrorException Match.make_next(node, pattern, binder) end + @testset "Malformed named tuples" begin + let line = 0, file = @__FILE__ + try + line = (@__LINE__) + 2 + @eval @match x begin + (; 1, 2, 3, x, y, z) => x + end + @test false + catch ex + @test ex isa LoadError + e = ex.error + @test e isa ErrorException + @test e.msg == "$file:$line: Unexpected named parameter `1` in named tuple pattern `(; 1, 2, 3, x, y, z)`." + end + end + end + @testset "Abstract types" begin x = 2 @test (@__match__ x begin From 9c1ac4507625b80ddd4272287d7935ca4adee022 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Tue, 17 Dec 2024 18:07:17 +0100 Subject: [PATCH 20/20] more tests --- src/binding.jl | 32 ++++++++++++++++++++++++-------- test/coverage.jl | 26 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/binding.jl b/src/binding.jl index 395e716..2f25d32 100644 --- a/src/binding.jl +++ b/src/binding.jl @@ -399,9 +399,8 @@ function bind_pattern!( # We'd like to just test that input isa @NamedTuple($(field_names)...) # But, NamedTuples are not covariant, so that test would fail if the tuple element is # not inferred as exactly Any. - pattern = shred_where_clause( - Expr(:call, :hasfield, Expr(:call, :typeof, input), QuoteNode(field_name)), - false, location, binder, assigned) + guard = Expr(:call, :(Base.hasfield), Expr(:call, :(Base.typeof), input), QuoteNode(field_name)) + pattern = bind_where_clause(guard, false, location, binder, assigned) push!(conjuncts, pattern) # Bind the field pattern. @@ -594,14 +593,31 @@ function shred_where_clause( 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]) + return bind_where_clause(guard, inverted, location, binder, assigned) end end +# +# Compile a shredded where clause. +# +function bind_where_clause( + guard::Any, + inverted::Bool, + location::LineNumberNode, + binder::BinderContext, + assigned::ImmutableDict{Symbol, Symbol})::BoundPattern + + @assert !@capture(guard, !g_) + @assert !@capture(guard, g1_ && g2_) + @assert !@capture(guard, g1_ || g2_) + + 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 + # # getvars # diff --git a/test/coverage.jl b/test/coverage.jl index 89969eb..155012b 100644 --- a/test/coverage.jl +++ b/test/coverage.jl @@ -544,6 +544,32 @@ end # of automaton end end + @testset "Ensure named tuples work if there is a local typeof" begin + @eval module M1 + import Match + import ..Foo + # Do not overload Base.typeof + typeof(x) = Int + f() = Match.@match Foo(1,2) begin + (; x, y) => x+y + end + end + @eval M1.f() == 3 + end + + @testset "Ensure named tuples work if there is a local hasfield" begin + @eval module M2 + import Match + import ..Foo + # Do not overload Base.hasfield + hasfield(x, y) = false + f() = Match.@match Foo(1,2) begin + (; x, y) => x+y + end + end + @eval M2.f() == 3 + end + @testset "Abstract types" begin x = 2 @test (@__match__ x begin