merge(yants): Integrate yants into depot at //depot/nix/yants
This commit is contained in:
commit
b98c60ecca
9 changed files with 477 additions and 0 deletions
1
nix/yants/.skip-subtree
Normal file
1
nix/yants/.skip-subtree
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Yants subtree contains no further derivations.
|
86
nix/yants/README.md
Normal file
86
nix/yants/README.md
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
yants
|
||||||
|
=====
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/tazjin/yants.svg?branch=master)](https://travis-ci.org/tazjin/yants)
|
||||||
|
|
||||||
|
This is a tiny type-checker for data in Nix, written in Nix.
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
* Checking of primitive types (`int`, `string` etc.)
|
||||||
|
* Checking polymorphic types (`option`, `list`, `either`)
|
||||||
|
* Defining & checking struct/record types
|
||||||
|
* Defining & matching enum types
|
||||||
|
* Defining & matching sum types
|
||||||
|
* Defining function signatures (including curried functions)
|
||||||
|
* Types are composable! `option string`! `list (either int (option float))`!
|
||||||
|
* Type errors also compose!
|
||||||
|
|
||||||
|
Currently lacking:
|
||||||
|
|
||||||
|
* Any kind of inference
|
||||||
|
* Convenient syntax for attribute-set function signatures
|
||||||
|
|
||||||
|
## Primitives & simple polymorphism
|
||||||
|
|
||||||
|
![simple](screenshots/simple.png)
|
||||||
|
|
||||||
|
## Structs
|
||||||
|
|
||||||
|
![structs](screenshots/structs.png)
|
||||||
|
|
||||||
|
## Nested structs!
|
||||||
|
|
||||||
|
![nested structs](screenshots/nested-structs.png)
|
||||||
|
|
||||||
|
## Enums!
|
||||||
|
|
||||||
|
![enums](screenshots/enums.png)
|
||||||
|
|
||||||
|
## Functions!
|
||||||
|
|
||||||
|
![functions](screenshots/functions.png)
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
Yants can be imported from its `default.nix`. A single attribute (`lib`) can be
|
||||||
|
passed, which will otherwise be imported from `<nixpkgs>`.
|
||||||
|
|
||||||
|
Examples for the most common import methods would be:
|
||||||
|
|
||||||
|
1. Import into scope with `with`:
|
||||||
|
```nix
|
||||||
|
with (import ./default.nix {});
|
||||||
|
# ... Nix code that uses yants ...
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Import as a named variable:
|
||||||
|
```nix
|
||||||
|
let yants = import ./default.nix {};
|
||||||
|
in yants.string "foo" # or other uses ...
|
||||||
|
````
|
||||||
|
|
||||||
|
3. Overlay into `pkgs.lib`:
|
||||||
|
```nix
|
||||||
|
# wherever you import your package set (e.g. from <nixpkgs>):
|
||||||
|
import <nixpkgs> {
|
||||||
|
overlays = [
|
||||||
|
(self: super: {
|
||||||
|
lib = super.lib // { yants = import ./default.nix { inherit (super) lib; }; };
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
# yants now lives at lib.yants, besides the other library functions!
|
||||||
|
```
|
||||||
|
|
||||||
|
Please see my [Nix one-pager](https://github.com/tazjin/nix-1p) for more generic
|
||||||
|
information about the Nix language and what the above constructs mean.
|
||||||
|
|
||||||
|
# Stability
|
||||||
|
|
||||||
|
The current API of Yants is **not yet** considered stable, but it works fine and
|
||||||
|
should continue to do so even if used at an older version.
|
||||||
|
|
||||||
|
Yants' tests use Nix versions above 2.2 - compatibility with older versions is
|
||||||
|
not guaranteed.
|
298
nix/yants/default.nix
Normal file
298
nix/yants/default.nix
Normal file
|
@ -0,0 +1,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;
|
||||||
|
})
|
BIN
nix/yants/screenshots/enums.png
Normal file
BIN
nix/yants/screenshots/enums.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
BIN
nix/yants/screenshots/functions.png
Normal file
BIN
nix/yants/screenshots/functions.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
nix/yants/screenshots/nested-structs.png
Normal file
BIN
nix/yants/screenshots/nested-structs.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
BIN
nix/yants/screenshots/simple.png
Normal file
BIN
nix/yants/screenshots/simple.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
BIN
nix/yants/screenshots/structs.png
Normal file
BIN
nix/yants/screenshots/structs.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
92
nix/yants/tests.nix
Normal file
92
nix/yants/tests.nix
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
with builtins;
|
||||||
|
with (import ./default.nix {});
|
||||||
|
|
||||||
|
# Note: Derivations are not included in the tests below as they cause
|
||||||
|
# issues with deepSeq.
|
||||||
|
|
||||||
|
deepSeq rec {
|
||||||
|
# Test that all primitive types match
|
||||||
|
primitives = [
|
||||||
|
(int 15)
|
||||||
|
(bool false)
|
||||||
|
(float 13.37)
|
||||||
|
(string "Hello!")
|
||||||
|
(function (x: x * 2))
|
||||||
|
(path /nix)
|
||||||
|
];
|
||||||
|
|
||||||
|
# Test that polymorphic types work as intended
|
||||||
|
poly = [
|
||||||
|
(option int null)
|
||||||
|
(list string [ "foo" "bar" ])
|
||||||
|
(either int float 42)
|
||||||
|
];
|
||||||
|
|
||||||
|
# Test that structures work as planned.
|
||||||
|
person = struct "person" {
|
||||||
|
name = string;
|
||||||
|
age = int;
|
||||||
|
|
||||||
|
contact = option (struct {
|
||||||
|
email = string;
|
||||||
|
phone = option string;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
testPerson = person {
|
||||||
|
name = "Brynhjulf";
|
||||||
|
age = 42;
|
||||||
|
contact.email = "brynhjulf@yants.nix";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Test enum definitions & matching
|
||||||
|
colour = enum "colour" [ "red" "blue" "green" ];
|
||||||
|
testMatch = colour.match "red" {
|
||||||
|
red = "It is in fact red!";
|
||||||
|
blue = throw "It should not be blue!";
|
||||||
|
green = throw "It should not be green!";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Test sum type definitions
|
||||||
|
creature = sum "creature" {
|
||||||
|
human = struct {
|
||||||
|
name = string;
|
||||||
|
age = option int;
|
||||||
|
};
|
||||||
|
|
||||||
|
pet = enum "pet" [ "dog" "lizard" "cat" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
testSum = 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!";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Test curried function definitions
|
||||||
|
func = defun [ string int string ]
|
||||||
|
(name: age: "${name} is ${toString age} years old");
|
||||||
|
|
||||||
|
testFunc = 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; })
|
||||||
|
];
|
||||||
|
} "All tests passed!\n"
|
Loading…
Reference in a new issue