# Copyright 2019 Google LLC # SPDX-License-Identifier: Apache-2.0 # # Provides a "type-system" for Nix that provides various primitive & # polymorphic types as well as the ability to define & check records. # # All types (should) compose as expected. { lib ? (import { }).lib, ... }: with builtins; let prettyPrint = lib.generators.toPretty { }; # typedef' :: struct { # name = string; # checkType = function; (a -> result) # checkToBool = option function; (result -> bool) # toError = option function; (a -> result -> string) # def = option any; # match = option function; # } -> type # -> (a -> b) # -> (b -> bool) # -> (a -> b -> string) # -> type # # This function creates an attribute set that acts as a type. # # It receives a type name, a function that is used to perform a # check on an arbitrary value, a function that can translate the # return of that check to a boolean that informs whether the value # is type-conformant, and a function that can construct error # messages from the check result. # # 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); }; typeError = type: val: "expected type '${type}', but value '${prettyPrint val}' is of type '${typeOf val}'"; # typedef :: string -> (a -> bool) -> type # # typedef is the simplified version of typedef' which uses a default # error message constructor. typedef = name: check: typedef' { inherit name; 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; 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: { # 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"); function = typedef "function" (x: isFunction x || (isAttrs x && x ? "__functor" && isFunction x.__functor)); # Type for types themselves. Useful when defining polymorphic types. type = typedef "type" (x: isAttrs x && hasAttr "name" x && self.string.check x.name && hasAttr "checkType" x && self.function.check x.checkType && hasAttr "checkToBool" x && self.function.check x.checkToBool && hasAttr "toError" x && self.function.check x.toError ); # Polymorphic types option = t: typedef' rec { name = "option<${t.name}>"; checkType = v: let res = t.checkType v; 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; }; }; eitherN = tn: typedef "either<${concatStringsSep ", " (map (x: x.name) tn)}>" (x: any (t: (self.type t).check x) tn); either = t1: t2: self.eitherN [ t1 t2 ]; list = t: typedef' rec { name = "list<${t.name}>"; checkType = v: if isList v then checkEach name (self.type t) v else { ok = false; err = typeError name v; }; }; attrs = t: typedef' rec { name = "attrs<${t.name}>"; checkType = v: if isAttrs v then checkEach name (self.type t) (attrValues v) else { ok = false; err = typeError name v; }; }; # Structs / record types # # Checks that all fields match their declared types, no optional # fields are missing and no unexpected fields occur in the struct. # # Anonymous structs are supported (e.g. for nesting) by omitting the # name. # # TODO: Support open records? struct = # Struct checking is more involved than the simpler types above. # To make the actual type definition more readable, several # helpers are defined below. let # 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"; }; # 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) { ok = false; err = acc.err + "unexpected struct field '${head has}'\n"; }; # checkStruct combines all structure checks and creates one # typecheck result from them checkStruct = def: value: let 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); 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 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; }; in arg: if isString arg then (struct' arg) else (struct' "anon" arg); # Enums & pattern matching enum = 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); # 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; } 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); # Typed function definitions # # These definitions wrap the supplied function in type-checking # forms that are evaluated when the function is called. # # Note that typed functions themselves are not types and can not be # used to check values for conformity. defun = let mkFunc = sig: f: { inherit sig; __toString = self: foldl' (s: t: "${s} -> ${t.name}") "λ :: ${(head self.sig).name}" (tail self.sig); __functor = _: f; }; 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 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"; }; })