diff options
Diffstat (limited to 'nix/yants')
-rw-r--r-- | nix/yants/README.md | 6 | ||||
-rw-r--r-- | nix/yants/default.nix | 323 | ||||
-rw-r--r-- | nix/yants/tests/default.nix | 155 |
3 files changed, 308 insertions, 176 deletions
diff --git a/nix/yants/README.md b/nix/yants/README.md index d17ea61b52..98e6642e2d 100644 --- a/nix/yants/README.md +++ b/nix/yants/README.md @@ -44,9 +44,9 @@ Currently lacking: Yants can be imported from its `default.nix`. A single attribute (`lib`) can be passed, which will otherwise be imported from `<nixpkgs>`. -TIP: You do not need to clone my whole repository to use Yants! It is split out -into the `nix/yants` branch which you can clone with, for example, `git clone -b -nix/yants https://git.tazj.in yants`. +TIP: You do not need to clone the entire TVL repository to use Yants! +You can clone just this project through josh: `git clone +https://code.tvl.fyi/depot.git:/nix/yants.git` Examples for the most common import methods would be: diff --git a/nix/yants/default.nix b/nix/yants/default.nix index 6da99fa3c8..cb9fc08287 100644 --- a/nix/yants/default.nix +++ b/nix/yants/default.nix @@ -6,10 +6,10 @@ # # All types (should) compose as expected. -{ lib ? (import <nixpkgs> {}).lib, ... }: +{ lib ? (import <nixpkgs> { }).lib, ... }: with builtins; let - prettyPrint = lib.generators.toPretty {}; + prettyPrint = lib.generators.toPretty { }; # typedef' :: struct { # name = string; @@ -34,41 +34,44 @@ with builtins; let # # This function is the low-level primitive used to create types. For # many cases the higher-level 'typedef' function is more appropriate. - typedef' = { name, checkType - , checkToBool ? (result: result.ok) - , toError ? (_: result: result.err) - , def ? null - , match ? null }: { - inherit name checkToBool toError; - - # check :: a -> bool - # - # This function is used to determine whether a given type is - # conformant. - check = value: checkToBool (checkType value); - - # checkType :: a -> struct { ok = bool; err = option string; } - # - # This function checks whether the passed value is type conformant - # and returns an optional type error string otherwise. - inherit checkType; - - # __functor :: a -> a - # - # This function checks whether the passed value is type conformant - # and throws an error if it is not. - # - # The name of this function is a special attribute in Nix that - # makes it possible to execute a type attribute set like a normal - # function. - __functor = self: value: - let result = self.checkType value; - in if checkToBool result then value - else throw (toError value result); - }; + typedef' = + { name + , checkType + , checkToBool ? (result: result.ok) + , toError ? (_: result: result.err) + , def ? null + , match ? null + }: { + inherit name checkToBool toError; + + # check :: a -> bool + # + # This function is used to determine whether a given type is + # conformant. + check = value: checkToBool (checkType value); + + # checkType :: a -> struct { ok = bool; err = option string; } + # + # This function checks whether the passed value is type conformant + # and returns an optional type error string otherwise. + inherit checkType; + + # __functor :: a -> a + # + # This function checks whether the passed value is type conformant + # and throws an error if it is not. + # + # The name of this function is a special attribute in Nix that + # makes it possible to execute a type attribute set like a normal + # function. + __functor = self: value: + let result = self.checkType value; + in if checkToBool result then value + else throw (toError value result); + }; typeError = type: val: - "expected type '${type}', but value '${prettyPrint val}' is of type '${typeOf val}'"; + "expected type '${type}', but value '${prettyPrint val}' is of type '${typeOf val}'"; # typedef :: string -> (a -> bool) -> type # @@ -76,32 +79,43 @@ with builtins; let # error message constructor. typedef = name: check: typedef' { inherit name; - checkType = check; - checkToBool = r: r; - toError = value: _result: typeError name value; + checkType = v: + let res = check v; + in { + ok = res; + } // (lib.optionalAttrs (!res) { + err = typeError name v; + }); }; - checkEach = name: t: l: foldl' (acc: e: - let res = t.checkType e; + checkEach = name: t: l: foldl' + (acc: e: + let + res = t.checkType e; isT = t.checkToBool res; - in { - ok = acc.ok && isT; - err = if isT - then acc.err - else acc.err + "${prettyPrint e}: ${t.toError e res}\n"; - }) { ok = true; err = "expected type ${name}, but found:\n"; } l; -in lib.fix (self: { + in + { + ok = acc.ok && isT; + err = + if isT + then acc.err + else acc.err + "${prettyPrint e}: ${t.toError e res}\n"; + }) + { ok = true; err = "expected type ${name}, but found:\n"; } + l; +in +lib.fix (self: { # Primitive types - any = typedef "any" (_: true); - unit = typedef "unit" (v: v == {}); - int = typedef "int" isInt; - bool = typedef "bool" isBool; - float = typedef "float" isFloat; - string = typedef "string" isString; - path = typedef "path" (x: typeOf x == "path"); - drv = typedef "derivation" (x: isAttrs x && x ? "type" && x.type == "derivation"); + any = typedef "any" (_: true); + unit = typedef "unit" (v: v == { }); + int = typedef "int" isInt; + bool = typedef "bool" isBool; + float = typedef "float" isFloat; + string = typedef "string" isString; + path = typedef "path" (x: typeOf x == "path"); + drv = typedef "derivation" (x: isAttrs x && x ? "type" && x.type == "derivation"); function = typedef "function" (x: isFunction x || (isAttrs x && x ? "__functor" - && isFunction x.__functor)); + && isFunction x.__functor)); # Type for types themselves. Useful when defining polymorphic types. type = typedef "type" (x: @@ -120,7 +134,7 @@ in lib.fix (self: { in { ok = isNull v || (self.type t).checkToBool res; err = "expected type ${name}, but value does not conform to '${t.name}': " - + t.toError v res; + + t.toError v res; }; }; @@ -132,7 +146,8 @@ in lib.fix (self: { list = t: typedef' rec { name = "list<${t.name}>"; - checkType = v: if isList v + checkType = v: + if isList v then checkEach name (self.type t) v else { ok = false; @@ -143,7 +158,8 @@ in lib.fix (self: { attrs = t: typedef' rec { name = "attrs<${t.name}>"; - checkType = v: if isAttrs v + checkType = v: + if isAttrs v then checkEach name (self.type t) (attrValues v) else { ok = false; @@ -168,20 +184,23 @@ in lib.fix (self: { # checkField checks an individual field of the struct against # its definition and creates a typecheck result. These results # are aggregated during the actual checking. - checkField = def: name: value: let result = def.checkType value; in rec { - ok = def.checkToBool result; - err = if !ok && isNull value - then "missing required ${def.name} field '${name}'\n" - else "field '${name}': ${def.toError value result}\n"; - }; + checkField = def: name: value: + let result = def.checkType value; in rec { + ok = def.checkToBool result; + err = + if !ok && isNull value + then "missing required ${def.name} field '${name}'\n" + else "field '${name}': ${def.toError value result}\n"; + }; # checkExtraneous determines whether a (closed) struct contains # any fields that are not part of the definition. checkExtraneous = def: has: acc: if (length has) == 0 then acc else if (hasAttr (head has) def) - then checkExtraneous def (tail has) acc - else checkExtraneous def (tail has) { + then checkExtraneous def (tail has) acc + else + checkExtraneous def (tail has) { ok = false; err = acc.err + "unexpected struct field '${head has}'\n"; }; @@ -193,85 +212,102 @@ in lib.fix (self: { init = { ok = true; err = ""; }; extraneous = checkExtraneous def (attrNames value) init; - checkedFields = map (n: - let v = if hasAttr n value then value."${n}" else null; - in checkField def."${n}" n v) (attrNames def); + checkedFields = map + (n: + let v = if hasAttr n value then value."${n}" else null; + in checkField def."${n}" n v) + (attrNames def); - combined = foldl' (acc: res: { - ok = acc.ok && res.ok; - err = if !res.ok then acc.err + res.err else acc.err; - }) init checkedFields; - in { + combined = foldl' + (acc: res: { + ok = acc.ok && res.ok; + err = if !res.ok then acc.err + res.err else acc.err; + }) + init + checkedFields; + in + { ok = combined.ok && extraneous.ok; err = combined.err + extraneous.err; }; struct' = name: def: typedef' { inherit name def; - checkType = value: if isAttrs value + checkType = value: + if isAttrs value then (checkStruct (self.attrs self.type def) value) else { ok = false; err = typeError name value; }; - toError = _: result: "expected '${name}'-struct, but found:\n" + result.err; + toError = _: result: "expected '${name}'-struct, but found:\n" + result.err; }; - in arg: if isString arg then (struct' arg) else (struct' "anon" arg); + in + arg: if isString arg then (struct' arg) else (struct' "anon" arg); # Enums & pattern matching enum = - let - plain = name: def: typedef' { - inherit name def; + let + plain = name: def: typedef' { + inherit name def; - checkType = (x: isString x && elem x def); - checkToBool = x: x; - toError = value: _: "'${prettyPrint value} is not a member of enum ${name}"; - }; - enum' = name: def: lib.fix (e: (plain name def) // { - match = x: actions: deepSeq (map e (attrNames actions)) ( - let - actionKeys = attrNames actions; - missing = foldl' (m: k: if (elem k actionKeys) then m else m ++ [ k ]) [] def; - in if (length missing) > 0 - then throw "Missing match action for members: ${prettyPrint missing}" - else actions."${e x}"); - }); - in arg: if isString arg then (enum' arg) else (enum' "anon" arg); + checkType = (x: isString x && elem x def); + checkToBool = x: x; + toError = value: _: "'${prettyPrint value} is not a member of enum ${name}"; + }; + enum' = name: def: lib.fix (e: (plain name def) // { + match = x: actions: deepSeq (map e (attrNames actions)) ( + let + actionKeys = attrNames actions; + missing = foldl' (m: k: if (elem k actionKeys) then m else m ++ [ k ]) [ ] def; + in + if (length missing) > 0 + then throw "Missing match action for members: ${prettyPrint missing}" + else actions."${e x}" + ); + }); + in + arg: if isString arg then (enum' arg) else (enum' "anon" arg); # Sum types # # The representation of a sum type is an attribute set with only one # value, where the key of the value denotes the variant of the type. sum = - let - plain = name: def: typedef' { - inherit name def; - checkType = (x: - let variant = elemAt (attrNames x) 0; - in if isAttrs x && length (attrNames x) == 1 && hasAttr variant def - then let t = def."${variant}"; - v = x."${variant}"; - res = t.checkType v; - in if t.checkToBool res - then { ok = true; } - else { - ok = false; - err = "while checking '${name}' variant '${variant}': " - + t.toError v res; - } + let + plain = name: def: typedef' { + inherit name def; + checkType = (x: + let variant = elemAt (attrNames x) 0; + in if isAttrs x && length (attrNames x) == 1 && hasAttr variant def + then + let + t = def."${variant}"; + v = x."${variant}"; + res = t.checkType v; + in + if t.checkToBool res + then { ok = true; } + else { + ok = false; + err = "while checking '${name}' variant '${variant}': " + + t.toError v res; + } else { ok = false; err = typeError name x; } - ); - }; - sum' = name: def: lib.fix (s: (plain name def) // { - match = x: actions: - let variant = deepSeq (s x) (elemAt (attrNames x) 0); - actionKeys = attrNames actions; - defKeys = attrNames def; - missing = foldl' (m: k: if (elem k actionKeys) then m else m ++ [ k ]) [] defKeys; - in if (length missing) > 0 - then throw "Missing match action for variants: ${prettyPrint missing}" - else actions."${variant}" x."${variant}"; - }); - in arg: if isString arg then (sum' arg) else (sum' "anon" arg); + ); + }; + sum' = name: def: lib.fix (s: (plain name def) // { + match = x: actions: + let + variant = deepSeq (s x) (elemAt (attrNames x) 0); + actionKeys = attrNames actions; + defKeys = attrNames def; + missing = foldl' (m: k: if (elem k actionKeys) then m else m ++ [ k ]) [ ] defKeys; + in + if (length missing) > 0 + then throw "Missing match action for variants: ${prettyPrint missing}" + else actions."${variant}" x."${variant}"; + }); + in + arg: if isString arg then (sum' arg) else (sum' "anon" arg); # Typed function definitions # @@ -285,15 +321,48 @@ in lib.fix (self: { mkFunc = sig: f: { inherit sig; __toString = self: foldl' (s: t: "${s} -> ${t.name}") - "λ :: ${(head self.sig).name}" (tail self.sig); + "λ :: ${(head self.sig).name}" + (tail self.sig); __functor = _: f; }; - defun' = sig: func: if length sig > 2 + defun' = sig: func: + if length sig > 2 then mkFunc sig (x: defun' (tail sig) (func ((head sig) x))) else mkFunc sig (x: ((head (tail sig)) (func ((head sig) x)))); - in sig: func: if length sig < 2 + in + sig: func: + if length sig < 2 then (throw "Signature must at least have two types (a -> b)") else defun' sig func; + + # Restricting types + # + # `restrict` wraps a type `t`, and uses a predicate `pred` to further + # restrict the values, giving the restriction a descriptive `name`. + # + # First, the wrapped type definition is checked (e.g. int) and then the + # value is checked with the predicate, so the predicate can already + # depend on the value being of the wrapped type. + restrict = name: pred: t: + let restriction = "${t.name}[${name}]"; in typedef' { + name = restriction; + checkType = v: + let res = t.checkType v; + in + if !(t.checkToBool res) + then res + else + let + iok = pred v; + in + if isBool iok then { + ok = iok; + err = "${prettyPrint v} does not conform to restriction '${restriction}'"; + } else + # use throw here to avoid spamming the build log + throw "restriction '${restriction}' predicate returned unexpected value '${prettyPrint iok}' instead of boolean"; + }; + }) diff --git a/nix/yants/tests/default.nix b/nix/yants/tests/default.nix index da539ca356..0c7ec24188 100644 --- a/nix/yants/tests/default.nix +++ b/nix/yants/tests/default.nix @@ -1,34 +1,50 @@ { depot, pkgs, ... }: -with builtins; with depot.nix.yants; # Note: Derivations are not included in the tests below as they cause # issues with deepSeq. -deepSeq rec { - # Test that all primitive types match - primitives = [ - (unit {}) - (int 15) - (bool false) - (float 13.37) - (string "Hello!") - (function (x: x * 2)) - (path /nix) +let + + inherit (depot.nix.runTestsuite) + runTestsuite + it + assertEq + assertThrows + assertDoesNotThrow + ; + + # this derivation won't throw if evaluated with deepSeq + # unlike most things even remotely related with nixpkgs + trivialDerivation = derivation { + name = "trivial-derivation"; + inherit (pkgs.stdenv) system; + builder = "/bin/sh"; + args = [ "-c" "echo hello > $out" ]; + }; + + testPrimitives = it "checks that all primitive types match" [ + (assertDoesNotThrow "unit type" (unit { })) + (assertDoesNotThrow "int type" (int 15)) + (assertDoesNotThrow "bool type" (bool false)) + (assertDoesNotThrow "float type" (float 13.37)) + (assertDoesNotThrow "string type" (string "Hello!")) + (assertDoesNotThrow "function type" (function (x: x * 2))) + (assertDoesNotThrow "path type" (path /nix)) + (assertDoesNotThrow "derivation type" (drv trivialDerivation)) ]; - # Test that polymorphic types work as intended - poly = [ - (option int null) - (list string [ "foo" "bar" ]) - (either int float 42) + testPoly = it "checks that polymorphic types work as intended" [ + (assertDoesNotThrow "option type" (option int null)) + (assertDoesNotThrow "list type" (list string [ "foo" "bar" ])) + (assertDoesNotThrow "either type" (either int float 42)) ]; # Test that structures work as planned. person = struct "person" { name = string; - age = int; + age = int; contact = option (struct { email = string; @@ -36,20 +52,33 @@ deepSeq rec { }); }; - testPerson = person { - name = "Brynhjulf"; - age = 42; - contact.email = "brynhjulf@yants.nix"; - }; + testStruct = it "checks that structures work as intended" [ + (assertDoesNotThrow "person struct" (person { + name = "Brynhjulf"; + age = 42; + contact.email = "brynhjulf@yants.nix"; + })) + ]; # Test enum definitions & matching colour = enum "colour" [ "red" "blue" "green" ]; - testMatch = colour.match "red" { + colourMatcher = { red = "It is in fact red!"; - blue = throw "It should not be blue!"; - green = throw "It should not be green!"; + blue = "It should not be blue!"; + green = "It should not be green!"; }; + testEnum = it "checks enum definitions and matching" [ + (assertEq "enum is matched correctly" + "It is in fact red!" + (colour.match "red" colourMatcher)) + (assertThrows "out of bounds enum fails" + (colour.match "alpha" (colourMatcher // { + alpha = "This should never happen"; + })) + ) + ]; + # Test sum type definitions creature = sum "creature" { human = struct { @@ -59,37 +88,71 @@ deepSeq rec { pet = enum "pet" [ "dog" "lizard" "cat" ]; }; - - testSum = creature { + some-human = creature { human = { name = "Brynhjulf"; age = 42; }; }; - testSumMatch = creature.match testSum { - human = v: "It's a human named ${v.name}"; - pet = v: throw "It's not supposed to be a pet!"; - }; + testSum = it "checks sum types definitions and matching" [ + (assertDoesNotThrow "creature sum type" some-human) + (assertEq "sum type is matched correctly" + "It's a human named Brynhjulf" + (creature.match some-human { + human = v: "It's a human named ${v.name}"; + pet = v: "It's not supposed to be a pet!"; + }) + ) + ]; # Test curried function definitions func = defun [ string int string ] - (name: age: "${name} is ${toString age} years old"); + (name: age: "${name} is ${toString age} years old"); - testFunc = func "Brynhjulf" 42; + testFunctions = it "checks function definitions" [ + (assertDoesNotThrow "function application" (func "Brynhjulf" 42)) + ]; # Test that all types are types. - testTypes = map type [ - any bool drv float int string path - - (attrs int) - (eitherN [ int string bool ]) - (either int string) - (enum [ "foo" "bar" ]) - (list string) - (option int) - (option (list string)) - (struct { a = int; b = option string; }) - (sum { a = int; b = option string; }) + assertIsType = name: t: + assertDoesNotThrow "${name} is a type" (type t); + testTypes = it "checks that all types are types" [ + (assertIsType "any" any) + (assertIsType "bool" bool) + (assertIsType "drv" drv) + (assertIsType "float" float) + (assertIsType "int" int) + (assertIsType "string" string) + (assertIsType "path" path) + + (assertIsType "attrs int" (attrs int)) + (assertIsType "eitherN [ ... ]" (eitherN [ int string bool ])) + (assertIsType "either int string" (either int string)) + (assertIsType "enum [ ... ]" (enum [ "foo" "bar" ])) + (assertIsType "list string" (list string)) + (assertIsType "option int" (option int)) + (assertIsType "option (list string)" (option (list string))) + (assertIsType "struct { ... }" (struct { a = int; b = option string; })) + (assertIsType "sum { ... }" (sum { a = int; b = option string; })) ]; -} (pkgs.writeText "yants-tests" "All tests passed!") + + testRestrict = it "checks restrict types" [ + (assertDoesNotThrow "< 42" ((restrict "< 42" (i: i < 42) int) 25)) + (assertDoesNotThrow "list length < 3" + ((restrict "not too long" (l: builtins.length l < 3) (list int)) [ 1 2 ])) + (assertDoesNotThrow "list eq 5" + (list (restrict "eq 5" (v: v == 5) any) [ 5 5 5 ])) + ]; + +in +runTestsuite "yants" [ + testPrimitives + testPoly + testStruct + testEnum + testSum + testFunctions + testTypes + testRestrict +] |