diff --git a/lib/elixir/lib/calendar/date_range.ex b/lib/elixir/lib/calendar/date_range.ex index 986070f8ed6..a92b9d124f5 100644 --- a/lib/elixir/lib/calendar/date_range.ex +++ b/lib/elixir/lib/calendar/date_range.ex @@ -171,8 +171,12 @@ defmodule Date.Range do when step < 0 and first_days < last_days, do: 0 - defp size(%Date.Range{first_in_iso_days: first_days, last_in_iso_days: last_days, step: step}), - do: abs(div(last_days - first_days, step)) + 1 + defp size(%Date.Range{ + first_in_iso_days: first_days, + last_in_iso_days: last_days, + step: step + }), + do: abs(div(last_days - first_days, step)) + 1 # TODO: Remove me on v2.0 defp size( diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex index ec4730b8f7c..7e1ea6ca876 100644 --- a/lib/elixir/lib/code/formatter.ex +++ b/lib/elixir/lib/code/formatter.ex @@ -617,9 +617,12 @@ defmodule Code.Formatter do end doc = - with_next_break_fits(next_break_fits?(right_arg, state), right, fn right -> - concat(group(left), group(nest(glue(op, group(right)), 2, :break))) - end) + concat( + group(left), + with_next_break_fits(next_break_fits?(right_arg, state), right, fn right -> + nest(glue(op, right), 2, :break) + end) + ) {doc, state} end @@ -818,9 +821,8 @@ defmodule Code.Formatter do {" " <> op_string, with_next_break_fits(next_break_fits?, right, fn right -> - right = nest(concat(break(), group(right)), nesting, :break) - right = if eol?, do: force_unfit(right), else: right - group(right) + right = nest(concat(break(), right), nesting, :break) + if eol?, do: force_unfit(right), else: right end)} end @@ -1262,7 +1264,7 @@ defmodule Code.Formatter do args_doc = if skip_parens? do left_doc - |> concat(next_break_fits(group(right_doc, :inherit), :enabled)) + |> concat(group(right_doc, :optimistic)) |> nest(:cursor, :break) else right_doc = @@ -1270,8 +1272,7 @@ defmodule Code.Formatter do |> nest(2, :break) |> concat(break("")) |> concat(")") - |> group(:inherit) - |> next_break_fits(:enabled) + |> group(:optimistic) concat(nest(left_doc, 2, :break), right_doc) end @@ -1314,13 +1315,11 @@ defmodule Code.Formatter do |> concat(args_doc) |> nest(2) |> concat(extra) - |> group() skip_parens? -> " " |> concat(args_doc) |> concat(extra) - |> group() true -> "(" @@ -1328,13 +1327,12 @@ defmodule Code.Formatter do |> nest(2, :break) |> concat(args_doc) |> concat(extra) - |> group() end if next_break_fits? do - {next_break_fits(doc, :disabled), state} + {group(doc, :pessimistic), state} else - {doc, state} + {group(doc), state} end end @@ -1800,10 +1798,17 @@ defmodule Code.Formatter do doc = case args do - [_ | _] -> concat_to_last_group(doc, ",") - [] when last_arg_mode == :force_comma -> concat_to_last_group(doc, ",") - [] when last_arg_mode == :next_break_fits -> next_break_fits(doc, :enabled) - [] when last_arg_mode == :none -> doc + [_ | _] -> + concat_to_last_group(doc, ",") + + [] when last_arg_mode == :force_comma -> + concat_to_last_group(doc, ",") + + [] when last_arg_mode == :next_break_fits -> + doc |> ungroup_if_group() |> group(:optimistic) + + [] when last_arg_mode == :none -> + doc end {{doc, @empty, 1}, state} @@ -2321,11 +2326,14 @@ defmodule Code.Formatter do defp with_next_break_fits(condition, doc, fun) do if condition do doc - |> next_break_fits(:enabled) + |> group(:optimistic) |> fun.() - |> next_break_fits(:disabled) + |> group(:pessimistic) else - fun.(doc) + doc + |> group() + |> fun.() + |> group() end end diff --git a/lib/elixir/lib/inspect/algebra.ex b/lib/elixir/lib/inspect/algebra.ex index ea44c037b28..d51310e665d 100644 --- a/lib/elixir/lib/inspect/algebra.ex +++ b/lib/elixir/lib/inspect/algebra.ex @@ -237,8 +237,8 @@ defmodule Inspect.Algebra do Flex breaks, however, are re-evaluated on every occurrence and may still be rendered flat. See `break/1` and `flex_break/1` for more information. - This implementation also adds `force_unfit/1` and `next_break_fits/2` which - give more control over the document fitting. + This implementation also adds `force_unfit/1` and optimistic/pessimistic + groups which give more control over the document fitting. [0]: https://lindig.github.io/papers/strictly-pretty-2000.pdf @@ -247,7 +247,6 @@ defmodule Inspect.Algebra do @container_separator "," @tail_separator " |" @newline "\n" - @next_break_fits :enabled # Functional interface to "doc" records @@ -291,7 +290,7 @@ defmodule Inspect.Algebra do quote do: {:doc_break, unquote(break), unquote(mode)} end - @typep doc_group :: {:doc_group, t, :inherit | :self} + @typep doc_group :: {:doc_group, t, :normal | :optimistic | :pessimistic | :inherit} defmacrop doc_group(group, mode) do quote do: {:doc_group, unquote(group), unquote(mode)} end @@ -744,48 +743,11 @@ defmodule Inspect.Algebra do @doc """ Considers the next break as fit. - - `mode` can be `:enabled` or `:disabled`. When `:enabled`, - it will consider the document as fit as soon as it finds - the next break, effectively cancelling the break. It will - also ignore any `force_unfit/1` in search of the next break. - - When disabled, it behaves as usual and it will ignore - any further `next_break_fits/2` instruction. - - ## Examples - - This is used by Elixir's code formatter to avoid breaking - code at some specific locations. For example, consider this - code: - - some_function_call(%{..., key: value, ...}) - - Now imagine that this code does not fit its line. The code - formatter introduces breaks inside `(` and `)` and inside - `%{` and `}`. Therefore the document would break as: - - some_function_call( - %{ - ..., - key: value, - ... - } - ) - - The formatter wraps the algebra document representing the - map in `next_break_fits/1` so the code is formatted as: - - some_function_call(%{ - ..., - key: value, - ... - }) - """ - @doc since: "1.6.0" - @spec next_break_fits(t, :enabled | :disabled) :: doc_fits - def next_break_fits(doc, mode \\ @next_break_fits) + # TODO: Deprecate me on Elixir v1.23 + @doc deprecated: "Pass the optimistic/pessimistic type to group/2 instead" + @spec next_break_fits(t, :enabled | :disabled) :: doc_group + def next_break_fits(doc, mode \\ :enabled) when is_doc(doc) and mode in [:enabled, :disabled] do doc_fits(doc, mode) end @@ -872,10 +834,31 @@ defmodule Inspect.Algebra do Returns a group containing the specified document `doc`. Documents in a group are attempted to be rendered together - to the best of the renderer ability. + to the best of the renderer ability. If there are `break/1`s + in the group and the group does not fit the given width, + the breaks are converted into lines. Otherwise the breaks + are rendered as text based on their string contents. + + There are three types of groups, described next. - The group mode can also be set to `:inherit`, which means it - automatically breaks if the parent group has broken too. + ## Group modes + + * `:normal` - the group fits if it fits within the given width + + * `:optimistic` - the group fits if it fits within the given + width. However, when nested within another group, the parent + group will assume this group fits as long as it has a single + break, even if the optimistic group has a `force_unfit/1` + document within it. Overall, this has an effect similar + to swapping the order groups break. For example, if you have + a `parent_group(child_group)` and they do not fit, the parent + converts breaks into newlines first, allowing the child to compute + if it fits. However, if the child group is optimistic and it + has breaks, then the parent assumes it fits, leaving the overall + fitting decision to the child + + * `:pessimistic` - the group fits if it fits within the given + width. However it disables any optimistic group within it ## Examples @@ -902,10 +885,50 @@ defmodule Inspect.Algebra do iex> Inspect.Algebra.format(doc, 6) ["Hello,", "\n", "A", "\n", "B"] + ## Mode examples + + The different groups modes are used by Elixir's code formatter + to avoid breaking code at some specific locations. For example, + consider this code: + + some_function_call(%{..., key: value, ...}) + + Now imagine that this code does not fit its line. The code + formatter introduces breaks inside `(` and `)` and inside + `%{` and `}`, each within their own group. Therefore the + document would break as: + + some_function_call( + %{ + ..., + key: value, + ... + } + ) + + To address this, the formatter marks the inner group as optimistic. + This means the first group, which is `(...)` will consider the document + fits and avoids adding breaks around the parens. So overall the code + is formatted as: + + some_function_call(%{ + ..., + key: value, + ... + }) + """ - @spec group(t, :self | :inherit) :: doc_group - def group(doc, mode \\ :self) when is_doc(doc) do - doc_group(doc, mode) + @spec group(t, :normal | :optimistic | :pessimistic) :: doc_group + def group(doc, mode \\ :normal) when is_doc(doc) do + doc_group( + doc, + case mode do + # TODO: Deprecate :self and :inherit on Elixir v1.23 + :self -> :normal + :inherit -> :inherit + mode when mode in [:normal, :optimistic, :pessimistic] -> mode + end + ) end @doc ~S""" @@ -1019,17 +1042,20 @@ defmodule Inspect.Algebra do # # * flat - represents a document with breaks as flats (a break may fit, as it may break) # * break - represents a document with breaks as breaks (a break always fits, since it breaks) + # + # These other two modes only affect fitting: + # # * flat_no_break - represents a document with breaks as flat not allowed to enter in break mode # * break_no_flat - represents a document with breaks as breaks not allowed to enter in flat mode # @typep mode :: :flat | :flat_no_break | :break | :break_no_flat - @spec fits( + @spec fits?( width :: non_neg_integer() | :infinity, column :: non_neg_integer(), break? :: boolean(), entries - ) :: :fit | :no_fit | :break_next + ) :: boolean() when entries: maybe_improper_list( {integer(), mode(), t()} | :group_over, @@ -1041,79 +1067,80 @@ defmodule Inspect.Algebra do # # In case we have groups and the group fits, we need to consider the group # parent without the child breaks, hence {:tail, b?, t} below. - defp fits(w, k, b?, _) when k > w and b?, do: :no_fit - defp fits(_, _, _, []), do: :fit - defp fits(w, k, _, {:tail, b?, t}), do: fits(w, k, b?, t) + defp fits?(w, k, b?, _) when k > w and b?, do: false + defp fits?(_, _, _, []), do: true + defp fits?(w, k, _, {:tail, b?, t}), do: fits?(w, k, b?, t) + + ## Group over + # If we get to the end of the group and if fits, it is because + # something already broke elsewhere, so we can consider the group + # fits. This only appears when checking if a flex break and fitting. + + defp fits?(_w, _k, b?, [:group_over | _]), + do: b? ## Flat no break - defp fits(w, k, b?, [{i, _, doc_fits(x, :disabled)} | t]), - do: fits(w, k, b?, [{i, :flat_no_break, x} | t]) + defp fits?(w, k, b?, [{i, _, doc_fits(x, :disabled)} | t]), + do: fits?(w, k, b?, [{i, :flat_no_break, x} | t]) - defp fits(w, k, b?, [{i, :flat_no_break, doc_fits(x, _)} | t]), - do: fits(w, k, b?, [{i, :flat_no_break, x} | t]) + defp fits?(w, k, b?, [{i, :flat_no_break, doc_fits(x, _)} | t]), + do: fits?(w, k, b?, [{i, :flat_no_break, x} | t]) - ## Breaks no flat + defp fits?(w, k, b?, [{i, _, doc_group(x, :pessimistic)} | t]), + do: fits?(w, k, b?, [{i, :flat_no_break, x} | t]) - defp fits(w, k, b?, [{i, _, doc_fits(x, :enabled)} | t]), - do: fits(w, k, b?, [{i, :break_no_flat, x} | t]) + defp fits?(w, k, b?, [{i, :flat_no_break, doc_group(x, _)} | t]), + do: fits?(w, k, b?, [{i, :flat_no_break, x} | t]) - defp fits(w, k, b?, [{i, :break_no_flat, doc_force(x)} | t]), - do: fits(w, k, b?, [{i, :break_no_flat, x} | t]) + ## Breaks no flat - defp fits(w, k, b?, [{i, :break_no_flat, x} | t]) - when x == :doc_line or (is_tuple(x) and elem(x, 0) == :doc_break) do - case fits(w, k, b?, [{i, :flat, x} | t]) do - :no_fit -> :break_next - fits -> fits - end - end + defp fits?(w, k, b?, [{i, _, doc_fits(x, :enabled)} | t]), + do: fits?(w, k, b?, [{i, :break_no_flat, x} | t]) - ## Group over - # If we get to the end of the group and if fits, it is because - # something already broke elsewhere, so we can consider the group - # fits. This only appears when checking if a flex break fits. + defp fits?(w, k, b?, [{i, _, doc_group(x, :optimistic)} | t]), + do: fits?(w, k, b?, [{i, :break_no_flat, x} | t]) - defp fits(_w, _k, true, [:group_over | _]), - do: :fit + defp fits?(w, k, b?, [{i, :break_no_flat, doc_force(x)} | t]), + do: fits?(w, k, b?, [{i, :break_no_flat, x} | t]) - defp fits(w, k, b?, [:group_over | t]), - do: fits(w, k, b?, t) + defp fits?(_, _, _, [{_, :break_no_flat, doc_break(_, _)} | _]), do: true + defp fits?(_, _, _, [{_, :break_no_flat, :doc_line} | _]), do: true ## Breaks - defp fits(_, _, _, [{_, :break, doc_break(_, _)} | _]), do: :fit - defp fits(_, _, _, [{_, :break, :doc_line} | _]), do: :fit + defp fits?(_, _, _, [{_, :break, doc_break(_, _)} | _]), do: true + defp fits?(_, _, _, [{_, :break, :doc_line} | _]), do: true - defp fits(w, k, b?, [{i, :break, doc_group(x, _)} | t]), - do: fits(w, k, b?, [{i, :flat, x} | {:tail, b?, t}]) + defp fits?(w, k, b?, [{i, :break, doc_group(x, _)} | t]), + do: fits?(w, k, b?, [{i, :flat, x} | {:tail, b?, t}]) ## Catch all - defp fits(w, _, _, [{i, _, :doc_line} | t]), do: fits(w, i, false, t) - defp fits(w, k, b?, [{_, _, :doc_nil} | t]), do: fits(w, k, b?, t) - defp fits(w, _, b?, [{i, _, doc_collapse(_)} | t]), do: fits(w, i, b?, t) - defp fits(w, k, b?, [{i, m, doc_color(x, _)} | t]), do: fits(w, k, b?, [{i, m, x} | t]) - defp fits(w, k, b?, [{_, _, doc_string(_, l)} | t]), do: fits(w, k + l, b?, t) - defp fits(w, k, b?, [{_, _, s} | t]) when is_binary(s), do: fits(w, k + byte_size(s), b?, t) - defp fits(_, _, _, [{_, _, doc_force(_)} | _]), do: :no_fit - defp fits(w, k, _, [{_, _, doc_break(s, _)} | t]), do: fits(w, k + byte_size(s), true, t) - defp fits(w, k, b?, [{i, m, doc_nest(x, _, :break)} | t]), do: fits(w, k, b?, [{i, m, x} | t]) + defp fits?(w, _, _, [{i, _, :doc_line} | t]), do: fits?(w, i, false, t) + defp fits?(w, k, b?, [{_, _, :doc_nil} | t]), do: fits?(w, k, b?, t) + defp fits?(w, _, b?, [{i, _, doc_collapse(_)} | t]), do: fits?(w, i, b?, t) + defp fits?(w, k, b?, [{i, m, doc_color(x, _)} | t]), do: fits?(w, k, b?, [{i, m, x} | t]) + defp fits?(w, k, b?, [{_, _, doc_string(_, l)} | t]), do: fits?(w, k + l, b?, t) + defp fits?(w, k, b?, [{_, _, s} | t]) when is_binary(s), do: fits?(w, k + byte_size(s), b?, t) + defp fits?(_, _, _, [{_, _, doc_force(_)} | _]), do: false + defp fits?(w, k, _, [{_, _, doc_break(s, _)} | t]), do: fits?(w, k + byte_size(s), true, t) + defp fits?(w, k, b?, [{i, m, doc_nest(x, _, :break)} | t]), do: fits?(w, k, b?, [{i, m, x} | t]) - defp fits(w, k, b?, [{i, m, doc_nest(x, j, _)} | t]), - do: fits(w, k, b?, [{apply_nesting(i, k, j), m, x} | t]) + defp fits?(w, k, b?, [{i, m, doc_nest(x, j, _)} | t]), + do: fits?(w, k, b?, [{apply_nesting(i, k, j), m, x} | t]) - defp fits(w, k, b?, [{i, m, doc_cons(x, y)} | t]), - do: fits(w, k, b?, [{i, m, x}, {i, m, y} | t]) + defp fits?(w, k, b?, [{i, m, doc_cons(x, y)} | t]), + do: fits?(w, k, b?, [{i, m, x}, {i, m, y} | t]) - defp fits(w, k, b?, [{i, m, doc_group(x, _)} | t]), - do: fits(w, k, b?, [{i, m, x} | {:tail, b?, t}]) + defp fits?(w, k, b?, [{i, m, doc_group(x, _)} | t]), + do: fits?(w, k, b?, [{i, m, x} | {:tail, b?, t}]) - defp fits(w, k, b?, [{i, m, doc_limit(x, :infinity)} | t]) when w != :infinity, - do: fits(:infinity, k, b?, [{i, :flat, x}, {i, m, doc_limit(empty(), w)} | t]) + defp fits?(w, k, b?, [{i, m, doc_limit(x, :infinity)} | t]) when w != :infinity, + do: fits?(:infinity, k, b?, [{i, :flat, x}, {i, m, doc_limit(empty(), w)} | t]) - defp fits(_w, k, b?, [{i, m, doc_limit(x, w)} | t]), - do: fits(w, k, b?, [{i, m, x} | t]) + defp fits?(_w, k, b?, [{i, m, doc_limit(x, w)} | t]), + do: fits?(w, k, b?, [{i, m, x} | t]) @spec format( width :: non_neg_integer() | :infinity, @@ -1128,10 +1155,6 @@ defmodule Inspect.Algebra do defp format(w, k, [{_, _, doc_string(s, l)} | t]), do: [s | format(w, k + l, t)] defp format(w, k, [{_, _, s} | t]) when is_binary(s), do: [s | format(w, k + byte_size(s), t)] defp format(w, k, [{i, m, doc_force(x)} | t]), do: format(w, k, [{i, m, x} | t]) - - defp format(w, k, [{i, :flat_no_break, doc_fits(x, :enabled)} | t]), - do: format(w, k, [{i, :break_no_flat, x} | t]) - defp format(w, k, [{i, m, doc_fits(x, _)} | t]), do: format(w, k, [{i, m, x} | t]) defp format(w, _, [{i, _, doc_collapse(max)} | t]), do: collapse(format(w, i, t), max, 0, i) @@ -1139,7 +1162,7 @@ defmodule Inspect.Algebra do defp format(w, k, [{i, m, doc_break(s, :flex)} | t]) do k = k + byte_size(s) - if w == :infinity or m == :flat or fits(w, k, true, t) != :no_fit do + if w == :infinity or m == :flat or fits?(w, k, true, t) do [s | format(w, k, t)] else [indent(i) | format(w, i, t)] @@ -1169,21 +1192,24 @@ defmodule Inspect.Algebra do format(w, k, t) end + # TODO: Deprecate me in Elixir v1.23 defp format(w, k, [{i, :break, doc_group(x, :inherit)} | t]) do format(w, k, [{i, :break, x} | t]) end - defp format(w, k, [{i, :break_no_flat, doc_group(x, _)} | t]) do - format(w, k, [{i, :break, x} | t]) + defp format(w, k, [{i, :flat, doc_group(x, :optimistic)} | t]) do + if w == :infinity or fits?(w, k, false, [{i, :flat, x} | t]) do + format(w, k, [{i, :flat, x}, :group_over | t]) + else + format(w, k, [{i, :break, x}, :group_over | t]) + end end defp format(w, k, [{i, _, doc_group(x, _)} | t]) do - fits = if w == :infinity, do: :fit, else: fits(w, k, false, [{i, :flat, x}]) - - case fits do - :fit -> format(w, k, [{i, :flat, x} | t]) - :no_fit -> format(w, k, [{i, :break, x}, :group_over | t]) - :break_next -> format(w, k, [{i, :flat_no_break, x}, :group_over | t]) + if w == :infinity or fits?(w, k, false, [{i, :flat, x}]) do + format(w, k, [{i, :flat, x}, :group_over | t]) + else + format(w, k, [{i, :break, x}, :group_over | t]) end end diff --git a/lib/elixir/test/elixir/inspect/algebra_test.exs b/lib/elixir/test/elixir/inspect/algebra_test.exs index faaa174655a..e969bd0b35e 100644 --- a/lib/elixir/test/elixir/inspect/algebra_test.exs +++ b/lib/elixir/test/elixir/inspect/algebra_test.exs @@ -196,25 +196,29 @@ defmodule Inspect.AlgebraTest do test "group doc" do # Consistent with definitions - assert group("ab") == {:doc_group, "ab", :self} - assert group(empty()) == {:doc_group, empty(), :self} + assert group("ab") == {:doc_group, "ab", :normal} + assert group(empty()) == {:doc_group, empty(), :normal} # Consistent formatting doc = concat(glue(glue(glue("hello", "a"), "b"), "c"), "d") assert render(group(doc), 5) == "hello\na\nb\ncd" end - test "group doc with inherit" do - # Consistent with definitions - assert group("ab", :inherit) == {:doc_group, "ab", :inherit} - assert group(empty(), :inherit) == {:doc_group, empty(), :inherit} + test "group modes doc" do + doc = glue(glue("hello", "a"), "b") + assert render(doc, 10) == "hello a b" - # Consistent formatting - doc = concat(glue(glue(group(glue("a", "b"), :self), "c"), "d"), "hello") - assert render(group(doc), 5) == "a b\nc\ndhello" + assert render(doc |> glue("c") |> group(), 10) == + "hello\na\nb\nc" + + assert render(doc |> group() |> glue("c") |> group() |> glue("d"), 10) == + "hello a b\nc\nd" + + assert render(doc |> group(:optimistic) |> glue("c") |> group() |> glue("d"), 10) == + "hello\na\nb c d" - doc = concat(glue(glue(group(glue("a", "b"), :inherit), "c"), "d"), "hello") - assert render(group(doc), 5) == "a\nb\nc\ndhello" + assert render(doc |> group(:optimistic) |> glue("c") |> group(:pessimistic) |> glue("d"), 10) == + "hello\na\nb c\nd" end test "no limit doc" do @@ -258,19 +262,18 @@ defmodule Inspect.AlgebraTest do assert force_unfit("ab") == {:doc_force, "ab"} assert force_unfit(empty()) == {:doc_force, empty()} - # Consistent with definitions - assert next_break_fits("ab") == {:doc_fits, "ab", :enabled} - assert next_break_fits(empty()) == {:doc_fits, empty(), :enabled} - assert next_break_fits("ab", :disabled) == {:doc_fits, "ab", :disabled} - assert next_break_fits(empty(), :disabled) == {:doc_fits, empty(), :disabled} - # Consistent formatting - doc = force_unfit(concat(glue(glue(glue("hello", "a"), "b"), "c"), "d")) - assert render(doc, 20) == "hello\na\nb\ncd" - assert render(next_break_fits(doc, :enabled), 20) == "hello a b cd" + doc = force_unfit(glue(glue("hello", "a"), "b")) + assert render(doc, 20) == "hello\na\nb" + + assert render(doc |> glue("c") |> group(), 20) == + "hello\na\nb\nc" + + assert render(doc |> group(:optimistic) |> glue("c") |> group() |> glue("d"), 20) == + "hello\na\nb c d" - assert render(next_break_fits(next_break_fits(doc, :enabled), :disabled), 20) == - "hello\na\nb\ncd" + assert render(doc |> group(:optimistic) |> glue("c") |> group(:pessimistic) |> glue("d"), 20) == + "hello\na\nb c\nd" end test "formatting groups with lines" do