diff --git a/CHANGELOG.md b/CHANGELOG.md index be98cc7..bdc3232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to - `~CID` sigil for easily constructing known CIDs. - New inspect string for CIDs using the new sigil. +### Changed + +- Memory usage improvements for DRISL and CAR encoding & decoding. + ## [0.1.0] - 2026-04-08 Initial release, with CID, DRISL, and CAR support. diff --git a/README.md b/README.md index 8cfc7e7..e5c1cb9 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ IPFS/IPLD ecosystem while remaining minimal and self-contained. This library provides: -- `DASL.CID`: content identifiers, a compact, self-describing pointer to a - piece of data. +- `DASL.CID`: content identifiers, a compact, self-describing pointer to a piece + of data. - `DASL.DRISL`: deterministic CBOR serialization. - `DASL.CAR`: Content-Addressable aRchive encoding and decoding, as well as a stream decoder. diff --git a/bench/dasl_bench.exs b/bench/dasl_bench.exs new file mode 100644 index 0000000..7af88f6 --- /dev/null +++ b/bench/dasl_bench.exs @@ -0,0 +1,190 @@ +## +## DASL Benchmark +## +## Benchmarks DRISL encode/decode and CAR decode (both in-memory and streaming) +## against the sample CAR files in ./tmp. +## +## Run with: +## mix run bench/dasl_bench.exs +## +## Options (env vars): +## BENCH_TIME - seconds per benchmark job (default: 5) +## BENCH_WARMUP - warmup seconds (default: 2) +## BENCH_MEMORY - memory measurement seconds (default: 1) +## + +alias DASL.{CAR, DRISL} + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +defmodule Bench.Helpers do + @doc """ + Reads a CAR file and extracts the first DRISL-encoded block's raw bytes, + plus the entire file binary for full-load benchmarks. + """ + def load_car!(path) do + binary = File.read!(path) + size_kb = Float.round(byte_size(binary) / 1024, 1) + IO.puts(" Loaded #{Path.basename(path)} (#{size_kb} KB)") + binary + end + + @doc """ + Decodes a CAR binary and returns `{car, block_raw, block_term}`: + + - `block_raw` — raw DRISL binary of the first block (input for decode bench) + - `block_term` — decoded Elixir term of the first block (input for encode bench) + """ + def decode_car!(binary) do + {:ok, car} = CAR.decode(binary, verify: false) + block_raw = car.blocks |> Map.values() |> List.first() + {:ok, block_term, _} = DRISL.decode(block_raw) + {car, block_raw, block_term} + end + + @doc """ + Returns a label with the file size tier for Benchee job names. + """ + def label(path) do + bytes = File.stat!(path).size + + tier = + cond do + bytes < 50_000 -> "small" + bytes < 500_000 -> "medium" + true -> "large" + end + + "#{Path.basename(path)} (#{tier})" + end +end + +# --------------------------------------------------------------------------- +# Load fixtures +# --------------------------------------------------------------------------- + +IO.puts("\nLoading CAR fixtures...") + +car_files = + ~w[tmp/alt.car tmp/comet.car tmp/ovyerus.car] + |> Enum.filter(&File.exists?/1) + +if car_files == [] do + IO.puts("No CAR files found in ./tmp — aborting.") + System.halt(1) +end + +fixtures = + Enum.map(car_files, fn path -> + binary = Bench.Helpers.load_car!(path) + {car, block_raw, block_term} = Bench.Helpers.decode_car!(binary) + label = Bench.Helpers.label(path) + {label, path, binary, car, block_raw, block_term} + end) + +IO.puts("") + +# --------------------------------------------------------------------------- +# Benchee configuration +# --------------------------------------------------------------------------- + +bench_time = String.to_integer(System.get_env("BENCH_TIME", "5")) +bench_warmup = String.to_integer(System.get_env("BENCH_WARMUP", "2")) +bench_memory = String.to_integer(System.get_env("BENCH_MEMORY", "1")) + +shared_opts = [ + time: bench_time, + warmup: bench_warmup, + memory_time: bench_memory, + print: [fast_warning: false] +] + +# --------------------------------------------------------------------------- +# 1. DRISL encode + decode — one benchmark per file's first block +# --------------------------------------------------------------------------- + +IO.puts("=" |> String.duplicate(72)) +IO.puts("DRISL encode / decode (per-block)") +IO.puts("=" |> String.duplicate(72)) + +drisl_jobs = + Enum.flat_map(fixtures, fn {label, _path, _binary, _car, block_raw, block_term} -> + [ + {"encode #{label}", fn -> DRISL.encode(block_term) end}, + {"decode #{label}", fn -> DRISL.decode(block_raw) end} + ] + end) + |> Map.new() + +Benchee.run(drisl_jobs, shared_opts) + +# --------------------------------------------------------------------------- +# 2. CAR decode (in-memory) — full file load into a CAR struct +# --------------------------------------------------------------------------- + +IO.puts("=" |> String.duplicate(72)) +IO.puts("CAR.decode/2 (in-memory, verify: true)") +IO.puts("=" |> String.duplicate(72)) + +car_decode_jobs = + Enum.map(fixtures, fn {label, _path, binary, _car, _block_raw, _block_term} -> + {"decode #{label}", fn -> CAR.decode(binary) end} + end) + |> Map.new() + +Benchee.run(car_decode_jobs, shared_opts) + +# --------------------------------------------------------------------------- +# 3. CAR.decode/2 without CID verification — isolates parsing overhead +# --------------------------------------------------------------------------- + +IO.puts("=" |> String.duplicate(72)) +IO.puts("CAR.decode/2 (in-memory, verify: false)") +IO.puts("=" |> String.duplicate(72)) + +car_decode_noverify_jobs = + Enum.map(fixtures, fn {label, _path, binary, _car, _block_raw, _block_term} -> + {"decode (no verify) #{label}", fn -> CAR.decode(binary, verify: false) end} + end) + |> Map.new() + +Benchee.run(car_decode_noverify_jobs, shared_opts) + +# --------------------------------------------------------------------------- +# 4. CAR.stream_decode/2 — streaming via File.stream! +## Skipped for the 39 MB file at default settings to keep total run time +## reasonable; enable by setting BENCH_STREAM_LARGE=1. +# --------------------------------------------------------------------------- + +IO.puts("=" |> String.duplicate(72)) +IO.puts("CAR.stream_decode/2 (File.stream!, 64 KB chunks)") +IO.puts("=" |> String.duplicate(72)) + +stream_large? = System.get_env("BENCH_STREAM_LARGE", "0") == "1" +chunk_size = 65_536 + +stream_jobs = + fixtures + |> Enum.reject(fn {label, _path, binary, _car, _block_raw, _block_term} -> + large? = byte_size(binary) > 10_000_000 + skip = large? and not stream_large? + if skip, do: IO.puts(" Skipping #{label} (set BENCH_STREAM_LARGE=1 to include)") + skip + end) + |> Enum.map(fn {label, path, _binary, _car, _block_raw, _block_term} -> + { + "stream #{label}", + fn -> + File.stream!(path, chunk_size) + |> CAR.stream_decode() + |> Stream.run() + end + } + end) + |> Map.new() + +if map_size(stream_jobs) > 0, do: Benchee.run(stream_jobs, shared_opts) + +IO.puts("\nDone.") diff --git a/lib/dasl/car/decoder.ex b/lib/dasl/car/decoder.ex index 4ded144..ca4ef7c 100644 --- a/lib/dasl/car/decoder.ex +++ b/lib/dasl/car/decoder.ex @@ -92,20 +92,9 @@ defmodule DASL.CAR.Decoder do @spec parse_cid(binary()) :: {:ok, CID.t()} | block_error() defp parse_cid(cid_bytes) do - case CID.decode(cid_bytes) do - {:ok, fields} -> - {:ok, - struct!(CID, - version: fields.version, - codec: fields.codec, - hash_type: fields.hash_type, - hash_size: fields.hash_size, - digest: fields.digest, - bytes: cid_bytes - )} - - {:error, reason} -> - {:error, :block, {:invalid_cid, reason}} + case CID.from_bytes(cid_bytes) do + {:ok, cid} -> {:ok, cid} + {:error, reason} -> {:error, :block, {:invalid_cid, reason}} end end diff --git a/lib/dasl/car/encoder.ex b/lib/dasl/car/encoder.ex index e6a51b4..d1ff64d 100644 --- a/lib/dasl/car/encoder.ex +++ b/lib/dasl/car/encoder.ex @@ -19,17 +19,17 @@ defmodule DASL.CAR.Encoder do def encode(%CAR{version: 1, roots: roots, blocks: blocks}, opts \\ []) do verify = Keyword.get(opts, :verify, true) - with {:ok, header_bin} <- encode_header(roots), - {:ok, blocks_bin} <- encode_blocks(blocks, verify) do - {:ok, header_bin <> blocks_bin} + with {:ok, header_iodata} <- encode_header(roots), + {:ok, blocks_iodata} <- encode_blocks(blocks, verify) do + {:ok, IO.iodata_to_binary([header_iodata, blocks_iodata])} end end - @spec encode_header(list(CID.t())) :: {:ok, binary()} | header_error() + @spec encode_header(list(CID.t())) :: {:ok, iodata()} | header_error() defp encode_header(roots) do with :ok <- validate_roots(roots), {:ok, metadata_bin} <- DRISL.encode(%{"version" => 1, "roots" => roots}) do - {:ok, LEB128.encode(byte_size(metadata_bin)) <> metadata_bin} + {:ok, [LEB128.encode(byte_size(metadata_bin)), metadata_bin]} else {:error, :header, _} = err -> err {:error, reason} -> {:error, :header, reason} @@ -47,20 +47,20 @@ defmodule DASL.CAR.Encoder do end end - @spec encode_blocks(map(), boolean()) :: {:ok, binary()} | block_error() + @spec encode_blocks(map(), boolean()) :: {:ok, iodata()} | block_error() defp encode_blocks(blocks, verify) do - Enum.reduce_while(blocks, {:ok, <<>>}, fn {cid, data}, {:ok, acc} -> + Enum.reduce_while(blocks, {:ok, []}, fn {cid, data}, {:ok, acc} -> case encode_block(cid, data, verify) do - {:ok, block_bin} -> {:cont, {:ok, acc <> block_bin}} + {:ok, block_iodata} -> {:cont, {:ok, [acc | [block_iodata]]}} {:error, _, _} = err -> {:halt, err} end end) end - @spec encode_block(CID.t(), binary(), boolean()) :: {:ok, binary()} | block_error() + @spec encode_block(CID.t(), binary(), boolean()) :: {:ok, iodata()} | block_error() defp encode_block(%CID{bytes: cid_bytes} = cid, data, verify) when is_binary(data) do with :ok <- maybe_verify(verify, cid, data) do - {:ok, LEB128.encode(byte_size(cid_bytes) + byte_size(data)) <> cid_bytes <> data} + {:ok, [LEB128.encode(byte_size(cid_bytes) + byte_size(data)), cid_bytes, data]} end end diff --git a/lib/dasl/car/stream_decoder.ex b/lib/dasl/car/stream_decoder.ex index aab2471..4281d71 100644 --- a/lib/dasl/car/stream_decoder.ex +++ b/lib/dasl/car/stream_decoder.ex @@ -204,19 +204,9 @@ defmodule DASL.CAR.StreamDecoder do <> = frame cid = - case CID.decode(cid_bytes) do - {:ok, fields} -> - struct!(CID, - version: fields.version, - codec: fields.codec, - hash_type: fields.hash_type, - hash_size: fields.hash_size, - digest: fields.digest, - bytes: cid_bytes - ) - - {:error, reason} -> - raise "CAR stream: invalid CID (#{inspect(reason)})" + case CID.from_bytes(cid_bytes) do + {:ok, cid} -> cid + {:error, reason} -> raise "CAR stream: invalid CID (#{inspect(reason)})" end if verify and not CID.verify?(cid, data) do diff --git a/lib/dasl/cid.ex b/lib/dasl/cid.ex index 728bacd..dfd281d 100644 --- a/lib/dasl/cid.ex +++ b/lib/dasl/cid.ex @@ -117,6 +117,47 @@ defmodule DASL.CID do def decode(_), do: {:error, "malformed CID bytes"} + @doc """ + Constructs a `DASL.CID` struct directly from raw CID bytes. + + Equivalent to `decode/1` followed by `struct!/2`, but skips the intermediate + anonymous map allocation. Intended for hot paths in decoders. + + ## Examples + + iex> bytes = <<1, 85, 18, 32, 185, 77, 39, 185, 147, 77, 62, 8, 165, 46, 82, 215, + ...> 218, 125, 171, 250, 196, 132, 239, 227, 122, 83, 128, 238, 144, 136, + ...> 247, 172, 226, 239, 205, 233>> + iex> {:ok, cid} = DASL.CID.from_bytes(bytes) + iex> cid.codec + :raw + + iex> DASL.CID.from_bytes(<<2, 85, 18, 32>> <> :binary.copy(<<0>>, 32)) + {:error, "unsupported CID version: 2"} + + """ + @spec from_bytes(binary()) :: {:ok, t()} | {:error, String.t()} + def from_bytes(<<1, codec_byte, @hash_sha256, hash_size, digest::binary>> = bytes) + when hash_size == @hash_size and byte_size(digest) == @hash_size do + with {:ok, codec} <- decode_codec(codec_byte) do + {:ok, + struct!(__MODULE__, + version: 1, + codec: codec, + hash_type: @hash_sha256, + hash_size: @hash_size, + digest: digest, + bytes: bytes + )} + end + end + + def from_bytes(bytes) when is_binary(bytes) do + case decode(bytes) do + {:error, _} = err -> err + end + end + @doc """ Constructs a `DASL.CID` from a string-encoded CID. @@ -138,17 +179,8 @@ defmodule DASL.CID do """ @spec new(String.t()) :: {:ok, t()} | {:error, String.t()} def new(cid_string) when is_binary(cid_string) do - with {:ok, bytes} <- parse(cid_string), - {:ok, fields} <- decode(bytes) do - {:ok, - struct!(__MODULE__, - version: fields.version, - codec: fields.codec, - hash_type: fields.hash_type, - hash_size: fields.hash_size, - digest: fields.digest, - bytes: bytes - )} + with {:ok, bytes} <- parse(cid_string) do + from_bytes(bytes) end end @@ -214,17 +246,7 @@ defmodule DASL.CID do tag: 42, value: %CBOR.Tag{tag: :bytes, value: <<0, cid_bytes::binary>>} }) do - with {:ok, fields} <- decode(cid_bytes) do - {:ok, - struct!(__MODULE__, - version: fields.version, - codec: fields.codec, - hash_type: fields.hash_type, - hash_size: fields.hash_size, - digest: fields.digest, - bytes: cid_bytes - )} - end + from_bytes(cid_bytes) end def from_cbor(_), do: {:error, "invalid CBOR CID tag"} diff --git a/lib/dasl/drisl/encoder.ex b/lib/dasl/drisl/encoder.ex index 0c6aa62..10f6c0b 100644 --- a/lib/dasl/drisl/encoder.ex +++ b/lib/dasl/drisl/encoder.ex @@ -39,12 +39,14 @@ defmodule DASL.DRISL.Encoder do @spec encode(any()) :: {:ok, binary()} | {:error, atom()} def encode(term) do case encode_value(term) do - {:ok, binary} -> {:ok, binary} + {:ok, iodata} -> {:ok, IO.iodata_to_binary(iodata)} {:error, _} = err -> err end end - @spec encode_value(any()) :: {:ok, binary()} | {:error, atom()} + # Returns {:ok, iodata} internally — callers must not assume a flat binary + # until encode/1 flattens at the boundary. + @spec encode_value(any()) :: {:ok, iodata()} | {:error, atom()} defp encode_value(nil), do: {:ok, <<0xF6>>} defp encode_value(false), do: {:ok, <<0xF4>>} @@ -60,12 +62,12 @@ defmodule DASL.DRISL.Encoder do defp encode_value(i) when is_integer(i) and i >= 0x10000000000000000 do bytes = :binary.encode_unsigned(i) - {:ok, encode_head(6, 2) <> encode_string(2, bytes)} + {:ok, [encode_head(6, 2), encode_string(2, bytes)]} end defp encode_value(i) when is_integer(i) do bytes = :binary.encode_unsigned(-i - 1) - {:ok, encode_head(6, 3) <> encode_string(2, bytes)} + {:ok, [encode_head(6, 3), encode_string(2, bytes)]} end defp encode_value(f) when is_float(f) do @@ -80,32 +82,30 @@ defmodule DASL.DRISL.Encoder do {:ok, encode_string(2, data)} end - defp encode_value(%DASL.CID{} = cid) do - %CBOR.Tag{tag: 42, value: %CBOR.Tag{tag: :bytes, value: cid_bytes}} = DASL.CID.to_cbor(cid) - {:ok, encode_head(6, 42) <> encode_string(2, cid_bytes)} + defp encode_value(%DASL.CID{bytes: cid_bytes}) do + {:ok, [encode_head(6, 42), encode_string(2, <<0, cid_bytes::binary>>)]} end defp encode_value(list) when is_list(list) do - with {:ok, items_bin} <- encode_list_items(list) do - {:ok, encode_head(4, length(list)) <> items_bin} + with {:ok, items_iodata} <- encode_list_items(list) do + {:ok, [encode_head(4, length(list)) | items_iodata]} end end defp encode_value(%{} = map) do with :ok <- validate_map_keys(map), {:ok, sorted_pairs} <- encode_and_sort_map_pairs(map) do - pairs_bin = Enum.map_join(sorted_pairs, fn {k_bin, v_bin} -> k_bin <> v_bin end) - {:ok, encode_head(5, map_size(map)) <> pairs_bin} + {:ok, [encode_head(5, map_size(map)) | sorted_pairs]} end end defp encode_value(_), do: {:error, :unsupported_type} - @spec encode_list_items(list()) :: {:ok, binary()} | {:error, atom()} + @spec encode_list_items(list()) :: {:ok, iodata()} | {:error, atom()} defp encode_list_items(list) do - Enum.reduce_while(list, {:ok, <<>>}, fn item, {:ok, acc} -> + Enum.reduce_while(list, {:ok, []}, fn item, {:ok, acc} -> case encode_value(item) do - {:ok, bin} -> {:cont, {:ok, acc <> bin}} + {:ok, iodata} -> {:cont, {:ok, [acc | [iodata]]}} {:error, _} = err -> {:halt, err} end end) @@ -122,21 +122,28 @@ defmodule DASL.DRISL.Encoder do # Encodes all key/value pairs, then sorts by bytewise-lexicographic order # of the encoded key — length-first (RFC 7049 §3.9 canonical CBOR sort). - # Example from spec: {"a": ..., "b": ..., "aa": ...} - @spec encode_and_sort_map_pairs(map()) :: {:ok, [{binary(), binary()}]} | {:error, atom()} + # Returns a flat iodata list of interleaved [k_bin, v_bin, ...] after sorting. + @spec encode_and_sort_map_pairs(map()) :: {:ok, iodata()} | {:error, atom()} defp encode_and_sort_map_pairs(map) do map |> Enum.reduce_while({:ok, []}, fn {k, v}, {:ok, acc} -> with {:ok, k_bin} <- encode_value(k), - {:ok, v_bin} <- encode_value(v) do - {:cont, {:ok, [{k_bin, v_bin} | acc]}} + {:ok, v_iodata} <- encode_value(v) do + # Flatten k to a binary now so we can sort on it cheaply; + # v stays as iodata. + k_flat = IO.iodata_to_binary(k_bin) + {:cont, {:ok, [{k_flat, v_iodata} | acc]}} else {:error, _} = err -> {:halt, err} end end) |> case do {:ok, pairs} -> - sorted = Enum.sort_by(pairs, fn {k_bin, _} -> {byte_size(k_bin), k_bin} end) + sorted = + pairs + |> Enum.sort_by(fn {k_flat, _} -> {byte_size(k_flat), k_flat} end) + |> Enum.flat_map(fn {k_flat, v_iodata} -> [k_flat, v_iodata] end) + {:ok, sorted} err -> diff --git a/mix.exs b/mix.exs index 05003c2..f6d5f70 100644 --- a/mix.exs +++ b/mix.exs @@ -33,7 +33,8 @@ defmodule DASL.MixProject do {:typedstruct, "~> 0.5"}, {:varint, "~> 1.4"}, {:ex_doc, "~> 0.34", only: :dev, runtime: false}, - {:credo, "~> 1.7", only: [:dev, :test], runtime: false} + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:benchee, "~> 1.3", only: :dev, runtime: false} ] end diff --git a/mix.lock b/mix.lock index dc7454b..313c1f8 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,9 @@ %{ + "benchee": {:hex, :benchee, "1.5.0", "4d812c31d54b0ec0167e91278e7de3f596324a78a096fd3d0bea68bb0c513b10", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "5b075393aea81b8ae74eadd1c28b1d87e8a63696c649d8293db7c4df3eb67535"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "cbor": {:hex, :cbor, "1.0.2", "9b0af85af291a556e10a0ffd48ba9a21a75e711828fafd3af193d56d95f0907f", [:mix], [], "hexpm", "edbc9b4a16eb93a582437b9b249c340a75af03958e338fb43d8c1be9fc65b864"}, "credo": {:hex, :credo, "1.7.17", "f92b6aa5b26301eaa5a35e4d48ebf5aa1e7094ac00ae38f87086c562caf8a22f", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1eb5645c835f0b6c9b5410f94b5a185057bcf6d62a9c2b476da971cde8749645"}, + "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, @@ -10,6 +12,7 @@ "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"}, "typedstruct": {:hex, :typedstruct, "0.5.4", "d1d33d58460a74f413e9c26d55e66fd633abd8ac0fb12639add9a11a60a0462a", [:make, :mix], [], "hexpm", "ffaef36d5dbaebdbf4ed07f7fb2ebd1037b2c1f757db6fb8e7bcbbfabbe608d8"}, "varint": {:hex, :varint, "1.5.1", "17160c70d0428c3f8a7585e182468cac10bbf165c2360cf2328aaa39d3fb1795", [:mix], [], "hexpm", "24f3deb61e91cb988056de79d06f01161dd01be5e0acae61d8d936a552f1be73"}, }