From 3fad3c514108b0e73ded36e7d9347b9e5a1a46c2 Mon Sep 17 00:00:00 2001 From: goose Date: Sat, 15 Jul 2023 18:59:49 +0700 Subject: [PATCH 1/9] Implement type inferences from guard expressions Potential improvements for future: 1. Better handle `and` operator in guards: current implementation picks one and discards the other 2. Better handle `or` operator in guards: current implementation discards both sides. We can represent them as union types. In real use cases, this should happen rarely, so I'll leave it like this for now Closes #204 --- lib/elixir_sense/core/metadata_builder.ex | 99 ++++++++++++++++- .../core/metadata_builder_test.exs | 100 ++++++++++++++++++ 2 files changed, 198 insertions(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index c9326586..adb0b42e 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -219,6 +219,11 @@ defmodule ElixirSense.Core.MetadataBuilder do |> find_vars(params) |> merge_same_name_vars() + vars = + if options[:guards], + do: infer_type_from_guards(options[:guards], vars, state), + else: vars + {position, end_position} = extract_range(meta) options = Keyword.put(options, :generated, state.generated) @@ -555,7 +560,7 @@ defmodule ElixirSense.Core.MetadataBuilder do ) when def_name in @defs do ast_without_params = {def_name, meta, [{name, add_no_call(meta2), []}, guards, body]} - pre_func(ast_without_params, state, meta, name, params) + pre_func(ast_without_params, state, meta, name, params, guards: guards) end defp pre( @@ -1663,6 +1668,98 @@ defmodule ElixirSense.Core.MetadataBuilder do {ast, {vars, match_context}} end + def infer_type_from_guards(guard_ast, vars, state) do + type_info = type_information_from_guards(guard_ast, state) + + Enum.reduce(type_info, vars, fn {var, type}, acc -> + index = Enum.find_index(acc, &(&1.name == var)) + + if index, + do: List.update_at(acc, index, &Map.put(&1, :type, type)), + else: acc + end) + end + + # A guard expression can be in either these form: + # :and :or + # / \ or / \ or guard_expr + # guard_expr guard_expr guard_expr guard_expr + # + # type information from :and subtrees are mergeable + # type information from :or subtrees are discarded + defp type_information_from_guards({:and, _, [guard_l, guard_r]}, state) do + left = type_information_from_guards(guard_l, state) + right = type_information_from_guards(guard_r, state) + + Keyword.merge(left, right, fn _k, v1, v2 -> + case {v1, v2} do + # func my_func(x) when is_map_key(x, :a) and is_map_key(x, :b) + {{:map, fields1, _}, {:map, fields2, _}} -> + {:map, Enum.uniq_by(fields1 ++ fields2, &elem(&1, 0)), nil} + + # In case we can't merge, just pick one + _ -> + v1 + end + end) + end + + defp type_information_from_guards({:or, _, [_guard_l, _guard_r]}, _state), do: [] + + defp type_information_from_guards(guard_ast, state) do + {_, acc} = + Macro.prewalk(guard_ast, [], fn + # Standalone variable: func my_func(x) when x + {var, _, nil} = node, acc -> + {node, [{var, :boolean} | acc]} + + {guard_predicate, _, params} = node, acc -> + if type = guard_predicate_type(guard_predicate, params, state) do + [{var, _, nil} | _] = params + # If we found the predicate type, we can prematurely exit traversing the subtree + {[], [{var, type} | acc]} + else + {node, acc} + end + + node, acc -> + {node, acc} + end) + + acc + end + + defp guard_predicate_type(p, _, _) + when p in [:is_number, :is_float, :is_integer, :round, :trunc, :div, :rem, :abs], + do: :number + + defp guard_predicate_type(p, _, _) when p in [:is_binary, :binary_part], do: :binary + + defp guard_predicate_type(p, _, _) when p in [:is_bitstring, :bit_size, :byte_size], + do: :bitstring + + defp guard_predicate_type(p, _, _) when p in [:is_list, :hd, :tl, :length], do: :list + defp guard_predicate_type(p, _, _) when p in [:is_tuple, :tuple_size, :elem], do: :tuple + defp guard_predicate_type(:is_map, _, _), do: {:map, [], nil} + + defp guard_predicate_type(:is_map_key, [_, key], state) do + case get_binding_type(state, key) do + {:atom, key} -> {:map, [{key, nil}], nil} + nil when is_binary(key) -> {:map, [{key, nil}], nil} + _ -> {:map, [], nil} + end + end + + defp guard_predicate_type(:is_atom, _, _), do: :atom + defp guard_predicate_type(:is_boolean, _, _), do: :boolean + + defp guard_predicate_type(:is_struct, [_, {:__aliases__, _, list}], state) do + {:struct, [], {:atom, expand_alias(state, list)}, nil} + end + + defp guard_predicate_type(:is_struct, _, _), do: :struct + defp guard_predicate_type(_, _, _), do: nil + # struct or struct update def get_binding_type( state, diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 475ded6b..67d0ef1c 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1477,6 +1477,106 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(4) end + describe "infer vars type information from guards" do + defp var_with_guards(guard) do + """ + defmodule MyModule do + def func(x) when #{guard} do + x + end + end + """ + |> string_to_state() + |> get_line_vars(3) + |> hd() + end + + test "number guards" do + assert %VarInfo{name: :x, type: :number} = var_with_guards("is_number(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("is_float(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("is_integer(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("round(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("trunc(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("div(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("rem(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("abs(x)") + end + + test "binary guards" do + assert %VarInfo{name: :x, type: :binary} = var_with_guards("is_binary(x)") + assert %VarInfo{name: :x, type: :binary} = var_with_guards(~s/binary_part(x, 0, 1) == "a"/) + end + + test "bitstring guards" do + assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("is_bitstring(x)") + assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("bit_size(x) == 1") + assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("byte_size(x) == 1") + end + + test "list guards" do + assert %VarInfo{name: :x, type: :list} = var_with_guards("is_list(x)") + assert %VarInfo{name: :x, type: :list} = var_with_guards("hd(x) == 1") + assert %VarInfo{name: :x, type: :list} = var_with_guards("tl(x) == 1") + assert %VarInfo{name: :x, type: :list} = var_with_guards("length(x) == 1") + end + + test "tuple guards" do + assert %VarInfo{name: :x, type: :tuple} = var_with_guards("is_tuple(x)") + assert %VarInfo{name: :x, type: :tuple} = var_with_guards("tuple_size(x) == 1") + assert %VarInfo{name: :x, type: :tuple} = var_with_guards("elem(x, 0) == 1") + end + + test "atom guards" do + assert %VarInfo{name: :x, type: :atom} = var_with_guards("is_atom(x)") + end + + test "boolean guards" do + assert %VarInfo{name: :x, type: :boolean} = var_with_guards("is_boolean(x)") + end + + test "map guards" do + assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("is_map(x)") + + assert %VarInfo{name: :x, type: {:map, [a: nil], nil}} = + var_with_guards("is_map_key(x, :a)") + + assert %VarInfo{name: :x, type: {:map, [{"a", nil}], nil}} = + var_with_guards(~s/is_map_key(x, "a")/) + end + + test "struct guards" do + assert %VarInfo{name: :x, type: :struct} = var_with_guards("is_struct(x)") + + assert %VarInfo{name: :x, type: {:struct, [], {:atom, URI}, nil}} = + var_with_guards("is_struct(x, URI)") + + assert %VarInfo{name: :x, type: {:struct, [], {:atom, URI}, nil}} = + """ + defmodule MyModule do + alias URI, as: MyURI + + def func(x) when is_struct(x, MyURI) do + x + end + end + """ + |> string_to_state() + |> get_line_vars(5) + |> hd() + end + + test "and combination predicate guards can be merge" do + assert %VarInfo{name: :x, type: :number} = var_with_guards("is_number(x) and x >= 1") + + assert %VarInfo{name: :x, type: {:map, [a: nil, b: nil], nil}} = + var_with_guards("is_map_key(x, :a) and is_map_key(x, :b)") + end + + test "or combination predicate guards can not be used" do + assert %VarInfo{name: :x, type: nil} = var_with_guards("is_number(x) or is_atom(x)") + end + end + test "aliases" do state = """ From ea32947181778082dcb8e9595fd96ceaa7351afc Mon Sep 17 00:00:00 2001 From: goose Date: Wed, 30 Aug 2023 21:15:38 +0700 Subject: [PATCH 2/9] Make type more specific --- lib/elixir_sense/core/metadata_builder.ex | 2 +- test/elixir_sense/core/metadata_builder_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index adb0b42e..2c74bfaa 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -1757,7 +1757,7 @@ defmodule ElixirSense.Core.MetadataBuilder do {:struct, [], {:atom, expand_alias(state, list)}, nil} end - defp guard_predicate_type(:is_struct, _, _), do: :struct + defp guard_predicate_type(:is_struct, _, _), do: {:struct, [], nil, nil} defp guard_predicate_type(_, _, _), do: nil # struct or struct update diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 67d0ef1c..ecbb7eb2 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1545,7 +1545,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end test "struct guards" do - assert %VarInfo{name: :x, type: :struct} = var_with_guards("is_struct(x)") + assert %VarInfo{name: :x, type: {:struct, [], nil, nil}} = var_with_guards("is_struct(x)") assert %VarInfo{name: :x, type: {:struct, [], {:atom, URI}, nil}} = var_with_guards("is_struct(x, URI)") From 554a6edb6cfd90243e25466ad0f961de6c20a4a2 Mon Sep 17 00:00:00 2001 From: goose Date: Wed, 30 Aug 2023 21:24:30 +0700 Subject: [PATCH 3/9] Support map_size guard --- lib/elixir_sense/core/metadata_builder.ex | 1 + test/elixir_sense/core/metadata_builder_test.exs | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 2c74bfaa..3e82ae62 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -1741,6 +1741,7 @@ defmodule ElixirSense.Core.MetadataBuilder do defp guard_predicate_type(p, _, _) when p in [:is_list, :hd, :tl, :length], do: :list defp guard_predicate_type(p, _, _) when p in [:is_tuple, :tuple_size, :elem], do: :tuple defp guard_predicate_type(:is_map, _, _), do: {:map, [], nil} + defp guard_predicate_type(:map_size, _, _), do: {:map, [], nil} defp guard_predicate_type(:is_map_key, [_, key], state) do case get_binding_type(state, key) do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index ecbb7eb2..56fdb26b 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1536,6 +1536,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "map guards" do assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("is_map(x)") + assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("map_size(x) == 1") assert %VarInfo{name: :x, type: {:map, [a: nil], nil}} = var_with_guards("is_map_key(x, :a)") From 0eb3564f961eff4ff34c8949b4ec480260fa815f Mon Sep 17 00:00:00 2001 From: goose Date: Wed, 30 Aug 2023 21:26:01 +0700 Subject: [PATCH 4/9] Fix incorrect fixture code --- test/elixir_sense/core/metadata_builder_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 56fdb26b..ae34ad72 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1516,7 +1516,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "list guards" do assert %VarInfo{name: :x, type: :list} = var_with_guards("is_list(x)") assert %VarInfo{name: :x, type: :list} = var_with_guards("hd(x) == 1") - assert %VarInfo{name: :x, type: :list} = var_with_guards("tl(x) == 1") + assert %VarInfo{name: :x, type: :list} = var_with_guards("tl(x) == [1]") assert %VarInfo{name: :x, type: :list} = var_with_guards("length(x) == 1") end From 11428b98127e2ad9b554149115d95ef161c77ab1 Mon Sep 17 00:00:00 2001 From: goose Date: Wed, 30 Aug 2023 22:16:52 +0700 Subject: [PATCH 5/9] Merge or guards into union type --- lib/elixir_sense/core/metadata_builder.ex | 14 +++++++++++++- test/elixir_sense/core/metadata_builder_test.exs | 8 ++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 3e82ae62..3330acee 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -1704,7 +1704,19 @@ defmodule ElixirSense.Core.MetadataBuilder do end) end - defp type_information_from_guards({:or, _, [_guard_l, _guard_r]}, _state), do: [] + defp type_information_from_guards({:or, _, [guard_l, guard_r]}, state) do + left = type_information_from_guards(guard_l, state) + right = type_information_from_guards(guard_r, state) + + Keyword.merge(left, right, fn _k, v1, v2 -> + case {v1, v2} do + {{:union, types_1}, {:union, types_2}} -> {:union, types_1 ++ types_2} + {{:union, types}, _} -> {:union, types ++ [v2]} + {_, {:union, types}} -> {:union, [v1 | types]} + _ -> {:union, [v1, v2]} + end + end) + end defp type_information_from_guards(guard_ast, state) do {_, acc} = diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index ae34ad72..f211be3c 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1573,8 +1573,12 @@ defmodule ElixirSense.Core.MetadataBuilderTest do var_with_guards("is_map_key(x, :a) and is_map_key(x, :b)") end - test "or combination predicate guards can not be used" do - assert %VarInfo{name: :x, type: nil} = var_with_guards("is_number(x) or is_atom(x)") + test "or combination predicate guards can be merge into union type" do + assert %VarInfo{name: :x, type: {:union, [:number, :atom]}} = + var_with_guards("is_number(x) or is_atom(x)") + + assert %VarInfo{name: :x, type: {:union, [:number, :atom, :binary]}} = + var_with_guards("is_number(x) or is_atom(x) or is_binary(x)") end end From 5ef6393807251571a9fdd0e475dffe28de49bd14 Mon Sep 17 00:00:00 2001 From: goose Date: Fri, 1 Sep 2023 08:06:02 +0700 Subject: [PATCH 6/9] Use intersection type --- lib/elixir_sense/core/metadata_builder.ex | 13 +------------ test/elixir_sense/core/metadata_builder_test.exs | 9 ++++++--- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 3330acee..ab34b753 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -1690,18 +1690,7 @@ defmodule ElixirSense.Core.MetadataBuilder do defp type_information_from_guards({:and, _, [guard_l, guard_r]}, state) do left = type_information_from_guards(guard_l, state) right = type_information_from_guards(guard_r, state) - - Keyword.merge(left, right, fn _k, v1, v2 -> - case {v1, v2} do - # func my_func(x) when is_map_key(x, :a) and is_map_key(x, :b) - {{:map, fields1, _}, {:map, fields2, _}} -> - {:map, Enum.uniq_by(fields1 ++ fields2, &elem(&1, 0)), nil} - - # In case we can't merge, just pick one - _ -> - v1 - end - end) + Keyword.merge(left, right, fn _k, v1, v2 -> {:intersection, [v1, v2]} end) end defp type_information_from_guards({:or, _, [guard_l, guard_r]}, state) do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index f211be3c..6e3e9d17 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1567,10 +1567,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end test "and combination predicate guards can be merge" do - assert %VarInfo{name: :x, type: :number} = var_with_guards("is_number(x) and x >= 1") + assert %VarInfo{name: :x, type: {:intersection, [:number, :boolean]}} = + var_with_guards("is_number(x) and x >= 1") - assert %VarInfo{name: :x, type: {:map, [a: nil, b: nil], nil}} = - var_with_guards("is_map_key(x, :a) and is_map_key(x, :b)") + assert %VarInfo{ + name: :x, + type: {:intersection, [{:map, [a: nil], nil}, {:map, [b: nil], nil}]} + } = var_with_guards("is_map_key(x, :a) and is_map_key(x, :b)") end test "or combination predicate guards can be merge into union type" do From fe7b2be09c015de44ac7800b3c8b126b19d47e8b Mon Sep 17 00:00:00 2001 From: goose Date: Tue, 31 Oct 2023 20:56:18 +0700 Subject: [PATCH 7/9] More refine type for list and tuple --- lib/elixir_sense/core/metadata_builder.ex | 99 ++++++++++++++----- .../core/metadata_builder_test.exs | 4 +- 2 files changed, 75 insertions(+), 28 deletions(-) diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index ab34b753..8f4f2ff4 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -13,6 +13,7 @@ defmodule ElixirSense.Core.MetadataBuilder do alias ElixirSense.Core.State alias ElixirSense.Core.State.VarInfo alias ElixirSense.Core.TypeInfo + alias ElixirSense.Core.Binding @scope_keywords [:for, :fn, :with] @block_keywords [:do, :else, :rescue, :catch, :after] @@ -1715,12 +1716,14 @@ defmodule ElixirSense.Core.MetadataBuilder do {node, [{var, :boolean} | acc]} {guard_predicate, _, params} = node, acc -> - if type = guard_predicate_type(guard_predicate, params, state) do - [{var, _, nil} | _] = params - # If we found the predicate type, we can prematurely exit traversing the subtree - {[], [{var, type} | acc]} - else - {node, acc} + case guard_predicate_type(guard_predicate, params, state) do + {type, binding} -> + {var, _, nil} = binding + # If we found the predicate type, we can prematurely exit traversing the subtree + {[], [{var, type} | acc]} + + nil -> + {node, acc} end node, acc -> @@ -1730,36 +1733,80 @@ defmodule ElixirSense.Core.MetadataBuilder do acc end - defp guard_predicate_type(p, _, _) + defp guard_predicate_type(p, params, _) when p in [:is_number, :is_float, :is_integer, :round, :trunc, :div, :rem, :abs], - do: :number + do: {:number, hd(params)} + + defp guard_predicate_type(p, params, _) when p in [:is_binary, :binary_part], + do: {:binary, hd(params)} + + defp guard_predicate_type(p, params, _) when p in [:is_bitstring, :bit_size, :byte_size], + do: {:bitstring, hd(params)} + + defp guard_predicate_type(p, params, _) when p in [:is_list, :length], do: {:list, hd(params)} + + defp guard_predicate_type(p, params, _) when p in [:hd, :tl], + do: {{:list, :boolean}, hd(params)} + + # when hd(x) == 1 + # when tl(x) <= 2 + defp guard_predicate_type(p, [{guard, _, guard_params}, rhs], _) + when p in [:==, :===, :>=, :>, :<=, :<] and guard in [:hd, :tl] do + rhs_type = + cond do + is_number(rhs) -> :number + is_binary(rhs) -> :binary + is_bitstring(rhs) -> :bitstring + is_atom(rhs) -> :atom + is_boolean(rhs) -> :boolean + true -> nil + end - defp guard_predicate_type(p, _, _) when p in [:is_binary, :binary_part], do: :binary + rhs_type = if rhs_type, do: {:list, rhs_type}, else: :list - defp guard_predicate_type(p, _, _) when p in [:is_bitstring, :bit_size, :byte_size], - do: :bitstring + {rhs_type, hd(guard_params)} + end - defp guard_predicate_type(p, _, _) when p in [:is_list, :hd, :tl, :length], do: :list - defp guard_predicate_type(p, _, _) when p in [:is_tuple, :tuple_size, :elem], do: :tuple - defp guard_predicate_type(:is_map, _, _), do: {:map, [], nil} - defp guard_predicate_type(:map_size, _, _), do: {:map, [], nil} + defp guard_predicate_type(p, params, _) when p in [:is_tuple, :elem], + do: {:tuple, hd(params)} - defp guard_predicate_type(:is_map_key, [_, key], state) do - case get_binding_type(state, key) do - {:atom, key} -> {:map, [{key, nil}], nil} - nil when is_binary(key) -> {:map, [{key, nil}], nil} - _ -> {:map, [], nil} - end + # when tuple_size(x) == 1 + # when tuple_size(x) == 2 + defp guard_predicate_type(p, [{:tuple_size, _, guard_params}, size], _) + when p in [:==, :===] do + type = + if is_integer(size) do + {:tuple, size, []} + else + :tuple + end + + {type, hd(guard_params)} + end + + defp guard_predicate_type(:is_map, params, _), do: {{:map, [], nil}, hd(params)} + defp guard_predicate_type(:map_size, params, _), do: {{:map, [], nil}, hd(params)} + + defp guard_predicate_type(:is_map_key, [var, key], state) do + type = + case get_binding_type(state, key) do + {:atom, key} -> {:map, [{key, nil}], nil} + nil when is_binary(key) -> {:map, [{key, nil}], nil} + _ -> {:map, [], nil} + end + + {type, var} end - defp guard_predicate_type(:is_atom, _, _), do: :atom - defp guard_predicate_type(:is_boolean, _, _), do: :boolean + defp guard_predicate_type(:is_atom, params, _), do: {:atom, hd(params)} + defp guard_predicate_type(:is_boolean, params, _), do: {:boolean, hd(params)} - defp guard_predicate_type(:is_struct, [_, {:__aliases__, _, list}], state) do - {:struct, [], {:atom, expand_alias(state, list)}, nil} + defp guard_predicate_type(:is_struct, [var, {:__aliases__, _, list}], state) do + type = {:struct, [], {:atom, expand_alias(state, list)}, nil} + {type, var} end - defp guard_predicate_type(:is_struct, _, _), do: {:struct, [], nil, nil} + defp guard_predicate_type(:is_struct, params, _), do: {{:struct, [], nil, nil}, hd(params)} defp guard_predicate_type(_, _, _), do: nil # struct or struct update diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 6e3e9d17..8f7f4015 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1515,14 +1515,14 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "list guards" do assert %VarInfo{name: :x, type: :list} = var_with_guards("is_list(x)") - assert %VarInfo{name: :x, type: :list} = var_with_guards("hd(x) == 1") + assert %VarInfo{name: :x, type: {:list, :number}} = var_with_guards("hd(x) == 1") assert %VarInfo{name: :x, type: :list} = var_with_guards("tl(x) == [1]") assert %VarInfo{name: :x, type: :list} = var_with_guards("length(x) == 1") end test "tuple guards" do assert %VarInfo{name: :x, type: :tuple} = var_with_guards("is_tuple(x)") - assert %VarInfo{name: :x, type: :tuple} = var_with_guards("tuple_size(x) == 1") + assert %VarInfo{name: :x, type: {:tuple, 1, []}} = var_with_guards("tuple_size(x) == 1") assert %VarInfo{name: :x, type: :tuple} = var_with_guards("elem(x, 0) == 1") end From 6b4c0d0d7e078b669fabc84230a974f05032daa3 Mon Sep 17 00:00:00 2001 From: goose Date: Tue, 31 Oct 2023 21:07:44 +0700 Subject: [PATCH 8/9] Split guard type infer logic into separate module --- lib/elixir_sense/core/guard.ex | 134 ++++++++++++++++++++++ lib/elixir_sense/core/metadata_builder.ex | 132 +-------------------- 2 files changed, 136 insertions(+), 130 deletions(-) create mode 100644 lib/elixir_sense/core/guard.ex diff --git a/lib/elixir_sense/core/guard.ex b/lib/elixir_sense/core/guard.ex new file mode 100644 index 00000000..1a246b0e --- /dev/null +++ b/lib/elixir_sense/core/guard.ex @@ -0,0 +1,134 @@ +defmodule ElixirSense.Core.Guard do + import ElixirSense.Core.State + + alias ElixirSense.Core.MetadataBuilder + + # A guard expression can be in either these form: + # :and :or + # / \ or / \ or guard_expr + # guard_expr guard_expr guard_expr guard_expr + # + # type information from :and subtrees are mergeable + # type information from :or subtrees are discarded + def type_information_from_guards({:and, _, [guard_l, guard_r]}, state) do + left = type_information_from_guards(guard_l, state) + right = type_information_from_guards(guard_r, state) + + Keyword.merge(left, right, fn _k, v1, v2 -> {:intersection, [v1, v2]} end) + end + + def type_information_from_guards({:or, _, [guard_l, guard_r]}, state) do + left = type_information_from_guards(guard_l, state) + right = type_information_from_guards(guard_r, state) + + Keyword.merge(left, right, fn _k, v1, v2 -> + case {v1, v2} do + {{:union, types_1}, {:union, types_2}} -> {:union, types_1 ++ types_2} + {{:union, types}, _} -> {:union, types ++ [v2]} + {_, {:union, types}} -> {:union, [v1 | types]} + _ -> {:union, [v1, v2]} + end + end) + end + + def type_information_from_guards(guard_ast, state) do + {_, acc} = + Macro.prewalk(guard_ast, [], fn + # Standalone variable: func my_func(x) when x + {var, _, nil} = node, acc -> + {node, [{var, :boolean} | acc]} + + {guard_predicate, _, params} = node, acc -> + case guard_predicate_type(guard_predicate, params, state) do + {type, binding} -> + {var, _, nil} = binding + # If we found the predicate type, we can prematurely exit traversing the subtree + {[], [{var, type} | acc]} + + nil -> + {node, acc} + end + + node, acc -> + {node, acc} + end) + + acc + end + + defp guard_predicate_type(p, params, _) + when p in [:is_number, :is_float, :is_integer, :round, :trunc, :div, :rem, :abs], + do: {:number, hd(params)} + + defp guard_predicate_type(p, params, _) when p in [:is_binary, :binary_part], + do: {:binary, hd(params)} + + defp guard_predicate_type(p, params, _) when p in [:is_bitstring, :bit_size, :byte_size], + do: {:bitstring, hd(params)} + + defp guard_predicate_type(p, params, _) when p in [:is_list, :length], do: {:list, hd(params)} + + defp guard_predicate_type(p, params, _) when p in [:hd, :tl], + do: {{:list, :boolean}, hd(params)} + + # when hd(x) == 1 + # when tl(x) <= 2 + defp guard_predicate_type(p, [{guard, _, guard_params}, rhs], _) + when p in [:==, :===, :>=, :>, :<=, :<] and guard in [:hd, :tl] do + rhs_type = + cond do + is_number(rhs) -> :number + is_binary(rhs) -> :binary + is_bitstring(rhs) -> :bitstring + is_atom(rhs) -> :atom + is_boolean(rhs) -> :boolean + true -> nil + end + + rhs_type = if rhs_type, do: {:list, rhs_type}, else: :list + + {rhs_type, hd(guard_params)} + end + + defp guard_predicate_type(p, params, _) when p in [:is_tuple, :elem], + do: {:tuple, hd(params)} + + # when tuple_size(x) == 1 + # when tuple_size(x) == 2 + defp guard_predicate_type(p, [{:tuple_size, _, guard_params}, size], _) + when p in [:==, :===] do + type = + if is_integer(size) do + {:tuple, size, []} + else + :tuple + end + + {type, hd(guard_params)} + end + + defp guard_predicate_type(:is_map, params, _), do: {{:map, [], nil}, hd(params)} + defp guard_predicate_type(:map_size, params, _), do: {{:map, [], nil}, hd(params)} + + defp guard_predicate_type(:is_map_key, [var, key], state) do + type = + case MetadataBuilder.get_binding_type(state, key) do + {:atom, key} -> {:map, [{key, nil}], nil} + nil when is_binary(key) -> {:map, [{key, nil}], nil} + _ -> {:map, [], nil} + end + + {type, var} + end + + defp guard_predicate_type(:is_atom, params, _), do: {:atom, hd(params)} + defp guard_predicate_type(:is_boolean, params, _), do: {:boolean, hd(params)} + + defp guard_predicate_type(:is_struct, [var, {:__aliases__, _, list}], state) do + type = {:struct, [], {:atom, expand_alias(state, list)}, nil} + {type, var} + end + + defp guard_predicate_type(:is_struct, params, _), do: {{:struct, [], nil, nil}, hd(params)} + defp guard_predicate_type(_, _, _), do: nil +end diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 8f4f2ff4..9ea2c9e6 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -13,7 +13,7 @@ defmodule ElixirSense.Core.MetadataBuilder do alias ElixirSense.Core.State alias ElixirSense.Core.State.VarInfo alias ElixirSense.Core.TypeInfo - alias ElixirSense.Core.Binding + alias ElixirSense.Core.Guard @scope_keywords [:for, :fn, :with] @block_keywords [:do, :else, :rescue, :catch, :after] @@ -1670,7 +1670,7 @@ defmodule ElixirSense.Core.MetadataBuilder do end def infer_type_from_guards(guard_ast, vars, state) do - type_info = type_information_from_guards(guard_ast, state) + type_info = Guard.type_information_from_guards(guard_ast, state) Enum.reduce(type_info, vars, fn {var, type}, acc -> index = Enum.find_index(acc, &(&1.name == var)) @@ -1681,134 +1681,6 @@ defmodule ElixirSense.Core.MetadataBuilder do end) end - # A guard expression can be in either these form: - # :and :or - # / \ or / \ or guard_expr - # guard_expr guard_expr guard_expr guard_expr - # - # type information from :and subtrees are mergeable - # type information from :or subtrees are discarded - defp type_information_from_guards({:and, _, [guard_l, guard_r]}, state) do - left = type_information_from_guards(guard_l, state) - right = type_information_from_guards(guard_r, state) - Keyword.merge(left, right, fn _k, v1, v2 -> {:intersection, [v1, v2]} end) - end - - defp type_information_from_guards({:or, _, [guard_l, guard_r]}, state) do - left = type_information_from_guards(guard_l, state) - right = type_information_from_guards(guard_r, state) - - Keyword.merge(left, right, fn _k, v1, v2 -> - case {v1, v2} do - {{:union, types_1}, {:union, types_2}} -> {:union, types_1 ++ types_2} - {{:union, types}, _} -> {:union, types ++ [v2]} - {_, {:union, types}} -> {:union, [v1 | types]} - _ -> {:union, [v1, v2]} - end - end) - end - - defp type_information_from_guards(guard_ast, state) do - {_, acc} = - Macro.prewalk(guard_ast, [], fn - # Standalone variable: func my_func(x) when x - {var, _, nil} = node, acc -> - {node, [{var, :boolean} | acc]} - - {guard_predicate, _, params} = node, acc -> - case guard_predicate_type(guard_predicate, params, state) do - {type, binding} -> - {var, _, nil} = binding - # If we found the predicate type, we can prematurely exit traversing the subtree - {[], [{var, type} | acc]} - - nil -> - {node, acc} - end - - node, acc -> - {node, acc} - end) - - acc - end - - defp guard_predicate_type(p, params, _) - when p in [:is_number, :is_float, :is_integer, :round, :trunc, :div, :rem, :abs], - do: {:number, hd(params)} - - defp guard_predicate_type(p, params, _) when p in [:is_binary, :binary_part], - do: {:binary, hd(params)} - - defp guard_predicate_type(p, params, _) when p in [:is_bitstring, :bit_size, :byte_size], - do: {:bitstring, hd(params)} - - defp guard_predicate_type(p, params, _) when p in [:is_list, :length], do: {:list, hd(params)} - - defp guard_predicate_type(p, params, _) when p in [:hd, :tl], - do: {{:list, :boolean}, hd(params)} - - # when hd(x) == 1 - # when tl(x) <= 2 - defp guard_predicate_type(p, [{guard, _, guard_params}, rhs], _) - when p in [:==, :===, :>=, :>, :<=, :<] and guard in [:hd, :tl] do - rhs_type = - cond do - is_number(rhs) -> :number - is_binary(rhs) -> :binary - is_bitstring(rhs) -> :bitstring - is_atom(rhs) -> :atom - is_boolean(rhs) -> :boolean - true -> nil - end - - rhs_type = if rhs_type, do: {:list, rhs_type}, else: :list - - {rhs_type, hd(guard_params)} - end - - defp guard_predicate_type(p, params, _) when p in [:is_tuple, :elem], - do: {:tuple, hd(params)} - - # when tuple_size(x) == 1 - # when tuple_size(x) == 2 - defp guard_predicate_type(p, [{:tuple_size, _, guard_params}, size], _) - when p in [:==, :===] do - type = - if is_integer(size) do - {:tuple, size, []} - else - :tuple - end - - {type, hd(guard_params)} - end - - defp guard_predicate_type(:is_map, params, _), do: {{:map, [], nil}, hd(params)} - defp guard_predicate_type(:map_size, params, _), do: {{:map, [], nil}, hd(params)} - - defp guard_predicate_type(:is_map_key, [var, key], state) do - type = - case get_binding_type(state, key) do - {:atom, key} -> {:map, [{key, nil}], nil} - nil when is_binary(key) -> {:map, [{key, nil}], nil} - _ -> {:map, [], nil} - end - - {type, var} - end - - defp guard_predicate_type(:is_atom, params, _), do: {:atom, hd(params)} - defp guard_predicate_type(:is_boolean, params, _), do: {:boolean, hd(params)} - - defp guard_predicate_type(:is_struct, [var, {:__aliases__, _, list}], state) do - type = {:struct, [], {:atom, expand_alias(state, list)}, nil} - {type, var} - end - - defp guard_predicate_type(:is_struct, params, _), do: {{:struct, [], nil, nil}, hd(params)} - defp guard_predicate_type(_, _, _), do: nil - # struct or struct update def get_binding_type( state, From 7bcaa682f71dc4629d5aec31578bf00c83d668ca Mon Sep 17 00:00:00 2001 From: goose Date: Tue, 31 Oct 2023 21:15:15 +0700 Subject: [PATCH 9/9] Add moduledoc --- lib/elixir_sense/core/guard.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/elixir_sense/core/guard.ex b/lib/elixir_sense/core/guard.ex index 1a246b0e..aa7a93ac 100644 --- a/lib/elixir_sense/core/guard.ex +++ b/lib/elixir_sense/core/guard.ex @@ -1,4 +1,8 @@ defmodule ElixirSense.Core.Guard do + @moduledoc """ + This module is responsible for infer type information from guard expressions + """ + import ElixirSense.Core.State alias ElixirSense.Core.MetadataBuilder