-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, forall_round_trip_partial_with/5, forall_round_trip_partial/4, forall_round_trip_under_with/6, forall_round_trip_under/5]). -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(HDG, HDH) :: {mr, metamon@internal@runner:morph_spec(HDG, HDH)}. -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(HDS), metamon@relation:relation(HDU) ) -> mr(HDS, HDU). 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(HDY), metamon@transform:transform(HEA), metamon@relation:relation(HEA) ) -> mr(HDY, HEA). 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(HEL), fun((HEL) -> 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(HEJ), fun((HEJ) -> 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(HEQ), fun((HEQ) -> {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(HEN), fun((HEN) -> {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(HEY), mr(HEY, HFA), fun((HEY) -> HFA) ) -> 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(HET), mr(HET, HEV), fun((HET) -> HEV) ) -> 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(HFD, mr(HFD, HFE), fun((HFD) -> HFE)) -> 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(HFN), list(metamon@transform:transform(HFN)), metamon@relation:relation_n(HFR), fun((HFN) -> HFR) ) -> 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(HFH), list(metamon@transform:transform(HFH)), metamon@relation:relation_n(HFL), fun((HFH) -> HFL) ) -> 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(HFZ, HGA))) -> list(metamon@internal@runner:morph_spec(HFZ, HGA)). 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(HFT), list(mr(HFT, HFV)), fun((HFT) -> HFV) ) -> 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((HGH) -> HGH)) -> mr(HGH, HGH). 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(HGK)) -> mr(HGK, 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(HGP), metamon@transform:transform(HGR), metamon@relation:relation(HGR) ) -> mr(HGP, HGR). 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({HGW, HGW}, 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(HHG), binary(), fun((HHG) -> HHI), fun((HHI) -> {ok, HHG} | {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(HHA), binary(), fun((HHA) -> HHC), fun((HHC) -> {ok, HHA} | {error, any()}) ) -> nil. forall_round_trip(Gen, Name, Encode, Decode) -> forall_round_trip_with(default_config(), Gen, Name, Encode, Decode). -file("src/metamon.gleam", 428). ?DOC(" `forall_round_trip_partial` with an explicit configuration.\n"). -spec forall_round_trip_partial_with( metamon@config:config(), metamon@generator:generator(HHV), binary(), fun((HHV) -> {ok, HHX} | {error, any()}), fun((HHX) -> {ok, HHV} | {error, any()}) ) -> nil. forall_round_trip_partial_with(Cfg, Gen, Name, Encode, Decode) -> metamon@internal@runner:run_forall( Cfg, <<<<"round_trip["/utf8, Name/binary>>/binary, "]"/utf8>>, Gen, fun(Input) -> case Encode(Input) of {error, _} -> true; {ok, Encoded} -> case Decode(Encoded) of {ok, Decoded} -> Decoded =:= Input; {error, _} -> false end end end ). -file("src/metamon.gleam", 412). ?DOC( " Run a round-trip property where the encoder is partial: the\n" " encoder returns `Result(b, e_enc)` because not every generated\n" " input is a valid input for the codec. Inputs the encoder rejects\n" " (`Error(_)`) are treated as out of scope and skipped — the\n" " property succeeds for them.\n" "\n" " Use this variant for codecs whose encoder has structural\n" " preconditions: byte-alignment requirements, value-range checks,\n" " hrp / version constraints, etc. A typical pattern is to combine\n" " `forall_round_trip_partial` with a generator that produces the\n" " surrounding inputs (e.g. arbitrary `BitArray` plus arbitrary\n" " version bytes); the encoder filters down to the valid subset and\n" " the property checks the round-trip on that subset.\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_partial(\n" " gen: generator.tuple2(byte_gen, payload_gen),\n" " name: \"base58check\",\n" " encode: fn(pair) { base58check.encode(pair.0, pair.1) },\n" " decode: fn(s) {\n" " case base58check.decode(s) {\n" " Ok(decoded) -> Ok(#(decoded.version, decoded.payload))\n" " Error(e) -> Error(e)\n" " }\n" " },\n" " )\n" " ```\n" ). -spec forall_round_trip_partial( metamon@generator:generator(HHM), binary(), fun((HHM) -> {ok, HHO} | {error, any()}), fun((HHO) -> {ok, HHM} | {error, any()}) ) -> nil. forall_round_trip_partial(Gen, Name, Encode, Decode) -> forall_round_trip_partial_with(default_config(), Gen, Name, Encode, Decode). -file("src/metamon.gleam", 497). ?DOC(" `forall_round_trip_under` with an explicit configuration.\n"). -spec forall_round_trip_under_with( metamon@config:config(), metamon@generator:generator(HIL), binary(), fun((HIL) -> HIN), fun((HIN) -> {ok, HIL} | {error, any()}), metamon@relation:relation(HIL) ) -> nil. forall_round_trip_under_with(Cfg, Gen, Name, Encode, Decode, Equality) -> metamon@internal@runner:run_forall( Cfg, <<<<"round_trip["/utf8, Name/binary>>/binary, "]"/utf8>>, Gen, fun(Input) -> case Decode(Encode(Input)) of {ok, Decoded} -> (erlang:element(3, Equality))(Decoded, Input); {error, _} -> false end end ). -file("src/metamon.gleam", 479). ?DOC( " Run a round-trip property using a caller-supplied equality\n" " `Relation(a)` instead of structural `==`. The decoded value must\n" " satisfy `equality.holds(decoded, input)`.\n" "\n" " Use this when the source type carries values that are not\n" " preserved verbatim across encode → decode: opaque types whose\n" " decoded form normalises (e.g. multipart `Part` with re-derived\n" " convenience fields), MIME types that lowercase the essence, JSON\n" " values whose key order is implementation-defined. Composing with\n" " `relation.equivalent_under(via, name)` lets you compare on a\n" " projection (e.g. `headers + body` ignoring derived caches).\n" "\n" " The failure report header is `round_trip[]`. The relation's\n" " own `name` appears under `relation:` in the failure block, just\n" " like `forall_morph` failures.\n" "\n" " ```gleam\n" " metamon.forall_round_trip_under(\n" " gen: parts_gen(),\n" " name: \"multipart_round_trip\",\n" " encode: fn(parts) { multipartkit.encode(boundary, parts) },\n" " decode: fn(body) { multipartkit.parse(body, content_type) },\n" " equality: relation.equivalent_under(\n" " fn(parts) { list.map(parts, part_payload) },\n" " \"wire_payload\",\n" " ),\n" " )\n" " ```\n" ). -spec forall_round_trip_under( metamon@generator:generator(HIE), binary(), fun((HIE) -> HIG), fun((HIG) -> {ok, HIE} | {error, any()}), metamon@relation:relation(HIE) ) -> nil. forall_round_trip_under(Gen, Name, Encode, Decode, Equality) -> forall_round_trip_under_with( default_config(), Gen, Name, Encode, Decode, Equality ).