about summary refs log tree commit diff
path: root/nix/yants/default.nix
blob: 2167b6e8b71f969559dfcdb229f4e2a818582f43 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# 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 <nixpkgs> {}).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 = check;
    checkToBool = r: r;
    toError = value: _result: typeError name value;
  };

  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);
  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;
})