{lib ? null, ...}: let
  net =
    {
      ip = {
        # add :: (ip | mac | integer) -> ip -> ip
        #
        # Examples:
        #
        # Adding integer to IPv4:
        # > net.ip.add 100 "10.0.0.1"
        # "10.0.0.101"
        #
        # Adding IPv4 to IPv4:
        # > net.ip.add "127.0.0.1" "10.0.0.1"
        # "137.0.0.2"
        #
        # Adding IPv6 to IPv4:
        # > net.ip.add "::cafe:beef" "10.0.0.1"
        # "212.254.186.191"
        #
        # Adding MAC to IPv4 (overflows):
        # > net.ip.add "fe:ed:fa:ce:f0:0d" "10.0.0.1"
        # "4.206.240.14"
        #
        # Adding integer to IPv6:
        # > net.ip.add 100 "dead:cafe:beef::"
        # "dead:cafe:beef::64"
        #
        # Adding IPv4 to to IPv6:
        # > net.ip.add "127.0.0.1" "dead:cafe:beef::"
        # "dead:cafe:beef::7f00:1"
        #
        # Adding MAC to IPv6:
        # > net.ip.add "fe:ed:fa:ce:f0:0d" "dead:cafe:beef::"
        # "dead:cafe:beef::feed:face:f00d"
        add = delta: ip: let
          function = "net.ip.add";
          delta' = typechecks.numeric function "delta" delta;
          ip' = typechecks.ip function "ip" ip;
        in
          builders.ip (implementations.ip.add delta' ip');

        # diff :: ip -> ip -> (integer | ipv6)
        #
        # net.ip.diff is the reverse of net.ip.add:
        #
        # net.ip.diff (net.ip.add a b) a = b
        # net.ip.diff (net.ip.add a b) b = a
        #
        # The difference between net.ip.diff and net.ip.subtract is that
        # net.ip.diff will try its best to return an integer (falling back
        # to an IPv6 if the result is too big to fit in an integer). This is
        # useful if you have two hosts that you know are on the same network
        # and you just want to calculate the offset between them — a result
        # like "0.0.0.10" is not very useful (which is what you would get
        # from net.ip.subtract).
        diff = minuend: subtrahend: let
          function = "net.ip.diff";
          minuend' = typechecks.ip function "minuend" minuend;
          subtrahend' = typechecks.ip function "subtrahend" subtrahend;
          result = implementations.ip.diff minuend' subtrahend';
        in
          if result ? ipv6
          then builders.ipv6 result
          else result;

        # subtract :: (ip | mac | integer) -> ip -> ip
        #
        # net.ip.subtract is also the reverse of net.ip.add:
        #
        # net.ip.subtract a (net.ip.add a b) = b
        # net.ip.subtract b (net.ip.add a b) = a
        #
        # The difference between net.ip.subtract and net.ip.diff is that
        # net.ip.subtract will always return the same type as its "ip"
        # parameter. Its implementation takes the "delta" parameter,
        # coerces it to be the same type as the "ip" paramter, negates it
        # (using two's complement), and then adds it to "ip".
        subtract = delta: ip: let
          function = "net.ip.subtract";
          delta' = typechecks.numeric function "delta" delta;
          ip' = typechecks.ip function "ip" ip;
        in
          builders.ip (implementations.ip.subtract delta' ip');
      };

      mac = {
        # add :: (ip | mac | integer) -> mac -> mac
        #
        # Examples:
        #
        # Adding integer to MAC:
        # > net.mac.add 100 "fe:ed:fa:ce:f0:0d"
        # "fe:ed:fa:ce:f0:71"
        #
        # Adding IPv4 to MAC:
        # > net.mac.add "127.0.0.1" "fe:ed:fa:ce:f0:0d"
        # "fe:ee:79:ce:f0:0e"
        #
        # Adding IPv6 to MAC:
        # > net.mac.add "::cafe:beef" "fe:ed:fa:ce:f0:0d"
        # "fe:ee:c5:cd:aa:cb
        #
        # Adding MAC to MAC:
        # > net.mac.add "fe:ed:fa:00:00:00" "00:00:00:ce:f0:0d"
        # "fe:ed:fa:ce:f0:0d"
        add = delta: mac: let
          function = "net.mac.add";
          delta' = typechecks.numeric function "delta" delta;
          mac' = typechecks.mac function "mac" mac;
        in
          builders.mac (implementations.mac.add delta' mac');

        # diff :: mac -> mac -> integer
        #
        # net.mac.diff is the reverse of net.mac.add:
        #
        # net.mac.diff (net.mac.add a b) a = b
        # net.mac.diff (net.mac.add a b) b = a
        #
        # The difference between net.mac.diff and net.mac.subtract is that
        # net.mac.diff will always return an integer.
        diff = minuend: subtrahend: let
          function = "net.mac.diff";
          minuend' = typechecks.mac function "minuend" minuend;
          subtrahend' = typechecks.mac function "subtrahend" subtrahend;
        in
          implementations.mac.diff minuend' subtrahend';

        # subtract :: (ip | mac | integer) -> mac -> mac
        #
        # net.mac.subtract is also the reverse of net.ip.add:
        #
        # net.mac.subtract a (net.mac.add a b) = b
        # net.mac.subtract b (net.mac.add a b) = a
        #
        # The difference between net.mac.subtract and net.mac.diff is that
        # net.mac.subtract will always return a MAC address.
        subtract = delta: mac: let
          function = "net.mac.subtract";
          delta' = typechecks.numeric function "delta" delta;
          mac' = typechecks.mac function "mac" mac;
        in
          builders.mac (implementations.mac.subtract delta' mac');
      };

      cidr = {
        # add :: (ip | mac | integer) -> cidr -> cidr
        #
        # > net.cidr.add 2 "127.0.0.0/8"
        # "129.0.0.0/8"
        #
        # > net.cidr.add (-2) "127.0.0.0/8"
        # "125.0.0.0/8"
        add = delta: cidr: let
          function = "net.cidr.add";
          delta' = typechecks.numeric function "delta" delta;
          cidr' = typechecks.cidr function "cidr" cidr;
        in
          builders.cidr (implementations.cidr.add delta' cidr');

        # child :: cidr -> cidr -> bool
        #
        # > net.cidr.child "10.10.10.0/24" "10.0.0.0/8"
        # true
        #
        # > net.cidr.child "127.0.0.0/8" "10.0.0.0/8"
        # false
        child = subcidr: cidr: let
          function = "net.cidr.child";
          subcidr' = typechecks.cidr function "subcidr" subcidr;
          cidr' = typechecks.cidr function "cidr" cidr;
        in
          implementations.cidr.child subcidr' cidr';

        # contains :: ip -> cidr -> bool
        #
        # > net.cidr.contains "127.0.0.1" "127.0.0.0/8"
        # true
        #
        # > net.cidr.contains "127.0.0.1" "192.168.0.0/16"
        # false
        contains = ip: cidr: let
          function = "net.cidr.contains";
          ip' = typechecks.ip function "ip" ip;
          cidr' = typechecks.cidr function "cidr" cidr;
        in
          implementations.cidr.contains ip' cidr';

        # capacity :: cidr -> integer
        #
        # > net.cidr.capacity "172.16.0.0/12"
        # 1048576
        #
        # > net.cidr.capacity "dead:cafe:beef::/96"
        # 4294967296
        #
        # > net.cidr.capacity "dead:cafe:beef::/48" (saturates to maxBound)
        # 9223372036854775807
        capacity = cidr: let
          function = "net.cidr.capacity";
          cidr' = typechecks.cidr function "cidr" cidr;
        in
          implementations.cidr.capacity cidr';

        # host :: (ip | mac | integer) -> cidr -> ip
        #
        # > net.cidr.host 10000 "10.0.0.0/8"
        # 10.0.39.16
        #
        # > net.cidr.host 10000 "dead:cafe:beef::/64"
        # "dead:cafe:beef::2710"
        #
        # net.cidr.host "127.0.0.1" "dead:cafe:beef::/48"
        # > "dead:cafe:beef::7f00:1"
        #
        # Inpsired by:
        # https://www.terraform.io/docs/configuration/functions/cidrhost.html
        host = hostnum: cidr: let
          function = "net.cidr.host";
          hostnum' = typechecks.numeric function "hostnum" hostnum;
          cidr' = typechecks.cidr function "cidr" cidr;
        in
          builders.ip (implementations.cidr.host hostnum' cidr');

        # length :: cidr -> integer
        #
        # > net.cidr.prefix "127.0.0.0/8"
        # 8
        #
        # > net.cidr.prefix "dead:cafe:beef::/48"
        # 48
        length = cidr: let
          function = "net.cidr.length";
          cidr' = typechecks.cidr function "cidr" cidr;
        in
          implementations.cidr.length cidr';

        # make :: integer -> ip -> cidr
        #
        # > net.cidr.make 24 "192.168.0.150"
        # "192.168.0.0/24"
        #
        # > net.cidr.make 40 "dead:cafe:beef::feed:face:f00d"
        # "dead:cafe:be00::/40"
        make = length: base: let
          function = "net.cidr.make";
          length' = typechecks.int function "length" length;
          base' = typechecks.ip function "base" base;
        in
          builders.cidr (implementations.cidr.make length' base');

        # netmask :: cidr -> ip
        #
        # > net.cidr.netmask "192.168.0.0/24"
        # "255.255.255.0"
        #
        # > net.cidr.netmask "dead:cafe:beef::/64"
        # "ffff:ffff:ffff:ffff::"
        netmask = cidr: let
          function = "net.cidr.netmask";
          cidr' = typechecks.cidr function "cidr" cidr;
        in
          builders.ip (implementations.cidr.netmask cidr');

        # size :: cidr -> integer
        #
        # > net.cidr.prefix "127.0.0.0/8"
        # 24
        #
        # > net.cidr.prefix "dead:cafe:beef::/48"
        # 80
        size = cidr: let
          function = "net.cidr.size";
          cidr' = typechecks.cidr function "cidr" cidr;
        in
          implementations.cidr.size cidr';

        # subnet :: integer -> (ip | mac | integer) -> cidr -> cidr
        #
        # > net.cidr.subnet 4 2 "172.16.0.0/12"
        # "172.18.0.0/16"
        #
        # > net.cidr.subnet 4 15 "10.1.2.0/24"
        # "10.1.2.240/28"
        #
        # > net.cidr.subnet 16 162 "fd00:fd12:3456:7890::/56"
        # "fd00:fd12:3456:7800:a200::/72"
        #
        # Inspired by:
        # https://www.terraform.io/docs/configuration/functions/cidrsubnet.html
        subnet = length: netnum: cidr: let
          function = "net.cidr.subnet";
          length' = typechecks.int function "length" length;
          netnum' = typechecks.numeric function "netnum" netnum;
          cidr' = typechecks.cidr function "cidr" cidr;
        in
          builders.cidr (implementations.cidr.subnet length' netnum' cidr');
      };
    }
    // (
      if builtins.isNull lib
      then {}
      else {
        types = let
          mkParsedOptionType = {
            name,
            description,
            parser,
            builder,
          }: let
            normalize = def:
              def
              // {
                value = builder (parser def.value);
              };
          in
            lib.mkOptionType {
              inherit name description;
              check = x: builtins.isString x && parser x != null;
              merge = loc: defs: lib.mergeEqualOption loc (map normalize defs);
            };

          dependent-ip = type: cidr: let
            cidrs =
              if builtins.isList cidr
              then cidr
              else [cidr];
          in
            lib.types.addCheck type (i: lib.any (net.cidr.contains i) cidrs)
            // {
              description = type.description + " in ${builtins.concatStringsSep " or " cidrs}";
            };

          dependent-cidr = type: cidr: let
            cidrs =
              if builtins.isList cidr
              then cidr
              else [cidr];
          in
            lib.types.addCheck type (i: lib.any (net.cidr.child i) cidrs)
            // {
              description = type.description + " in ${builtins.concatStringsSep " or " cidrs}";
            };
        in rec {
          ip = mkParsedOptionType {
            name = "ip";
            description = "IPv4 or IPv6 address";
            parser = parsers.ip;
            builder = builders.ip;
          };

          ip-in = dependent-ip ip;

          ipv4 = mkParsedOptionType {
            name = "ipv4";
            description = "IPv4 address";
            parser = parsers.ipv4;
            builder = builders.ipv4;
          };

          ipv4-in = dependent-ip ipv4;

          ipv6 = mkParsedOptionType {
            name = "ipv6";
            description = "IPv6 address";
            parser = parsers.ipv6;
            builder = builders.ipv6;
          };

          ipv6-in = dependent-ip ipv6;

          cidr = mkParsedOptionType {
            name = "cidr";
            description = "IPv4 or IPv6 address range in CIDR notation";
            parser = parsers.cidr;
            builder = builders.cidr;
          };

          cidr-in = dependent-cidr cidr;

          cidrv4 = mkParsedOptionType {
            name = "cidrv4";
            description = "IPv4 address range in CIDR notation";
            parser = parsers.cidrv4;
            builder = builders.cidrv4;
          };

          cidrv4-in = dependent-cidr cidrv4;

          cidrv6 = mkParsedOptionType {
            name = "cidrv6";
            description = "IPv6 address range in CIDR notation";
            parser = parsers.cidrv6;
            builder = builders.cidrv6;
          };

          cidrv6-in = dependent-cidr cidrv6;

          mac = mkParsedOptionType {
            name = "mac";
            description = "MAC address";
            parser = parsers.mac;
            builder = builders.mac;
          };
        };
      }
    );

  list = {
    cons = a: b: [a] ++ b;
  };

  bit = let
    shift = n: x:
      if n < 0
      then x * math.pow 2 (-n)
      else let
        safeDiv = n: d:
          if d == 0
          then 0
          else n / d;
        d = math.pow 2 n;
      in
        if x < 0
        then not (safeDiv (not x) d)
        else safeDiv x d;

    left = n: shift (-n);

    right = shift;

    and = builtins.bitAnd;

    or = builtins.bitOr;

    xor = builtins.bitXor;

    not = xor (-1);

    mask = n: and (left n 1 - 1);
  in {
    inherit left right and or xor not mask;
  };

  math = rec {
    max = a: b:
      if a > b
      then a
      else b;

    min = a: b:
      if a < b
      then a
      else b;

    clamp = a: b: c: max a (min b c);

    pow = x: n:
      if n == 0
      then 1
      else if bit.and n 1 != 0
      then x * pow (x * x) ((n - 1) / 2)
      else pow (x * x) (n / 2);
  };

  parsers = let
    # fmap :: (a -> b) -> parser a -> parser b
    fmap = f: ma: bind ma (a: pure (f a));

    # pure :: a -> parser a
    pure = a: string: {
      leftovers = string;
      result = a;
    };

    # liftA2 :: (a -> b -> c) -> parser a -> parser b -> parser c
    liftA2 = f: ma: mb: bind ma (a: bind mb (b: pure (f a b)));
    liftA3 = f: a: b: ap (liftA2 f a b);
    liftA4 = f: a: b: c: ap (liftA3 f a b c);
    liftA5 = f: a: b: c: d: ap (liftA4 f a b c d);
    liftA6 = f: a: b: c: d: e: ap (liftA5 f a b c d e);

    # ap :: parser (a -> b) -> parser a -> parser b
    ap = liftA2 (a: a);

    # then_ :: parser a -> parser b -> parser b
    then_ = liftA2 (a: b: b);

    # empty :: parser a
    empty = string: null;

    # alt :: parser a -> parser a -> parser a
    alt = left: right: string: let
      result = left string;
    in
      if builtins.isNull result
      then right string
      else result;

    # guard :: bool -> parser {}
    guard = condition:
      if condition
      then pure {}
      else empty;

    # mfilter :: (a -> bool) -> parser a -> parser a
    mfilter = f: parser: bind parser (a: then_ (guard (f a)) (pure a));

    # some :: parser a -> parser [a]
    some = v: liftA2 list.cons v (many v);

    # many :: parser a -> parser [a]
    many = v: alt (some v) (pure []);

    # bind :: parser a -> (a -> parser b) -> parser b
    bind = parser: f: string: let
      a = parser string;
    in
      if builtins.isNull a
      then null
      else f a.result a.leftovers;

    # run :: parser a -> string -> maybe a
    run = parser: string: let
      result = parser string;
    in
      if builtins.isNull result || result.leftovers != ""
      then null
      else result.result;

    next = string:
      if string == ""
      then null
      else {
        leftovers = builtins.substring 1 (-1) string;
        result = builtins.substring 0 1 string;
      };

    # Count how many characters were consumed by a parser
    count = parser: string: let
      result = parser string;
    in
      if builtins.isNull result
      then null
      else
        result
        // {
          result = {
            inherit (result) result;
            count = with result;
              builtins.stringLength string - builtins.stringLength leftovers;
          };
        };

    # Limit the parser to n characters at most
    limit = n: parser:
      fmap (a: a.result) (mfilter (a: a.count <= n) (count parser));

    # Ensure the parser consumes exactly n characters
    exactly = n: parser:
      fmap (a: a.result) (mfilter (a: a.count == n) (count parser));

    char = c: bind next (c': guard (c == c'));

    string = css:
      if css == ""
      then pure {}
      else let
        c = builtins.substring 0 1 css;
        cs = builtins.substring 1 (-1) css;
      in
        then_ (char c) (string cs);

    digit = set:
      bind next (
        c:
          then_
          (guard (builtins.hasAttr c set))
          (pure (builtins.getAttr c set))
      );

    decimalDigits = {
      "0" = 0;
      "1" = 1;
      "2" = 2;
      "3" = 3;
      "4" = 4;
      "5" = 5;
      "6" = 6;
      "7" = 7;
      "8" = 8;
      "9" = 9;
    };

    hexadecimalDigits =
      decimalDigits
      // {
        "a" = 10;
        "b" = 11;
        "c" = 12;
        "d" = 13;
        "e" = 14;
        "f" = 15;
        "A" = 10;
        "B" = 11;
        "C" = 12;
        "D" = 13;
        "E" = 14;
        "F" = 15;
      };

    fromDecimalDigits = builtins.foldl' (a: c: a * 10 + c) 0;
    fromHexadecimalDigits = builtins.foldl' (a: bit.or (bit.left 4 a)) 0;

    # disallow leading zeros
    decimal = bind (digit decimalDigits) (
      n:
        if n == 0
        then pure 0
        else
          fmap
          (ns: fromDecimalDigits (list.cons n ns))
          (many (digit decimalDigits))
    );

    hexadecimal = fmap fromHexadecimalDigits (some (digit hexadecimalDigits));

    ipv4 = let
      dot = char ".";

      octet = mfilter (n: n < 256) decimal;

      octet' = then_ dot octet;

      fromOctets = a: b: c: d: {
        ipv4 = bit.or (bit.left 8 (bit.or (bit.left 8 (bit.or (bit.left 8 a) b)) c)) d;
      };
    in
      liftA4 fromOctets octet octet' octet' octet';

    # This is more or less a literal translation of
    # https://hackage.haskell.org/package/ip/docs/src/Net.IPv6.html#parser
    ipv6 = let
      colon = char ":";

      hextet = limit 4 hexadecimal;

      hextet' = then_ colon hextet;

      fromHextets = hextets:
        if builtins.length hextets != 8
        then empty
        else let
          a = builtins.elemAt hextets 0;
          b = builtins.elemAt hextets 1;
          c = builtins.elemAt hextets 2;
          d = builtins.elemAt hextets 3;
          e = builtins.elemAt hextets 4;
          f = builtins.elemAt hextets 5;
          g = builtins.elemAt hextets 6;
          h = builtins.elemAt hextets 7;
        in
          pure {
            ipv6 = {
              a = bit.or (bit.left 16 a) b;
              b = bit.or (bit.left 16 c) d;
              c = bit.or (bit.left 16 e) f;
              d = bit.or (bit.left 16 g) h;
            };
          };

      ipv4' =
        fmap
        (
          address: let
            upper = bit.right 16 address.ipv4;
            lower = bit.mask 16 address.ipv4;
          in [upper lower]
        )
        ipv4;

      part = n: let
        n' = n + 1;
        hex =
          liftA2 list.cons hextet
          (
            then_ colon
            (
              alt
              (then_ colon (doubleColon n'))
              (part n')
            )
          );
      in
        if n == 7
        then fmap (a: [a]) hextet
        else if n == 6
        then alt ipv4' hex
        else hex;

      doubleColon = n:
        bind (alt afterDoubleColon (pure [])) (
          rest: let
            missing = 8 - n - builtins.length rest;
          in
            if missing < 0
            then empty
            else pure (builtins.genList (_: 0) missing ++ rest)
        );

      afterDoubleColon =
        alt ipv4'
        (
          liftA2 list.cons hextet
          (
            alt
            (then_ colon afterDoubleColon)
            (pure [])
          )
        );
    in
      bind
      (
        alt
        (
          then_
          (string "::")
          (doubleColon 0)
        )
        (part 0)
      )
      fromHextets;

    cidrv4 =
      liftA2
      (base: length: implementations.cidr.make length base)
      ipv4
      (then_ (char "/") (mfilter (n: n <= 32) decimal));

    cidrv6 =
      liftA2
      (base: length: implementations.cidr.make length base)
      ipv6
      (then_ (char "/") (mfilter (n: n <= 128) decimal));

    mac = let
      colon = char ":";

      octet = exactly 2 hexadecimal;

      octet' = then_ colon octet;

      fromOctets = a: b: c: d: e: f: {
        mac = bit.or (bit.left 8 (bit.or (bit.left 8 (bit.or (bit.left 8 (bit.or (bit.left 8 (bit.or (bit.left 8 a) b)) c)) d)) e)) f;
      };
    in
      liftA6 fromOctets octet octet' octet' octet' octet' octet';
  in {
    ipv4 = run ipv4;
    ipv6 = run ipv6;
    ip = run (alt ipv4 ipv6);
    cidrv4 = run cidrv4;
    cidrv6 = run cidrv6;
    cidr = run (alt cidrv4 cidrv6);
    mac = run mac;
    numeric = run (alt (alt ipv4 ipv6) mac);
  };

  builders = let
    ipv4 = address: let
      abcd = address.ipv4;
      abc = bit.right 8 abcd;
      ab = bit.right 8 abc;
      a = bit.right 8 ab;
      b = bit.mask 8 ab;
      c = bit.mask 8 abc;
      d = bit.mask 8 abcd;
    in
      builtins.concatStringsSep "." (map toString [a b c d]);

    # This is more or less a literal translation of
    # https://hackage.haskell.org/package/ip/docs/src/Net.IPv6.html#encode
    ipv6 = address: let
      digits = "0123456789abcdef";

      toHexString = n: let
        rest = bit.right 4 n;
        current = bit.mask 4 n;
        prefix =
          if rest == 0
          then ""
          else toHexString rest;
      in "${prefix}${builtins.substring current 1 digits}";
    in
      if (with address.ipv6; a == 0 && b == 0 && c == 0 && d > 65535)
      then "::${ipv4 {ipv4 = address.ipv6.d;}}"
      else if (with address.ipv6; a == 0 && b == 0 && c == 65535)
      then "::ffff:${ipv4 {ipv4 = address.ipv6.d;}}"
      else let
        a = bit.right 16 address.ipv6.a;
        b = bit.mask 16 address.ipv6.a;
        c = bit.right 16 address.ipv6.b;
        d = bit.mask 16 address.ipv6.b;
        e = bit.right 16 address.ipv6.c;
        f = bit.mask 16 address.ipv6.c;
        g = bit.right 16 address.ipv6.d;
        h = bit.mask 16 address.ipv6.d;

        hextets = [a b c d e f g h];

        # calculate the position and size of the longest sequence of
        # zeroes within the list of hextets
        longest = let
          go = i: current: best:
            if i < builtins.length hextets
            then let
              n = builtins.elemAt hextets i;

              current' =
                if n == 0
                then
                  if builtins.isNull current
                  then {
                    size = 1;
                    position = i;
                  }
                  else
                    current
                    // {
                      size = current.size + 1;
                    }
                else null;

              best' =
                if n == 0
                then
                  if builtins.isNull best
                  then current'
                  else if current'.size > best.size
                  then current'
                  else best
                else best;
            in
              go (i + 1) current' best'
            else best;
        in
          go 0 null null;

        format = hextets:
          builtins.concatStringsSep ":" (map toHexString hextets);
      in
        if builtins.isNull longest
        then format hextets
        else let
          sublist = i: length: xs:
            map
            (builtins.elemAt xs)
            (builtins.genList (x: x + i) length);

          end = longest.position + longest.size;

          before = sublist 0 longest.position hextets;

          after = sublist end (builtins.length hextets - end) hextets;
        in "${format before}::${format after}";

    ip = address:
      if address ? ipv4
      then ipv4 address
      else ipv6 address;

    cidrv4 = cidr: "${ipv4 cidr.base}/${toString cidr.length}";

    cidrv6 = cidr: "${ipv6 cidr.base}/${toString cidr.length}";

    cidr = cidr: "${ip cidr.base}/${toString cidr.length}";

    mac = address: let
      digits = "0123456789abcdef";
      octet = n: let
        upper = bit.right 4 n;
        lower = bit.mask 4 n;
      in "${builtins.substring upper 1 digits}${builtins.substring lower 1 digits}";
    in let
      a = bit.mask 8 (bit.right 40 address.mac);
      b = bit.mask 8 (bit.right 32 address.mac);
      c = bit.mask 8 (bit.right 24 address.mac);
      d = bit.mask 8 (bit.right 16 address.mac);
      e = bit.mask 8 (bit.right 8 address.mac);
      f = bit.mask 8 (bit.right 0 address.mac);
    in "${octet a}:${octet b}:${octet c}:${octet d}:${octet e}:${octet f}";
  in {
    inherit ipv4 ipv6 ip cidrv4 cidrv6 cidr mac;
  };

  arithmetic = rec {
    # or :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer)
    or = a_: b: let
      a = coerce b a_;
    in
      if a ? ipv6
      then {
        ipv6 = {
          a = bit.or a.ipv6.a b.ipv6.a;
          b = bit.or a.ipv6.b b.ipv6.b;
          c = bit.or a.ipv6.c b.ipv6.c;
          d = bit.or a.ipv6.d b.ipv6.d;
        };
      }
      else if a ? ipv4
      then {
        ipv4 = bit.or a.ipv4 b.ipv4;
      }
      else if a ? mac
      then {
        mac = bit.or a.mac b.mac;
      }
      else bit.or a b;

    # and :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer)
    and = a_: b: let
      a = coerce b a_;
    in
      if a ? ipv6
      then {
        ipv6 = {
          a = bit.and a.ipv6.a b.ipv6.a;
          b = bit.and a.ipv6.b b.ipv6.b;
          c = bit.and a.ipv6.c b.ipv6.c;
          d = bit.and a.ipv6.d b.ipv6.d;
        };
      }
      else if a ? ipv4
      then {
        ipv4 = bit.and a.ipv4 b.ipv4;
      }
      else if a ? mac
      then {
        mac = bit.and a.mac b.mac;
      }
      else bit.and a b;

    # not :: (ip | mac | integer) -> (ip | mac | integer)
    not = a:
      if a ? ipv6
      then {
        ipv6 = {
          a = bit.mask 32 (bit.not a.ipv6.a);
          b = bit.mask 32 (bit.not a.ipv6.b);
          c = bit.mask 32 (bit.not a.ipv6.c);
          d = bit.mask 32 (bit.not a.ipv6.d);
        };
      }
      else if a ? ipv4
      then {
        ipv4 = bit.mask 32 (bit.not a.ipv4);
      }
      else if a ? mac
      then {
        mac = bit.mask 48 (bit.not a.mac);
      }
      else bit.not a;

    # add :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer)
    add = let
      split = a: {
        fst = bit.mask 32 (bit.right 32 a);
        snd = bit.mask 32 a;
      };
    in
      a_: b: let
        a = coerce b a_;
      in
        if a ? ipv6
        then let
          a' = split (a.ipv6.a + b.ipv6.a + b'.fst);
          b' = split (a.ipv6.b + b.ipv6.b + c'.fst);
          c' = split (a.ipv6.c + b.ipv6.c + d'.fst);
          d' = split (a.ipv6.d + b.ipv6.d);
        in {
          ipv6 = {
            a = a'.snd;
            b = b'.snd;
            c = c'.snd;
            d = d'.snd;
          };
        }
        else if a ? ipv4
        then {
          ipv4 = bit.mask 32 (a.ipv4 + b.ipv4);
        }
        else if a ? mac
        then {
          mac = bit.mask 48 (a.mac + b.mac);
        }
        else a + b;

    # subtract :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer)
    subtract = a: b: add (add 1 (not (coerce b a))) b;

    # diff :: (ip | mac | integer) -> (ip | mac | integer) -> (ipv6 | integer)
    diff = a: b: let
      toIPv6 = coerce {ipv6.a = 0;};
      result = (subtract b (toIPv6 a)).ipv6;
      max32 = bit.left 32 1 - 1;
    in
      if result.a == 0 && result.b == 0 && bit.right 31 result.c == 0 || result.a == max32 && result.b == max32 && bit.right 31 result.c == 1
      then bit.or (bit.left 32 result.c) result.d
      else {
        ipv6 = result;
      };

    # left :: integer -> (ip | mac | integer) -> (ip | mac | integer)
    left = i: right (-i);

    # right :: integer -> (ip | mac | integer) -> (ip | mac | integer)
    right = let
      step = i: x: {
        _1 = bit.mask 32 (bit.right (i + 96) x);
        _2 = bit.mask 32 (bit.right (i + 64) x);
        _3 = bit.mask 32 (bit.right (i + 32) x);
        _4 = bit.mask 32 (bit.right i x);
        _5 = bit.mask 32 (bit.right (i - 32) x);
        _6 = bit.mask 32 (bit.right (i - 64) x);
        _7 = bit.mask 32 (bit.right (i - 96) x);
      };
      ors = builtins.foldl' bit.or 0;
    in
      i: x:
        if x ? ipv6
        then let
          a' = step i x.ipv6.a;
          b' = step i x.ipv6.b;
          c' = step i x.ipv6.c;
          d' = step i x.ipv6.d;
        in {
          ipv6 = {
            a = ors [a'._4 b'._3 c'._2 d'._1];
            b = ors [a'._5 b'._4 c'._3 d'._2];
            c = ors [a'._6 b'._5 c'._4 d'._3];
            d = ors [a'._7 b'._6 c'._5 d'._4];
          };
        }
        else if x ? ipv4
        then {
          ipv4 = bit.mask 32 (bit.right i x.ipv4);
        }
        else if x ? mac
        then {
          mac = bit.mask 48 (bit.right i x.mac);
        }
        else bit.right i x;

    # shadow :: integer -> (ip | mac | integer) -> (ip | mac | integer)
    shadow = n: a: and (right n (left n (coerce a (-1)))) a;

    # coshadow :: integer -> (ip | mac | integer) -> (ip | mac | integer)
    coshadow = n: a: and (not (right n (left n (coerce a (-1))))) a;

    # coerce :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer)
    coerce = target: value:
      if target ? ipv6
      then
        if value ? ipv6
        then value
        else if value ? ipv4
        then {
          ipv6 = {
            a = 0;
            b = 0;
            c = 0;
            d = value.ipv4;
          };
        }
        else if value ? mac
        then {
          ipv6 = {
            a = 0;
            b = 0;
            c = bit.right 32 value.mac;
            d = bit.mask 32 value.mac;
          };
        }
        else {
          ipv6 = {
            a = bit.mask 32 (bit.right 96 value);
            b = bit.mask 32 (bit.right 64 value);
            c = bit.mask 32 (bit.right 32 value);
            d = bit.mask 32 value;
          };
        }
      else if target ? ipv4
      then
        if value ? ipv6
        then {
          ipv4 = value.ipv6.d;
        }
        else if value ? ipv4
        then value
        else if value ? mac
        then {
          ipv4 = bit.mask 32 value.mac;
        }
        else {
          ipv4 = bit.mask 32 value;
        }
      else if target ? mac
      then
        if value ? ipv6
        then {
          mac = bit.or (bit.left 32 (bit.mask 16 value.ipv6.c)) value.ipv6.d;
        }
        else if value ? ipv4
        then {
          mac = value.ipv4;
        }
        else if value ? mac
        then value
        else {
          mac = bit.mask 48 value;
        }
      else if value ? ipv6
      then
        builtins.foldl' bit.or 0
        [
          (bit.left 96 value.ipv6.a)
          (bit.left 64 value.ipv6.b)
          (bit.left 32 value.ipv6.c)
          value.ipv6.d
        ]
      else if value ? ipv4
      then value.ipv4
      else if value ? mac
      then value.mac
      else value;
  };

  implementations = {
    ip = {
      # add :: (ip | mac | integer) -> ip -> ip
      add = arithmetic.add;

      # diff :: ip -> ip -> (ipv6 | integer)
      diff = arithmetic.diff;

      # subtract :: (ip | mac | integer) -> ip -> ip
      subtract = arithmetic.subtract;
    };

    mac = {
      # add :: (ip | mac | integer) -> mac -> mac
      add = arithmetic.add;

      # diff :: mac -> mac -> (ipv6 | integer)
      diff = arithmetic.diff;

      # subtract :: (ip | mac | integer) -> mac -> mac
      subtract = arithmetic.subtract;
    };

    cidr = rec {
      # add :: (ip | mac | integer) -> cidr -> cidr
      add = delta: cidr: let
        size' = size cidr;
      in {
        base = arithmetic.left size' (arithmetic.add delta (arithmetic.right size' cidr.base));
        inherit (cidr) length;
      };

      # capacity :: cidr -> integer
      capacity = cidr: let
        size' = size cidr;
      in
        if size' > 62
        then 9223372036854775807 # maxBound to prevent overflow
        else bit.left size' 1;

      # child :: cidr -> cidr -> bool
      child = subcidr: cidr:
        length subcidr > length cidr && contains (host 0 subcidr) cidr;

      # contains :: ip -> cidr -> bool
      contains = ip: cidr: host 0 (make cidr.length ip) == host 0 cidr;

      # host :: (ip | mac | integer) -> cidr -> ip
      host = index: cidr: let
        index' = arithmetic.coerce cidr.base index;
      in
        arithmetic.or (arithmetic.shadow cidr.length index') cidr.base;

      # length :: cidr -> integer
      length = cidr: cidr.length;

      # netmask :: cidr -> ip
      netmask = cidr: arithmetic.coshadow cidr.length (arithmetic.coerce cidr.base (-1));

      # size :: cidr -> integer
      size = cidr:
        (
          if cidr.base ? ipv6
          then 128
          else 32
        )
        - cidr.length;

      # subnet :: integer -> (ip | mac | integer) -> cidr -> cidr
      subnet = length: index: cidr: let
        length' = cidr.length + length;
        index' = arithmetic.coerce cidr.base index;
        size =
          (
            if cidr.base ? ipv6
            then 128
            else 32
          )
          - length';
      in
        make length' (host (arithmetic.left size index') cidr);

      # make :: integer -> ip -> cidr
      make = length: base: let
        length' =
          math.clamp 0 (
            if base ? ipv6
            then 128
            else 32
          )
          length;
      in {
        base = arithmetic.coshadow length' base;
        length = length';
      };
    };
  };

  typechecks = let
    fail = description: function: argument:
      builtins.throw "${function}: ${argument} parameter must be ${description}";

    meta = parser: description: function: argument: input: let
      error = fail description function argument;
    in
      if !builtins.isString input
      then error
      else let
        result = parser input;
      in
        if builtins.isNull result
        then error
        else result;
  in {
    int = function: argument: input:
      if builtins.isInt input
      then input
      else fail "an integer" function argument;
    ip = meta parsers.ip "an IPv4 or IPv6 address";
    cidr = meta parsers.cidr "an IPv4 or IPv6 address range in CIDR notation";
    mac = meta parsers.mac "a MAC address";
    numeric = function: argument: input:
      if builtins.isInt input
      then input
      else meta parsers.numeric "an integer or IPv4, IPv6 or MAC address" function argument input;
  };
in {
  lib = {
    inherit net;
  };
}