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", "name": "nix-js-runtime",
"version": "0.1.0", "version": "0.1.0",
"dependencies": {
"js-sdsl": "^4.4.2"
},
"devDependencies": { "devDependencies": {
"esbuild": "^0.24.2", "esbuild": "^0.24.2",
"typescript": "^5.7.2" "typescript": "^5.7.2"
@@ -478,6 +481,16 @@
"@esbuild/win32-x64": "0.24.2" "@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": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",

View File

@@ -10,5 +10,8 @@
"devDependencies": { "devDependencies": {
"esbuild": "^0.24.2", "esbuild": "^0.24.2",
"typescript": "^5.7.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 type { NixBool, NixInt, NixNumber, NixValue } from "../types";
import { forceNumeric, coerceNumeric, forceInt } from "../type-assert"; import { forceNumeric, coerceNumeric, forceInt } from "../type-assert";
import { op } from "../operators";
export const add = export const add =
(a: NixValue) => (a: NixValue) =>
@@ -66,4 +67,4 @@ export const bitXor =
export const lessThan = export const lessThan =
(a: NixValue) => (a: NixValue) =>
(b: NixValue): NixBool => (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 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"; import { createThunk } from "../thunk";
export const attrNames = (set: NixValue): string[] => Object.keys(forceAttrs(set)).sort(); export const attrNames = (set: NixValue): string[] => Object.keys(forceAttrs(set)).sort();
@@ -24,12 +24,12 @@ export const attrValues = (set: NixValue): NixValue[] =>
export const getAttr = export const getAttr =
(s: NixValue) => (s: NixValue) =>
(set: NixValue): NixValue => (set: NixValue): NixValue =>
forceAttrs(set)[forceString(s)]; forceAttrs(set)[forceStringValue(s)];
export const hasAttr = export const hasAttr =
(s: NixValue) => (s: NixValue) =>
(set: NixValue): boolean => (set: NixValue): boolean =>
Object.hasOwn(forceAttrs(set), forceString(s)); Object.hasOwn(forceAttrs(set), forceStringValue(s));
export const mapAttrs = export const mapAttrs =
(f: NixValue) => (f: NixValue) =>
@@ -63,7 +63,7 @@ export const listToAttrs = (e: NixValue): NixAttrs => {
const forced_e = [...forceList(e)].reverse(); const forced_e = [...forceList(e)].reverse();
for (const obj of forced_e) { for (const obj of forced_e) {
const item = forceAttrs(obj); const item = forceAttrs(obj);
attrs[forceString(item.name)] = item.value; attrs[forceStringValue(item.name)] = item.value;
} }
return attrs; return attrs;
}; };
@@ -85,7 +85,7 @@ export const intersectAttrs =
export const catAttrs = export const catAttrs =
(attr: NixValue) => (attr: NixValue) =>
(list: NixValue): NixList => { (list: NixValue): NixList => {
const key = forceString(attr); const key = forceStringValue(attr);
return forceList(list) return forceList(list)
.map((set) => forceAttrs(set)[key]) .map((set) => forceAttrs(set)[key])
.filter((val) => val !== undefined); .filter((val) => val !== undefined);
@@ -98,7 +98,7 @@ export const groupBy =
const forced_f = forceFunction(f); const forced_f = forceFunction(f);
const forced_list = forceList(list); const forced_list = forceList(list);
for (const elem of forced_list) { for (const elem of forced_list) {
const key = forceString(forced_f(elem)); const key = forceStringValue(forced_f(elem));
if (!attrs[key]) attrs[key] = []; if (!attrs[key]) attrs[key] = [];
(attrs[key] as NixList).push(elem); (attrs[key] as NixList).push(elem);
} }

View File

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

View File

@@ -8,6 +8,7 @@ import { force } from "../thunk";
import { type NixStringContext, mkStringWithContext, addBuiltContext } from "../string-context"; import { type NixStringContext, mkStringWithContext, addBuiltContext } from "../string-context";
import { forceFunction } from "../type-assert"; import { forceFunction } from "../type-assert";
import { nixValueToJson } from "../conversion"; import { nixValueToJson } from "../conversion";
import { typeOf } from "./type-check";
const convertJsonToNix = (json: unknown): NixValue => { const convertJsonToNix = (json: unknown): NixValue => {
if (json === null) { if (json === null) {
@@ -41,7 +42,7 @@ const convertJsonToNix = (json: unknown): NixValue => {
export const fromJSON = (e: NixValue): NixValue => { export const fromJSON = (e: NixValue): NixValue => {
const str = force(e); const str = force(e);
if (typeof str !== "string" && !isStringWithContext(str)) { 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; const jsonStr = isStringWithContext(str) ? str.value : str;
try { try {
@@ -82,25 +83,6 @@ export enum StringCoercionMode {
ToString = 2, 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 { export interface CoerceResult {
value: string; value: string;
context: NixStringContext; context: NixStringContext;
@@ -196,7 +178,7 @@ export const coerceToString = (
} }
// Attribute sets without __toString or outPath cannot be coerced // 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 // 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 type { NixValue, NixAttrs } from "../types";
import { forceString, forceList } from "../type-assert"; import { forceStringValue, forceList } from "../type-assert";
import { force } from "../thunk"; import { force } from "../thunk";
import { type DerivationData, type OutputInfo, generateAterm } from "../derivation-helpers"; import { type DerivationData, type OutputInfo, generateAterm } from "../derivation-helpers";
import { coerceToString, StringCoercionMode } from "./conversion"; import { coerceToString, StringCoercionMode } from "./conversion";
@@ -25,7 +25,7 @@ const validateName = (attrs: NixAttrs): string => {
if (!("name" in attrs)) { if (!("name" in attrs)) {
throw new Error("derivation: missing required attribute 'name'"); throw new Error("derivation: missing required attribute 'name'");
} }
const name = forceString(attrs.name); const name = forceStringValue(attrs.name);
if (!name) { if (!name) {
throw new Error("derivation: 'name' cannot be empty"); throw new Error("derivation: 'name' cannot be empty");
} }
@@ -46,7 +46,7 @@ const validateSystem = (attrs: NixAttrs): string => {
if (!("system" in attrs)) { if (!("system" in attrs)) {
throw new Error("derivation: missing required attribute 'system'"); throw new Error("derivation: missing required attribute 'system'");
} }
return forceString(attrs.system); return forceStringValue(attrs.system);
}; };
const extractOutputs = (attrs: NixAttrs): string[] => { const extractOutputs = (attrs: NixAttrs): string[] => {
@@ -54,7 +54,7 @@ const extractOutputs = (attrs: NixAttrs): string[] => {
return ["out"]; return ["out"];
} }
const outputsList = forceList(attrs.outputs); const outputsList = forceList(attrs.outputs);
const outputs = outputsList.map((o) => forceString(o)); const outputs = outputsList.map((o) => forceStringValue(o));
if (outputs.length === 0) { if (outputs.length === 0) {
throw new Error("derivation: outputs list cannot be empty"); throw new Error("derivation: outputs list cannot be empty");
@@ -141,9 +141,9 @@ const extractFixedOutputInfo = (attrs: NixAttrs): FixedOutputInfo | null => {
return null; return null;
} }
const hash = forceString(attrs.outputHash); const hash = forceStringValue(attrs.outputHash);
const hashAlgo = "outputHashAlgo" in attrs ? forceString(attrs.outputHashAlgo) : "sha256"; const hashAlgo = "outputHashAlgo" in attrs ? forceStringValue(attrs.outputHashAlgo) : "sha256";
const hashMode = "outputHashMode" in attrs ? forceString(attrs.outputHashMode) : "flat"; const hashMode = "outputHashMode" in attrs ? forceStringValue(attrs.outputHashMode) : "flat";
if (hashMode !== "flat" && hashMode !== "recursive") { if (hashMode !== "flat" && hashMode !== "recursive") {
throw new Error(`derivation: invalid outputHashMode '${hashMode}' (must be 'flat' or '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 * 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) * 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) * @param applied - Number of arguments already applied (default: 0)
* @returns The marked function * @returns The marked function
*/ */
export const mkPrimop = <T extends Function>( export const mkPrimop = (
func: T, func: (...args: NixValue[]) => NixValue,
name: string, name: string,
arity: number, arity: number,
applied: number = 0, applied: number = 0,
): T => { ): Function => {
// Mark this function as a primop // Mark this function as a primop
(func as any)[PRIMOP_METADATA] = { (func as any)[PRIMOP_METADATA] = {
name, name,
arity, arity,
applied, applied,
} as PrimopMetadata; } satisfies PrimopMetadata;
// If this is a curried function and not fully applied, // If this is a curried function and not fully applied,
// wrap it to mark the next layer too // wrap it to mark the next layer too
if (applied < arity - 1) { if (applied < arity - 1) {
const wrappedFunc = ((...args: any[]) => { const wrappedFunc = ((...args: NixValue[]) => {
const result = func(...args); const result = func(...args);
// If result is a function, mark it as the next layer // If result is a function, mark it as the next layer
if (typeof result === "function") { if (typeof result === "function") {
@@ -63,9 +78,9 @@ export const mkPrimop = <T extends Function>(
name, name,
arity, arity,
applied, applied,
} as PrimopMetadata; } satisfies PrimopMetadata;
return wrappedFunc as T; return wrappedFunc;
} }
return func; return func;
@@ -97,20 +112,6 @@ export const get_primop_metadata = (func: unknown): PrimopMetadata | undefined =
return 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 * The global builtins object
* Contains 80+ Nix builtin functions plus metadata * Contains 80+ Nix builtin functions plus metadata
@@ -134,16 +135,16 @@ export const builtins: any = {
ceil: mkPrimop(math.ceil, "ceil", 1), ceil: mkPrimop(math.ceil, "ceil", 1),
floor: mkPrimop(math.floor, "floor", 1), floor: mkPrimop(math.floor, "floor", 1),
isAttrs: mkPrimop(typeCheck.isAttrs, "isAttrs", 1), isAttrs: mkPrimop((e: NixValue) => typeCheck.isAttrs(force(e)), "isAttrs", 1),
isBool: mkPrimop(typeCheck.isBool, "isBool", 1), isBool: mkPrimop((e: NixValue) => typeCheck.isBool(force(e)), "isBool", 1),
isFloat: mkPrimop(typeCheck.isFloat, "isFloat", 1), isFloat: mkPrimop((e: NixValue) => typeCheck.isFloat(force(e)), "isFloat", 1),
isFunction: mkPrimop(typeCheck.isFunction, "isFunction", 1), isFunction: mkPrimop((e: NixValue) => typeCheck.isFunction(force(e)), "isFunction", 1),
isInt: mkPrimop(typeCheck.isInt, "isInt", 1), isInt: mkPrimop((e: NixValue) => typeCheck.isInt(force(e)), "isInt", 1),
isList: mkPrimop(typeCheck.isList, "isList", 1), isList: mkPrimop((e: NixValue) => typeCheck.isList(force(e)), "isList", 1),
isNull: mkPrimop(typeCheck.isNull, "isNull", 1), isNull: mkPrimop((e: NixValue) => typeCheck.isNull(force(e)), "isNull", 1),
isPath: mkPrimop(typeCheck.isPath, "isPath", 1), isPath: mkPrimop((e: NixValue) => typeCheck.isPath(force(e)), "isPath", 1),
isString: mkPrimop(typeCheck.isString, "isString", 1), isString: mkPrimop((e: NixValue) => typeCheck.isString(force(e)), "isString", 1),
typeOf: mkPrimop(typeCheck.typeOf, "typeOf", 1), typeOf: mkPrimop((e: NixValue) => typeCheck.typeOf(force(e)), "typeOf", 1),
map: mkPrimop(list.map, "map", 2), map: mkPrimop(list.map, "map", 2),
filter: mkPrimop(list.filter, "filter", 2), filter: mkPrimop(list.filter, "filter", 2),

View File

@@ -3,7 +3,7 @@
* Implemented via Rust ops exposed through deno_core * 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 type { NixValue, NixAttrs } from "../types";
import { isNixPath } from "../types"; import { isNixPath } from "../types";
import { force } from "../thunk"; import { force } from "../thunk";
@@ -92,10 +92,14 @@ const normalizeUrlInput = (
return { url: forced }; return { url: forced };
} }
const attrs = forceAttrs(args); const attrs = forceAttrs(args);
const url = forceString(attrs.url); const url = forceStringValue(attrs.url);
const hash = const hash =
"sha256" in attrs ? forceString(attrs.sha256) : "hash" in attrs ? forceString(attrs.hash) : undefined; "sha256" in attrs
const name = "name" in attrs ? forceString(attrs.name) : undefined; ? 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; const executable = "executable" in attrs ? forceBool(attrs.executable) : false;
return { url, hash, name, executable }; return { url, hash, name, executable };
}; };
@@ -108,15 +112,15 @@ const normalizeTarballInput = (
return { url: forced }; return { url: forced };
} }
const attrs = forceAttrs(args); const attrs = forceAttrs(args);
const url = forceString(attrs.url); const url = forceStringValue(attrs.url);
const hash = "hash" in attrs ? forceString(attrs.hash) : undefined; const hash = "hash" in attrs ? forceStringValue(attrs.hash) : undefined;
const narHash = const narHash =
"narHash" in attrs "narHash" in attrs
? forceString(attrs.narHash) ? forceStringValue(attrs.narHash)
: "sha256" in attrs : "sha256" in attrs
? forceString(attrs.sha256) ? forceStringValue(attrs.sha256)
: undefined; : undefined;
const name = "name" in attrs ? forceString(attrs.name) : undefined; const name = "name" in attrs ? forceStringValue(attrs.name) : undefined;
return { url, hash, narHash, name }; return { url, hash, narHash, name };
}; };
@@ -159,13 +163,13 @@ export const fetchGit = (args: NixValue): NixAttrs => {
}; };
} }
const attrs = forceAttrs(args); const attrs = forceAttrs(args);
const url = forceString(attrs.url); const url = forceStringValue(attrs.url);
const gitRef = "ref" in attrs ? forceString(attrs.ref) : null; const gitRef = "ref" in attrs ? forceStringValue(attrs.ref) : null;
const rev = "rev" in attrs ? forceString(attrs.rev) : null; const rev = "rev" in attrs ? forceStringValue(attrs.rev) : null;
const shallow = "shallow" in attrs ? forceBool(attrs.shallow) : false; const shallow = "shallow" in attrs ? forceBool(attrs.shallow) : false;
const submodules = "submodules" in attrs ? forceBool(attrs.submodules) : false; const submodules = "submodules" in attrs ? forceBool(attrs.submodules) : false;
const allRefs = "allRefs" in attrs ? forceBool(attrs.allRefs) : 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( const result: FetchGitResult = Deno.core.ops.op_fetch_git(
url, url,
@@ -191,9 +195,9 @@ export const fetchGit = (args: NixValue): NixAttrs => {
export const fetchMercurial = (args: NixValue): NixAttrs => { export const fetchMercurial = (args: NixValue): NixAttrs => {
const attrs = forceAttrs(args); const attrs = forceAttrs(args);
const url = forceString(attrs.url); const url = forceStringValue(attrs.url);
const rev = "rev" in attrs ? forceString(attrs.rev) : null; const rev = "rev" in attrs ? forceStringValue(attrs.rev) : null;
const name = "name" in attrs ? forceString(attrs.name) : null; const name = "name" in attrs ? forceStringValue(attrs.name) : null;
const result: FetchHgResult = Deno.core.ops.op_fetch_hg(url, rev, name); 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 => { export const fetchTree = (args: NixValue): NixAttrs => {
const attrs = forceAttrs(args); const attrs = forceAttrs(args);
const type = "type" in attrs ? forceString(attrs.type) : "auto"; const type = "type" in attrs ? forceStringValue(attrs.type) : "auto";
switch (type) { switch (type) {
case "git": case "git":
@@ -221,7 +225,7 @@ export const fetchTree = (args: NixValue): NixAttrs => {
case "file": case "file":
return { outPath: fetchurl(args) }; return { outPath: fetchurl(args) };
case "path": { case "path": {
const path = forceString(attrs.path); const path = forceStringValue(attrs.path);
return { outPath: path }; return { outPath: path };
} }
case "github": case "github":
@@ -235,10 +239,11 @@ export const fetchTree = (args: NixValue): NixAttrs => {
}; };
const fetchGitForge = (forge: string, attrs: NixAttrs): NixAttrs => { const fetchGitForge = (forge: string, attrs: NixAttrs): NixAttrs => {
const owner = forceString(attrs.owner); const owner = forceStringValue(attrs.owner);
const repo = forceString(attrs.repo); const repo = forceStringValue(attrs.repo);
const rev = "rev" in attrs ? forceString(attrs.rev) : "ref" in attrs ? forceString(attrs.ref) : "HEAD"; const rev =
const host = "host" in attrs ? forceString(attrs.host) : undefined; "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; let tarballUrl: string;
switch (forge) { switch (forge) {
@@ -271,7 +276,7 @@ const fetchGitForge = (forge: string, attrs: NixAttrs): NixAttrs => {
}; };
const autoDetectAndFetch = (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")) { if (url.endsWith(".git") || url.includes("github.com") || url.includes("gitlab.com")) {
return fetchGit(attrs); return fetchGit(attrs);
} }
@@ -332,17 +337,17 @@ export const path = (args: NixValue): string => {
if (isNixPath(pathValue)) { if (isNixPath(pathValue)) {
pathStr = getPathValue(pathValue); pathStr = getPathValue(pathValue);
} else { } else {
pathStr = forceString(pathValue); pathStr = forceStringValue(pathValue);
} }
// Optional: name parameter (defaults to basename in Rust) // 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) // Optional: recursive parameter (default: true)
const recursive = "recursive" in attrs ? forceBool(attrs.recursive) : true; const recursive = "recursive" in attrs ? forceBool(attrs.recursive) : true;
// Optional: sha256 parameter // 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 // TODO: Handle filter parameter
if ("filter" in attrs) { if ("filter" in attrs) {
@@ -358,7 +363,7 @@ export const path = (args: NixValue): string => {
export const toFile = export const toFile =
(nameArg: NixValue) => (nameArg: NixValue) =>
(contentsArg: NixValue): StringWithContext => { (contentsArg: NixValue): StringWithContext => {
const name = forceString(nameArg); const name = forceStringValue(nameArg);
if (name.includes("/")) { if (name.includes("/")) {
throw new Error("builtins.toFile: name cannot contain '/'"); throw new Error("builtins.toFile: name cannot contain '/'");
@@ -392,5 +397,5 @@ export const findFile =
}; };
export const getEnv = (s: NixValue): string => { 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 { force } from "../thunk";
import { CatchableError } from "../types"; import { CatchableError } from "../types";
import type { NixBool, NixStrictValue, NixValue } 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 * 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 = export const addErrorContext =
(e1: NixValue) => (e1: NixValue) =>
@@ -49,8 +52,8 @@ export const addDrvOutputDependencies = context.addDrvOutputDependencies;
export const compareVersions = export const compareVersions =
(s1: NixValue) => (s1: NixValue) =>
(s2: NixValue): NixValue => { (s2: NixValue): NixValue => {
const str1 = forceString(s1); const str1 = forceStringValue(s1);
const str2 = forceString(s2); const str2 = forceStringValue(s2);
let i1 = 0; let i1 = 0;
let i2 = 0; let i2 = 0;
@@ -152,8 +155,53 @@ export const functionArgs = (f: NixValue): never => {
throw new Error("Not implemented: functionArgs"); throw new Error("Not implemented: functionArgs");
}; };
export const genericClosure = (args: NixValue): never => { const checkComparable = (value: NixStrictValue): void => {
throw new Error("Not implemented: genericClosure"); 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 => { export const getFlake = (attrs: NixValue): never => {
@@ -188,7 +236,7 @@ export const replaceStrings =
(s: NixValue): NixValue => { (s: NixValue): NixValue => {
const fromList = forceList(from); const fromList = forceList(from);
const toList = forceList(to); const toList = forceList(to);
const inputStr = forceString(s); const inputStr = forceStringValue(s);
if (fromList.length !== toList.length) { if (fromList.length !== toList.length) {
throw new Error("'from' and 'to' arguments passed to builtins.replaceStrings have different lengths"); throw new Error("'from' and 'to' arguments passed to builtins.replaceStrings have different lengths");
@@ -203,13 +251,13 @@ export const replaceStrings =
let found = false; let found = false;
for (let i = 0; i < fromList.length; i++) { for (let i = 0; i < fromList.length; i++) {
const pattern = forceString(fromList[i]); const pattern = forceStringValue(fromList[i]);
if (inputStr.substring(pos).startsWith(pattern)) { if (inputStr.substring(pos).startsWith(pattern)) {
found = true; found = true;
if (!toCache.has(i)) { if (!toCache.has(i)) {
toCache.set(i, forceString(toList[i])); toCache.set(i, forceStringValue(toList[i]));
} }
const replacement = toCache.get(i)!; const replacement = toCache.get(i)!;
@@ -239,7 +287,7 @@ export const replaceStrings =
}; };
export const splitVersion = (s: NixValue): NixValue => { export const splitVersion = (s: NixValue): NixValue => {
const version = forceString(s); const version = forceStringValue(s);
const components: string[] = []; const components: string[] = [];
let idx = 0; let idx = 0;

View File

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

View File

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

View File

@@ -3,8 +3,8 @@
*/ */
import type { NixValue, NixAttrs, NixBool, NixString, NixPath } from "./types"; import type { NixValue, NixAttrs, NixBool, NixString, NixPath } from "./types";
import { forceAttrs, forceBool, forceFunction, forceString, typeName } from "./type-assert"; import { forceAttrs, forceBool, forceFunction, forceStringValue } from "./type-assert";
import { isAttrs } from "./builtins/type-check"; import { isAttrs, typeOf } from "./builtins/type-check";
import { coerceToString, StringCoercionMode } from "./builtins/conversion"; import { coerceToString, StringCoercionMode } from "./builtins/conversion";
import { type NixStringContext, mkStringWithContext, isStringWithContext } from "./string-context"; import { type NixStringContext, mkStringWithContext, isStringWithContext } from "./string-context";
import { force } from "./thunk"; import { force } from "./thunk";
@@ -169,14 +169,14 @@ export const concatStringsWithContext = (parts: NixValue[]): NixString | NixPath
* @returns NixPath object with absolute path * @returns NixPath object with absolute path
*/ */
export const resolvePath = (currentDir: string, path: NixValue): NixPath => { 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); const resolved = Deno.core.ops.op_resolve_path(currentDir, pathStr);
return mkPath(resolved); return mkPath(resolved);
}; };
export const select = (obj: NixValue, attrpath: NixValue[], span?: string): NixValue => { export const select = (obj: NixValue, attrpath: NixValue[], span?: string): NixValue => {
if (STACK_TRACE.enabled && span) { if (STACK_TRACE.enabled && span) {
const pathStrings = attrpath.map((a) => forceString(a)); const pathStrings = attrpath.map((a) => forceStringValue(a));
const path = pathStrings.join("."); const path = pathStrings.join(".");
const message = path ? `while selecting attribute [${path}]` : "while selecting attribute"; 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); let attrs = forceAttrs(obj);
for (const attr of attrpath.slice(0, -1)) { for (const attr of attrpath.slice(0, -1)) {
const key = forceString(attr); const key = forceStringValue(attr);
if (!(key in attrs)) { if (!(key in attrs)) {
throw new Error(`Attribute '${key}' not found`); throw new Error(`Attribute '${key}' not found`);
} }
const cur = forceAttrs(attrs[forceString(attr)]); const cur = forceAttrs(attrs[forceStringValue(attr)]);
attrs = cur; attrs = cur;
} }
const last = forceString(attrpath[attrpath.length - 1]); const last = forceStringValue(attrpath[attrpath.length - 1]);
if (!(last in attrs)) { if (!(last in attrs)) {
throw new Error(`Attribute '${last}' not found`); throw new Error(`Attribute '${last}' not found`);
} }
@@ -222,7 +222,7 @@ export const selectWithDefault = (
span?: string, span?: string,
): NixValue => { ): NixValue => {
if (STACK_TRACE.enabled && span) { if (STACK_TRACE.enabled && span) {
const pathStrings = attrpath.map((a) => forceString(a)); const pathStrings = attrpath.map((a) => forceStringValue(a));
const path = pathStrings.join("."); const path = pathStrings.join(".");
const message = path ? `while selecting attribute [${path}]` : "while selecting attribute"; 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); let attrs = forceAttrs(obj);
for (const attr of attrpath.slice(0, -1)) { for (const attr of attrpath.slice(0, -1)) {
const key = forceString(attr); const key = forceStringValue(attr);
if (!(key in attrs)) { if (!(key in attrs)) {
return default_val; return default_val;
} }
@@ -257,7 +257,7 @@ function selectWithDefault_impl(obj: NixValue, attrpath: NixValue[], default_val
attrs = cur; attrs = cur;
} }
const last = forceString(attrpath[attrpath.length - 1]); const last = forceStringValue(attrpath[attrpath.length - 1]);
if (last in attrs) { if (last in attrs) {
return attrs[last]; return attrs[last];
} }
@@ -272,14 +272,14 @@ export const hasAttr = (obj: NixValue, attrpath: NixValue[]): NixBool => {
let attrs = forced; let attrs = forced;
for (const attr of attrpath.slice(0, -1)) { for (const attr of attrpath.slice(0, -1)) {
const cur = force(attrs[forceString(attr)]); const cur = force(attrs[forceStringValue(attr)]);
if (!isAttrs(cur)) { if (!isAttrs(cur)) {
return false; return false;
} }
attrs = cur; 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); const functor = forceFunction(forcedFunc.__functor);
return forceFunction(functor(forcedFunc))(arg); 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 => { 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 type { NixValue, NixList, NixAttrs, NixString, NixPath } from "./types";
import { isStringWithContext, isNixPath } from "./types"; import { isNixPath } from "./types";
import { force } from "./thunk"; import { force } from "./thunk";
import { forceNumeric, forceList, forceAttrs, coerceNumeric } from "./type-assert"; import { forceNumeric, forceList, forceAttrs, coerceNumeric } from "./type-assert";
import { getStringValue, getStringContext, mergeContexts, mkStringWithContext } from "./string-context"; import { getStringValue, getStringContext, mergeContexts, mkStringWithContext } from "./string-context";
import { coerceToString, StringCoercionMode } from "./builtins/conversion"; import { coerceToString, StringCoercionMode } from "./builtins/conversion";
import { mkPath } from "./path"; import { mkPath } from "./path";
import { typeOf, isNixString } from "./builtins/type-check";
const isNixString = (v: unknown): v is NixString => {
return typeof v === "string" || isStringWithContext(v);
};
const canCoerceToString = (v: NixValue): boolean => { const canCoerceToString = (v: NixValue): boolean => {
const forced = force(v); const forced = force(v);
@@ -24,6 +21,73 @@ const canCoerceToString = (v: NixValue): boolean => {
return false; 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 * Operator object exported as Nix.op
* All operators referenced by codegen (e.g., Nix.op.add, Nix.op.eq) * 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 av = force(a);
const bv = force(b); const bv = force(b);
// Path comparison // Pointer equality
if (isNixPath(av) && isNixPath(bv)) { if (av === bv) return true;
return av.value === bv.value;
}
// String comparison // Special case: int == float type compatibility
if (isNixString(av) && isNixString(bv)) {
return getStringValue(av) === getStringValue(bv);
}
// Numeric comparison with type coercion
if (typeof av === "bigint" && typeof bv === "number") { if (typeof av === "bigint" && typeof bv === "number") {
return Number(av) === bv; return Number(av) === bv;
} }
@@ -134,7 +191,27 @@ export const op = {
return av === Number(bv); 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 (Array.isArray(av) && Array.isArray(bv)) {
if (av.length !== bv.length) return false; if (av.length !== bv.length) return false;
for (let i = 0; i < av.length; i++) { for (let i = 0; i < av.length; i++) {
@@ -143,105 +220,55 @@ export const op = {
return true; return true;
} }
// Attrset comparison if (typeA === "set") {
if ( const attrsA = av as NixAttrs;
typeof av === "object" && const attrsB = bv as NixAttrs;
av !== null &&
!Array.isArray(av) && // If both denote a derivation (type = "derivation"), compare their outPaths
typeof bv === "object" && const isDerivationA = "type" in attrsA && force(attrsA.type) === "derivation";
bv !== null && const isDerivationB = "type" in attrsB && force(attrsB.type) === "derivation";
!Array.isArray(bv) &&
!isNixString(av) && if (isDerivationA && isDerivationB) {
!isNixString(bv) && if ("outPath" in attrsA && "outPath" in attrsB) {
!isNixPath(av) && return op.eq(attrsA.outPath, attrsB.outPath);
!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;
} }
}
// 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 true;
} }
return av === bv; // Functions are incomparable
if (typeof av === "function") {
return false;
}
return false;
}, },
neq: (a: NixValue, b: NixValue): boolean => { neq: (a: NixValue, b: NixValue): boolean => {
return !op.eq(a, b); return !op.eq(a, b);
}, },
lt: (a: NixValue, b: NixValue): boolean => { lt: (a: NixValue, b: NixValue): boolean => {
const av = force(a); return compareValues(a, b) < 0;
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);
}, },
lte: (a: NixValue, b: NixValue): boolean => { lte: (a: NixValue, b: NixValue): boolean => {
const av = force(a); return compareValues(a, b) <= 0;
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);
}, },
gt: (a: NixValue, b: NixValue): boolean => { gt: (a: NixValue, b: NixValue): boolean => {
const av = force(a); return compareValues(a, b) > 0;
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);
}, },
gte: (a: NixValue, b: NixValue): boolean => { gte: (a: NixValue, b: NixValue): boolean => {
const av = force(a); return compareValues(a, b) >= 0;
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);
}, },
bnot: (a: NixValue): boolean => !force(a), bnot: (a: NixValue): boolean => !force(a),

View File

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

View File

@@ -17,23 +17,7 @@ import type {
import { isStringWithContext, isNixPath } from "./types"; import { isStringWithContext, isNixPath } from "./types";
import { force } from "./thunk"; import { force } from "./thunk";
import { getStringValue } from "./string-context"; import { getStringValue } from "./string-context";
import { isAttrs, isFunction, typeOf } from "./builtins/type-check";
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}`);
};
/** /**
* Force a value and assert it's a list * 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 => { export const forceList = (value: NixValue): NixList => {
const forced = force(value); const forced = force(value);
if (!Array.isArray(forced)) { if (!Array.isArray(forced)) {
throw new TypeError(`Expected list, got ${typeName(forced)}`); throw new TypeError(`Expected list, got ${typeOf(forced)}`);
} }
return forced; return forced;
}; };
@@ -53,8 +37,8 @@ export const forceList = (value: NixValue): NixList => {
*/ */
export const forceFunction = (value: NixValue): NixFunction => { export const forceFunction = (value: NixValue): NixFunction => {
const forced = force(value); const forced = force(value);
if (typeof forced !== "function") { if (!isFunction(forced)) {
throw new TypeError(`Expected function, got ${typeName(forced)}`); throw new TypeError(`Expected function, got ${typeOf(forced)}`);
} }
return forced; return forced;
}; };
@@ -65,14 +49,8 @@ export const forceFunction = (value: NixValue): NixFunction => {
*/ */
export const forceAttrs = (value: NixValue): NixAttrs => { export const forceAttrs = (value: NixValue): NixAttrs => {
const forced = force(value); const forced = force(value);
if ( if (!isAttrs(forced)) {
typeof forced !== "object" || throw new TypeError(`Expected attribute set, got ${typeOf(forced)}`);
Array.isArray(forced) ||
forced === null ||
isStringWithContext(forced) ||
isNixPath(forced)
) {
throw new TypeError(`Expected attribute set, got ${typeName(forced)}`);
} }
return 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) * Force a value and assert it's a string (plain or with context)
* @throws TypeError if value is not a string after forcing * @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); const forced = force(value);
if (typeof forced === "string") { if (typeof forced === "string") {
return forced; return forced;
@@ -89,14 +67,14 @@ export const forceString = (value: NixValue): string => {
if (isStringWithContext(forced)) { if (isStringWithContext(forced)) {
return forced.value; 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) * Force a value and assert it's a string, returning NixString (preserving context)
* @throws TypeError if value is not a string after forcing * @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); const forced = force(value);
if (typeof forced === "string") { if (typeof forced === "string") {
return forced; return forced;
@@ -104,7 +82,7 @@ export const forceNixString = (value: NixValue): NixString => {
if (isStringWithContext(forced)) { if (isStringWithContext(forced)) {
return 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 => { export const forceBool = (value: NixValue): boolean => {
const forced = force(value); const forced = force(value);
if (typeof forced !== "boolean") { if (typeof forced !== "boolean") {
throw new TypeError(`Expected boolean, got ${typeName(forced)}`); throw new TypeError(`Expected boolean, got ${typeOf(forced)}`);
} }
return forced; return forced;
}; };
@@ -135,7 +113,7 @@ export const forceInt = (value: NixValue): NixInt => {
if (typeof forced === "bigint") { if (typeof forced === "bigint") {
return forced; 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") { if (typeof forced === "number") {
return forced; 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") { if (typeof forced === "bigint" || typeof forced === "number") {
return forced; 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 either is float, convert both to float
if (!aIsInt || !bIsInt) { if (!aIsInt || !bIsInt) {
return [aIsInt ? Number(a) : a, bIsInt ? Number(b) : b]; return [Number(a), Number(b)];
} }
// Both are integers // Both are integers
@@ -189,5 +167,5 @@ export const forceNixPath = (value: NixValue): NixPath => {
if (isNixPath(forced)) { if (isNixPath(forced)) {
return 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 => { 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 // Nix primitive types
@@ -28,6 +28,7 @@ export type NixNull = null;
// Nix composite types // Nix composite types
export type NixList = NixValue[]; export type NixList = NixValue[];
// FIXME: reject contextful string
export type NixAttrs = { [key: string]: NixValue }; export type NixAttrs = { [key: string]: NixValue };
export type NixFunction = (arg: NixValue) => NixValue; export type NixFunction = (arg: NixValue) => NixValue;

View File

@@ -260,3 +260,19 @@ fn builtins_compare_versions_complex() {
Value::Int(-1) 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),
);
}

8
typos.toml Normal file
View File

@@ -0,0 +1,8 @@
[files]
extend-exclude = [
"nix-js/tests/regex.rs"
]
[default.extend-words]
contextful = "contextful"
contextfull = "contextful"