From affeace6419271d79dc52b38c5008cc8f4506ca3 Mon Sep 17 00:00:00 2001 From: Emile Clark-Boman Date: Sat, 24 Jan 2026 19:02:20 +1000 Subject: [PATCH] holy mega commit there's no way this runs :( --- nt/default.nix | 6 ++ nt/primitives/README.md | 9 ++ nt/primitives/default.nix | 10 +++ nt/primitives/nt.nix | 104 +++++++++++++++++++++ nt/primitives/util/default.nix | 12 +++ nt/primitives/util/enforce.nix | 36 ++++++++ nt/primitives/util/nt.nix | 59 ++++++++++++ nt/primitives/util/parse.nix | 58 ++++++++++++ nt/primitives/util/sig.nix | 103 +++++++++++++++++++++ nt/primitives/util/trapdoor.nix | 60 +++++++++++++ nt/primitives/util/util.nix | 154 ++++++++++++++++++++++++++++++++ nt/primitives/util/wrap.nix | 28 ++++++ 12 files changed, 639 insertions(+) create mode 100644 nt/default.nix create mode 100644 nt/primitives/README.md create mode 100644 nt/primitives/default.nix create mode 100644 nt/primitives/nt.nix create mode 100644 nt/primitives/util/default.nix create mode 100644 nt/primitives/util/enforce.nix create mode 100644 nt/primitives/util/nt.nix create mode 100644 nt/primitives/util/parse.nix create mode 100644 nt/primitives/util/sig.nix create mode 100644 nt/primitives/util/trapdoor.nix create mode 100644 nt/primitives/util/util.nix create mode 100644 nt/primitives/util/wrap.nix diff --git a/nt/default.nix b/nt/default.nix new file mode 100644 index 0000000..1362c9f --- /dev/null +++ b/nt/default.nix @@ -0,0 +1,6 @@ +{mix, ...} @ inputs: +mix.newMixture inputs (mixture: { + includes.public = [ + ./primitives + ]; +}) diff --git a/nt/primitives/README.md b/nt/primitives/README.md new file mode 100644 index 0000000..d12502d --- /dev/null +++ b/nt/primitives/README.md @@ -0,0 +1,9 @@ +OVERRIDE ALL NIX BUILTINS +ie at the root of your flake you modify what builtins is / use an input +you should never have to depend on builtins or pkgs.lib, only nt + + +Naming Convention + +MyName -> Type +MyName' -> Typeclass diff --git a/nt/primitives/default.nix b/nt/primitives/default.nix new file mode 100644 index 0000000..36d43d5 --- /dev/null +++ b/nt/primitives/default.nix @@ -0,0 +1,10 @@ +{mix, ...} @ inputs: +mix.newMixture inputs (mixture: { + includes.public = [ + ./nt.nix + ]; + imports.public = [ + # TODO: make ./util private + ./util + ]; +}) diff --git a/nt/primitives/nt.nix b/nt/primitives/nt.nix new file mode 100644 index 0000000..f479c7d --- /dev/null +++ b/nt/primitives/nt.nix @@ -0,0 +1,104 @@ +{this, ...}: let + inherit + (builtins) + isFunction + length + mapAttrs + partition + ; + + inherit + (this.util) + enfType + enfIsClassSig + flipCurry + hasAttrAt + mkTrapdoorSet + ntTrapdoorKey + parseClassSig + projectOnto + removeAttrsRec + typeSig + Wrap + ; + + recdef = def: let + Self = def Self; + in + Self; + + classDecl = { + derive = Wrap []; + ops = Wrap {}; + }; + + unwrapBuilder = builder: Self: + if isFunction builder + then builder Self + else builder; + + parseDecl = base: decl: + assert enfType "parseDecl" "set" decl; + # ^^^^ "Type declaration must be provided as an attribute set, got "${typeOf decl}" instead!" + decl |> projectOnto base; + + # Algorithm: given a full set of ops, iterate each op and + # IF IT MATCHES A DERIVE BY FULL NAMESPACE + # THEN remove it from state.req + # ELSE IF IT IS SPECIFIED BY NAMESPACE + # THEN add it to a list of all invalid ops (errors) + # ELSE add it to a list of ops belonging solely to self + parseOps = ops: req: let + reqPaths = + req + |> mapAttrs (name: let + segs = parseClassSig name; + in + value: segs ++ [value]); + + # XXX: TODO: having to specify the full namespace sucks :( + + matches = partition (flipCurry hasAttrAt ops) reqPaths; + + pathsMissing = matches.wrong; + opsSelf = removeAttrsRec matches.right ops; + opsDerived = removeAttrsRec matches.wrong ops; + in { + inherit opsSelf opsDerived pathsMissing; + success = length pathsMissing == 0; + }; + + mkClass = sig: decl: + assert enfIsClassSig "mkClass" sig; let + allDerivedClasses = + decl.derive + |> map (class: typeSig class ++ class.${ntTrapdoorKey}.derive); + + parseResult = parseOps decl.ops decl.req; + inherit + (parseResult) + opsSelf + opsDerived + ; + in + # XXX: WARNING: classes currently *shouldn't* be able to inherit ops (i think?) + assert parseResult.success || throw "TODO"; + mkTrapdoorSet { + default = opsSelf; + unlock = { + # TODO: rename derive to deriveSigs (EXCEPT in the classDecl) + ${ntTrapdoorKey} = { + inherit sig; + derive = allDerivedClasses; + ops = {${sig} = opsSelf;} // opsDerived; + req = null; # XXX: TODO make it more advanced + }; + }; + }; +in { + Class = sig: builder: + recdef (Self: + unwrapBuilder builder Self + |> parseDecl classDecl + |> mkClass sig); +} diff --git a/nt/primitives/util/default.nix b/nt/primitives/util/default.nix new file mode 100644 index 0000000..3f593e4 --- /dev/null +++ b/nt/primitives/util/default.nix @@ -0,0 +1,12 @@ +{mix, ...} @ inputs: +mix.newMixture inputs (mixture: { + includes.public = [ + ./enforce.nix + ./nt.nix + ./parse.nix + ./sig.nix + ./trapdoor.nix + ./util.nix + ./wrap.nix + ]; +}) diff --git a/nt/primitives/util/enforce.nix b/nt/primitives/util/enforce.nix new file mode 100644 index 0000000..9f10ed6 --- /dev/null +++ b/nt/primitives/util/enforce.nix @@ -0,0 +1,36 @@ +{this, ...}: let + inherit + (builtins) + hasAttr + typeOf + ; + + inherit + (this) + isClassSig + isNT + isTypeSig + ; +in rec { + enfType = msg: type: value: let + got = typeOf value; + in + got == type || throw "${msg}: expected primitive nix type \"${type}\" but got \"${got}\""; + + # NOTE: doesn't check if xs is type set, use enfHasAttr instead + enfHasAttr' = msg: name: xs: + hasAttr name xs || throw "${msg}: missing required attribute \"${name}\""; + + # NOTE: use enfHasAttr' if you can guarantee xs is type set + enfHasAttr = msg: name: xs: + enfType "set" xs msg && enfHasAttr' name xs msg; + + enfIsClassSig = msg: sig: + isClassSig sig || throw "${msg}: given value \"${toString sig}\" of primitive nix type \"${typeOf sig}\" is not a valid Typeclass signature"; + + enfIsTypeSig = msg: sig: + isTypeSig sig || throw "${msg}: given value \"${toString sig}\" of primitive nix type \"${typeOf sig}\" is not a valid Type signature"; + + enfIsNT = msg: T: + isNT T || throw "${msg}: expected nt compatible type but got \"${toString T}\" of primitive nix type \"${typeOf T}\""; +} diff --git a/nt/primitives/util/nt.nix b/nt/primitives/util/nt.nix new file mode 100644 index 0000000..29fb638 --- /dev/null +++ b/nt/primitives/util/nt.nix @@ -0,0 +1,59 @@ +{this, ...}: let + inherit + (builtins) + all + attrNames + elem + isAttrs + ; + + inherit + (this) + enfIsNT + ntTrapdoorKey + openTrapdoorFn + toTypeSig + ; +in rec { + # check if a value is an nt type/class + isNT = T: let + content = openTrapdoorFn ntTrapdoorKey T; + names = attrNames content; + in + isAttrs content + && all (name: elem name names) ["sig" "derive" "ops" "req"]; + + isNixClass = T: let + content = openTrapdoorFn ntTrapdoorKey T; + in + isAttrs content + && attrNames content == ["sig" "derive" "ops" "req"]; + + isNixType = T: let + content = openTrapdoorFn ntTrapdoorKey T; + in + isAttrs content + && attrNames content == ["instance" "sig" "derive" "ops" "req"] + && content.instance == false; + + isNixTypeInstance = T: let + content = openTrapdoorFn ntTrapdoorKey T; + in + isAttrs content + && attrNames content == ["instance" "sig" "derive" "ops" "req"] + && content.instance == true; + + # check if a type/class implements a signature + # NOTE: unsafe variant, use typeSig if you can't guarantee `isNT T` holds + impls' = type: T: elem (toTypeSig type) T.${ntTrapdoorKey}.derive; + + # NOTE safe variant, use impls' if you can guarantee `isNT T` holds + impls = type: T: assert enfIsNT "nt.impls" T; impls' type T; + + # check if a type/class implements a signature + # NOTE: unsafe variant, use `is` if you can't guarantee `isNT T` holds + is' = type: T: T.${ntTrapdoorKey}.sig == toTypeSig type; + + # NOTE safe variant, use `is'` if you can guarantee `isNT T` holds + is = type: T: assert enfIsNT "nt.is" T; is' type T; +} diff --git a/nt/primitives/util/parse.nix b/nt/primitives/util/parse.nix new file mode 100644 index 0000000..f33d77e --- /dev/null +++ b/nt/primitives/util/parse.nix @@ -0,0 +1,58 @@ +{this, ...}: let + inherit + (builtins) + foldl' + hasAttr + isAttrs + ; + + inherit + (this) + enfType + is + Wrap + ; +in rec { + # form: getAttrAt :: list string -> set -> null | Wrap Any + # given path as a list of strings, return that value of an + # attribute set at that path + getAttrAt = path: xs: + assert enfType "set" xs "getAttrAt"; + foldl' (left: right: + if left != null && isAttrs left.value && hasAttr right left.value + then Wrap left.value.${right} + else null) + (Wrap xs) + path; + + # form: hasAttrAt :: list string -> set -> bool + # given path as a list of strings, return that value of an + # attribute set at that path + hasAttrAt = path: xs: + assert enfType "set" xs "hasAttrAt"; + getAttrAt path xs != null; # NOTE: inefficient (im lazy) + + # Alternative to mapAttrsRecursiveCond + # Allows mapping directly from a child path + recmap = let + recmapFrom = path: f: T: + if builtins.isAttrs T && ! is Wrap T + then builtins.mapAttrs (attr: leaf: recmapFrom (path ++ [attr]) f leaf) T + else f path T; + in + recmapFrom []; + + projectOnto = f: dst: src: + dst + |> recmap + (path: dstLeaf: let + srcLeaf = getAttrAt path src; + newLeaf = + if srcLeaf != null + then srcLeaf + else dstLeaf; + in + if is Wrap newLeaf + then newLeaf.value + else newLeaf); +} diff --git a/nt/primitives/util/sig.nix b/nt/primitives/util/sig.nix new file mode 100644 index 0000000..a615f27 --- /dev/null +++ b/nt/primitives/util/sig.nix @@ -0,0 +1,103 @@ +{this, ...}: let + inherit + (builtins) + isString + split + stringLength + ; + + inherit + (this) + enfIsNT + filterEven + init + last + ntTrapdoorKey + nullOr + stringHead + stringTail + ; +in rec { + parseSig = sig: let + result = split "::" sig |> filterEven; + in + if last result == "" + then null + else result; + + isTypeName = name: stringHead name != "&"; + isClassName = name: stringHead name == "&" && stringLength name != 1; + + isTypeSig = sig: + parseSig sig |> nullOr (result: last result |> isTypeName); + + isClassSig = sig: + parseSig sig |> nullOr (result: last result |> isClassSig); + + parseTypeSig = sig: let + result = parseSig sig; + in + if result != null && (last result |> isTypeName) + then result + else null; + + parseClassSig = sig: let + result = parseSig sig; + in + if result != null && (last result |> isClassName) + then init result ++ (last result |> stringTail) + else null; + + # NOTE: unsafe variant, use typeSig if you can't guarantee `isNT T` holds + typeSig' = T: T.${ntTrapdoorKey}.sig; + + # NOTE: safe variant, use typeSig' if you can guarantee `isNT T` holds + typeSig = T: assert enfIsNT "nt.typeSig" T; typeSig' T; + + toTypeSig = x: + if isString x + then x + else typeSig x; + + # NOTE: we're testing how similar `list` is to `toTypeSig type` (non-commutative) + # NOTE: we measure similarity in the reverse order (ie end of signature is most important) + # sigSimilarity = type: list: let + # # XXX: TODO: mkClass must enforce that type names can't begin with & + # trimClassPrefix = sig: + # if stringHead sig == "&" + # then stringTail sig + # else sig; + # S = toTypeSig type |> parseTypeSig |> map trimClassPrefix; + + # progress = l: x: let + # index = firstIndexOf x l; + # in + # if index == null + # then [] + # else sublist index (length l - index) l; + + # op = state: el: let + # acc' = progress state.acc el; + # in + # # Continue progression in healthy condition + # if state.acc != [] && acc' != [] + # then { + # score = state.score + 1; + # acc = acc'; + # } + # # We didn't match the final element (ABORT with score=0) + # else if state.score == 0 + # then { + # score = 0; + # acc = []; + # } + # # We didn't match this element but maybe next time :) + # else state; + # in + # list + # |> foldr op { + # score = 0; + # acc = reverse S; + # } + # |> getAttr "score"; +} diff --git a/nt/primitives/util/trapdoor.nix b/nt/primitives/util/trapdoor.nix new file mode 100644 index 0000000..5cda0dc --- /dev/null +++ b/nt/primitives/util/trapdoor.nix @@ -0,0 +1,60 @@ +{this, ...}: let + inherit + (builtins) + attrNames + elem + isFunction + ; + + inherit + (this) + enfHasAttr + enfHasAttr' + enfIsType + ; + + masterkey = "_''traps''_"; +in rec { + defaultTrapdoorKey = "_'"; + mkTrapdoorKey = id: "${defaultTrapdoorKey}${id}"; + ntTrapdoorKey = mkTrapdoorKey "nt"; + + mkTrapdoorFn = key: decl: + assert enfHasAttr "default" decl "mkTrapdoorFn"; + assert enfHasAttr' "unlock" decl "mkTrapdoorFn"; + # return trapdoor function + (x: let + keys = attrNames decl.unlock; + in + if elem key keys + then decl.unlock.${key} + else if key == masterkey + then keys + else decl.default); + + mkTrapdoorSet = key: decl: + assert enfHasAttr "default" decl "mkTrapdoorSet"; + assert enfHasAttr' "unlock" decl "mkTrapdoorSet"; + # return trapdoor set + let + keys = attrNames decl.unlock; + in + decl.default + // { + ${key} = decl.unlock.${key}; + ${masterkey} = keys; + }; + revealTrapdoors = openTrapdoor masterkey; + + openTrapdoorFn = key: f: f key; + + openTrapdoorSet = key: xs: xs.${key}; + + # TODO: implement a function called enfIsTypeAny (for cases like this where it might be function or set) + openTrapdoor = key: T: + if isFunction T + then openTrapdoorFn key T + else + assert enfIsType "set" T "openTrapdoor"; + openTrapdoorSet key T; +} diff --git a/nt/primitives/util/util.nix b/nt/primitives/util/util.nix new file mode 100644 index 0000000..478c7c0 --- /dev/null +++ b/nt/primitives/util/util.nix @@ -0,0 +1,154 @@ +# TODO: move these declarations to a separate module (maybe?) +{...}: let + inherit + (builtins) + elem + elemAt + filter + foldl' + head + genList + length + mapAttrs + partition + removeAttrs + stringLength + substring + tail + ; +in rec { + id = x: x; + flipCurry = f: a: b: f b a; + + inc = x: x + 1; + dec = x: x - 1; + + sublist = start: count: list: let + len = length list; + in + genList (n: elemAt list (n + start)) ( + if start >= len + then 0 + else if start + count > len + then len - start + else count + ); + + take = count: sublist 0 count; + + init = list: + assert (list != []) || throw "lists.init: list must not be empty!"; + take (length list - 1) list; + + last = list: + assert (list != []) || throw "lists.last: list must not be empty!"; + elemAt list (length list - 1); + + # REF: pkgs.lib.lists.reverseList + reverse = xs: let + l = length xs; + in + genList (n: elemAt xs (l - n - 1)) l; + + # REF: pkgs.lib.lists.foldr + foldr = op: nul: list: let + len = length list; + fold' = n: + if n == len + then nul + else op (elemAt list n) (fold' (n + 1)); + in + fold' 0; + + # REF: pkgs.lib.lists.findFirstIndex [MODIFIED] + firstIndexOf = x: list: let + resultIndex = + foldl' ( + index: el: + if index < 0 + then + # No match yet before the current index, we need to check the element + if el == x + then + # We have a match! Turn it into the actual index to prevent future iterations from modifying it + -index - 1 + else + # Still no match, update the index to the next element (we're counting down, so minus one) + index - 1 + else + # There's already a match, propagate the index without evaluating anything + index + ) (-1) + list; + in + if resultIndex < 0 + then null + else resultIndex; + + nullOr = f: x: + if x != null + then f x + else x; + + stringElem = i: substring i 1; + stringTake = substring 0; + stringHead = stringTake 1; + stringTail = x: x |> substring 1 (stringLength x - 1); + stringInit = x: x |> stringTake 1; + stringLast = x: stringElem (stringLength x - 1); + + countEvensLeq = n: n / 2; + countOddsLeq = n: (n + 1) / 2; + + nats = genList id; + + odds = genList (x: 2 * x + 1); + oddsLeq = n: countOddsLeq n |> genList (x: 2 * x + 1); + + evens = genList (x: 2 * x); + evensLeq = n: countEvensLeq n |> genList (x: 2 * x); + + # WARNING: mapOdd/mapEven assuming the indexing set begins even (ie start counting from 0) + mapOdd = f: list: oddsLeq (length list - 1) |> map (i: f (elemAt list i)); + mapEven = f: list: evensLeq (length list + 1) |> map (i: f (elemAt list i)); + + # WARNING: filterOdd/filterEven assuming the indexing set begins even (ie start counting from 0) + filterOdd = mapOdd id; + filterEven = mapEven id; + + mergeAttrsList = list: let + # `binaryMerge start end` merges the elements at indices `index` of `list` such that `start <= index < end` + # Type: Int -> Int -> Attrs + binaryMerge = start: end: + # assert start < end; # Invariant + if end - start >= 2 + then + # If there's at least 2 elements, split the range in two, recurse on each part and merge the result + # The invariant is satisfied because each half will have at least 1 element + binaryMerge start (start + (end - start) / 2) // binaryMerge (start + (end - start) / 2) end + else + # Otherwise there will be exactly 1 element due to the invariant, in which case we just return it directly + elemAt list start; + in + if list == [] + then + # Calling binaryMerge as below would not satisfy its invariant + {} + else binaryMerge 0 (length list); + + removeAttrsRec = paths: xs: let + parts = partition (p: length p == 1) paths; + here = parts.right; + next = parts.wrong; + in + xs + |> flipCurry removeAttrs here + |> mapAttrs (name: + if ! elem name next + then id + else + next + |> filter (x: head x == name) + |> map tail + |> removeAttrsRec); +} diff --git a/nt/primitives/util/wrap.nix b/nt/primitives/util/wrap.nix new file mode 100644 index 0000000..72aa5e4 --- /dev/null +++ b/nt/primitives/util/wrap.nix @@ -0,0 +1,28 @@ +{this, ...}: let + inherit + (this) + ntTrapdoorKey + mkTrapdoorFn + mkTrapdoorSet + ; +in { + # NOTE: Wrap is used to simplify parsing Type/Class declarations + # NOTE: and therefore must be implemented manually + Wrap = let + meta = instance: { + inherit instance; + sig = "nt::Wrap"; + derive = []; + ops = {}; + req = {}; + }; + in + mkTrapdoorFn ntTrapdoorKey { + default = value: + mkTrapdoorSet ntTrapdoorKey { + default = {inherit value;}; + unlock = meta true; + }; + unlock = meta false; + }; +}