Files
nix-js/nix-js/runtime-ts/src/builtins/misc.ts
2026-02-20 16:12:54 +08:00

345 lines
8.8 KiB
TypeScript

import { OrderedSet } from "js-sdsl";
import { select } from "../helpers";
import { compareValues } from "../operators";
import {
getStringContext,
getStringValue,
mkStringWithContext,
type NixStringContext,
} from "../string-context";
import { force } from "../thunk";
import {
forceAttrs,
forceFunction,
forceList,
forceString,
forceStringNoCtx,
forceStringValue,
} from "../type-assert";
import type { NixAttrs, NixStrictValue, NixValue } from "../types";
import { ATTR_POSITIONS, CatchableError } from "../types";
import * as context from "./context";
import { isBool, isFloat, isInt, isList, isString, typeOf } from "./type-check";
export const addErrorContext =
(_e1: NixValue) =>
(e2: NixValue): NixValue => {
// FIXME:
// console.log("[WARNING]: addErrorContext not implemented");
return e2;
};
export const appendContext = context.appendContext;
export const getContext = context.getContext;
export const hasContext = context.hasContext;
export const unsafeDiscardOutputDependency = context.unsafeDiscardOutputDependency;
export const unsafeDiscardStringContext = context.unsafeDiscardStringContext;
export const addDrvOutputDependencies = context.addDrvOutputDependencies;
export const compareVersions =
(s1: NixValue) =>
(s2: NixValue): NixValue => {
const str1 = forceStringValue(s1);
const str2 = forceStringValue(s2);
let i1 = 0;
let i2 = 0;
while (i1 < str1.length || i2 < str2.length) {
const c1 = nextComponent(str1, i1);
const c2 = nextComponent(str2, i2);
i1 = c1.nextIndex;
i2 = c2.nextIndex;
if (componentsLt(c1.component, c2.component)) {
return -1n;
} else if (componentsLt(c2.component, c1.component)) {
return 1n;
}
}
return 0n;
};
interface ComponentResult {
component: string;
nextIndex: number;
}
function nextComponent(s: string, startIdx: number): ComponentResult {
let p = startIdx;
// Skip any dots and dashes (component separators)
while (p < s.length && (s[p] === "." || s[p] === "-")) {
p++;
}
if (p >= s.length) {
return { component: "", nextIndex: p };
}
const start = p;
// If the first character is a digit, consume the longest sequence of digits
if (s[p] >= "0" && s[p] <= "9") {
while (p < s.length && s[p] >= "0" && s[p] <= "9") {
p++;
}
} else {
// Otherwise, consume the longest sequence of non-digit, non-separator characters
while (p < s.length && !(s[p] >= "0" && s[p] <= "9") && s[p] !== "." && s[p] !== "-") {
p++;
}
}
return { component: s.substring(start, p), nextIndex: p };
}
function componentsLt(c1: string, c2: string): boolean {
const n1 = c1.match(/^[0-9]+$/) ? BigInt(c1) : null;
const n2 = c2.match(/^[0-9]+$/) ? BigInt(c2) : null;
// Both are numbers: compare numerically
if (n1 !== null && n2 !== null) {
return n1 < n2;
}
// Empty string < number
if (c1 === "" && n2 !== null) {
return true;
}
// Special case: "pre" comes before everything except another "pre"
if (c1 === "pre" && c2 !== "pre") {
return true;
}
if (c2 === "pre") {
return false;
}
// Assume that `2.3a' < `2.3.1'
if (n2 !== null) {
return true;
}
if (n1 !== null) {
return false;
}
// Both are strings: compare lexicographically
return c1 < c2;
}
export const functionArgs = (f: NixValue): NixAttrs => {
const func = forceFunction(f);
if (func.args) {
const ret: NixAttrs = new Map();
for (const key of func.args.required) {
ret.set(key, false);
}
for (const key of func.args.optional) {
ret.set(key, true);
}
const positions = func.args.positions;
if (positions) {
ret[ATTR_POSITIONS] = positions;
}
return ret;
}
return new Map();
};
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 = select(forcedArgs, ["startSet"]);
const operator = select(forcedArgs, ["operator"]);
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(select(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(select(newItemAttrs, ["key"]));
checkComparable(key);
if (resultSet.find(key).equals(resultSet.end())) {
resultSet.insert(key);
resultList.push(newItemAttrs);
queue.push(newItemAttrs);
}
}
}
return resultList;
};
export const outputOf =
(_drv: NixValue) =>
(_out: NixValue): never => {
throw new Error("Not implemented: outputOf (part of dynamic-derivation)");
};
export const parseDrvName = (s: NixValue): NixAttrs => {
const fullName = forceStringNoCtx(s);
let name = fullName;
let version = "";
for (let i = 0; i < fullName.length; ++i) {
if (fullName[i] === "-" && i + 1 < fullName.length && !/[a-zA-Z]/.test(fullName[i + 1])) {
name = fullName.substring(0, i);
version = fullName.substring(i + 1);
break;
}
}
return new Map<string, NixValue>([
["name", name],
["version", version],
]);
};
export const placeholder = (output: NixValue): NixValue => {
const outputStr = forceStringNoCtx(output);
return Deno.core.ops.op_make_placeholder(outputStr);
};
export const replaceStrings =
(from: NixValue) =>
(to: NixValue) =>
(s: NixValue): NixValue => {
const fromList = forceList(from);
const toList = forceList(to);
const inputStr = forceString(s);
const inputStrValue = getStringValue(inputStr);
const resultContext: NixStringContext = getStringContext(inputStr);
if (fromList.length !== toList.length) {
throw new Error("'from' and 'to' arguments passed to builtins.replaceStrings have different lengths");
}
const toCache = new Map<number, string>();
const toContextCache = new Map<number, NixStringContext>();
let result = "";
let pos = 0;
while (pos <= inputStrValue.length) {
let found = false;
for (let i = 0; i < fromList.length; i++) {
const pattern = forceStringValue(fromList[i]);
if (inputStrValue.substring(pos).startsWith(pattern)) {
found = true;
if (!toCache.has(i)) {
const replacementStr = forceString(toList[i]);
const replacementValue = getStringValue(replacementStr);
const replacementContext = getStringContext(replacementStr);
toCache.set(i, replacementValue);
toContextCache.set(i, replacementContext);
for (const elem of replacementContext) {
resultContext.add(elem);
}
}
const replacement = toCache.get(i) as string;
result += replacement;
if (pattern.length === 0) {
if (pos < inputStrValue.length) {
result += inputStrValue[pos];
}
pos++;
} else {
pos += pattern.length;
}
break;
}
}
if (!found) {
if (pos < inputStrValue.length) {
result += inputStrValue[pos];
}
pos++;
}
}
if (resultContext.size === 0) {
return result;
}
return mkStringWithContext(result, resultContext);
};
export const splitVersion = (s: NixValue): NixValue => {
const version = forceStringValue(s);
const components: string[] = [];
let idx = 0;
while (idx < version.length) {
const result = nextComponent(version, idx);
if (result.component === "") {
break;
}
components.push(result.component);
idx = result.nextIndex;
}
return components;
};
export const traceVerbose =
(_e1: NixValue) =>
(e2: NixValue): NixStrictValue => {
// TODO: implement traceVerbose
return force(e2);
};
export const tryEval = (e: NixValue): NixAttrs => {
try {
return new Map<string, NixValue>([
["success", true],
["value", force(e)],
]);
} catch (err) {
if (err instanceof CatchableError) {
return new Map<string, NixValue>([
["success", false],
["value", false],
]);
} else {
throw err;
}
}
};