feat: builtins.genericClosure; refactor type check
This commit is contained in:
13
nix-js/runtime-ts/package-lock.json
generated
13
nix-js/runtime-ts/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -10,5 +10,8 @@
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.24.2",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"js-sdsl": "^4.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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')`);
|
||||
|
||||
@@ -3,7 +3,22 @@
|
||||
* Combines all builtin function categories into the global `builtins` object
|
||||
*/
|
||||
|
||||
import { createThunk } from "../thunk";
|
||||
// Import all builtin categories
|
||||
import * as arithmetic from "./arithmetic";
|
||||
import * as math from "./math";
|
||||
import * as typeCheck from "./type-check";
|
||||
import * as list from "./list";
|
||||
import * as attrs from "./attrs";
|
||||
import * as string from "./string";
|
||||
import * as pathOps from "./path";
|
||||
import * as functional from "./functional";
|
||||
import * as io from "./io";
|
||||
import * as conversion from "./conversion";
|
||||
import * as misc from "./misc";
|
||||
import * as derivation from "./derivation";
|
||||
|
||||
import type { NixValue } from "../types";
|
||||
import { createThunk, force } from "../thunk";
|
||||
|
||||
/**
|
||||
* Symbol used to mark functions as primops (primitive operations)
|
||||
@@ -33,23 +48,23 @@ export interface PrimopMetadata {
|
||||
* @param applied - Number of arguments already applied (default: 0)
|
||||
* @returns The marked function
|
||||
*/
|
||||
export const mkPrimop = <T extends Function>(
|
||||
func: T,
|
||||
export const mkPrimop = (
|
||||
func: (...args: NixValue[]) => NixValue,
|
||||
name: string,
|
||||
arity: number,
|
||||
applied: number = 0,
|
||||
): T => {
|
||||
): Function => {
|
||||
// Mark this function as a primop
|
||||
(func as any)[PRIMOP_METADATA] = {
|
||||
name,
|
||||
arity,
|
||||
applied,
|
||||
} as PrimopMetadata;
|
||||
} satisfies PrimopMetadata;
|
||||
|
||||
// If this is a curried function and not fully applied,
|
||||
// wrap it to mark the next layer too
|
||||
if (applied < arity - 1) {
|
||||
const wrappedFunc = ((...args: any[]) => {
|
||||
const wrappedFunc = ((...args: NixValue[]) => {
|
||||
const result = func(...args);
|
||||
// If result is a function, mark it as the next layer
|
||||
if (typeof result === "function") {
|
||||
@@ -63,9 +78,9 @@ export const mkPrimop = <T extends Function>(
|
||||
name,
|
||||
arity,
|
||||
applied,
|
||||
} as PrimopMetadata;
|
||||
} satisfies PrimopMetadata;
|
||||
|
||||
return wrappedFunc as T;
|
||||
return wrappedFunc;
|
||||
}
|
||||
|
||||
return func;
|
||||
@@ -97,20 +112,6 @@ export const get_primop_metadata = (func: unknown): PrimopMetadata | undefined =
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Import all builtin categories
|
||||
import * as arithmetic from "./arithmetic";
|
||||
import * as math from "./math";
|
||||
import * as typeCheck from "./type-check";
|
||||
import * as list from "./list";
|
||||
import * as attrs from "./attrs";
|
||||
import * as string from "./string";
|
||||
import * as pathOps from "./path";
|
||||
import * as functional from "./functional";
|
||||
import * as io from "./io";
|
||||
import * as conversion from "./conversion";
|
||||
import * as misc from "./misc";
|
||||
import * as derivation from "./derivation";
|
||||
|
||||
/**
|
||||
* The global builtins object
|
||||
* Contains 80+ Nix builtin functions plus metadata
|
||||
@@ -134,16 +135,16 @@ export const builtins: any = {
|
||||
ceil: mkPrimop(math.ceil, "ceil", 1),
|
||||
floor: mkPrimop(math.floor, "floor", 1),
|
||||
|
||||
isAttrs: mkPrimop(typeCheck.isAttrs, "isAttrs", 1),
|
||||
isBool: mkPrimop(typeCheck.isBool, "isBool", 1),
|
||||
isFloat: mkPrimop(typeCheck.isFloat, "isFloat", 1),
|
||||
isFunction: mkPrimop(typeCheck.isFunction, "isFunction", 1),
|
||||
isInt: mkPrimop(typeCheck.isInt, "isInt", 1),
|
||||
isList: mkPrimop(typeCheck.isList, "isList", 1),
|
||||
isNull: mkPrimop(typeCheck.isNull, "isNull", 1),
|
||||
isPath: mkPrimop(typeCheck.isPath, "isPath", 1),
|
||||
isString: mkPrimop(typeCheck.isString, "isString", 1),
|
||||
typeOf: mkPrimop(typeCheck.typeOf, "typeOf", 1),
|
||||
isAttrs: mkPrimop((e: NixValue) => typeCheck.isAttrs(force(e)), "isAttrs", 1),
|
||||
isBool: mkPrimop((e: NixValue) => typeCheck.isBool(force(e)), "isBool", 1),
|
||||
isFloat: mkPrimop((e: NixValue) => typeCheck.isFloat(force(e)), "isFloat", 1),
|
||||
isFunction: mkPrimop((e: NixValue) => typeCheck.isFunction(force(e)), "isFunction", 1),
|
||||
isInt: mkPrimop((e: NixValue) => typeCheck.isInt(force(e)), "isInt", 1),
|
||||
isList: mkPrimop((e: NixValue) => typeCheck.isList(force(e)), "isList", 1),
|
||||
isNull: mkPrimop((e: NixValue) => typeCheck.isNull(force(e)), "isNull", 1),
|
||||
isPath: mkPrimop((e: NixValue) => typeCheck.isPath(force(e)), "isPath", 1),
|
||||
isString: mkPrimop((e: NixValue) => typeCheck.isString(force(e)), "isString", 1),
|
||||
typeOf: mkPrimop((e: NixValue) => typeCheck.typeOf(force(e)), "typeOf", 1),
|
||||
|
||||
map: mkPrimop(list.map, "map", 2),
|
||||
filter: mkPrimop(list.filter, "filter", 2),
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
import { force } from "../thunk";
|
||||
import { CatchableError } from "../types";
|
||||
import type { NixBool, NixStrictValue, NixValue } from "../types";
|
||||
import { forceList, forceAttrs, forceFunction, forceString } from "../type-assert";
|
||||
import { forceList, forceStringValue, forceAttrs, forceFunction } from "../type-assert";
|
||||
import * as context from "./context";
|
||||
import { compareValues, op } from "../operators";
|
||||
import { isBool, isFloat, isInt, isList, isString, typeOf } from "./type-check";
|
||||
import { OrderedSet } from "js-sdsl";
|
||||
|
||||
export const addErrorContext =
|
||||
(e1: NixValue) =>
|
||||
@@ -49,8 +52,8 @@ export const addDrvOutputDependencies = context.addDrvOutputDependencies;
|
||||
export const compareVersions =
|
||||
(s1: NixValue) =>
|
||||
(s2: NixValue): NixValue => {
|
||||
const str1 = forceString(s1);
|
||||
const str2 = forceString(s2);
|
||||
const str1 = forceStringValue(s1);
|
||||
const str2 = forceStringValue(s2);
|
||||
|
||||
let i1 = 0;
|
||||
let i2 = 0;
|
||||
@@ -152,8 +155,53 @@ export const functionArgs = (f: NixValue): never => {
|
||||
throw new Error("Not implemented: functionArgs");
|
||||
};
|
||||
|
||||
export const genericClosure = (args: NixValue): never => {
|
||||
throw new Error("Not implemented: genericClosure");
|
||||
const checkComparable = (value: NixStrictValue): void => {
|
||||
if (isString(value) || isInt(value) || isFloat(value) || isBool(value) || isList(value)) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`Unsupported key type for genericClosure: ${typeOf(value)}`);
|
||||
};
|
||||
|
||||
export const genericClosure = (args: NixValue): NixValue => {
|
||||
const forcedArgs = forceAttrs(args);
|
||||
const { startSet, operator } = forcedArgs;
|
||||
|
||||
const initialList = forceList(startSet);
|
||||
const opFunction = forceFunction(operator);
|
||||
|
||||
const resultSet = new OrderedSet<NixStrictValue>(undefined, compareValues);
|
||||
const resultList: NixStrictValue[] = [];
|
||||
const queue: NixStrictValue[] = [];
|
||||
|
||||
for (const item of initialList) {
|
||||
const itemAttrs = forceAttrs(item);
|
||||
const key = force(itemAttrs.key);
|
||||
checkComparable(key);
|
||||
if (resultSet.find(key).equals(resultSet.end())) {
|
||||
resultSet.insert(key);
|
||||
resultList.push(itemAttrs);
|
||||
queue.push(itemAttrs);
|
||||
}
|
||||
}
|
||||
|
||||
let head = 0;
|
||||
while (head < queue.length) {
|
||||
const currentItem = queue[head++];
|
||||
const newItems = forceList(opFunction(currentItem));
|
||||
|
||||
for (const newItem of newItems) {
|
||||
const newItemAttrs = forceAttrs(newItem);
|
||||
const key = force(newItemAttrs.key);
|
||||
checkComparable(key);
|
||||
if (resultSet.find(key).equals(resultSet.end())) {
|
||||
resultSet.insert(key);
|
||||
resultList.push(newItemAttrs);
|
||||
queue.push(newItemAttrs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultList;
|
||||
};
|
||||
|
||||
export const getFlake = (attrs: NixValue): never => {
|
||||
@@ -188,7 +236,7 @@ export const replaceStrings =
|
||||
(s: NixValue): NixValue => {
|
||||
const fromList = forceList(from);
|
||||
const toList = forceList(to);
|
||||
const inputStr = forceString(s);
|
||||
const inputStr = forceStringValue(s);
|
||||
|
||||
if (fromList.length !== toList.length) {
|
||||
throw new Error("'from' and 'to' arguments passed to builtins.replaceStrings have different lengths");
|
||||
@@ -203,13 +251,13 @@ export const replaceStrings =
|
||||
let found = false;
|
||||
|
||||
for (let i = 0; i < fromList.length; i++) {
|
||||
const pattern = forceString(fromList[i]);
|
||||
const pattern = forceStringValue(fromList[i]);
|
||||
|
||||
if (inputStr.substring(pos).startsWith(pattern)) {
|
||||
found = true;
|
||||
|
||||
if (!toCache.has(i)) {
|
||||
toCache.set(i, forceString(toList[i]));
|
||||
toCache.set(i, forceStringValue(toList[i]));
|
||||
}
|
||||
const replacement = toCache.get(i)!;
|
||||
|
||||
@@ -239,7 +287,7 @@ export const replaceStrings =
|
||||
};
|
||||
|
||||
export const splitVersion = (s: NixValue): NixValue => {
|
||||
const version = forceString(s);
|
||||
const version = forceStringValue(s);
|
||||
const components: string[] = [];
|
||||
let idx = 0;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
};
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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)}`);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
8
typos.toml
Normal file
8
typos.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[files]
|
||||
extend-exclude = [
|
||||
"nix-js/tests/regex.rs"
|
||||
]
|
||||
|
||||
[default.extend-words]
|
||||
contextful = "contextful"
|
||||
contextfull = "contextful"
|
||||
Reference in New Issue
Block a user