-module(metamon). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/metamon.gleam"). -export([seed/1, random_seed/0, default_config/0, with_seed/2, with_runs/2, with_max_size/2, with_shrink_limit/2, with_max_edges/2, with_regression_file/2, with_diff_enabled/2, with_runs_or_panic/2, with_max_size_or_panic/2, with_shrink_limit_or_panic/2, with_max_edges_or_panic/2, with_regression_file_or_panic/2, with_output_format/2, mr/3, mr_equivariant/4, name_of/1, forall_with/3, forall/2, forall_observable_with/3, forall_observable/2, forall_morph_with/4, forall_morph/3, assert_morph/3, forall_morph_n_with/5, forall_morph_n/4, forall_morphs/3, idempotency_of/2, invariant_under/2, equivariant_under/4, commutativity_of/1, forall_round_trip_with/5, forall_round_trip/4]). -export_type([mr/2]). -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( " Metamon top-level public API.\n" "\n" " `metamon` exports the small surface that most tests interact with:\n" " `forall`, `forall_observable`, `forall_morph`, `assert_morph`,\n" " `forall_morphs`, `forall_round_trip`, the `Mr` smart constructors,\n" " and a handful of metamorphic-relation templates (`idempotency_of`,\n" " `invariant_under`, `equivariant_under`, `commutativity_of`).\n" "\n" " Configuration lives in `metamon/config`; generators in\n" " `metamon/generator`; transforms in `metamon/transform`; relations\n" " in `metamon/relation`; per-property context in `metamon/annotate`\n" " and `metamon/coverage`; structural diff in `metamon/diff`.\n" ). -opaque mr(HCO, HCP) :: {mr, metamon@internal@runner:morph_spec(HCO, HCP)}. -file("src/metamon.gleam", 31). ?DOC(" Construct a deterministic seed from an integer.\n"). -spec seed(integer()) -> metamon@generator@seed:seed(). seed(Value) -> metamon@generator@seed:seed(Value). -file("src/metamon.gleam", 36). ?DOC(" Convenience: a fresh random seed.\n"). -spec random_seed() -> metamon@generator@seed:seed(). random_seed() -> metamon@generator@seed:random_seed(). -file("src/metamon.gleam", 41). ?DOC(" Re-export of `Config` and `default_config`.\n"). -spec default_config() -> metamon@config:config(). default_config() -> metamon@config:default_config(). -file("src/metamon.gleam", 46). ?DOC(" Re-export of `with_seed`.\n"). -spec with_seed(metamon@config:config(), metamon@generator@seed:seed()) -> metamon@config:config(). with_seed(C, S) -> metamon@config:with_seed(C, S). -file("src/metamon.gleam", 51). ?DOC(" Re-export of `with_runs`.\n"). -spec with_runs(metamon@config:config(), integer()) -> {ok, metamon@config:config()} | {error, metamon@config:config_error()}. with_runs(C, N) -> metamon@config:with_runs(C, N). -file("src/metamon.gleam", 56). ?DOC(" Re-export of `with_max_size`.\n"). -spec with_max_size(metamon@config:config(), integer()) -> {ok, metamon@config:config()} | {error, metamon@config:config_error()}. with_max_size(C, N) -> metamon@config:with_max_size(C, N). -file("src/metamon.gleam", 61). ?DOC(" Re-export of `with_shrink_limit`.\n"). -spec with_shrink_limit(metamon@config:config(), integer()) -> {ok, metamon@config:config()} | {error, metamon@config:config_error()}. with_shrink_limit(C, N) -> metamon@config:with_shrink_limit(C, N). -file("src/metamon.gleam", 69). ?DOC(" Re-export of `with_max_edges`.\n"). -spec with_max_edges(metamon@config:config(), integer()) -> {ok, metamon@config:config()} | {error, metamon@config:config_error()}. with_max_edges(C, N) -> metamon@config:with_max_edges(C, N). -file("src/metamon.gleam", 74). ?DOC(" Re-export of `with_regression_file`.\n"). -spec with_regression_file(metamon@config:config(), binary()) -> {ok, metamon@config:config()} | {error, metamon@config:config_error()}. with_regression_file(C, Path) -> metamon@config:with_regression_file(C, Path). -file("src/metamon.gleam", 82). ?DOC(" Re-export of `with_diff_enabled`.\n"). -spec with_diff_enabled(metamon@config:config(), boolean()) -> metamon@config:config(). with_diff_enabled(C, Enabled) -> metamon@config:with_diff_enabled(C, Enabled). -file("src/metamon.gleam", 89). ?DOC( " Re-export of `with_runs_or_panic`. Use in test code where the bound\n" " is statically known and the `let assert Ok(c) = ...` arm would be\n" " dead code.\n" ). -spec with_runs_or_panic(metamon@config:config(), integer()) -> metamon@config:config(). with_runs_or_panic(C, N) -> metamon@config:with_runs_or_panic(C, N). -file("src/metamon.gleam", 94). ?DOC(" Re-export of `with_max_size_or_panic`.\n"). -spec with_max_size_or_panic(metamon@config:config(), integer()) -> metamon@config:config(). with_max_size_or_panic(C, N) -> metamon@config:with_max_size_or_panic(C, N). -file("src/metamon.gleam", 99). ?DOC(" Re-export of `with_shrink_limit_or_panic`.\n"). -spec with_shrink_limit_or_panic(metamon@config:config(), integer()) -> metamon@config:config(). with_shrink_limit_or_panic(C, N) -> metamon@config:with_shrink_limit_or_panic(C, N). -file("src/metamon.gleam", 104). ?DOC(" Re-export of `with_max_edges_or_panic`.\n"). -spec with_max_edges_or_panic(metamon@config:config(), integer()) -> metamon@config:config(). with_max_edges_or_panic(C, N) -> metamon@config:with_max_edges_or_panic(C, N). -file("src/metamon.gleam", 109). ?DOC(" Re-export of `with_regression_file_or_panic`.\n"). -spec with_regression_file_or_panic(metamon@config:config(), binary()) -> metamon@config:config(). with_regression_file_or_panic(C, Path) -> metamon@config:with_regression_file_or_panic(C, Path). -file("src/metamon.gleam", 120). ?DOC( " Choose the failure-report output format. `Text` (default) is\n" " human-friendly; `Json` is single-line JSON for CI / LLM consumers.\n" ). -spec with_output_format( metamon@config:config(), metamon@config:output_format() ) -> metamon@config:config(). with_output_format(C, Fmt) -> metamon@config:with_output_format(C, Fmt). -file("src/metamon.gleam", 138). ?DOC( " Construct a Plain MR. The relation is checked between\n" " `f(source_input)` and `f(transform.apply(source_input))`.\n" ). -spec mr( binary(), metamon@transform:transform(HDA), metamon@relation:relation(HDC) ) -> mr(HDA, HDC). mr(Name, Transform, Relation) -> {mr, metamon@internal@runner:plain(Name, Transform, Relation)}. -file("src/metamon.gleam", 149). ?DOC( " Construct an Equivariant MR. The relation is checked between\n" " `output_transform.apply(f(source_input))` and\n" " `f(input_transform.apply(source_input))`.\n" ). -spec mr_equivariant( binary(), metamon@transform:transform(HDG), metamon@transform:transform(HDI), metamon@relation:relation(HDI) ) -> mr(HDG, HDI). mr_equivariant(Name, Input_transform, Output_transform, Relation) -> {mr, metamon@internal@runner:equivariant( Name, Input_transform, Output_transform, Relation )}. -file("src/metamon.gleam", 159). ?DOC(" Get the user-facing name of an MR.\n"). -spec name_of(mr(any(), any())) -> binary(). name_of(M) -> metamon@internal@runner:morph_name(erlang:element(2, M)). -file("src/metamon.gleam", 171). ?DOC(" Run a property with an explicit configuration.\n"). -spec forall_with( metamon@config:config(), metamon@generator:generator(HDT), fun((HDT) -> boolean()) ) -> nil. forall_with(Cfg, G, Property) -> metamon@internal@runner:run_forall(Cfg, <<"forall"/utf8>>, G, Property). -file("src/metamon.gleam", 166). ?DOC(" Run a property over many random inputs.\n"). -spec forall(metamon@generator:generator(HDR), fun((HDR) -> boolean())) -> nil. forall(G, Property) -> forall_with(default_config(), G, Property). -file("src/metamon.gleam", 191). ?DOC(" `forall_observable` with an explicit configuration.\n"). -spec forall_observable_with( metamon@config:config(), metamon@generator:generator(HDY), fun((HDY) -> {any(), boolean()}) ) -> nil. forall_observable_with(Cfg, G, Predicate) -> metamon@internal@runner:run_forall( Cfg, <<"forall_observable"/utf8>>, G, fun(Input) -> {Observed, Holds} = Predicate(Input), metamon@annotate:annotate_value( <<"predicate value"/utf8>>, Observed ), Holds end ). -file("src/metamon.gleam", 186). ?DOC( " Run a property whose predicate also exposes its intermediate value.\n" "\n" " `predicate` returns `#(observation, holds)`. `holds` decides whether\n" " the property is satisfied (same as `forall`); `observation` is\n" " recorded under the label `predicate value` and shown in the failure\n" " report — no manual `annotate.annotate_value` is needed.\n" "\n" " Use this when the predicate's intermediate value (typically `f(input)`)\n" " is what determines the branch. Without it, `forall` failure reports\n" " only show the shrunk source input, which can force a debug round-trip\n" " to recover what `f(input)` actually was.\n" ). -spec forall_observable( metamon@generator:generator(HDV), fun((HDV) -> {any(), boolean()}) ) -> nil. forall_observable(G, Predicate) -> forall_observable_with(default_config(), G, Predicate). -file("src/metamon.gleam", 209). ?DOC(" Run a metamorphic relation with an explicit configuration.\n"). -spec forall_morph_with( metamon@config:config(), metamon@generator:generator(HEG), mr(HEG, HEI), fun((HEG) -> HEI) ) -> nil. forall_morph_with(Cfg, G, M, F) -> metamon@internal@runner:run_forall_morph( Cfg, <<"forall_morph"/utf8>>, G, erlang:element(2, M), F ). -file("src/metamon.gleam", 204). ?DOC(" Run a metamorphic relation over many random inputs.\n"). -spec forall_morph( metamon@generator:generator(HEB), mr(HEB, HED), fun((HEB) -> HED) ) -> nil. forall_morph(G, M, F) -> forall_morph_with(default_config(), G, M, F). -file("src/metamon.gleam", 219). ?DOC(" Run a metamorphic relation against a single input. Generator-free.\n"). -spec assert_morph(HEL, mr(HEL, HEM), fun((HEL) -> HEM)) -> nil. assert_morph(Input, M, F) -> metamon@internal@runner:run_assert_morph( <<"assert_morph"/utf8>>, erlang:element(2, M), F, Input ). -file("src/metamon.gleam", 239). ?DOC(" `forall_morph_n` with an explicit configuration.\n"). -spec forall_morph_n_with( metamon@config:config(), metamon@generator:generator(HEV), list(metamon@transform:transform(HEV)), metamon@relation:relation_n(HEZ), fun((HEV) -> HEZ) ) -> nil. forall_morph_n_with(Cfg, G, Transforms, Rel, F) -> metamon@internal@runner:run_forall_morph_n( Cfg, <<"forall_morph_n"/utf8>>, G, Transforms, Rel, F ). -file("src/metamon.gleam", 229). ?DOC( " Run an N-ary metamorphic relation: apply each of `transforms` to\n" " the source input to build follow-up inputs, then assert that the\n" " resulting outputs `[f(x), f(T0(x)), ..., f(Tn(x))]` satisfy\n" " `relation`. Useful when the property requires comparing more than\n" " two outputs in one shot (e.g. `(a, b, c) ↦ op(op(a,b), c)` and its\n" " re-associations all agree).\n" ). -spec forall_morph_n( metamon@generator:generator(HEP), list(metamon@transform:transform(HEP)), metamon@relation:relation_n(HET), fun((HEP) -> HET) ) -> nil. forall_morph_n(G, Transforms, Rel, F) -> forall_morph_n_with(default_config(), G, Transforms, Rel, F). -file("src/metamon.gleam", 262). -spec list_map_specs(list(mr(HFH, HFI))) -> list(metamon@internal@runner:morph_spec(HFH, HFI)). list_map_specs(Ms) -> case Ms of [] -> []; [First | Rest] -> [erlang:element(2, First) | list_map_specs(Rest)] end. -file("src/metamon.gleam", 252). ?DOC( " Run multiple metamorphic relations against the same generator.\n" " Each MR is tried independently; failures are collected and reported\n" " together at the end.\n" ). -spec forall_morphs( metamon@generator:generator(HFB), list(mr(HFB, HFD)), fun((HFB) -> HFD) ) -> nil. forall_morphs(G, Ms, F) -> metamon@internal@runner:run_forall_morphs( default_config(), <<"forall_morphs"/utf8>>, G, list_map_specs(Ms), F ). -file("src/metamon.gleam", 289). ?DOC( " `f(f(x)) == f(x)`. Idempotency.\n" "\n" " Encoded as a Plain MR whose transform is `f` itself and whose\n" " relation is structural equality.\n" ). -spec idempotency_of(binary(), fun((HFP) -> HFP)) -> mr(HFP, HFP). idempotency_of(Name, F) -> Apply_t = metamon@transform:new(<<"apply "/utf8, Name/binary>>, F), mr(Name, Apply_t, metamon@relation:equal()). -file("src/metamon.gleam", 295). ?DOC(" `f(T(x)) == f(x)` — `f` is invariant under the input transform.\n"). -spec invariant_under(binary(), metamon@transform:transform(HFS)) -> mr(HFS, any()). invariant_under(Name, Under) -> mr(Name, Under, metamon@relation:equal()). -file("src/metamon.gleam", 301). ?DOC( " `R(U(f(x)), f(T(x)))`. Equivariance: the input transform `T` and\n" " the output transform `U` commute with `f` modulo `R`.\n" ). -spec equivariant_under( binary(), metamon@transform:transform(HFX), metamon@transform:transform(HFZ), metamon@relation:relation(HFZ) ) -> mr(HFX, HFZ). equivariant_under(Name, Input_transform, Output_transform, Rel) -> mr_equivariant(Name, Input_transform, Output_transform, Rel). -file("src/metamon.gleam", 328). ?DOC( " `op(a, b) == op(b, a)` — `op` is commutative.\n" "\n" " The MR is over the input pair `#(a, a)` and the output type `b`.\n" " Use it as:\n" "\n" " ```gleam\n" " let mr = metamon.commutativity_of(name: \"add_commutative\")\n" " metamon.forall_morph(\n" " generator.tuple2(int_gen, int_gen),\n" " mr,\n" " fn(pair) { add(pair.0, pair.1) },\n" " )\n" " ```\n" ). -spec commutativity_of(binary()) -> mr({HGE, HGE}, any()). commutativity_of(Name) -> Swap = metamon@transform:new( <<"swap"/utf8>>, fun(Pair) -> {erlang:element(2, Pair), erlang:element(1, Pair)} end ), mr(Name, Swap, metamon@relation:equal()). -file("src/metamon.gleam", 365). ?DOC(" `forall_round_trip` with an explicit configuration.\n"). -spec forall_round_trip_with( metamon@config:config(), metamon@generator:generator(HGO), binary(), fun((HGO) -> HGQ), fun((HGQ) -> {ok, HGO} | {error, any()}) ) -> nil. forall_round_trip_with(Cfg, Gen, Name, Encode, Decode) -> metamon@internal@runner:run_forall( Cfg, <<<<"round_trip["/utf8, Name/binary>>/binary, "]"/utf8>>, Gen, fun(Input) -> case Decode(Encode(Input)) of {ok, Decoded} -> Decoded =:= Input; {error, _} -> false end end ). -file("src/metamon.gleam", 349). ?DOC( " Run a round-trip property over many random inputs:\n" " `decode(encode(x))` must equal `Ok(x)` for every generated `x`.\n" "\n" " The failure report header is `round_trip[]` so it is\n" " immediately obvious from the panic which round-trip broke. The\n" " underlying machinery is the same as `forall`, including shrinking\n" " of the source input.\n" "\n" " ```gleam\n" " metamon.forall_round_trip(\n" " gen: generator.bit_array(range.constant(0, 16)),\n" " name: \"base64\",\n" " encode: base64.encode,\n" " decode: base64.decode,\n" " )\n" " ```\n" ). -spec forall_round_trip( metamon@generator:generator(HGI), binary(), fun((HGI) -> HGK), fun((HGK) -> {ok, HGI} | {error, any()}) ) -> nil. forall_round_trip(Gen, Name, Encode, Decode) -> forall_round_trip_with(default_config(), Gen, Name, Encode, Decode).