feat(users/sterni/nix): add sternis nix lib

What you see here is mostly the fallout of me implementing a correct
urlencode implementation in nix for Profpatsch's blog implementation
(although they'll probably keep it at arm's length).

Where I want to go from here:

* Extend this library towards general purpose nix™, mainly by
  implementing missing interfaces which you'd still have to use
  <nixpkgs/lib> for right now. Reexposing parts of <nixpkgs/lib>
  with better naming is fine for now, at some point I'd contemplate
  making this depend on nothing outside of depot, maybe even itself
  (should be easy we only use yants for an easily replaceable check).

* Improve error messages possibly by carefully reintroducing yants. I
  originally typed essentially everything using yants, but turns out
  this can a) be dangerous when stuff you are handling throws because
  type checking means evaluating and b) has a incredible performance
  cost in some cases.

* Reexpose builtins with better naming and slightly wrapped so they
  don't unrecoverably throw in cases where a null or something would
  suffice.

Change-Id: I33ab08ca4e62dbc16b86c66c653935686e6b0e79
Reviewed-on: https://cl.tvl.fyi/c/depot/+/2541
Reviewed-by: sterni <sternenseemann@systemli.org>
Reviewed-by: Profpatsch <mail@profpatsch.de>
Tested-by: BuildkiteCI
This commit is contained in:
sterni 2021-02-22 14:32:45 +01:00
parent 369f504250
commit 3b33c1bd76
12 changed files with 811 additions and 0 deletions

View file

@ -0,0 +1,121 @@
{ depot, lib, ... }:
let
# TODO(sterni): implement nix.float and figure out which of these
# functions can be split out into a common nix.num
# library.
inherit (depot.users.sterni.nix)
string
;
inherit (builtins)
bitOr
bitAnd
bitXor
mul
div
add
sub
;
abs = i: if i < 0 then -i else i;
exp = base: pow:
if pow > 0
then base * (exp base (pow - 1))
else if pow < 0
then 1.0 / exp base (abs pow)
else 1;
bitShiftR = bit: count:
if count == 0
then bit
else div (bitShiftR bit (count - 1)) 2;
bitShiftL = bit: count:
if count == 0
then bit
else 2 * (bitShiftL bit (count - 1));
hexdigits = "0123456789ABCDEF";
toHex = int:
let
go = i:
if i == 0
then ""
else go (bitShiftR i 4)
+ string.charAt (bitAnd i 15) hexdigits;
sign = lib.optionalString (int < 0) "-";
in
if int == 0
then "0"
else "${sign}${go (abs int)}";
fromHexMap = builtins.listToAttrs
(lib.imap0 (i: c: { name = c; value = i; })
(lib.stringToCharacters hexdigits));
fromHex = literal:
let
negative = string.charAt 0 literal == "-";
start = if negative then 1 else 0;
len = builtins.stringLength literal;
# reversed list of all digits
digits = builtins.genList
(i: string.charAt (len - 1 - i) literal)
(len - start);
parsed = builtins.foldl'
(v: d: {
val = v.val + (fromHexMap."${d}" * v.mul);
mul = v.mul * 16;
})
{ val = 0; mul = 1; } digits;
in
if negative
then -parsed.val
else parsed.val;
# A nix integer is a 64bit signed integer
maxBound = 9223372036854775807;
# fun fact: -9223372036854775808 is the lower bound
# for a nix integer (as you would expect), but you can't
# use it as an integer literal or you'll be greeted with:
# error: invalid integer '9223372036854775808'
# This is because all int literals when parsing are
# positive, negative "literals" are positive literals
# which are preceded by the arithmetric negation operator.
minBound = -9223372036854775807 - 1;
odd = x: bitAnd x 1 == 1;
even = x: bitAnd x 1 == 0;
# div and mod behave like quot and rem in Haskell,
# i. e. they truncate towards 0
mod = a: b: let res = a / b; in a - (res * b);
in {
inherit
maxBound
minBound
abs
exp
odd
even
add
sub
mul
div
mod
bitShiftR
bitShiftL
bitOr
bitAnd
bitXor
toHex
fromHex
;
}

View file

@ -0,0 +1,204 @@
{ depot, lib, ... }:
let
inherit (depot.nix.runTestsuite)
runTestsuite
it
assertEq
;
inherit (depot.users.sterni.nix)
int
string
fun
flow
;
testBounds = it "checks minBound and maxBound" [
# this is gonna blow up in my face because
# integer overflow is undefined behavior in
# C++, so most likely anything could happen?
(assertEq "maxBound is the maxBound" true
(int.maxBound + 1 < int.maxBound))
(assertEq "minBound is the minBound" true
(int.minBound - 1 > int.minBound))
(assertEq "maxBound overflows to minBound"
(int.maxBound + 1)
int.minBound)
(assertEq "minBound overflows to maxBound"
(int.minBound - 1)
int.maxBound)
];
expectedBytes = [
"00" "01" "02" "03" "04" "05" "06" "07" "08" "09" "0A" "0B" "0C" "0D" "0E" "0F"
"10" "11" "12" "13" "14" "15" "16" "17" "18" "19" "1A" "1B" "1C" "1D" "1E" "1F"
"20" "21" "22" "23" "24" "25" "26" "27" "28" "29" "2A" "2B" "2C" "2D" "2E" "2F"
"30" "31" "32" "33" "34" "35" "36" "37" "38" "39" "3A" "3B" "3C" "3D" "3E" "3F"
"40" "41" "42" "43" "44" "45" "46" "47" "48" "49" "4A" "4B" "4C" "4D" "4E" "4F"
"50" "51" "52" "53" "54" "55" "56" "57" "58" "59" "5A" "5B" "5C" "5D" "5E" "5F"
"60" "61" "62" "63" "64" "65" "66" "67" "68" "69" "6A" "6B" "6C" "6D" "6E" "6F"
"70" "71" "72" "73" "74" "75" "76" "77" "78" "79" "7A" "7B" "7C" "7D" "7E" "7F"
"80" "81" "82" "83" "84" "85" "86" "87" "88" "89" "8A" "8B" "8C" "8D" "8E" "8F"
"90" "91" "92" "93" "94" "95" "96" "97" "98" "99" "9A" "9B" "9C" "9D" "9E" "9F"
"A0" "A1" "A2" "A3" "A4" "A5" "A6" "A7" "A8" "A9" "AA" "AB" "AC" "AD" "AE" "AF"
"B0" "B1" "B2" "B3" "B4" "B5" "B6" "B7" "B8" "B9" "BA" "BB" "BC" "BD" "BE" "BF"
"C0" "C1" "C2" "C3" "C4" "C5" "C6" "C7" "C8" "C9" "CA" "CB" "CC" "CD" "CE" "CF"
"D0" "D1" "D2" "D3" "D4" "D5" "D6" "D7" "D8" "D9" "DA" "DB" "DC" "DD" "DE" "DF"
"E0" "E1" "E2" "E3" "E4" "E5" "E6" "E7" "E8" "E9" "EA" "EB" "EC" "ED" "EE" "EF"
"F0" "F1" "F2" "F3" "F4" "F5" "F6" "F7" "F8" "F9" "FA" "FB" "FC" "FD" "FE" "FF"
];
hexByte = i: string.fit { width = 2; char = "0"; } (int.toHex i);
hexInts = [
{ left = 0; right = "0"; }
{ left = 1; right = "1"; }
{ left = 11; right = "B"; }
{ left = 123; right = "7B"; }
{ left = 9000; right = "2328"; }
{ left = 2323; right = "913"; }
{ left = 4096; right = "1000"; }
{ left = int.maxBound; right = "7FFFFFFFFFFFFFFF"; }
{ left = int.minBound; right = "-8000000000000000"; }
];
testHex = it "checks conversion to hex" (lib.flatten [
(lib.imap0 (i: hex: [
(assertEq "hexByte ${toString i} == ${hex}" (hexByte i) hex)
(assertEq "${toString i} == fromHex ${hex}" i (int.fromHex hex))
]) expectedBytes)
(builtins.map ({ left, right }: [
(assertEq "toHex ${toString left} == ${right}" (int.toHex left) right)
(assertEq "${toString left} == fromHex ${right}" left (int.fromHex right))
]) hexInts)
]);
testBasic = it "checks basic int operations" [
(assertEq "122 is even" (int.even 122 && !(int.odd 122)) true)
(assertEq "123 is odd" (int.odd 123 && !(int.even 123)) true)
(assertEq "abs -4959" (int.abs (-4959)) 4959)
];
expNumbers = [
{ left = -3; right = 0.125; }
{ left = -2; right = 0.25; }
{ left = -1; right = 0.5; }
{ left = 0; right = 1; }
{ left = 1; right = 2; }
{ left = 2; right = 4; }
{ left = 3; right = 8; }
{ left = 4; right = 16; }
{ left = 5; right = 32; }
{ left = 16; right = 65536; }
];
testExp = it "checks exponentiation"
(builtins.map ({ left, right }:
assertEq
"2 ^ ${toString left} == ${toString right}"
(int.exp 2 left) right) expNumbers);
shifts = [
{ a = 2; b = 5; c = 64; op = "<<"; }
{ a = -2; b = 5; c = -64; op = "<<"; }
{ a = 123; b = 4; c = 1968; op = "<<"; }
{ a = 1; b = 8; c = 256; op = "<<"; }
{ a = 256; b = 8; c = 1; op = ">>"; }
{ a = 374; b = 2; c = 93; op = ">>"; }
{ a = 2; b = 2; c = 0; op = ">>"; }
{ a = 99; b = 9; c = 0; op = ">>"; }
];
checkShift = { a, b, c, op }@args:
let
f = flow.match op {
"<<" = int.bitShiftL;
">>" = int.bitShiftR;
};
in assertEq "${toString a} ${op} ${toString b} == ${toString c}" (f a b) c;
checkShiftRDivExp = n:
assertEq "${toString n} >> 5 == ${toString n} / 2 ^ 5"
(int.bitShiftR n 5) (int.div n (int.exp 2 5));
checkShiftLMulExp = n:
assertEq "${toString n} >> 6 == ${toString n} * 2 ^ 6"
(int.bitShiftL n 5) (int.mul n (int.exp 2 5));
testBit = it "checks bitwise operations" (lib.flatten [
(builtins.map checkShift shifts)
(builtins.map checkShiftRDivExp [
1
2
3
5
7
23
1623
238
34
348
2834
834
348
])
(builtins.map checkShiftLMulExp [
1
2
3
5
7
23
384
3
2
5991
85109
38
])
]);
divisions = [
{ a = 2; b = 1; c = 2; mod = 0;}
{ a = 2; b = 2; c = 1; mod = 0;}
{ a = 20; b = 10; c = 2; mod = 0;}
{ a = 12; b = 5; c = 2; mod = 2;}
{ a = 23; b = 4; c = 5; mod = 3;}
];
checkDiv = n: { a, b, c, mod }: [
(assertEq "${n}: div result" (int.div a b) c)
(assertEq "${n}: mod result" (int.mod a b) mod)
(assertEq "${n}: divMod law" ((int.div a b) * b + (int.mod a b)) a)
];
testDivMod = it "checks integer division and modulo"
(lib.flatten [
(builtins.map (checkDiv "+a / +b") divisions)
(builtins.map (fun.rl (checkDiv "-a / +b") (x: x // {
a = -x.a;
c = -x.c;
mod = -x.mod;
})) divisions)
(builtins.map (fun.rl (checkDiv "+a / -b") (x: x // {
b = -x.b;
c = -x.c;
})) divisions)
(builtins.map (fun.rl (checkDiv "-a / -b") (x: x // {
a = -x.a;
b = -x.b;
mod = -x.mod;
})) divisions)
]);
in
runTestsuite "nix.int" [
testBounds
testHex
testBasic
testExp
testBit
testDivMod
]