-module(metamon@relation). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/metamon/relation.gleam"). -export([new/2, equal/0, not_equal/0, equivalent_under/2, approximately/1, permutation_of/0, subset_of/0, set_subset_of/0, monotone/1, implies/2, 'and'/2, 'or'/2, invert/1, rename/2, n_new/2, all_equal/0, pairwise/1]). -export_type([relation/1, relation_n/1]). -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( " Named two-argument predicates used by metamorphic relations.\n" "\n" " A `Relation(b)` answers \"do these two outputs satisfy the\n" " constraint?\". The `name` is surfaced in failure reports so the\n" " user immediately sees which property broke.\n" ). -type relation(GNU) :: {relation, binary(), fun((GNU, GNU) -> boolean())}. -type relation_n(GNV) :: {relation_n, binary(), fun((list(GNV)) -> boolean())}. -file("src/metamon/relation.gleam", 17). ?DOC(" Construct a relation from a name and a binary predicate.\n"). -spec new(binary(), fun((GNW, GNW) -> boolean())) -> relation(GNW). new(Name, Holds) -> {relation, Name, Holds}. -file("src/metamon/relation.gleam", 22). ?DOC(" Equality (Gleam's structural `==`).\n"). -spec equal() -> relation(any()). equal() -> {relation, <<"equal"/utf8>>, fun(Left, Right) -> Left =:= Right end}. -file("src/metamon/relation.gleam", 27). ?DOC(" Inequality.\n"). -spec not_equal() -> relation(any()). not_equal() -> {relation, <<"not_equal"/utf8>>, fun(Left, Right) -> Left /= Right end}. -file("src/metamon/relation.gleam", 33). ?DOC( " Equality after a shared normalisation step. Useful for\n" " \"equal modulo whitespace / key order / formatting\".\n" ). -spec equivalent_under(fun((GOC) -> any()), binary()) -> relation(GOC). equivalent_under(Via, Name) -> {relation, <<<<"equivalent_under("/utf8, Name/binary>>/binary, ")"/utf8>>, fun(Left, Right) -> Via(Left) =:= Via(Right) end}. -file("src/metamon/relation.gleam", 47). ?DOC( " Floats within `epsilon` of each other.\n" "\n" " Panics at construction if `epsilon` is negative. A negative epsilon\n" " would silently yield a degenerate \"always false\" relation (the\n" " absolute difference is always non-negative, so `diff <=. epsilon`\n" " can never hold). Failing visibly on a sign mistake matches the\n" " project's posture for malformed numeric inputs in `Range.constant`,\n" " `Range.linear`, etc.\n" ). -spec approximately(float()) -> relation(float()). approximately(Epsilon) -> case Epsilon < +0.0 of true -> erlang:error(#{gleam_error => panic, message => <<"metamon.approximately: epsilon must be >= 0.0 (a negative epsilon would make every pair compare False, including a value with itself)"/utf8>>, file => <>, module => <<"metamon/relation"/utf8>>, function => <<"approximately"/utf8>>, line => 50}); false -> {relation, <<<<"approximately("/utf8, (gleam_stdlib:float_to_string(Epsilon))/binary>>/binary, ")"/utf8>>, fun(Left, Right) -> Diff = case Left >= Right of true -> Left - Right; false -> Right - Left end, Diff =< Epsilon end} end. -file("src/metamon/relation.gleam", 93). -spec remove_one_loop(GOV, list(GOV), list(GOV)) -> {ok, list(GOV)} | {error, nil}. remove_one_loop(Target, Items, Acc) -> case Items of [] -> {error, nil}; [First | Rest] -> case First =:= Target of true -> {ok, lists:append(lists:reverse(Acc), Rest)}; false -> remove_one_loop(Target, Rest, [First | Acc]) end end. -file("src/metamon/relation.gleam", 89). -spec remove_one(GOQ, list(GOQ)) -> {ok, list(GOQ)} | {error, nil}. remove_one(Target, Items) -> remove_one_loop(Target, Items, []). -file("src/metamon/relation.gleam", 77). -spec remove_each(list(GOM), list(GOM)) -> list(GOM). remove_each(To_remove, Remaining) -> case To_remove of [] -> Remaining; [First | Rest] -> case remove_one(First, Remaining) of {ok, Remaining_after} -> remove_each(Rest, Remaining_after); {error, _} -> [First] end end. -file("src/metamon/relation.gleam", 70). -spec is_permutation(list(GOJ), list(GOJ)) -> boolean(). is_permutation(Left, Right) -> case erlang:length(Left) =:= erlang:length(Right) of false -> false; true -> remove_each(Left, Right) =:= [] end. -file("src/metamon/relation.gleam", 66). ?DOC(" Two lists are permutations of each other (same multiset).\n"). -spec permutation_of() -> relation(list(any())). permutation_of() -> {relation, <<"permutation_of"/utf8>>, fun is_permutation/2}. -file("src/metamon/relation.gleam", 132). -spec is_subset(list(GPH), list(GPH)) -> boolean(). is_subset(Left, Right) -> case Left of [] -> true; [First | Rest] -> case remove_one(First, Right) of {ok, Remaining} -> is_subset(Rest, Remaining); {error, _} -> false end end. -file("src/metamon/relation.gleam", 116). ?DOC( " Multiset subset: every element of `left` is matched against a\n" " *distinct* element of `right` (one-to-one, by structural equality).\n" " `subset_of([1, 1], [1])` is `False`; `subset_of([1, 1], [1, 1, 2])`\n" " is `True`. This matches the semantics of `permutation_of` (which is\n" " also multiset-based).\n" "\n" " For the set-style interpretation that ignores multiplicity, use\n" " `set_subset_of()` instead.\n" ). -spec subset_of() -> relation(list(any())). subset_of() -> {relation, <<"subset_of"/utf8>>, fun(Left, Right) -> is_subset(Left, Right) end}. -file("src/metamon/relation.gleam", 143). -spec is_set_subset(list(GPK), list(GPK)) -> boolean(). is_set_subset(Left, Right) -> case Left of [] -> true; [First | Rest] -> case gleam@list:contains(Right, First) of true -> is_set_subset(Rest, Right); false -> false end end. -file("src/metamon/relation.gleam", 126). ?DOC( " Set subset: every element of `left` is contained somewhere in\n" " `right`, ignoring multiplicity. `set_subset_of([1, 1], [1])` is\n" " `True` because the only distinct value `1` is present.\n" "\n" " For the multiset interpretation that requires distinct matches\n" " per occurrence, use `subset_of()` instead.\n" ). -spec set_subset_of() -> relation(list(any())). set_subset_of() -> {relation, <<"set_subset_of"/utf8>>, fun(Left, Right) -> is_set_subset(Left, Right) end}. -file("src/metamon/relation.gleam", 155). ?DOC(" `cmp(left, right)` returns `Lt` or `Eq` (i.e. left ≤ right).\n"). -spec monotone(fun((GPN, GPN) -> gleam@order:order())) -> relation(GPN). monotone(Cmp) -> {relation, <<"monotone"/utf8>>, fun(Left, Right) -> case Cmp(Left, Right) of lt -> true; eq -> true; gt -> false end end}. -file("src/metamon/relation.gleam", 168). ?DOC( " Conditional relation: if `antecedent(left, right)` is `True`, the\n" " inner relation must hold. Otherwise the relation is trivially\n" " satisfied.\n" ). -spec implies(fun((GPP, GPP) -> boolean()), relation(GPP)) -> relation(GPP). implies(Antecedent, Inner) -> {relation, <<<<"implies("/utf8, (erlang:element(2, Inner))/binary>>/binary, ")"/utf8>>, fun(Left, Right) -> case Antecedent(Left, Right) of false -> true; true -> (erlang:element(3, Inner))(Left, Right) end end}. -file("src/metamon/relation.gleam", 178). ?DOC(" Both `r1` and `r2` must hold.\n"). -spec 'and'(relation(GPS), relation(GPS)) -> relation(GPS). 'and'(R1, R2) -> {relation, <<<<(erlang:element(2, R1))/binary, " and "/utf8>>/binary, (erlang:element(2, R2))/binary>>, fun(Left, Right) -> (erlang:element(3, R1))(Left, Right) andalso (erlang:element(3, R2))( Left, Right ) end}. -file("src/metamon/relation.gleam", 185). ?DOC(" At least one of `r1` and `r2` must hold.\n"). -spec 'or'(relation(GPW), relation(GPW)) -> relation(GPW). 'or'(R1, R2) -> {relation, <<<<(erlang:element(2, R1))/binary, " or "/utf8>>/binary, (erlang:element(2, R2))/binary>>, fun(Left, Right) -> (erlang:element(3, R1))(Left, Right) orelse (erlang:element(3, R2))( Left, Right ) end}. -file("src/metamon/relation.gleam", 192). ?DOC(" Negate a relation.\n"). -spec invert(relation(GQA)) -> relation(GQA). invert(R) -> {relation, <<<<"not("/utf8, (erlang:element(2, R))/binary>>/binary, ")"/utf8>>, fun(Left, Right) -> not (erlang:element(3, R))(Left, Right) end}. -file("src/metamon/relation.gleam", 199). ?DOC(" Override a relation's name.\n"). -spec rename(relation(GQD), binary()) -> relation(GQD). rename(R, Name) -> {relation, Name, erlang:element(3, R)}. -file("src/metamon/relation.gleam", 215). ?DOC(" Construct an N-ary relation directly.\n"). -spec n_new(binary(), fun((list(GQG)) -> boolean())) -> relation_n(GQG). n_new(Name, Holds) -> {relation_n, Name, Holds}. -file("src/metamon/relation.gleam", 225). -spec all_equal_loop(list(any())) -> boolean(). all_equal_loop(Items) -> case Items of [] -> true; [_] -> true; [First, Second | Rest] -> case First =:= Second of true -> all_equal_loop([Second | Rest]); false -> false end end. -file("src/metamon/relation.gleam", 221). ?DOC( " All elements of the input list are equal under structural `==`.\n" " Trivially `True` for lists of length 0 or 1.\n" ). -spec all_equal() -> relation_n(any()). all_equal() -> {relation_n, <<"all_equal"/utf8>>, fun(Items) -> all_equal_loop(Items) end}. -file("src/metamon/relation.gleam", 247). -spec pairwise_loop(relation(GQQ), list(GQQ)) -> boolean(). pairwise_loop(R, Items) -> case Items of [] -> true; [_] -> true; [First, Second | Rest] -> case (erlang:element(3, R))(First, Second) of true -> pairwise_loop(R, [Second | Rest]); false -> false end end. -file("src/metamon/relation.gleam", 241). ?DOC( " Lift a binary `Relation(b)` to an N-ary relation by checking it\n" " pairwise: every consecutive pair `(items[i], items[i+1])` must\n" " satisfy `r`. For `name == \"equal\"` this gives a chain of equal\n" " values; for `monotone` it gives a sorted chain.\n" ). -spec pairwise(relation(GQN)) -> relation_n(GQN). pairwise(R) -> {relation_n, <<<<"pairwise("/utf8, (erlang:element(2, R))/binary>>/binary, ")"/utf8>>, fun(Items) -> pairwise_loop(R, Items) end}.