Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support named tuples #117

Merged
merged 23 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,13 @@ 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`
* `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`
* `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`; 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`
* `[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
Expand All @@ -57,8 +60,10 @@ 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
* Expressions can be interpolated in as constants via standard interpolation syntax `\$(x)`. Interpolations may use previously bound variables.
* `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.

Expand Down
14 changes: 10 additions & 4 deletions src/Match.jl
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,13 @@ 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`
* `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`
* `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`; 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`
* `[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.
Expand All @@ -64,7 +67,10 @@ 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`
* Anything else is treated as a constant and tested for equality
* `if condition end` A boolean computed pattern. `x && if condition end` is another way of writing `x where condition`.
* `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.
Expand Down
50 changes: 50 additions & 0 deletions src/binding.jl
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,34 @@ 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)
nystrom marked this conversation as resolved.
Show resolved Hide resolved
nystrom marked this conversation as resolved.
Show resolved Hide resolved
params = source.args[1]

conjuncts = BoundPattern[]

for param in params.args
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)...)
# But, NamedTuples are not covariant, so that test would fail if the tuple element is
# not inferred as exactly Any.
pattern = shred_where_clause(
nystrom marked this conversation as resolved.
Show resolved Hide resolved
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!(
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
Expand Down Expand Up @@ -482,6 +510,28 @@ function push_pattern!(patterns::Vector{BoundPattern}, binder::BinderContext, pa
get_temp(binder, pat)
end

function parse_kw_param(param, source)
if is_expr(param, :kw, 2)
# (; x = p)
field_name = param.args[1]
# 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) -- 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) -- equivalent to (; x=x)
field_name = param
pattern_source = param
else
error("$(location.file):$(location.line): Unexpected named parameter " *
nystrom marked this conversation as resolved.
Show resolved Hide resolved
"`$param` in named tuple pattern `$source`.")
end
return (field_name, pattern_source)
end

function split_where(T, location)
type = T
where_clause = nothing
Expand Down
61 changes: 61 additions & 0 deletions test/rematch.jl
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,67 @@ end
end)
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)

@test (@match Foo(1,2) begin
(; 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)

@test (@match Foo(1,2) begin
(; 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)
end) == (1, 2)

# Check that field names are bound for `::` patterns too.
@test (@match Foo(1,2) begin
(; x::Int, y::Int) => (x, y)
end) == (1,2)

# 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 patterns after `=` bind.
@test (@match Foo(1,2) begin
(; x=z, y) => (y, z)
end) == (2,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
_ => false
end)
end

@testset "Miscellanea" begin
# match against fiddly symbols (https://github.com/JuliaServices/Match.jl/issues/32)
@test (@match :(@when a < b) begin
Expand Down
14 changes: 14 additions & 0 deletions test/rematch2.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading