feat: builtins.genericClosure; refactor type check

This commit is contained in:
2026-01-22 20:13:31 +08:00
parent 58c3e67409
commit 56a8ba9475
19 changed files with 400 additions and 307 deletions

View File

@@ -7,6 +7,9 @@
"": {
"name": "nix-js-runtime",
"version": "0.1.0",
"dependencies": {
"js-sdsl": "^4.4.2"
},
"devDependencies": {
"esbuild": "^0.24.2",
"typescript": "^5.7.2"
@@ -478,6 +481,16 @@
"@esbuild/win32-x64": "0.24.2"
}
},
"node_modules/js-sdsl": {
"version": "4.4.2",
"resolved": "https://registry.npmmirror.com/js-sdsl/-/js-sdsl-4.4.2.tgz",
"integrity": "sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",

View File

@@ -10,5 +10,8 @@
"devDependencies": {
"esbuild": "^0.24.2",
"typescript": "^5.7.2"
},
"dependencies": {
"js-sdsl": "^4.4.2"
}
}

View File

@@ -4,6 +4,7 @@
import type { NixBool, NixInt, NixNumber, NixValue } from "../types";
import { forceNumeric, coerceNumeric, forceInt } from "../type-assert";
import { op } from "../operators";
export const add =
(a: NixValue) =>
@@ -66,4 +67,4 @@ export const bitXor =
export const lessThan =
(a: NixValue) =>
(b: NixValue): NixBool =>
forceNumeric(a) < forceNumeric(b);
op.lt(a, b);

View File

@@ -3,7 +3,7 @@
*/
import type { NixValue, NixAttrs, NixList } from "../types";
import { forceAttrs, forceString, forceFunction, forceList } from "../type-assert";
import { forceAttrs, forceStringValue, forceFunction, forceList } from "../type-assert";
import { createThunk } from "../thunk";
export const attrNames = (set: NixValue): string[] => Object.keys(forceAttrs(set)).sort();
@@ -24,12 +24,12 @@ export const attrValues = (set: NixValue): NixValue[] =>
export const getAttr =
(s: NixValue) =>
(set: NixValue): NixValue =>
forceAttrs(set)[forceString(s)];
forceAttrs(set)[forceStringValue(s)];
export const hasAttr =
(s: NixValue) =>
(set: NixValue): boolean =>
Object.hasOwn(forceAttrs(set), forceString(s));
Object.hasOwn(forceAttrs(set), forceStringValue(s));
export const mapAttrs =
(f: NixValue) =>
@@ -63,7 +63,7 @@ export const listToAttrs = (e: NixValue): NixAttrs => {
const forced_e = [...forceList(e)].reverse();
for (const obj of forced_e) {
const item = forceAttrs(obj);
attrs[forceString(item.name)] = item.value;
attrs[forceStringValue(item.name)] = item.value;
}
return attrs;
};
@@ -85,7 +85,7 @@ export const intersectAttrs =
export const catAttrs =
(attr: NixValue) =>
(list: NixValue): NixList => {
const key = forceString(attr);
const key = forceStringValue(attr);
return forceList(list)
.map((set) => forceAttrs(set)[key])
.filter((val) => val !== undefined);
@@ -98,7 +98,7 @@ export const groupBy =
const forced_f = forceFunction(f);
const forced_list = forceList(list);
for (const elem of forced_list) {
const key = forceString(forced_f(elem));
const key = forceStringValue(forced_f(elem));
if (!attrs[key]) attrs[key] = [];
(attrs[key] as NixList).push(elem);
}

View File

@@ -1,6 +1,6 @@
import type { NixValue, NixAttrs, NixString } from "../types";
import { isStringWithContext } from "../types";
import { forceNixString, forceAttrs, forceList, forceString } from "../type-assert";
import { forceString, forceAttrs, forceList, forceStringValue } from "../type-assert";
import { force } from "../thunk";
import {
type NixStringContext,
@@ -17,7 +17,7 @@ import {
* Returns true if the string has any store path references.
*/
export const hasContext = (value: NixValue): boolean => {
const s = forceNixString(value);
const s = forceString(value);
return isStringWithContext(s) && s.context.size > 0;
};
@@ -28,7 +28,7 @@ export const hasContext = (value: NixValue): boolean => {
* Use with caution as it removes derivation dependencies.
*/
export const unsafeDiscardStringContext = (value: NixValue): string => {
const s = forceNixString(value);
const s = forceString(value);
return getStringValue(s);
};
@@ -39,7 +39,7 @@ export const unsafeDiscardStringContext = (value: NixValue): string => {
* Preserves other context types unchanged.
*/
export const unsafeDiscardOutputDependency = (value: NixValue): NixString => {
const s = forceNixString(value);
const s = forceString(value);
const strValue = getStringValue(s);
const context = getStringContext(s);
@@ -71,7 +71,7 @@ export const unsafeDiscardOutputDependency = (value: NixValue): NixString => {
* The string must have exactly one context element which must be a .drv path.
*/
export const addDrvOutputDependencies = (value: NixValue): NixString => {
const s = forceNixString(value);
const s = forceString(value);
const strValue = getStringValue(s);
const context = getStringContext(s);
@@ -109,7 +109,7 @@ export const addDrvOutputDependencies = (value: NixValue): NixString => {
* - outputs: list of specific output names (built, encoded as !output!path)
*/
export const getContext = (value: NixValue): NixAttrs => {
const s = forceNixString(value);
const s = forceString(value);
const context = getStringContext(s);
const infoMap = parseContextToInfoMap(context);
@@ -147,7 +147,7 @@ export const getContext = (value: NixValue): NixAttrs => {
export const appendContext =
(strValue: NixValue) =>
(ctxValue: NixValue): NixString => {
const s = forceNixString(strValue);
const s = forceString(strValue);
const strVal = getStringValue(s);
const existingContext = getStringContext(s);
@@ -188,7 +188,7 @@ export const appendContext =
);
}
for (const output of outputs) {
const outputName = forceString(output);
const outputName = forceStringValue(output);
newContext.add(`!${outputName}!${path}`);
}
}

View File

@@ -8,6 +8,7 @@ import { force } from "../thunk";
import { type NixStringContext, mkStringWithContext, addBuiltContext } from "../string-context";
import { forceFunction } from "../type-assert";
import { nixValueToJson } from "../conversion";
import { typeOf } from "./type-check";
const convertJsonToNix = (json: unknown): NixValue => {
if (json === null) {
@@ -41,7 +42,7 @@ const convertJsonToNix = (json: unknown): NixValue => {
export const fromJSON = (e: NixValue): NixValue => {
const str = force(e);
if (typeof str !== "string" && !isStringWithContext(str)) {
throw new TypeError(`builtins.fromJSON: expected a string, got ${typeName(str)}`);
throw new TypeError(`builtins.fromJSON: expected a string, got ${typeOf(str)}`);
}
const jsonStr = isStringWithContext(str) ? str.value : str;
try {
@@ -82,25 +83,6 @@ export enum StringCoercionMode {
ToString = 2,
}
/**
* Helper function to get human-readable type names for error messages
*/
const typeName = (value: NixValue): string => {
const val = force(value);
if (typeof val === "bigint") return "int";
if (typeof val === "number") return "float";
if (typeof val === "boolean") return "boolean";
if (typeof val === "string") return "string";
if (isStringWithContext(val)) return "string";
if (val === null) return "null";
if (Array.isArray(val)) return "list";
if (typeof val === "function") return "lambda";
if (typeof val === "object") return "attribute set";
return `unknown type`;
};
export interface CoerceResult {
value: string;
context: NixStringContext;
@@ -196,7 +178,7 @@ export const coerceToString = (
}
// Attribute sets without __toString or outPath cannot be coerced
throw new TypeError(`cannot coerce ${typeName(v)} to a string`);
throw new TypeError(`cannot coerce ${typeOf(v)} to a string`);
}
// Integer coercion is allowed in Interpolation and ToString modes
@@ -264,7 +246,7 @@ export const coerceToString = (
}
}
throw new TypeError(`cannot coerce ${typeName(v)} to a string`);
throw new TypeError(`cannot coerce ${typeOf(v)} to a string`);
};
/**

View File

@@ -1,5 +1,5 @@
import type { NixValue, NixAttrs } from "../types";
import { forceString, forceList } from "../type-assert";
import { forceStringValue, forceList } from "../type-assert";
import { force } from "../thunk";
import { type DerivationData, type OutputInfo, generateAterm } from "../derivation-helpers";
import { coerceToString, StringCoercionMode } from "./conversion";
@@ -25,7 +25,7 @@ const validateName = (attrs: NixAttrs): string => {
if (!("name" in attrs)) {
throw new Error("derivation: missing required attribute 'name'");
}
const name = forceString(attrs.name);
const name = forceStringValue(attrs.name);
if (!name) {
throw new Error("derivation: 'name' cannot be empty");
}
@@ -46,7 +46,7 @@ const validateSystem = (attrs: NixAttrs): string => {
if (!("system" in attrs)) {
throw new Error("derivation: missing required attribute 'system'");
}
return forceString(attrs.system);
return forceStringValue(attrs.system);
};
const extractOutputs = (attrs: NixAttrs): string[] => {
@@ -54,7 +54,7 @@ const extractOutputs = (attrs: NixAttrs): string[] => {
return ["out"];
}
const outputsList = forceList(attrs.outputs);
const outputs = outputsList.map((o) => forceString(o));
const outputs = outputsList.map((o) => forceStringValue(o));
if (outputs.length === 0) {
throw new Error("derivation: outputs list cannot be empty");
@@ -141,9 +141,9 @@ const extractFixedOutputInfo = (attrs: NixAttrs): FixedOutputInfo | null => {
return null;
}
const hash = forceString(attrs.outputHash);
const hashAlgo = "outputHashAlgo" in attrs ? forceString(attrs.outputHashAlgo) : "sha256";
const hashMode = "outputHashMode" in attrs ? forceString(attrs.outputHashMode) : "flat";
const hash = forceStringValue(attrs.outputHash);
const hashAlgo = "outputHashAlgo" in attrs ? forceStringValue(attrs.outputHashAlgo) : "sha256";
const hashMode = "outputHashMode" in attrs ? forceStringValue(attrs.outputHashMode) : "flat";
if (hashMode !== "flat" && hashMode !== "recursive") {
throw new Error(`derivation: invalid outputHashMode '${hashMode}' (must be 'flat' or 'recursive')`);

View File

@@ -3,7 +3,22 @@
* Combines all builtin function categories into the global `builtins` object
*/
import { createThunk } from "../thunk";
// Import all builtin categories
import * as arithmetic from "./arithmetic";
import * as math from "./math";
import * as typeCheck from "./type-check";
import * as list from "./list";
import * as attrs from "./attrs";
import * as string from "./string";
import * as pathOps from "./path";
import * as functional from "./functional";
import * as io from "./io";
import * as conversion from "./conversion";
import * as misc from "./misc";
import * as derivation from "./derivation";
import type { NixValue } from "../types";
import { createThunk, force } from "../thunk";
/**
* Symbol used to mark functions as primops (primitive operations)
@@ -33,23 +48,23 @@ export interface PrimopMetadata {
* @param applied - Number of arguments already applied (default: 0)
* @returns The marked function
*/
export const mkPrimop = <T extends Function>(
func: T,
export const mkPrimop = (
func: (...args: NixValue[]) => NixValue,
name: string,
arity: number,
applied: number = 0,
): T => {
): Function => {
// Mark this function as a primop
(func as any)[PRIMOP_METADATA] = {
name,
arity,
applied,
} as PrimopMetadata;
} satisfies PrimopMetadata;
// If this is a curried function and not fully applied,
// wrap it to mark the next layer too
if (applied < arity - 1) {
const wrappedFunc = ((...args: any[]) => {
const wrappedFunc = ((...args: NixValue[]) => {
const result = func(...args);
// If result is a function, mark it as the next layer
if (typeof result === "function") {
@@ -63,9 +78,9 @@ export const mkPrimop = <T extends Function>(
name,
arity,
applied,
} as PrimopMetadata;
} satisfies PrimopMetadata;
return wrappedFunc as T;
return wrappedFunc;
}
return func;
@@ -97,20 +112,6 @@ export const get_primop_metadata = (func: unknown): PrimopMetadata | undefined =
return undefined;
};
// Import all builtin categories
import * as arithmetic from "./arithmetic";
import * as math from "./math";
import * as typeCheck from "./type-check";
import * as list from "./list";
import * as attrs from "./attrs";
import * as string from "./string";
import * as pathOps from "./path";
import * as functional from "./functional";
import * as io from "./io";
import * as conversion from "./conversion";
import * as misc from "./misc";
import * as derivation from "./derivation";
/**
* The global builtins object
* Contains 80+ Nix builtin functions plus metadata
@@ -134,16 +135,16 @@ export const builtins: any = {
ceil: mkPrimop(math.ceil, "ceil", 1),
floor: mkPrimop(math.floor, "floor", 1),
isAttrs: mkPrimop(typeCheck.isAttrs, "isAttrs", 1),
isBool: mkPrimop(typeCheck.isBool, "isBool", 1),
isFloat: mkPrimop(typeCheck.isFloat, "isFloat", 1),
isFunction: mkPrimop(typeCheck.isFunction, "isFunction", 1),
isInt: mkPrimop(typeCheck.isInt, "isInt", 1),
isList: mkPrimop(typeCheck.isList, "isList", 1),
isNull: mkPrimop(typeCheck.isNull, "isNull", 1),
isPath: mkPrimop(typeCheck.isPath, "isPath", 1),
isString: mkPrimop(typeCheck.isString, "isString", 1),
typeOf: mkPrimop(typeCheck.typeOf, "typeOf", 1),
isAttrs: mkPrimop((e: NixValue) => typeCheck.isAttrs(force(e)), "isAttrs", 1),
isBool: mkPrimop((e: NixValue) => typeCheck.isBool(force(e)), "isBool", 1),
isFloat: mkPrimop((e: NixValue) => typeCheck.isFloat(force(e)), "isFloat", 1),
isFunction: mkPrimop((e: NixValue) => typeCheck.isFunction(force(e)), "isFunction", 1),
isInt: mkPrimop((e: NixValue) => typeCheck.isInt(force(e)), "isInt", 1),
isList: mkPrimop((e: NixValue) => typeCheck.isList(force(e)), "isList", 1),
isNull: mkPrimop((e: NixValue) => typeCheck.isNull(force(e)), "isNull", 1),
isPath: mkPrimop((e: NixValue) => typeCheck.isPath(force(e)), "isPath", 1),
isString: mkPrimop((e: NixValue) => typeCheck.isString(force(e)), "isString", 1),
typeOf: mkPrimop((e: NixValue) => typeCheck.typeOf(force(e)), "typeOf", 1),
map: mkPrimop(list.map, "map", 2),
filter: mkPrimop(list.filter, "filter", 2),

View File

@@ -3,7 +3,7 @@
* Implemented via Rust ops exposed through deno_core
*/
import { forceAttrs, forceBool, forceString } from "../type-assert";
import { forceAttrs, forceBool, forceStringValue } from "../type-assert";
import type { NixValue, NixAttrs } from "../types";
import { isNixPath } from "../types";
import { force } from "../thunk";
@@ -92,10 +92,14 @@ const normalizeUrlInput = (
return { url: forced };
}
const attrs = forceAttrs(args);
const url = forceString(attrs.url);
const url = forceStringValue(attrs.url);
const hash =
"sha256" in attrs ? forceString(attrs.sha256) : "hash" in attrs ? forceString(attrs.hash) : undefined;
const name = "name" in attrs ? forceString(attrs.name) : undefined;
"sha256" in attrs
? forceStringValue(attrs.sha256)
: "hash" in attrs
? forceStringValue(attrs.hash)
: undefined;
const name = "name" in attrs ? forceStringValue(attrs.name) : undefined;
const executable = "executable" in attrs ? forceBool(attrs.executable) : false;
return { url, hash, name, executable };
};
@@ -108,15 +112,15 @@ const normalizeTarballInput = (
return { url: forced };
}
const attrs = forceAttrs(args);
const url = forceString(attrs.url);
const hash = "hash" in attrs ? forceString(attrs.hash) : undefined;
const url = forceStringValue(attrs.url);
const hash = "hash" in attrs ? forceStringValue(attrs.hash) : undefined;
const narHash =
"narHash" in attrs
? forceString(attrs.narHash)
? forceStringValue(attrs.narHash)
: "sha256" in attrs
? forceString(attrs.sha256)
? forceStringValue(attrs.sha256)
: undefined;
const name = "name" in attrs ? forceString(attrs.name) : undefined;
const name = "name" in attrs ? forceStringValue(attrs.name) : undefined;
return { url, hash, narHash, name };
};
@@ -159,13 +163,13 @@ export const fetchGit = (args: NixValue): NixAttrs => {
};
}
const attrs = forceAttrs(args);
const url = forceString(attrs.url);
const gitRef = "ref" in attrs ? forceString(attrs.ref) : null;
const rev = "rev" in attrs ? forceString(attrs.rev) : null;
const url = forceStringValue(attrs.url);
const gitRef = "ref" in attrs ? forceStringValue(attrs.ref) : null;
const rev = "rev" in attrs ? forceStringValue(attrs.rev) : null;
const shallow = "shallow" in attrs ? forceBool(attrs.shallow) : false;
const submodules = "submodules" in attrs ? forceBool(attrs.submodules) : false;
const allRefs = "allRefs" in attrs ? forceBool(attrs.allRefs) : false;
const name = "name" in attrs ? forceString(attrs.name) : null;
const name = "name" in attrs ? forceStringValue(attrs.name) : null;
const result: FetchGitResult = Deno.core.ops.op_fetch_git(
url,
@@ -191,9 +195,9 @@ export const fetchGit = (args: NixValue): NixAttrs => {
export const fetchMercurial = (args: NixValue): NixAttrs => {
const attrs = forceAttrs(args);
const url = forceString(attrs.url);
const rev = "rev" in attrs ? forceString(attrs.rev) : null;
const name = "name" in attrs ? forceString(attrs.name) : null;
const url = forceStringValue(attrs.url);
const rev = "rev" in attrs ? forceStringValue(attrs.rev) : null;
const name = "name" in attrs ? forceStringValue(attrs.name) : null;
const result: FetchHgResult = Deno.core.ops.op_fetch_hg(url, rev, name);
@@ -208,7 +212,7 @@ export const fetchMercurial = (args: NixValue): NixAttrs => {
export const fetchTree = (args: NixValue): NixAttrs => {
const attrs = forceAttrs(args);
const type = "type" in attrs ? forceString(attrs.type) : "auto";
const type = "type" in attrs ? forceStringValue(attrs.type) : "auto";
switch (type) {
case "git":
@@ -221,7 +225,7 @@ export const fetchTree = (args: NixValue): NixAttrs => {
case "file":
return { outPath: fetchurl(args) };
case "path": {
const path = forceString(attrs.path);
const path = forceStringValue(attrs.path);
return { outPath: path };
}
case "github":
@@ -235,10 +239,11 @@ export const fetchTree = (args: NixValue): NixAttrs => {
};
const fetchGitForge = (forge: string, attrs: NixAttrs): NixAttrs => {
const owner = forceString(attrs.owner);
const repo = forceString(attrs.repo);
const rev = "rev" in attrs ? forceString(attrs.rev) : "ref" in attrs ? forceString(attrs.ref) : "HEAD";
const host = "host" in attrs ? forceString(attrs.host) : undefined;
const owner = forceStringValue(attrs.owner);
const repo = forceStringValue(attrs.repo);
const rev =
"rev" in attrs ? forceStringValue(attrs.rev) : "ref" in attrs ? forceStringValue(attrs.ref) : "HEAD";
const host = "host" in attrs ? forceStringValue(attrs.host) : undefined;
let tarballUrl: string;
switch (forge) {
@@ -271,7 +276,7 @@ const fetchGitForge = (forge: string, attrs: NixAttrs): NixAttrs => {
};
const autoDetectAndFetch = (attrs: NixAttrs): NixAttrs => {
const url = forceString(attrs.url);
const url = forceStringValue(attrs.url);
if (url.endsWith(".git") || url.includes("github.com") || url.includes("gitlab.com")) {
return fetchGit(attrs);
}
@@ -332,17 +337,17 @@ export const path = (args: NixValue): string => {
if (isNixPath(pathValue)) {
pathStr = getPathValue(pathValue);
} else {
pathStr = forceString(pathValue);
pathStr = forceStringValue(pathValue);
}
// Optional: name parameter (defaults to basename in Rust)
const name = "name" in attrs ? forceString(attrs.name) : null;
const name = "name" in attrs ? forceStringValue(attrs.name) : null;
// Optional: recursive parameter (default: true)
const recursive = "recursive" in attrs ? forceBool(attrs.recursive) : true;
// Optional: sha256 parameter
const sha256 = "sha256" in attrs ? forceString(attrs.sha256) : null;
const sha256 = "sha256" in attrs ? forceStringValue(attrs.sha256) : null;
// TODO: Handle filter parameter
if ("filter" in attrs) {
@@ -358,7 +363,7 @@ export const path = (args: NixValue): string => {
export const toFile =
(nameArg: NixValue) =>
(contentsArg: NixValue): StringWithContext => {
const name = forceString(nameArg);
const name = forceStringValue(nameArg);
if (name.includes("/")) {
throw new Error("builtins.toFile: name cannot contain '/'");
@@ -392,5 +397,5 @@ export const findFile =
};
export const getEnv = (s: NixValue): string => {
return Deno.core.ops.op_get_env(forceString(s))
return Deno.core.ops.op_get_env(forceStringValue(s));
};

View File

@@ -5,8 +5,11 @@
import { force } from "../thunk";
import { CatchableError } from "../types";
import type { NixBool, NixStrictValue, NixValue } from "../types";
import { forceList, forceAttrs, forceFunction, forceString } from "../type-assert";
import { forceList, forceStringValue, forceAttrs, forceFunction } from "../type-assert";
import * as context from "./context";
import { compareValues, op } from "../operators";
import { isBool, isFloat, isInt, isList, isString, typeOf } from "./type-check";
import { OrderedSet } from "js-sdsl";
export const addErrorContext =
(e1: NixValue) =>
@@ -49,8 +52,8 @@ export const addDrvOutputDependencies = context.addDrvOutputDependencies;
export const compareVersions =
(s1: NixValue) =>
(s2: NixValue): NixValue => {
const str1 = forceString(s1);
const str2 = forceString(s2);
const str1 = forceStringValue(s1);
const str2 = forceStringValue(s2);
let i1 = 0;
let i2 = 0;
@@ -152,8 +155,53 @@ export const functionArgs = (f: NixValue): never => {
throw new Error("Not implemented: functionArgs");
};
export const genericClosure = (args: NixValue): never => {
throw new Error("Not implemented: genericClosure");
const checkComparable = (value: NixStrictValue): void => {
if (isString(value) || isInt(value) || isFloat(value) || isBool(value) || isList(value)) {
return;
}
throw new Error(`Unsupported key type for genericClosure: ${typeOf(value)}`);
};
export const genericClosure = (args: NixValue): NixValue => {
const forcedArgs = forceAttrs(args);
const { startSet, operator } = forcedArgs;
const initialList = forceList(startSet);
const opFunction = forceFunction(operator);
const resultSet = new OrderedSet<NixStrictValue>(undefined, compareValues);
const resultList: NixStrictValue[] = [];
const queue: NixStrictValue[] = [];
for (const item of initialList) {
const itemAttrs = forceAttrs(item);
const key = force(itemAttrs.key);
checkComparable(key);
if (resultSet.find(key).equals(resultSet.end())) {
resultSet.insert(key);
resultList.push(itemAttrs);
queue.push(itemAttrs);
}
}
let head = 0;
while (head < queue.length) {
const currentItem = queue[head++];
const newItems = forceList(opFunction(currentItem));
for (const newItem of newItems) {
const newItemAttrs = forceAttrs(newItem);
const key = force(newItemAttrs.key);
checkComparable(key);
if (resultSet.find(key).equals(resultSet.end())) {
resultSet.insert(key);
resultList.push(newItemAttrs);
queue.push(newItemAttrs);
}
}
}
return resultList;
};
export const getFlake = (attrs: NixValue): never => {
@@ -188,7 +236,7 @@ export const replaceStrings =
(s: NixValue): NixValue => {
const fromList = forceList(from);
const toList = forceList(to);
const inputStr = forceString(s);
const inputStr = forceStringValue(s);
if (fromList.length !== toList.length) {
throw new Error("'from' and 'to' arguments passed to builtins.replaceStrings have different lengths");
@@ -203,13 +251,13 @@ export const replaceStrings =
let found = false;
for (let i = 0; i < fromList.length; i++) {
const pattern = forceString(fromList[i]);
const pattern = forceStringValue(fromList[i]);
if (inputStr.substring(pos).startsWith(pattern)) {
found = true;
if (!toCache.has(i)) {
toCache.set(i, forceString(toList[i]));
toCache.set(i, forceStringValue(toList[i]));
}
const replacement = toCache.get(i)!;
@@ -239,7 +287,7 @@ export const replaceStrings =
};
export const splitVersion = (s: NixValue): NixValue => {
const version = forceString(s);
const version = forceStringValue(s);
const components: string[] = [];
let idx = 0;

View File

@@ -3,7 +3,7 @@
*/
import type { NixInt, NixValue, NixString } from "../types";
import { forceString, forceList, forceInt, forceNixString } from "../type-assert";
import { forceStringValue, forceList, forceInt, forceString } from "../type-assert";
import { coerceToString, StringCoercionMode } from "./conversion";
import {
type NixStringContext,
@@ -12,7 +12,7 @@ import {
mkStringWithContext,
} from "../string-context";
export const stringLength = (e: NixValue): NixInt => BigInt(forceString(e).length);
export const stringLength = (e: NixValue): NixInt => BigInt(forceStringValue(e).length);
/**
* builtins.substring - Extract substring while preserving string context
@@ -35,7 +35,7 @@ export const substring =
throw new Error("negative start position in 'substring'");
}
const str = forceNixString(s);
const str = forceString(s);
const strValue = getStringValue(str);
const context = getStringContext(str);
@@ -81,7 +81,7 @@ export const concatStringsSep =
};
export const baseNameOf = (x: NixValue): string => {
const str = forceString(x);
const str = forceStringValue(x);
if (str.length === 0) return "";
let last = str.length - 1;
@@ -152,8 +152,8 @@ function posixToJsRegex(pattern: string, fullMatch: boolean = false): RegExp {
export const match =
(regex: NixValue) =>
(str: NixValue): NixValue => {
const regexStr = forceString(regex);
const inputStr = forceString(str);
const regexStr = forceStringValue(regex);
const inputStr = forceStringValue(str);
try {
const re = posixToJsRegex(regexStr, true);
@@ -177,8 +177,8 @@ export const match =
export const split =
(regex: NixValue) =>
(str: NixValue): NixValue => {
const regexStr = forceString(regex);
const inputStr = forceString(str);
const regexStr = forceStringValue(regex);
const inputStr = forceStringValue(str);
try {
const re = posixToJsRegex(regexStr);

View File

@@ -5,7 +5,8 @@
import {
HAS_CONTEXT,
isNixPath,
NixPath,
isStringWithContext,
type NixPath,
type NixAttrs,
type NixBool,
type NixFloat,
@@ -14,52 +15,61 @@ import {
type NixList,
type NixNull,
type NixString,
type NixValue,
type NixStrictValue,
} from "../types";
import { force } from "../thunk";
export const isAttrs = (e: NixValue): e is NixAttrs => {
const val = force(e);
return typeof val === "object" && !Array.isArray(val) && val !== null && !(HAS_CONTEXT in val);
/**
* Check if a value is a Nix string (plain string or StringWithContext)
* This works on already-forced values (NixStrictValue).
*/
export const isNixString = (v: NixStrictValue): v is NixString => {
return typeof v === "string" || isStringWithContext(v);
};
export const isBool = (e: NixValue): e is NixBool => typeof force(e) === "boolean";
export const isAttrs = (e: NixStrictValue): e is NixAttrs => {
const val = e;
return (
typeof val === "object" && !Array.isArray(val) && val !== null && !(HAS_CONTEXT in val) && !isPath(val)
);
};
export const isFloat = (e: NixValue): e is NixFloat => {
const val = force(e);
export const isBool = (e: NixStrictValue): e is NixBool => typeof e === "boolean";
export const isFloat = (e: NixStrictValue): e is NixFloat => {
const val = e;
return typeof val === "number"; // Only number is float
};
export const isFunction = (e: NixValue): e is NixFunction => typeof force(e) === "function";
export const isFunction = (e: NixStrictValue): e is NixFunction => typeof e === "function";
export const isInt = (e: NixValue): e is NixInt => {
const val = force(e);
export const isInt = (e: NixStrictValue): e is NixInt => {
const val = e;
return typeof val === "bigint"; // Only bigint is int
};
export const isList = (e: NixValue): e is NixList => Array.isArray(force(e));
export const isList = (e: NixStrictValue): e is NixList => Array.isArray(e);
export const isNull = (e: NixValue): e is NixNull => force(e) === null;
export const isNull = (e: NixStrictValue): e is NixNull => e === null;
export const isPath = (e: NixValue): e is NixPath => {
const val = force(e);
export const isPath = (e: NixStrictValue): e is NixPath => {
const val = e;
return isNixPath(val);
};
export const isString = (e: NixValue): e is NixString => typeof force(e) === "string";
export const isString = (e: NixStrictValue): e is NixString =>
typeof e === "string" || isStringWithContext(e);
export const typeOf = (e: NixValue): string => {
const val = force(e);
export type NixType = "int" | "float" | "bool" | "string" | "path" | "null" | "list" | "lambda" | "set";
export const typeOf = (e: NixStrictValue): NixType => {
if (typeof e === "bigint") return "int";
if (typeof e === "number") return "float";
if (typeof e === "boolean") return "bool";
if (e === null) return "null";
if (isNixString(e)) return "string";
if (isNixPath(e)) return "path";
if (Array.isArray(e)) return "list";
if (typeof e === "object") return "set";
if (typeof e === "function") return "lambda";
if (isNixPath(val)) return "path";
if (typeof val === "bigint") return "int";
if (typeof val === "number") return "float";
if (typeof val === "boolean") return "bool";
if (typeof val === "string") return "string";
if (val === null) return "null";
if (Array.isArray(val)) return "list";
if (typeof val === "function") return "lambda";
if (typeof val === "object") return "set";
throw new TypeError(`Unknown Nix type: ${typeof val}`);
throw new TypeError(`Unknown Nix type: ${typeof e}`);
};

View File

@@ -3,8 +3,8 @@
*/
import type { NixValue, NixAttrs, NixBool, NixString, NixPath } from "./types";
import { forceAttrs, forceBool, forceFunction, forceString, typeName } from "./type-assert";
import { isAttrs } from "./builtins/type-check";
import { forceAttrs, forceBool, forceFunction, forceStringValue } from "./type-assert";
import { isAttrs, typeOf } from "./builtins/type-check";
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
import { type NixStringContext, mkStringWithContext, isStringWithContext } from "./string-context";
import { force } from "./thunk";
@@ -169,14 +169,14 @@ export const concatStringsWithContext = (parts: NixValue[]): NixString | NixPath
* @returns NixPath object with absolute path
*/
export const resolvePath = (currentDir: string, path: NixValue): NixPath => {
const pathStr = forceString(path);
const pathStr = forceStringValue(path);
const resolved = Deno.core.ops.op_resolve_path(currentDir, pathStr);
return mkPath(resolved);
};
export const select = (obj: NixValue, attrpath: NixValue[], span?: string): NixValue => {
if (STACK_TRACE.enabled && span) {
const pathStrings = attrpath.map((a) => forceString(a));
const pathStrings = attrpath.map((a) => forceStringValue(a));
const path = pathStrings.join(".");
const message = path ? `while selecting attribute [${path}]` : "while selecting attribute";
@@ -200,15 +200,15 @@ function select_impl(obj: NixValue, attrpath: NixValue[]): NixValue {
let attrs = forceAttrs(obj);
for (const attr of attrpath.slice(0, -1)) {
const key = forceString(attr);
const key = forceStringValue(attr);
if (!(key in attrs)) {
throw new Error(`Attribute '${key}' not found`);
}
const cur = forceAttrs(attrs[forceString(attr)]);
const cur = forceAttrs(attrs[forceStringValue(attr)]);
attrs = cur;
}
const last = forceString(attrpath[attrpath.length - 1]);
const last = forceStringValue(attrpath[attrpath.length - 1]);
if (!(last in attrs)) {
throw new Error(`Attribute '${last}' not found`);
}
@@ -222,7 +222,7 @@ export const selectWithDefault = (
span?: string,
): NixValue => {
if (STACK_TRACE.enabled && span) {
const pathStrings = attrpath.map((a) => forceString(a));
const pathStrings = attrpath.map((a) => forceStringValue(a));
const path = pathStrings.join(".");
const message = path ? `while selecting attribute [${path}]` : "while selecting attribute";
@@ -246,7 +246,7 @@ function selectWithDefault_impl(obj: NixValue, attrpath: NixValue[], default_val
let attrs = forceAttrs(obj);
for (const attr of attrpath.slice(0, -1)) {
const key = forceString(attr);
const key = forceStringValue(attr);
if (!(key in attrs)) {
return default_val;
}
@@ -257,7 +257,7 @@ function selectWithDefault_impl(obj: NixValue, attrpath: NixValue[], default_val
attrs = cur;
}
const last = forceString(attrpath[attrpath.length - 1]);
const last = forceStringValue(attrpath[attrpath.length - 1]);
if (last in attrs) {
return attrs[last];
}
@@ -272,14 +272,14 @@ export const hasAttr = (obj: NixValue, attrpath: NixValue[]): NixBool => {
let attrs = forced;
for (const attr of attrpath.slice(0, -1)) {
const cur = force(attrs[forceString(attr)]);
const cur = force(attrs[forceStringValue(attr)]);
if (!isAttrs(cur)) {
return false;
}
attrs = cur;
}
return forceString(attrpath[attrpath.length - 1]) in attrs;
return forceStringValue(attrpath[attrpath.length - 1]) in attrs;
};
/**
@@ -357,7 +357,7 @@ function call_impl(func: NixValue, arg: NixValue): NixValue {
const functor = forceFunction(forcedFunc.__functor);
return forceFunction(functor(forcedFunc))(arg);
}
throw new Error(`attempt to call something which is not a function but ${typeName(forcedFunc)}`);
throw new Error(`attempt to call something which is not a function but ${typeOf(forcedFunc)}`);
}
export const assert = (assertion: NixValue, expr: NixValue, assertionRaw: string, span: string): NixValue => {

View File

@@ -4,16 +4,13 @@
*/
import type { NixValue, NixList, NixAttrs, NixString, NixPath } from "./types";
import { isStringWithContext, isNixPath } from "./types";
import { isNixPath } from "./types";
import { force } from "./thunk";
import { forceNumeric, forceList, forceAttrs, coerceNumeric } from "./type-assert";
import { getStringValue, getStringContext, mergeContexts, mkStringWithContext } from "./string-context";
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
import { mkPath } from "./path";
const isNixString = (v: unknown): v is NixString => {
return typeof v === "string" || isStringWithContext(v);
};
import { typeOf, isNixString } from "./builtins/type-check";
const canCoerceToString = (v: NixValue): boolean => {
const forced = force(v);
@@ -24,6 +21,73 @@ const canCoerceToString = (v: NixValue): boolean => {
return false;
};
/**
* Compare two values, similar to Nix's CompareValues.
* Returns: -1 if a < b, 0 if a == b, 1 if a > b
* Throws TypeError for incomparable types.
*/
export const compareValues = (a: NixValue, b: NixValue): -1 | 0 | 1 => {
const av = force(a);
const bv = force(b);
// Handle float/int mixed comparisons
if (typeof av === "number" && typeof bv === "bigint") {
const cmp = av - Number(bv);
return cmp < 0 ? -1 : cmp > 0 ? 1 : 0;
}
if (typeof av === "bigint" && typeof bv === "number") {
const cmp = Number(av) - bv;
return cmp < 0 ? -1 : cmp > 0 ? 1 : 0;
}
const typeA = typeOf(av);
const typeB = typeOf(bv);
// Types must match (except float/int which is handled above)
if (typeA !== typeB) {
throw new TypeError(`cannot compare ${typeOf(av)} with ${typeOf(bv)}`);
}
// Int and float comparison
if (typeA === "int" || typeA === "float") {
return av! < bv! ? -1 : av === bv ? 0 : 1;
}
// String comparison (handles both plain strings and StringWithContext)
if (typeA === "string") {
const strA = getStringValue(av as NixString);
const strB = getStringValue(bv as NixString);
return strA < strB ? -1 : strA > strB ? 1 : 0;
}
// Path comparison
if (typeA === "path") {
const aPath = av as NixPath;
const bPath = bv as NixPath;
return aPath.value < bPath.value ? -1 : aPath.value > bPath.value ? 1 : 0;
}
// List comparison (lexicographic)
if (typeA === "list") {
const aList = av as NixList;
const bList = bv as NixList;
for (let i = 0; ; i++) {
if (i === bList.length) {
return i === aList.length ? 0 : 1; // Equal if same length, else aList > bList
} else if (i === aList.length) {
return -1; // aList < bList
} else if (!op.eq(aList[i], bList[i])) {
return compareValues(aList[i], bList[i]);
}
}
}
// Other types are incomparable
throw new TypeError(
`cannot compare ${typeOf(av)} with ${typeOf(bv)}; values of that type are incomparable`,
);
};
/**
* Operator object exported as Nix.op
* All operators referenced by codegen (e.g., Nix.op.add, Nix.op.eq)
@@ -116,17 +180,10 @@ export const op = {
const av = force(a);
const bv = force(b);
// Path comparison
if (isNixPath(av) && isNixPath(bv)) {
return av.value === bv.value;
}
// Pointer equality
if (av === bv) return true;
// String comparison
if (isNixString(av) && isNixString(bv)) {
return getStringValue(av) === getStringValue(bv);
}
// Numeric comparison with type coercion
// Special case: int == float type compatibility
if (typeof av === "bigint" && typeof bv === "number") {
return Number(av) === bv;
}
@@ -134,7 +191,27 @@ export const op = {
return av === Number(bv);
}
// List comparison
// Get type names for comparison (skip if already handled above)
const typeA = typeOf(av);
const typeB = typeOf(bv);
// All other types must match exactly
if (typeA !== typeB) return false;
if (typeA === "int" || typeA === "float" || typeA === "bool" || typeA === "null") {
return av === bv;
}
// String comparison (handles both plain strings and StringWithContext)
if (typeA === "string") {
return getStringValue(av as NixString) === getStringValue(bv as NixString);
}
// Path comparison
if (typeA === "path") {
return (av as NixPath).value === (bv as NixPath).value;
}
if (Array.isArray(av) && Array.isArray(bv)) {
if (av.length !== bv.length) return false;
for (let i = 0; i < av.length; i++) {
@@ -143,105 +220,55 @@ export const op = {
return true;
}
// Attrset comparison
if (
typeof av === "object" &&
av !== null &&
!Array.isArray(av) &&
typeof bv === "object" &&
bv !== null &&
!Array.isArray(bv) &&
!isNixString(av) &&
!isNixString(bv) &&
!isNixPath(av) &&
!isNixPath(bv)
) {
const keysA = Object.keys(av);
const keysB = Object.keys(bv);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!(key in bv)) return false;
if (!op.eq((av as NixAttrs)[key], (bv as NixAttrs)[key])) return false;
if (typeA === "set") {
const attrsA = av as NixAttrs;
const attrsB = bv as NixAttrs;
// If both denote a derivation (type = "derivation"), compare their outPaths
const isDerivationA = "type" in attrsA && force(attrsA.type) === "derivation";
const isDerivationB = "type" in attrsB && force(attrsB.type) === "derivation";
if (isDerivationA && isDerivationB) {
if ("outPath" in attrsA && "outPath" in attrsB) {
return op.eq(attrsA.outPath, attrsB.outPath);
}
}
// Otherwise, compare attributes one by one
const keysA = Object.keys(attrsA).sort();
const keysB = Object.keys(attrsB).sort();
if (keysA.length !== keysB.length) return false;
for (let i = 0; i < keysA.length; i++) {
if (keysA[i] !== keysB[i]) return false;
if (!op.eq(attrsA[keysA[i]], attrsB[keysB[i]])) return false;
}
return true;
}
return av === bv;
// Functions are incomparable
if (typeof av === "function") {
return false;
}
return false;
},
neq: (a: NixValue, b: NixValue): boolean => {
return !op.eq(a, b);
},
lt: (a: NixValue, b: NixValue): boolean => {
const av = force(a);
const bv = force(b);
// Path comparison
if (isNixPath(av) && isNixPath(bv)) {
return av.value < bv.value;
}
// String comparison
if (isNixString(av) && isNixString(bv)) {
return getStringValue(av) < getStringValue(bv);
}
// Numeric comparison
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (numA as any) < (numB as any);
return compareValues(a, b) < 0;
},
lte: (a: NixValue, b: NixValue): boolean => {
const av = force(a);
const bv = force(b);
// Path comparison
if (isNixPath(av) && isNixPath(bv)) {
return av.value <= bv.value;
}
// String comparison
if (isNixString(av) && isNixString(bv)) {
return getStringValue(av) <= getStringValue(bv);
}
// Numeric comparison
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (numA as any) <= (numB as any);
return compareValues(a, b) <= 0;
},
gt: (a: NixValue, b: NixValue): boolean => {
const av = force(a);
const bv = force(b);
// Path comparison
if (isNixPath(av) && isNixPath(bv)) {
return av.value > bv.value;
}
// String comparison
if (isNixString(av) && isNixString(bv)) {
return getStringValue(av) > getStringValue(bv);
}
// Numeric comparison
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (numA as any) > (numB as any);
return compareValues(a, b) > 0;
},
gte: (a: NixValue, b: NixValue): boolean => {
const av = force(a);
const bv = force(b);
// Path comparison
if (isNixPath(av) && isNixPath(bv)) {
return av.value >= bv.value;
}
// String comparison
if (isNixString(av) && isNixString(bv)) {
return getStringValue(av) >= getStringValue(bv);
}
// Numeric comparison
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (numA as any) >= (numB as any);
return compareValues(a, b) >= 0;
},
bnot: (a: NixValue): boolean => !force(a),

View File

@@ -23,6 +23,8 @@
* This implementation matches Lix's NixStringContext system.
*/
import { NixStrictValue } from "./types";
export const HAS_CONTEXT = Symbol("HAS_CONTEXT");
export interface StringContextOpaque {
@@ -51,10 +53,8 @@ export interface StringWithContext {
context: NixStringContext;
}
export const isStringWithContext = (v: unknown): v is StringWithContext => {
return (
typeof v === "object" && v !== null && HAS_CONTEXT in v && (v as StringWithContext)[HAS_CONTEXT] === true
);
export const isStringWithContext = (v: NixStrictValue): v is StringWithContext => {
return typeof v === "object" && v !== null && HAS_CONTEXT in v;
};
export const mkStringWithContext = (value: string, context: NixStringContext): StringWithContext => {

View File

@@ -17,23 +17,7 @@ import type {
import { isStringWithContext, isNixPath } from "./types";
import { force } from "./thunk";
import { getStringValue } from "./string-context";
export const typeName = (value: NixValue): string => {
const val = force(value);
if (isNixPath(val)) return "path";
if (typeof val === "bigint") return "int";
if (typeof val === "number") return "float";
if (typeof val === "boolean") return "boolean";
if (typeof val === "string") return "string";
if (isStringWithContext(val)) return "string";
if (val === null) return "null";
if (Array.isArray(val)) return "list";
if (typeof val === "function") return "lambda";
if (typeof val === "object") return "attribute set";
throw new TypeError(`Unknown Nix type: ${typeof val}`);
};
import { isAttrs, isFunction, typeOf } from "./builtins/type-check";
/**
* Force a value and assert it's a list
@@ -42,7 +26,7 @@ export const typeName = (value: NixValue): string => {
export const forceList = (value: NixValue): NixList => {
const forced = force(value);
if (!Array.isArray(forced)) {
throw new TypeError(`Expected list, got ${typeName(forced)}`);
throw new TypeError(`Expected list, got ${typeOf(forced)}`);
}
return forced;
};
@@ -53,8 +37,8 @@ export const forceList = (value: NixValue): NixList => {
*/
export const forceFunction = (value: NixValue): NixFunction => {
const forced = force(value);
if (typeof forced !== "function") {
throw new TypeError(`Expected function, got ${typeName(forced)}`);
if (!isFunction(forced)) {
throw new TypeError(`Expected function, got ${typeOf(forced)}`);
}
return forced;
};
@@ -65,14 +49,8 @@ export const forceFunction = (value: NixValue): NixFunction => {
*/
export const forceAttrs = (value: NixValue): NixAttrs => {
const forced = force(value);
if (
typeof forced !== "object" ||
Array.isArray(forced) ||
forced === null ||
isStringWithContext(forced) ||
isNixPath(forced)
) {
throw new TypeError(`Expected attribute set, got ${typeName(forced)}`);
if (!isAttrs(forced)) {
throw new TypeError(`Expected attribute set, got ${typeOf(forced)}`);
}
return forced;
};
@@ -81,7 +59,7 @@ export const forceAttrs = (value: NixValue): NixAttrs => {
* Force a value and assert it's a string (plain or with context)
* @throws TypeError if value is not a string after forcing
*/
export const forceString = (value: NixValue): string => {
export const forceStringValue = (value: NixValue): string => {
const forced = force(value);
if (typeof forced === "string") {
return forced;
@@ -89,14 +67,14 @@ export const forceString = (value: NixValue): string => {
if (isStringWithContext(forced)) {
return forced.value;
}
throw new TypeError(`Expected string, got ${typeName(forced)}`);
throw new TypeError(`Expected string, got ${typeOf(forced)}`);
};
/**
* Force a value and assert it's a string, returning NixString (preserving context)
* @throws TypeError if value is not a string after forcing
*/
export const forceNixString = (value: NixValue): NixString => {
export const forceString = (value: NixValue): NixString => {
const forced = force(value);
if (typeof forced === "string") {
return forced;
@@ -104,7 +82,7 @@ export const forceNixString = (value: NixValue): NixString => {
if (isStringWithContext(forced)) {
return forced;
}
throw new TypeError(`Expected string, got ${typeName(forced)}`);
throw new TypeError(`Expected string, got ${typeOf(forced)}`);
};
/**
@@ -121,7 +99,7 @@ export const nixStringValue = (s: NixString): string => {
export const forceBool = (value: NixValue): boolean => {
const forced = force(value);
if (typeof forced !== "boolean") {
throw new TypeError(`Expected boolean, got ${typeName(forced)}`);
throw new TypeError(`Expected boolean, got ${typeOf(forced)}`);
}
return forced;
};
@@ -135,7 +113,7 @@ export const forceInt = (value: NixValue): NixInt => {
if (typeof forced === "bigint") {
return forced;
}
throw new TypeError(`Expected int, got ${typeName(forced)}`);
throw new TypeError(`Expected int, got ${typeOf(forced)}`);
};
/**
@@ -147,7 +125,7 @@ export const forceFloat = (value: NixValue): NixFloat => {
if (typeof forced === "number") {
return forced;
}
throw new TypeError(`Expected float, got ${typeName(forced)}`);
throw new TypeError(`Expected float, got ${typeOf(forced)}`);
};
/**
@@ -159,7 +137,7 @@ export const forceNumeric = (value: NixValue): NixNumber => {
if (typeof forced === "bigint" || typeof forced === "number") {
return forced;
}
throw new TypeError(`Expected numeric type, got ${typeName(forced)}`);
throw new TypeError(`Expected numeric type, got ${typeOf(forced)}`);
};
/**
@@ -173,7 +151,7 @@ export const coerceNumeric = (a: NixNumber, b: NixNumber): [NixFloat, NixFloat]
// If either is float, convert both to float
if (!aIsInt || !bIsInt) {
return [aIsInt ? Number(a) : a, bIsInt ? Number(b) : b];
return [Number(a), Number(b)];
}
// Both are integers
@@ -189,5 +167,5 @@ export const forceNixPath = (value: NixValue): NixPath => {
if (isNixPath(forced)) {
return forced;
}
throw new TypeError(`Expected path, got ${typeName(forced)}`);
throw new TypeError(`Expected path, got ${typeOf(forced)}`);
};

View File

@@ -15,7 +15,7 @@ export interface NixPath {
}
export const isNixPath = (v: NixStrictValue): v is NixPath => {
return typeof v === "object" && v !== null && IS_PATH in v && (v as NixPath)[IS_PATH] === true;
return typeof v === "object" && v !== null && IS_PATH in v;
};
// Nix primitive types
@@ -28,6 +28,7 @@ export type NixNull = null;
// Nix composite types
export type NixList = NixValue[];
// FIXME: reject contextful string
export type NixAttrs = { [key: string]: NixValue };
export type NixFunction = (arg: NixValue) => NixValue;

View File

@@ -260,3 +260,19 @@ fn builtins_compare_versions_complex() {
Value::Int(-1)
);
}
#[test]
fn builtins_generic_closure() {
assert_eq!(
eval(
"with builtins; length (genericClosure { startSet = [ { key = 1; } ]; operator = { key }: [ { key = key / 1.; } ]; a = 1; })"
),
Value::Int(1),
);
assert_eq!(
eval(
"with builtins; (elemAt (genericClosure { startSet = [ { key = 1; } ]; operator = { key }: [ { key = key / 1.; } ]; a = 1; }) 0).key"
),
Value::Int(1),
);
}