-module(metamon@diff). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/metamon/diff.gleam"). -export([diff/2, diff_string/2, render/1]). -export_type([diff/0, indexed_diff/0, segment/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Structural diff for failure reports. Compares two values\n" " element-by-element and produces a tree that the formatter can\n" " render compactly.\n" "\n" " The implementation is type-erased: it inspects the dynamic shape\n" " of each side via `string.inspect`. This is sufficient for the\n" " failure-report use case (we only need to *display* the diff, not\n" " reason about it programmatically), and avoids requiring users to\n" " supply per-type formatters.\n" ). -type diff() :: {same, binary()} | {differ, binary(), binary()} | {list_diff, list(indexed_diff())} | {tuple_diff, list(indexed_diff())} | {string_diff, binary(), binary(), list(segment())}. -type indexed_diff() :: {indexed_diff, integer(), diff()}. -type segment() :: {common, binary()} | {removed, binary()} | {added, binary()}. -file("src/metamon/diff.gleam", 70). -spec is_list_inspect(binary()) -> boolean(). is_list_inspect(S) -> gleam_stdlib:string_starts_with(S, <<"["/utf8>>) andalso gleam_stdlib:string_ends_with( S, <<"]"/utf8>> ). -file("src/metamon/diff.gleam", 74). -spec is_tuple_inspect(binary()) -> boolean(). is_tuple_inspect(S) -> gleam_stdlib:string_starts_with(S, <<"#("/utf8>>) andalso gleam_stdlib:string_ends_with( S, <<")"/utf8>> ). -file("src/metamon/diff.gleam", 94). -spec drop_tuple_prefix(binary()) -> binary(). drop_tuple_prefix(Inspected) -> case gleam_stdlib:string_starts_with(Inspected, <<"#("/utf8>>) of true -> <<"("/utf8, (gleam@string:drop_start(Inspected, 2))/binary>>; false -> Inspected end. -file("src/metamon/diff.gleam", 101). -spec zip_items(list(binary()), list(binary()), integer()) -> list(indexed_diff()). zip_items(Left, Right, Index) -> case {Left, Right} of {[], []} -> []; {[], [R | Rs]} -> [{indexed_diff, Index, {differ, <<"·"/utf8>>, R}} | zip_items([], Rs, Index + 1)]; {[L | Ls], []} -> [{indexed_diff, Index, {differ, L, <<"·"/utf8>>}} | zip_items(Ls, [], Index + 1)]; {[L@1 | Ls@1], [R@1 | Rs@1]} -> Inner = case L@1 =:= R@1 of true -> {same, L@1}; false -> {differ, L@1, R@1} end, [{indexed_diff, Index, Inner} | zip_items(Ls@1, Rs@1, Index + 1)] end. -file("src/metamon/diff.gleam", 174). -spec finish_split(binary(), list(binary())) -> list(binary()). finish_split(Acc, Results) -> case gleam@string:trim(Acc) of <<""/utf8>> -> lists:reverse(Results); Trimmed -> lists:reverse([Trimmed | Results]) end. -file("src/metamon/diff.gleam", 154). -spec walk_and_split(list(binary()), integer(), binary(), list(binary())) -> list(binary()). walk_and_split(Graphemes, Depth, Acc, Results) -> case Graphemes of [] -> finish_split(Acc, Results); [First | Rest] -> case {First, Depth} of {<<"["/utf8>>, _} -> walk_and_split( Rest, Depth + 1, <>, Results ); {<<"]"/utf8>>, D} when D > 0 -> walk_and_split( Rest, D - 1, <>, Results ); {<<"("/utf8>>, _} -> walk_and_split( Rest, Depth + 1, <>, Results ); {<<")"/utf8>>, D@1} when D@1 > 0 -> walk_and_split( Rest, D@1 - 1, <>, Results ); {<<","/utf8>>, 0} -> walk_and_split( Rest, 0, <<""/utf8>>, [gleam@string:trim(Acc) | Results] ); {_, _} -> walk_and_split( Rest, Depth, <>, Results ) end end. -file("src/metamon/diff.gleam", 126). -spec split_top_level(binary()) -> list(binary()). split_top_level(Inspected) -> Inner = case {gleam_stdlib:string_starts_with(Inspected, <<"["/utf8>>), gleam_stdlib:string_ends_with(Inspected, <<"]"/utf8>>)} of {true, true} -> _pipe = Inspected, _pipe@1 = gleam@string:drop_start(_pipe, 1), gleam@string:drop_end(_pipe@1, 1); {_, _} -> case {gleam_stdlib:string_starts_with(Inspected, <<"("/utf8>>), gleam_stdlib:string_ends_with(Inspected, <<")"/utf8>>)} of {true, true} -> _pipe@2 = Inspected, _pipe@3 = gleam@string:drop_start(_pipe@2, 1), gleam@string:drop_end(_pipe@3, 1); {_, _} -> Inspected end end, walk_and_split(gleam@string:to_graphemes(Inner), 0, <<""/utf8>>, []). -file("src/metamon/diff.gleam", 78). -spec list_diff_from_inspect(binary(), binary()) -> diff(). list_diff_from_inspect(Left, Right) -> L_items = split_top_level(Left), R_items = split_top_level(Right), {list_diff, zip_items(L_items, R_items, 0)}. -file("src/metamon/diff.gleam", 84). -spec tuple_diff_from_inspect(binary(), binary()) -> diff(). tuple_diff_from_inspect(Left, Right) -> L_inner = drop_tuple_prefix(Left), R_inner = drop_tuple_prefix(Right), L_items = split_top_level(L_inner), R_items = split_top_level(R_inner), {tuple_diff, zip_items(L_items, R_items, 0)}. -file("src/metamon/diff.gleam", 55). -spec shape_aware_diff(GFI, GFI) -> diff(). shape_aware_diff(Left, Right) -> L_str = gleam@string:inspect(Left), R_str = gleam@string:inspect(Right), case {is_list_inspect(L_str), is_list_inspect(R_str), is_tuple_inspect(L_str), is_tuple_inspect(R_str)} of {true, true, _, _} -> list_diff_from_inspect(L_str, R_str); {_, _, true, true} -> tuple_diff_from_inspect(L_str, R_str); {_, _, _, _} -> {differ, L_str, R_str} end. -file("src/metamon/diff.gleam", 48). ?DOC( " Compute a structural diff between two values. Equal inputs collapse\n" " to `Same`; otherwise the result depends on whether\n" " `string.inspect` exposes list-shaped or string-shaped output.\n" ). -spec diff(GFH, GFH) -> diff(). diff(Left, Right) -> case Left =:= Right of true -> {same, gleam@string:inspect(Left)}; false -> shape_aware_diff(Left, Right) end. -file("src/metamon/diff.gleam", 234). -spec render_string_diff(list(segment()), binary()) -> binary(). render_string_diff(Segments, Indent) -> _pipe = gleam@list:map(Segments, fun(Segment) -> case Segment of {common, Text} -> <<<>/binary, Text/binary>>; {removed, Text@1} -> <<<>/binary, Text@1/binary>>; {added, Text@2} -> <<<>/binary, Text@2/binary>> end end), gleam@string:join(_pipe, <<"\n"/utf8>>). -file("src/metamon/diff.gleam", 278). -spec empty_row(list(binary())) -> list(integer()). empty_row(Right) -> [0 | gleam@list:map(Right, fun(_) -> 0 end)]. -file("src/metamon/diff.gleam", 310). -spec max_int(integer(), integer()) -> integer(). max_int(A, B) -> case A >= B of true -> A; false -> B end. -file("src/metamon/diff.gleam", 282). -spec build_row( binary(), list(binary()), list(integer()), list(integer()), integer() ) -> list(integer()). build_row(L, Remaining, Prev_row, Acc, Current_left_value) -> case {Remaining, Prev_row} of {[], _} -> lists:reverse([Current_left_value | Acc]); {[R | Rs], [_ | Rest_prev]} -> Above = case Rest_prev of [V | _] -> V; [] -> 0 end, Diagonal = case Prev_row of [V@1 | _] -> V@1; [] -> 0 end, Value = case L =:= R of true -> Diagonal + 1; false -> max_int(Above, Current_left_value) end, build_row(L, Rs, Rest_prev, [Current_left_value | Acc], Value); {_, []} -> lists:reverse([Current_left_value | Acc]) end. -file("src/metamon/diff.gleam", 257). -spec build_rows(list(binary()), list(binary()), list(list(integer()))) -> list(list(integer())). build_rows(Remaining_left, Right, Acc) -> case Remaining_left of [] -> Row_for_empty = gleam@list:map(Right, fun(_) -> 0 end), lists:reverse([[0 | Row_for_empty] | Acc]); [First | Rest] -> Prev_row = case Acc of [] -> empty_row(Right); [Most_recent | _] -> Most_recent end, Row = build_row(First, Right, Prev_row, [], 0), build_rows(Rest, Right, [Row | Acc]) end. -file("src/metamon/diff.gleam", 253). -spec build_lcs(list(binary()), list(binary())) -> list(list(integer())). build_lcs(Left, Right) -> build_rows(Left, Right, []). -file("src/metamon/diff.gleam", 317). -spec backtrack_diff(list(binary()), list(binary()), list(list(integer()))) -> list(segment()). backtrack_diff(Left, Right, _) -> case {Left, Right} of {[], []} -> []; {[], [R | Rs]} -> [{added, R} | backtrack_diff([], Rs, [])]; {[L | Ls], []} -> [{removed, L} | backtrack_diff(Ls, [], [])]; {[L@1 | Ls@1], [R@1 | Rs@1]} -> case L@1 =:= R@1 of true -> [{common, L@1} | backtrack_diff(Ls@1, Rs@1, [])]; false -> [{removed, L@1}, {added, R@1} | backtrack_diff(Ls@1, Rs@1, [])] end end. -file("src/metamon/diff.gleam", 185). ?DOC( " Multi-line string diff using a longest-common-subsequence (LCS) on\n" " lines. Lines unique to the left side are shown as `Removed`, lines\n" " unique to the right are shown as `Added`, lines present in both\n" " are shown as `Common`.\n" ). -spec diff_string(binary(), binary()) -> diff(). diff_string(Left, Right) -> case Left =:= Right of true -> {same, Left}; false -> L_lines = gleam@string:split(Left, <<"\n"/utf8>>), R_lines = gleam@string:split(Right, <<"\n"/utf8>>), Lcs_table = build_lcs(L_lines, R_lines), Segments = backtrack_diff(L_lines, R_lines, Lcs_table), {string_diff, Left, Right, Segments} end. -file("src/metamon/diff.gleam", 214). -spec render_list_diff(list(indexed_diff()), binary(), binary()) -> binary(). render_list_diff(Items, Indent, Prefix) -> Close = case Prefix of <<"#."/utf8>> -> <<""/utf8>>; _ -> <<"]"/utf8>> end, _pipe = gleam@list:map( Items, fun(Item) -> <<<<<<<<<>/binary, (erlang:integer_to_binary(erlang:element(2, Item)))/binary>>/binary, Close/binary>>/binary, "\n"/utf8>>/binary, (render_with_indent( erlang:element(3, Item), <> ))/binary>> end ), gleam@string:join(_pipe, <<"\n"/utf8>>). -file("src/metamon/diff.gleam", 203). -spec render_with_indent(diff(), binary()) -> binary(). render_with_indent(D, Indent) -> case D of {same, Rendered} -> <<<>/binary, Left/binary>>/binary, "\n"/utf8>>/binary, Indent/binary>>/binary, "+ "/utf8>>/binary, Right/binary>>; {list_diff, Items} -> render_list_diff(Items, Indent, <<"[#"/utf8>>); {tuple_diff, Items@1} -> render_list_diff(Items@1, Indent, <<"#."/utf8>>); {string_diff, _, _, Segments} -> render_string_diff(Segments, Indent) end. -file("src/metamon/diff.gleam", 199). ?DOC(" Render a `Diff` as a human-readable multi-line string.\n"). -spec render(diff()) -> binary(). render(D) -> render_with_indent(D, <<""/utf8>>).