From 56a8ba9475f9b04d6a23a78ce3e2eb45e82ad600 Mon Sep 17 00:00:00 2001 From: imxyy_soope_ Date: Thu, 22 Jan 2026 20:13:31 +0800 Subject: [PATCH] feat: builtins.genericClosure; refactor type check --- nix-js/runtime-ts/package-lock.json | 13 ++ nix-js/runtime-ts/package.json | 3 + nix-js/runtime-ts/src/builtins/arithmetic.ts | 3 +- nix-js/runtime-ts/src/builtins/attrs.ts | 12 +- nix-js/runtime-ts/src/builtins/context.ts | 16 +- nix-js/runtime-ts/src/builtins/conversion.ts | 26 +-- nix-js/runtime-ts/src/builtins/derivation.ts | 14 +- nix-js/runtime-ts/src/builtins/index.ts | 65 +++--- nix-js/runtime-ts/src/builtins/io.ts | 61 ++--- nix-js/runtime-ts/src/builtins/misc.ts | 66 +++++- nix-js/runtime-ts/src/builtins/string.ts | 16 +- nix-js/runtime-ts/src/builtins/type-check.ts | 70 +++--- nix-js/runtime-ts/src/helpers.ts | 26 +-- nix-js/runtime-ts/src/operators.ts | 227 +++++++++++-------- nix-js/runtime-ts/src/string-context.ts | 8 +- nix-js/runtime-ts/src/type-assert.ts | 54 ++--- nix-js/runtime-ts/src/types.ts | 3 +- nix-js/tests/builtins.rs | 16 ++ typos.toml | 8 + 19 files changed, 400 insertions(+), 307 deletions(-) create mode 100644 typos.toml diff --git a/nix-js/runtime-ts/package-lock.json b/nix-js/runtime-ts/package-lock.json index 26b9dad..d8066fd 100644 --- a/nix-js/runtime-ts/package-lock.json +++ b/nix-js/runtime-ts/package-lock.json @@ -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", diff --git a/nix-js/runtime-ts/package.json b/nix-js/runtime-ts/package.json index 40d00de..838ca5a 100644 --- a/nix-js/runtime-ts/package.json +++ b/nix-js/runtime-ts/package.json @@ -10,5 +10,8 @@ "devDependencies": { "esbuild": "^0.24.2", "typescript": "^5.7.2" + }, + "dependencies": { + "js-sdsl": "^4.4.2" } } diff --git a/nix-js/runtime-ts/src/builtins/arithmetic.ts b/nix-js/runtime-ts/src/builtins/arithmetic.ts index 00acfd4..b54159b 100644 --- a/nix-js/runtime-ts/src/builtins/arithmetic.ts +++ b/nix-js/runtime-ts/src/builtins/arithmetic.ts @@ -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); diff --git a/nix-js/runtime-ts/src/builtins/attrs.ts b/nix-js/runtime-ts/src/builtins/attrs.ts index 8883342..def31dc 100644 --- a/nix-js/runtime-ts/src/builtins/attrs.ts +++ b/nix-js/runtime-ts/src/builtins/attrs.ts @@ -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); } diff --git a/nix-js/runtime-ts/src/builtins/context.ts b/nix-js/runtime-ts/src/builtins/context.ts index 0778751..62aad33 100644 --- a/nix-js/runtime-ts/src/builtins/context.ts +++ b/nix-js/runtime-ts/src/builtins/context.ts @@ -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}`); } } diff --git a/nix-js/runtime-ts/src/builtins/conversion.ts b/nix-js/runtime-ts/src/builtins/conversion.ts index be2faed..e1fbf37 100644 --- a/nix-js/runtime-ts/src/builtins/conversion.ts +++ b/nix-js/runtime-ts/src/builtins/conversion.ts @@ -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`); }; /** diff --git a/nix-js/runtime-ts/src/builtins/derivation.ts b/nix-js/runtime-ts/src/builtins/derivation.ts index c616558..58e05db 100644 --- a/nix-js/runtime-ts/src/builtins/derivation.ts +++ b/nix-js/runtime-ts/src/builtins/derivation.ts @@ -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')`); diff --git a/nix-js/runtime-ts/src/builtins/index.ts b/nix-js/runtime-ts/src/builtins/index.ts index 1421723..f369d43 100644 --- a/nix-js/runtime-ts/src/builtins/index.ts +++ b/nix-js/runtime-ts/src/builtins/index.ts @@ -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 = ( - 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 = ( 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), diff --git a/nix-js/runtime-ts/src/builtins/io.ts b/nix-js/runtime-ts/src/builtins/io.ts index ea61920..308c196 100644 --- a/nix-js/runtime-ts/src/builtins/io.ts +++ b/nix-js/runtime-ts/src/builtins/io.ts @@ -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)); }; diff --git a/nix-js/runtime-ts/src/builtins/misc.ts b/nix-js/runtime-ts/src/builtins/misc.ts index 4079790..808913e 100644 --- a/nix-js/runtime-ts/src/builtins/misc.ts +++ b/nix-js/runtime-ts/src/builtins/misc.ts @@ -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(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; diff --git a/nix-js/runtime-ts/src/builtins/string.ts b/nix-js/runtime-ts/src/builtins/string.ts index f6462fa..96e0000 100644 --- a/nix-js/runtime-ts/src/builtins/string.ts +++ b/nix-js/runtime-ts/src/builtins/string.ts @@ -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); diff --git a/nix-js/runtime-ts/src/builtins/type-check.ts b/nix-js/runtime-ts/src/builtins/type-check.ts index 43fa0e3..99f2f83 100644 --- a/nix-js/runtime-ts/src/builtins/type-check.ts +++ b/nix-js/runtime-ts/src/builtins/type-check.ts @@ -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}`); }; diff --git a/nix-js/runtime-ts/src/helpers.ts b/nix-js/runtime-ts/src/helpers.ts index 3c5689c..b1768b3 100644 --- a/nix-js/runtime-ts/src/helpers.ts +++ b/nix-js/runtime-ts/src/helpers.ts @@ -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 => { diff --git a/nix-js/runtime-ts/src/operators.ts b/nix-js/runtime-ts/src/operators.ts index fa06582..8f30b38 100644 --- a/nix-js/runtime-ts/src/operators.ts +++ b/nix-js/runtime-ts/src/operators.ts @@ -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), diff --git a/nix-js/runtime-ts/src/string-context.ts b/nix-js/runtime-ts/src/string-context.ts index a3f93b6..2da8a04 100644 --- a/nix-js/runtime-ts/src/string-context.ts +++ b/nix-js/runtime-ts/src/string-context.ts @@ -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 => { diff --git a/nix-js/runtime-ts/src/type-assert.ts b/nix-js/runtime-ts/src/type-assert.ts index e98c962..ba60c70 100644 --- a/nix-js/runtime-ts/src/type-assert.ts +++ b/nix-js/runtime-ts/src/type-assert.ts @@ -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)}`); }; diff --git a/nix-js/runtime-ts/src/types.ts b/nix-js/runtime-ts/src/types.ts index ccbc8a5..8e2abda 100644 --- a/nix-js/runtime-ts/src/types.ts +++ b/nix-js/runtime-ts/src/types.ts @@ -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; diff --git a/nix-js/tests/builtins.rs b/nix-js/tests/builtins.rs index 4e0c2ed..49723a2 100644 --- a/nix-js/tests/builtins.rs +++ b/nix-js/tests/builtins.rs @@ -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), + ); +} diff --git a/typos.toml b/typos.toml new file mode 100644 index 0000000..1987e3e --- /dev/null +++ b/typos.toml @@ -0,0 +1,8 @@ +[files] +extend-exclude = [ + "nix-js/tests/regex.rs" +] + +[default.extend-words] +contextful = "contextful" +contextfull = "contextful"