chore(runtime-ts): fix linter errors

This commit is contained in:
2026-02-15 10:46:22 +08:00
parent cf4dd6c379
commit 2f2c690023
29 changed files with 527 additions and 917 deletions

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.9/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
@@ -20,10 +20,37 @@
"linter": {
"rules": {
"style": {
"useNamingConvention": "warn"
"useNamingConvention": {
"level": "warn",
"options": {
"strictCase": false,
"conventions": [
{
"selector": { "kind": "objectLiteralProperty" },
"formats": ["camelCase", "PascalCase", "CONSTANT_CASE"]
},
{
"selector": { "kind": "typeProperty" },
"formats": ["camelCase", "snake_case"]
}
]
}
}
}
}
},
"overrides": [
{
"includes": ["**/global.d.ts"],
"linter": {
"rules": {
"style": {
"useNamingConvention": "off"
}
}
}
}
],
"javascript": {
"formatter": {
"arrowParentheses": "always",

View File

@@ -1,30 +1,26 @@
/**
* Arithmetic builtin functions
*/
import type { NixBool, NixInt, NixNumber, NixValue } from "../types";
import { forceNumeric, coerceNumeric, forceInt } from "../type-assert";
import { op } from "../operators";
import { coerceNumeric, forceInt, forceNumeric } from "../type-assert";
import type { NixBool, NixInt, NixNumber, NixValue } from "../types";
export const add =
(a: NixValue) =>
(b: NixValue): bigint | number => {
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as any) + (bv as any);
return (av as never) + (bv as never);
};
export const sub =
(a: NixValue) =>
(b: NixValue): bigint | number => {
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as any) - (bv as any);
return (av as never) - (bv as never);
};
export const mul =
(a: NixValue) =>
(b: NixValue): bigint | number => {
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as any) * (bv as any);
return (av as never) * (bv as never);
};
export const div =
@@ -36,10 +32,9 @@ export const div =
throw new RangeError("Division by zero");
}
return (av as any) / (bv as any);
return (av as never) / (bv as never);
};
// Bitwise operations - only for integers
export const bitAnd =
(a: NixValue) =>
(b: NixValue): NixInt => {

View File

@@ -1,10 +1,6 @@
/**
* Attribute set operation builtin functions
*/
import type { NixValue, NixAttrs, NixList } from "../types";
import { forceAttrs, forceStringValue, forceFunction, forceList } from "../type-assert";
import { createThunk } from "../thunk";
import { forceAttrs, forceFunction, forceList, forceStringValue } from "../type-assert";
import type { NixAttrs, NixList, NixValue } from "../types";
export const attrNames = (set: NixValue): string[] => Object.keys(forceAttrs(set)).sort();
@@ -46,23 +42,23 @@ export const mapAttrs =
export const removeAttrs =
(attrs: NixValue) =>
(list: NixValue): NixAttrs => {
const new_attrs: NixAttrs = {};
const forced_attrs = forceAttrs(attrs);
const forced_list = forceList(list);
const keys_to_remove = new Set(forced_list.map(forceStringValue));
const newAttrs: NixAttrs = {};
const forcedAttrs = forceAttrs(attrs);
const forcedList = forceList(list);
const keysToRemove = new Set(forcedList.map(forceStringValue));
for (const key in forced_attrs) {
if (!keys_to_remove.has(key)) {
new_attrs[key] = forced_attrs[key];
for (const key in forcedAttrs) {
if (!keysToRemove.has(key)) {
newAttrs[key] = forcedAttrs[key];
}
}
return new_attrs;
return newAttrs;
};
export const listToAttrs = (e: NixValue): NixAttrs => {
const attrs: NixAttrs = {};
const forced_e = [...forceList(e)].reverse();
for (const obj of forced_e) {
const forcedE = [...forceList(e)].reverse();
for (const obj of forcedE) {
const item = forceAttrs(obj);
attrs[forceStringValue(item.name)] = item.value;
}
@@ -96,10 +92,10 @@ export const groupBy =
(f: NixValue) =>
(list: NixValue): NixAttrs => {
const attrs: NixAttrs = {};
const forced_f = forceFunction(f);
const forced_list = forceList(list);
for (const elem of forced_list) {
const key = forceStringValue(forced_f(elem));
const forcedF = forceFunction(f);
const forcedList = forceList(list);
for (const elem of forcedList) {
const key = forceStringValue(forcedF(elem));
if (!attrs[key]) attrs[key] = [];
(attrs[key] as NixList).push(elem);
}
@@ -111,28 +107,22 @@ export const zipAttrsWith =
(list: NixValue): NixValue => {
const listForced = forceList(list);
// Map to collect all values for each attribute name
const attrMap = new Map<string, NixValue[]>();
// Iterate through each attribute set in the list
for (const item of listForced) {
const attrs = forceAttrs(item);
// Collect all attribute names and their values
for (const [key, value] of Object.entries(attrs)) {
if (!attrMap.has(key)) {
attrMap.set(key, []);
}
attrMap.get(key)!.push(value);
(attrMap.get(key) as NixValue[]).push(value);
}
}
// Build the result attribute set
const result: Record<string, NixValue> = {};
for (const [name, values] of attrMap.entries()) {
// Apply f to name and values list
// f is curried: f name values
result[name] = createThunk(() => forceFunction(forceFunction(f)(name))(values));
}
@@ -149,7 +139,9 @@ export const unsafeGetAttrPos =
return null;
}
const positions = (attrs as any)[Nix.ATTR_POSITIONS];
const positions = (attrs as NixAttrs & Record<symbol, unknown>)[Nix.ATTR_POSITIONS] as
| Record<string, string>
| undefined;
if (!positions || !(name in positions)) {
return null;
}

View File

@@ -1,15 +1,15 @@
import type { NixValue, NixAttrs, NixString } from "../types";
import { isStringWithContext } from "../types";
import { forceString, forceAttrs, forceList, forceStringValue } from "../type-assert";
import { force } from "../thunk";
import {
type NixStringContext,
getStringValue,
getStringContext,
mkStringWithContext,
decodeContextElem,
getStringContext,
getStringValue,
mkStringWithContext,
type NixStringContext,
parseContextToInfoMap,
} from "../string-context";
import { force } from "../thunk";
import { forceAttrs, forceList, forceString, forceStringValue } from "../type-assert";
import type { NixAttrs, NixString, NixValue } from "../types";
import { isStringWithContext } from "../types";
/**
* builtins.hasContext - Check if string has context
@@ -118,13 +118,13 @@ export const getContext = (value: NixValue): NixAttrs => {
for (const [path, info] of infoMap) {
const attrs: NixAttrs = {};
if (info.path) {
attrs["path"] = true;
attrs.path = true;
}
if (info.allOutputs) {
attrs["allOutputs"] = true;
attrs.allOutputs = true;
}
if (info.outputs.length > 0) {
attrs["outputs"] = info.outputs;
attrs.outputs = info.outputs;
}
result[path] = attrs;
}
@@ -162,14 +162,14 @@ export const appendContext =
const info = forceAttrs(infoVal);
if ("path" in info) {
const pathVal = force(info["path"]);
const pathVal = force(info.path);
if (pathVal === true) {
newContext.add(path);
}
}
if ("allOutputs" in info) {
const allOutputs = force(info["allOutputs"]);
const allOutputs = force(info.allOutputs);
if (allOutputs === true) {
if (!path.endsWith(".drv")) {
throw new Error(
@@ -181,7 +181,7 @@ export const appendContext =
}
if ("outputs" in info) {
const outputs = forceList(info["outputs"]);
const outputs = forceList(info.outputs);
if (outputs.length > 0 && !path.endsWith(".drv")) {
throw new Error(
`tried to add derivation output context of ${path}, which is not a derivation, to a string`,

View File

@@ -2,12 +2,11 @@
* Conversion and serialization builtin functions
*/
import type { NixString, NixValue } from "../types";
import { isStringWithContext, isNixPath } from "../types";
import { force } from "../thunk";
import { type NixStringContext, mkStringWithContext, addBuiltContext } from "../string-context";
import { addBuiltContext, mkStringWithContext, type NixStringContext } from "../string-context";
import { force, isThunk } from "../thunk";
import { forceFunction, forceStringNoCtx } from "../type-assert";
import { nixValueToJson } from "../conversion";
import type { NixString, NixValue } from "../types";
import { HAS_CONTEXT, IS_PATH, isNixPath, isStringWithContext } from "../types";
import { isAttrs, isPath, typeOf } from "./type-check";
export const fromJSON = (e: NixValue): NixValue => {
@@ -16,12 +15,12 @@ export const fromJSON = (e: NixValue): NixValue => {
throw new TypeError(`builtins.fromJSON: expected a string, got ${typeOf(str)}`);
}
const jsonStr = isStringWithContext(str) ? str.value : str;
return Deno.core.ops.op_from_json(jsonStr);
return Deno.core.ops.op_from_json(jsonStr) as NixValue;
};
export const fromTOML = (e: NixValue): NixValue => {
const toml = forceStringNoCtx(e);
return Deno.core.ops.op_from_toml(toml);
return Deno.core.ops.op_from_toml(toml) as NixValue;
};
export const toJSON = (e: NixValue): NixString => {
@@ -33,7 +32,7 @@ export const toJSON = (e: NixValue): NixString => {
return mkStringWithContext(string, context);
};
export const toXML = (e: NixValue): never => {
export const toXML = (_e: NixValue): never => {
throw new Error("Not implemented: toXML");
};
@@ -290,3 +289,82 @@ export const coerceToPath = (value: NixValue, outContext?: NixStringContext): st
export const toStringFunc = (value: NixValue): NixString => {
return coerceToStringWithContext(value, StringCoercionMode.ToString, false);
};
export const nixValueToJson = (
value: NixValue,
strict: boolean,
outContext: NixStringContext,
copyToStore: boolean,
seen: Set<NixValue> = new Set(),
): unknown => {
const v = strict ? force(value) : value;
if (isThunk(v) || typeof v === "function")
throw new Error(`cannot convert ${isThunk(v) ? "thunk" : "lambda"} to JSON`);
if (v === null) return null;
if (typeof v === "bigint") {
const num = Number(v);
if (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER) {
console.warn(`integer ${v} exceeds safe range, precision may be lost`);
}
return num;
}
if (typeof v === "number") return v;
if (typeof v === "boolean") return v;
if (typeof v === "string") return v;
if (typeof v === "object" && HAS_CONTEXT in v) {
for (const elem of v.context) {
outContext.add(elem);
}
return v.value;
}
if (typeof v === "object" && IS_PATH in v) {
if (copyToStore) {
const storePath = Deno.core.ops.op_copy_path_to_store(v.value);
outContext.add(storePath);
return storePath;
} else {
return v.value;
}
}
// FIXME: is this check necessary?
// if (seen.has(v)) {
// throw new Error("cycle detected in toJSON");
// } else {
// seen.add(v)
// }
if (Array.isArray(v)) {
return v.map((item) => nixValueToJson(item, strict, outContext, copyToStore, seen));
}
// NixAttrs
if ("__toString" in v && typeof force(v.__toString) === "function") {
const toStringMethod = force(v.__toString) as (self: typeof v) => NixValue;
const result = force(toStringMethod(v));
if (typeof result === "string") {
return result;
}
if (isStringWithContext(result)) {
if (outContext) {
for (const elem of result.context) {
outContext.add(elem);
}
}
return result.value;
}
return nixValueToJson(result, strict, outContext, copyToStore, seen);
}
if ("outPath" in v) {
return nixValueToJson(v.outPath, strict, outContext, copyToStore, seen);
}
const result: Record<string, unknown> = {};
const keys = Object.keys(v).sort();
for (const key of keys) {
result[key] = nixValueToJson(v[key], strict, outContext, copyToStore, seen);
}
return result;
};

View File

@@ -1,40 +1,123 @@
import type { NixValue, NixAttrs } from "../types";
import { forceStringValue, forceList, forceStringNoCtx } from "../type-assert";
import { force } from "../thunk";
import {
type DerivationData,
type OutputInfo,
generateAterm,
generateAtermModulo,
} from "../derivation-helpers";
import { coerceToString, StringCoercionMode } from "./conversion";
import {
type NixStringContext,
extractInputDrvsAndSrcs,
isStringWithContext,
mkStringWithContext,
addDrvDeepContext,
addBuiltContext,
addDrvDeepContext,
extractInputDrvsAndSrcs,
mkStringWithContext,
type NixStringContext,
} from "../string-context";
import { nixValueToJson } from "../conversion";
import { isNixPath } from "../types";
import { force } from "../thunk";
import { forceAttrs, forceList, forceStringNoCtx, forceStringValue } from "../type-assert";
import type { NixAttrs, NixValue } from "../types";
import { coerceToString, nixValueToJson, StringCoercionMode } from "./conversion";
const drvHashCache = new Map<string, string>();
const forceAttrs = (value: NixValue): NixAttrs => {
const forced = force(value);
if (
typeof forced !== "object" ||
forced === null ||
Array.isArray(forced) ||
isStringWithContext(forced) ||
isNixPath(forced)
) {
throw new TypeError(`Expected attribute set for derivation, got ${typeof forced}`);
export interface OutputInfo {
path: string;
hashAlgo: string;
hash: string;
}
export interface DerivationData {
name: string;
outputs: Map<string, OutputInfo>;
inputDrvs: Map<string, Set<string>>;
inputSrcs: Set<string>;
platform: string;
builder: string;
args: string[];
env: Map<string, string>;
}
export const escapeString = (s: string): string => {
let result = "";
for (const char of s) {
switch (char) {
case '"':
result += '\\"';
break;
case "\\":
result += "\\\\";
break;
case "\n":
result += "\\n";
break;
case "\r":
result += "\\r";
break;
case "\t":
result += "\\t";
break;
default:
result += char;
}
return forced;
}
return `"${result}"`;
};
const quoteString = (s: string): string => `"${s}"`;
const cmpByKey = <T>(a: [string, T], b: [string, T]): number => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
export const generateAterm = (drv: DerivationData): string => {
const outputEntries: string[] = [];
const sortedOutputs = Array.from(drv.outputs.entries()).sort(cmpByKey);
for (const [name, info] of sortedOutputs) {
outputEntries.push(
`(${quoteString(name)},${quoteString(info.path)},${quoteString(info.hashAlgo)},${quoteString(info.hash)})`,
);
}
const outputs = outputEntries.join(",");
const inputDrvEntries: string[] = [];
const sortedInputDrvs = Array.from(drv.inputDrvs.entries()).sort(cmpByKey);
for (const [drvPath, outputs] of sortedInputDrvs) {
const sortedOuts = Array.from(outputs).sort();
const outList = `[${sortedOuts.map(quoteString).join(",")}]`;
inputDrvEntries.push(`(${quoteString(drvPath)},${outList})`);
}
const inputDrvs = inputDrvEntries.join(",");
const sortedInputSrcs = Array.from(drv.inputSrcs).sort();
const inputSrcs = sortedInputSrcs.map(quoteString).join(",");
const args = drv.args.map(escapeString).join(",");
const envs = Array.from(drv.env.entries())
.sort(cmpByKey)
.map(([k, v]) => `(${escapeString(k)},${escapeString(v)})`);
return `Derive([${outputs}],[${inputDrvs}],[${inputSrcs}],${quoteString(drv.platform)},${escapeString(drv.builder)},[${args}],[${envs}])`;
};
export const generateAtermModulo = (drv: DerivationData, inputDrvHashes: Map<string, string>): string => {
const outputEntries: string[] = [];
const sortedOutputs = Array.from(drv.outputs.entries()).sort(cmpByKey);
for (const [name, info] of sortedOutputs) {
outputEntries.push(
`(${quoteString(name)},${quoteString(info.path)},${quoteString(info.hashAlgo)},${quoteString(info.hash)})`,
);
}
const outputs = outputEntries.join(",");
const inputDrvEntries: string[] = [];
const sortedInputDrvHashes = Array.from(inputDrvHashes.entries()).sort(cmpByKey);
for (const [drvHash, outputs] of sortedInputDrvHashes) {
const sortedOuts = outputs.split(",").sort();
const outList = `[${sortedOuts.map(quoteString).join(",")}]`;
inputDrvEntries.push(`(${quoteString(drvHash)},${outList})`);
}
const inputDrvs = inputDrvEntries.join(",");
const sortedInputSrcs = Array.from(drv.inputSrcs).sort();
const inputSrcs = sortedInputSrcs.map(quoteString).join(",");
const args = drv.args.map(escapeString).join(",");
const envs = Array.from(drv.env.entries())
.sort(cmpByKey)
.map(([k, v]) => `(${escapeString(k)},${escapeString(v)})`);
return `Derive([${outputs}],[${inputDrvs}],[${inputSrcs}],${quoteString(drv.platform)},${escapeString(drv.builder)},[${args}],[${envs}])`;
};
const validateName = (attrs: NixAttrs): string => {
if (!("name" in attrs)) {
throw new Error("derivation: missing required attribute 'name'");
@@ -109,10 +192,10 @@ const extractArgs = (attrs: NixAttrs, outContext: NixStringContext): string[] =>
const outputPathName = (drvName: string, output: string) => {
if (output === "out") {
return drvName
return drvName;
}
return `${drvName}-${output}`
}
return `${drvName}-${output}`;
};
const structuredAttrsExcludedKeys = new Set([
"__structuredAttrs",
@@ -124,9 +207,9 @@ const structuredAttrsExcludedKeys = new Set([
const specialAttrs = new Set(["args", "__ignoreNulls", "__contentAddressed", "__impure"]);
const sortedJsonStringify = (obj: Record<string, any>): string => {
const sortedJsonStringify = (obj: Record<string, unknown>): string => {
const sortedKeys = Object.keys(obj).sort();
const sortedObj: Record<string, any> = {};
const sortedObj: Record<string, unknown> = {};
for (const key of sortedKeys) {
sortedObj[key] = obj[key];
}
@@ -143,7 +226,7 @@ const extractEnv = (
const env = new Map<string, string>();
if (structuredAttrs) {
const jsonAttrs: Record<string, any> = {};
const jsonAttrs: Record<string, unknown> = {};
for (const [key, value] of Object.entries(attrs)) {
if (!structuredAttrsExcludedKeys.has(key)) {
const forcedValue = force(value);
@@ -419,6 +502,6 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
return result;
};
export const derivation = (_: NixValue): NixAttrs => {
throw new Error("unreachable: placeholder derivation implementation called")
export const derivationStub = (_: NixValue): NixAttrs => {
throw new Error("unreachable: stub derivation implementation called");
};

View File

@@ -0,0 +1,17 @@
import type { NixValue } from "../types";
export const getFlake = (_attrs: NixValue): never => {
throw new Error("Not implemented: getFlake");
};
export const parseFlakeName = (_s: NixValue): never => {
throw new Error("Not implemented: parseFlakeName");
};
export const parseFlakeRef = (_s: NixValue): never => {
throw new Error("Not implemented: parseFlakeRef");
};
export const flakeRefToString = (_attrs: NixValue): never => {
throw new Error("Not implemented: flakeRefToString");
};

View File

@@ -1,11 +1,7 @@
/**
* Functional programming builtin functions
*/
import { CatchableError, type NixValue } from "../types";
import { force } from "../thunk";
import { coerceToString, StringCoercionMode } from "./conversion";
import { printValue } from "../print";
import { force } from "../thunk";
import { CatchableError, type NixValue } from "../types";
import { coerceToString, StringCoercionMode } from "./conversion";
import { isAttrs } from "./type-check";
export const seq =

View File

@@ -0,0 +1,19 @@
import { forceStringNoCtx } from "../type-assert";
import type { NixValue } from "../types";
export const hashFile =
(type: NixValue) =>
(_p: NixValue): never => {
const _ty = forceStringNoCtx(type);
throw new Error("Not implemented: hashFile");
};
export const hashString =
(_type: NixValue) =>
(_p: NixValue): never => {
throw new Error("Not implemented: hashString");
};
export const convertHash = (_args: NixValue): never => {
throw new Error("Not implemented: convertHash");
};

View File

@@ -1,81 +1,50 @@
/**
* Main builtins export
* Combines all builtin function categories into the global `builtins` object
*/
// 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 { createThunk, force } from "../thunk";
import type { NixValue } from "../types";
import { createThunk, force, isThunk } from "../thunk";
import { getTos } from "../helpers";
import * as arithmetic from "./arithmetic";
import * as attrs from "./attrs";
import * as conversion from "./conversion";
import * as derivation from "./derivation";
import * as flake from "./flake";
import * as functional from "./functional";
import * as hash from "./hash";
import * as io from "./io";
import * as list from "./list";
import * as math from "./math";
import * as misc from "./misc";
import * as pathOps from "./path";
import * as string from "./string";
import * as typeCheck from "./type-check";
/**
* Symbol used to mark functions as primops (primitive operations)
* This is similar to IS_THUNK but for builtin functions
*/
export const PRIMOP_METADATA = Symbol("primop_metadata");
/**
* Metadata interface for primop functions
*/
export interface PrimopMetadata {
/** The name of the primop (e.g., "add", "map") */
name: string;
/** Total arity of the function (number of arguments it expects) */
arity: number;
/** Number of arguments already applied (for partial applications) */
applied: number;
}
/**
* Mark a function as a primop with metadata
* For curried functions, this recursively marks each layer
*
* @param func - The function to mark
* @param name - Name of the primop
* @param arity - Total number of arguments expected
* @param applied - Number of arguments already applied (default: 0)
* @returns The marked function
*/
export const mkPrimop = (
func: (...args: NixValue[]) => NixValue,
name: string,
arity: number,
applied: number = 0,
): Function => {
// Mark this function as a primop
(func as any)[PRIMOP_METADATA] = {
): ((...args: NixValue[]) => NixValue) => {
(func as unknown as Record<symbol, unknown>)[PRIMOP_METADATA] = {
name,
arity,
applied,
} 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: NixValue[]) => {
const result = func(...args);
// If result is a function, mark it as the next layer
if (typeof result === "function") {
return mkPrimop(result, name, arity, applied + args.length);
}
return result;
}) as any;
}) as (...args: NixValue[]) => NixValue;
// Copy the primop metadata to the wrapper
wrappedFunc[PRIMOP_METADATA] = {
(wrappedFunc as unknown as Record<symbol, unknown>)[PRIMOP_METADATA] = {
name,
arity,
applied,
@@ -87,12 +56,9 @@ export const mkPrimop = (
return func;
};
/**
* Type guard to check if a value is a primop
* @param value - Value to check
* @returns true if value is marked as a primop
*/
export const is_primop = (value: unknown): value is Function & { [PRIMOP_METADATA]: PrimopMetadata } => {
export const isPrimop = (
value: unknown,
): value is ((...args: never[]) => unknown) & { [PRIMOP_METADATA]: PrimopMetadata } => {
return (
typeof value === "function" &&
PRIMOP_METADATA in value &&
@@ -101,29 +67,14 @@ export const is_primop = (value: unknown): value is Function & { [PRIMOP_METADAT
);
};
/**
* Get primop metadata from a function
* @param func - Function to get metadata from
* @returns Metadata if function is a primop, undefined otherwise
*/
export const get_primop_metadata = (func: unknown): PrimopMetadata | undefined => {
if (is_primop(func)) {
export const getPrimopMetadata = (func: unknown): PrimopMetadata | undefined => {
if (isPrimop(func)) {
return func[PRIMOP_METADATA];
}
return undefined;
};
/**
* The global builtins object
* Contains 80+ Nix builtin functions plus metadata
*
* All functions are curried for Nix semantics:
* - Single argument functions: (a) => result
* - Multi-argument functions: (a) => (b) => result
*
* All primop functions are marked with PRIMOP_METADATA symbol for runtime introspection
*/
export const builtins: any = {
export const builtins: Record<string, NixValue> = {
add: mkPrimop(arithmetic.add, "add", 2),
sub: mkPrimop(arithmetic.sub, "sub", 2),
mul: mkPrimop(arithmetic.mul, "mul", 2),
@@ -193,7 +144,7 @@ export const builtins: any = {
warn: mkPrimop(functional.warn, "warn", 2),
break: mkPrimop(functional.breakFunc, "break", 1),
derivation: undefined as any,
derivation: mkPrimop(derivation.derivationStub, "derivation", 1),
derivationStrict: mkPrimop(derivation.derivationStrict, "derivationStrict", 1),
import: mkPrimop(io.importFunc, "import", 1),
@@ -221,13 +172,19 @@ export const builtins: any = {
toXML: mkPrimop(conversion.toXML, "toXML", 1),
toString: mkPrimop(conversion.toStringFunc, "toString", 1),
hashFile: mkPrimop(hash.hashFile, "hashFile", 2),
hashString: mkPrimop(hash.hashString, "hashString", 2),
convertHash: mkPrimop(hash.convertHash, "convertHash", 2),
flakeRefToString: mkPrimop(flake.flakeRefToString, "flakeRefToString", 1),
getFlake: mkPrimop(flake.getFlake, "getFlake", 1),
parseFlakeName: mkPrimop(flake.parseFlakeName, "parseFlakeName", 1),
parseFlakeRef: mkPrimop(flake.parseFlakeRef, "parseFlakeRef", 1),
addErrorContext: mkPrimop(misc.addErrorContext, "addErrorContext", 1),
appendContext: mkPrimop(misc.appendContext, "appendContext", 1),
getContext: mkPrimop(misc.getContext, "getContext", 1),
hasContext: mkPrimop(misc.hasContext, "hasContext", 1),
hashFile: mkPrimop(misc.hashFile, "hashFile", 2),
hashString: mkPrimop(misc.hashString, "hashString", 2),
convertHash: mkPrimop(misc.convertHash, "convertHash", 2),
unsafeDiscardOutputDependency: mkPrimop(
misc.unsafeDiscardOutputDependency,
"unsafeDiscardOutputDependency",
@@ -236,14 +193,10 @@ export const builtins: any = {
unsafeDiscardStringContext: mkPrimop(misc.unsafeDiscardStringContext, "unsafeDiscardStringContext", 1),
addDrvOutputDependencies: mkPrimop(misc.addDrvOutputDependencies, "addDrvOutputDependencies", 2),
compareVersions: mkPrimop(misc.compareVersions, "compareVersions", 2),
flakeRefToString: mkPrimop(misc.flakeRefToString, "flakeRefToString", 1),
functionArgs: mkPrimop(misc.functionArgs, "functionArgs", 1),
genericClosure: mkPrimop(misc.genericClosure, "genericClosure", 1),
getFlake: mkPrimop(misc.getFlake, "getFlake", 1),
outputOf: mkPrimop(misc.outputOf, "outputOf", 2),
parseDrvName: mkPrimop(misc.parseDrvName, "parseDrvName", 1),
parseFlakeName: mkPrimop(misc.parseFlakeName, "parseFlakeName", 1),
parseFlakeRef: mkPrimop(misc.parseFlakeRef, "parseFlakeRef", 1),
placeholder: mkPrimop(misc.placeholder, "placeholder", 1),
replaceStrings: mkPrimop(misc.replaceStrings, "replaceStrings", 3),
splitVersion: mkPrimop(misc.splitVersion, "splitVersion", 1),
@@ -263,10 +216,7 @@ export const builtins: any = {
langVersion: 6,
nixPath: [],
nixVersion: "2.31.2",
storeDir: "INVALID_PATH",
__traceCaller: (e: NixValue) => {
console.log(`traceCaller: ${getTos()}`);
return e;
},
storeDir: createThunk(() => {
throw new Error("stub storeDir evaluated");
}),
};

View File

@@ -1,8 +1,7 @@
/**
* I/O and filesystem builtin functions
* Implemented via Rust ops exposed through deno_core
*/
import { getPathValue } from "../path";
import type { NixStringContext, StringWithContext } from "../string-context";
import { addOpaqueContext, mkStringWithContext } from "../string-context";
import { force } from "../thunk";
import {
forceAttrs,
forceBool,
@@ -11,15 +10,11 @@ import {
forceStringNoCtx,
forceStringValue,
} from "../type-assert";
import type { NixValue, NixAttrs, NixPath, NixString } from "../types";
import { isNixPath, IS_PATH, CatchableError } from "../types";
import { force } from "../thunk";
import type { NixAttrs, NixPath, NixString, NixValue } from "../types";
import { CatchableError, IS_PATH, isNixPath } from "../types";
import { coerceToPath, coerceToString, StringCoercionMode } from "./conversion";
import { getPathValue } from "../path";
import type { NixStringContext, StringWithContext } from "../string-context";
import { mkStringWithContext, addOpaqueContext } from "../string-context";
import { isAttrs, isPath } from "./type-check";
import { baseNameOf } from "./path";
import { isAttrs, isPath } from "./type-check";
const importCache = new Map<string, NixValue>();
@@ -84,7 +79,7 @@ export const storePath = (pathArg: NixValue): StringWithContext => {
return mkStringWithContext(validatedPath, context);
};
export const fetchClosure = (args: NixValue): never => {
export const fetchClosure = (_args: NixValue): never => {
throw new Error("Not implemented: fetchClosure");
};
@@ -136,7 +131,7 @@ const normalizeTarballInput = (args: NixValue): { url: string; sha256?: string;
const sha256 = "sha256" in forced ? forceStringNoCtx(forced.sha256) : undefined;
const nameRaw = "name" in forced ? forceStringNoCtx(forced.name) : undefined;
// FIXME: extract baseNameOfRaw
const name = nameRaw === "" ? baseNameOf(nameRaw) as string : nameRaw;
const name = nameRaw === "" ? (baseNameOf(nameRaw) as string) : nameRaw;
return { url, sha256, name };
} else {
return { url: forceStringNoCtx(forced) };
@@ -145,11 +140,11 @@ const normalizeTarballInput = (args: NixValue): { url: string; sha256?: string;
const resolvePseudoUrl = (url: string) => {
if (url.startsWith("channel:")) {
return `https://channels.nixos.org/${url.substring(8)}/nixexprs.tar.xz`
return `https://channels.nixos.org/${url.substring(8)}/nixexprs.tar.xz`;
} else {
return url
return url;
}
}
};
export const fetchurl = (args: NixValue): NixString => {
const { url, hash, name, executable } = normalizeUrlInput(args);
@@ -224,7 +219,7 @@ export const fetchGit = (args: NixValue): NixAttrs => {
};
export const fetchMercurial = (_args: NixValue): NixAttrs => {
throw new Error("Not implemented: fetchMercurial")
throw new Error("Not implemented: fetchMercurial");
};
export const fetchTree = (args: NixValue): NixAttrs => {
@@ -249,7 +244,6 @@ export const fetchTree = (args: NixValue): NixAttrs => {
case "gitlab":
case "sourcehut":
return fetchGitForge(type, attrs);
case "auto":
default:
return autoDetectAndFetch(attrs);
}
@@ -375,7 +369,7 @@ export const path = (args: NixValue): NixString => {
const includePaths: string[] = [];
for (const [relPath, fileType] of entries) {
const fullPath = pathStr + "/" + relPath;
const fullPath = `${pathStr}/${relPath}`;
const innerFn = forceFunction(filterFn(fullPath));
const shouldInclude = force(innerFn(fileType));
if (shouldInclude === true) {
@@ -383,13 +377,7 @@ export const path = (args: NixValue): NixString => {
}
}
storePath = Deno.core.ops.op_add_filtered_path(
pathStr,
name,
recursive,
sha256,
includePaths,
);
storePath = Deno.core.ops.op_add_filtered_path(pathStr, name, recursive, sha256, includePaths);
} else {
storePath = Deno.core.ops.op_add_path(pathStr, name, recursive, sha256);
}
@@ -421,13 +409,11 @@ export const toFile =
return mkStringWithContext(storePath, new Set([storePath]));
};
export const toPath = (name: NixValue, s: NixValue): never => {
throw new Error("Not implemented: toPath");
};
export const filterSource = (args: NixValue): never => {
export const filterSource =
(_filter: NixValue) =>
(_path: NixValue): never => {
throw new Error("Not implemented: filterSource");
};
};
const suffixIfPotentialMatch = (prefix: string, path: string): string | null => {
const n = prefix.length;

View File

@@ -1,12 +1,7 @@
/**
* List operation builtin functions
* All functions are properly curried
*/
import type { NixValue, NixList, NixAttrs } from "../types";
import { force } from "../thunk";
import { forceList, forceFunction, forceInt, forceBool } from "../type-assert";
import { op } from "../operators";
import { force } from "../thunk";
import { forceBool, forceFunction, forceInt, forceList } from "../type-assert";
import type { NixAttrs, NixList, NixValue } from "../types";
export const map =
(f: NixValue) =>
@@ -74,23 +69,23 @@ export const concatMap =
};
export const foldlPrime =
(op_fn: NixValue) =>
(opFn: NixValue) =>
(nul: NixValue) =>
(list: NixValue): NixValue => {
const forced_op = forceFunction(op_fn);
const forcedOp = forceFunction(opFn);
return forceList(list).reduce((acc: NixValue, cur: NixValue) => {
return forceFunction(forced_op(acc))(cur);
return forceFunction(forcedOp(acc))(cur);
}, nul);
};
export const sort =
(cmp: NixValue) =>
(list: NixValue): NixList => {
const forced_list = [...forceList(list)];
const forced_cmp = forceFunction(cmp);
return forced_list.sort((a, b) => {
if (force(forceFunction(forced_cmp(a))(b))) return -1;
if (force(forceFunction(forced_cmp(b))(a))) return 1;
const forcedList = [...forceList(list)];
const forcedCmp = forceFunction(cmp);
return forcedList.sort((a, b) => {
if (force(forceFunction(forcedCmp(a))(b))) return -1;
if (force(forceFunction(forcedCmp(b))(a))) return 1;
return 0;
});
};
@@ -98,14 +93,14 @@ export const sort =
export const partition =
(pred: NixValue) =>
(list: NixValue): NixAttrs => {
const forced_list = forceList(list);
const forced_pred = forceFunction(pred);
const forcedList = forceList(list);
const forcedPred = forceFunction(pred);
const attrs = {
right: [] as NixList,
wrong: [] as NixList,
};
for (const elem of forced_list) {
if (force(forced_pred(elem))) {
for (const elem of forcedList) {
if (force(forcedPred(elem))) {
attrs.right.push(elem);
} else {
attrs.wrong.push(elem);

View File

@@ -1,9 +1,5 @@
/**
* Math builtin functions
*/
import type { NixValue } from "../types";
import { forceNumeric } from "../type-assert";
import type { NixValue } from "../types";
export const ceil = (x: NixValue): bigint => {
const val = forceNumeric(x);

View File

@@ -1,31 +1,27 @@
/**
* Miscellaneous builtin functions
*/
import { force } from "../thunk";
import { CatchableError, ATTR_POSITIONS } from "../types";
import type { NixAttrs, NixBool, NixStrictValue, NixValue } from "../types";
import { OrderedSet } from "js-sdsl";
import { compareValues } from "../operators";
import {
getStringContext,
getStringValue,
mkStringWithContext,
type NixStringContext,
} from "../string-context";
import { force } from "../thunk";
import {
forceList,
forceAttrs,
forceFunction,
forceStringValue,
forceList,
forceString,
forceStringNoCtx,
forceStringValue,
} from "../type-assert";
import type { NixAttrs, NixBool, NixStrictValue, NixValue } from "../types";
import { ATTR_POSITIONS, CatchableError } from "../types";
import * as context from "./context";
import { compareValues } from "../operators";
import { isBool, isFloat, isInt, isList, isString, typeOf } from "./type-check";
import { OrderedSet } from "js-sdsl";
import {
type NixStringContext,
getStringValue,
getStringContext,
mkStringWithContext,
} from "../string-context";
export const addErrorContext =
(e1: NixValue) =>
(_e1: NixValue) =>
(e2: NixValue): NixValue => {
// FIXME:
// console.log("[WARNING]: addErrorContext not implemented");
@@ -38,23 +34,6 @@ export const getContext = context.getContext;
export const hasContext = context.hasContext;
export const hashFile =
(type: NixValue) =>
(p: NixValue): never => {
const ty = forceStringNoCtx(type);
throw new Error("Not implemented: hashFile");
};
export const hashString =
(type: NixValue) =>
(p: NixValue): never => {
throw new Error("Not implemented: hashString");
};
export const convertHash = (args: NixValue): never => {
throw new Error("Not implemented: convertHash");
};
export const unsafeDiscardOutputDependency = context.unsafeDiscardOutputDependency;
export const unsafeDiscardStringContext = context.unsafeDiscardStringContext;
@@ -77,9 +56,9 @@ export const compareVersions =
i1 = c1.nextIndex;
i2 = c2.nextIndex;
if (componentsLT(c1.component, c2.component)) {
if (componentsLt(c1.component, c2.component)) {
return -1n;
} else if (componentsLT(c2.component, c1.component)) {
} else if (componentsLt(c2.component, c1.component)) {
return 1n;
}
}
@@ -121,7 +100,7 @@ function nextComponent(s: string, startIdx: number): ComponentResult {
return { component: s.substring(start, p), nextIndex: p };
}
function componentsLT(c1: string, c2: string): boolean {
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;
@@ -155,25 +134,17 @@ function componentsLT(c1: string, c2: string): boolean {
return c1 < c2;
}
export const dirOf = (s: NixValue): never => {
throw new Error("Not implemented: dirOf");
};
export const flakeRefToString = (attrs: NixValue): never => {
throw new Error("Not implemented: flakeRefToString");
};
export const functionArgs = (f: NixValue): NixAttrs => {
const func = forceFunction(f);
if (func.args) {
const ret: NixAttrs = {};
for (const key of func.args!.required) {
for (const key of func.args.required) {
ret[key] = false;
}
for (const key of func.args!.optional) {
for (const key of func.args.optional) {
ret[key] = true;
}
const positions = func.args!.positions;
const positions = func.args.positions;
if (positions && Object.keys(positions).length > 0) {
Object.defineProperty(ret, ATTR_POSITIONS, {
value: positions,
@@ -235,13 +206,9 @@ export const genericClosure = (args: NixValue): NixValue => {
return resultList;
};
export const getFlake = (attrs: NixValue): never => {
throw new Error("Not implemented: getFlake");
};
export const outputOf =
(drv: NixValue) =>
(out: NixValue): never => {
(_drv: NixValue) =>
(_out: NixValue): never => {
throw new Error("Not implemented: outputOf");
};
@@ -262,14 +229,6 @@ export const parseDrvName = (s: NixValue): NixAttrs => {
};
};
export const parseFlakeName = (s: NixValue): never => {
throw new Error("Not implemented: parseFlakeName");
};
export const parseFlakeRef = (s: NixValue): never => {
throw new Error("Not implemented: parseFlakeRef");
};
export const placeholder = (output: NixValue): NixValue => {
const outputStr = forceStringNoCtx(output);
return Deno.core.ops.op_make_placeholder(outputStr);
@@ -314,7 +273,7 @@ export const replaceStrings =
resultContext.add(elem);
}
}
const replacement = toCache.get(i)!;
const replacement = toCache.get(i) as string;
result += replacement;
@@ -361,7 +320,7 @@ export const splitVersion = (s: NixValue): NixValue => {
return components;
};
export const traceVerbose = (e1: NixValue, e2: NixValue): never => {
export const traceVerbose = (_e1: NixValue, _e2: NixValue): never => {
throw new Error("Not implemented: traceVerbose");
};

View File

@@ -1,13 +1,9 @@
/**
* Path-related builtin functions
*/
import type { NixValue, NixString, NixPath } from "../types";
import { isNixPath, isStringWithContext } from "../types";
import { force } from "../thunk";
import { mkPath } from "../path";
import { coerceToString, StringCoercionMode, coerceToPath } from "./conversion";
import { mkStringWithContext, type NixStringContext } from "../string-context";
import { force } from "../thunk";
import type { NixPath, NixString, NixValue } from "../types";
import { isNixPath, isStringWithContext } from "../types";
import { coerceToPath, coerceToString, StringCoercionMode } from "./conversion";
/**
* builtins.baseNameOf

View File

@@ -1,29 +1,15 @@
/**
* String operation builtin functions
*/
import type { NixInt, NixValue, NixString } from "../types";
import { forceStringValue, forceList, forceInt, forceString } from "../type-assert";
import { coerceToString, StringCoercionMode } from "./conversion";
import {
type NixStringContext,
getStringValue,
getStringContext,
getStringValue,
mkStringWithContext,
type NixStringContext,
} from "../string-context";
import { forceInt, forceList, forceString, forceStringValue } from "../type-assert";
import type { NixInt, NixString, NixValue } from "../types";
import { coerceToString, StringCoercionMode } from "./conversion";
export const stringLength = (e: NixValue): NixInt => BigInt(forceStringValue(e).length);
/**
* builtins.substring - Extract substring while preserving string context
*
* IMPORTANT: String context must be preserved from the source string.
* This matches Lix behavior where substring operations maintain references
* to store paths and derivations.
*
* Special case: substring 0 0 str can be used idiomatically to capture
* string context efficiently without copying the string value.
*/
export const substring =
(start: NixValue) =>
(len: NixValue) =>
@@ -55,13 +41,6 @@ export const substring =
return mkStringWithContext(result, context);
};
/**
* builtins.concatStringsSep - Concatenate strings with separator, merging contexts
*
* IMPORTANT: String context must be collected from both the separator and all
* list elements, then merged into the result. This ensures that store path
* references are preserved when building paths like "/nix/store/xxx/bin:/nix/store/yyy/bin".
*/
export const concatStringsSep =
(sep: NixValue) =>
(list: NixValue): NixString => {

View File

@@ -1,12 +1,7 @@
/**
* Type checking builtin functions
*/
import {
HAS_CONTEXT,
isNixPath,
isStringWithContext,
type NixPath,
type NixAttrs,
type NixBool,
type NixFloat,
@@ -14,14 +9,11 @@ import {
type NixInt,
type NixList,
type NixNull,
type NixString,
type NixPath,
type NixStrictValue,
type NixString,
} from "../types";
/**
* 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);
};

View File

@@ -1,83 +0,0 @@
import { HAS_CONTEXT, NixStringContext } from "./string-context";
import { force, isThunk } from "./thunk";
import type { NixValue } from "./types";
import { isStringWithContext, IS_PATH } from "./types";
export const nixValueToJson = (
value: NixValue,
strict: boolean,
outContext: NixStringContext,
copyToStore: boolean,
seen: Set<NixValue> = new Set(),
): any => {
const v = strict ? force(value) : value;
if (isThunk(v) || typeof v === "function")
throw new Error(`cannot convert ${isThunk(v) ? "thunk" : "lambda"} to JSON`);
if (v === null) return null;
if (typeof v === "bigint") {
const num = Number(v);
if (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER) {
console.warn(`integer ${v} exceeds safe range, precision may be lost`);
}
return num;
}
if (typeof v === "number") return v;
if (typeof v === "boolean") return v;
if (typeof v === "string") return v;
if (typeof v === "object" && HAS_CONTEXT in v) {
for (const elem of v.context) {
outContext.add(elem);
}
return v.value;
}
if (typeof v === "object" && IS_PATH in v) {
if (copyToStore) {
const storePath = Deno.core.ops.op_copy_path_to_store(v.value);
outContext.add(storePath);
return storePath;
} else {
return v.value;
}
}
// FIXME: is this check necessary?
// if (seen.has(v)) {
// throw new Error("cycle detected in toJSON");
// } else {
// seen.add(v)
// }
if (Array.isArray(v)) {
return v.map((item) => nixValueToJson(item, strict, outContext, copyToStore, seen));
}
// NixAttrs
if ("__toString" in v && typeof force(v.__toString) === "function") {
const toStringMethod = force(v.__toString) as (self: typeof v) => NixValue;
const result = force(toStringMethod(v));
if (typeof result === "string") {
return result;
}
if (isStringWithContext(result)) {
if (outContext) {
for (const elem of result.context) {
outContext.add(elem);
}
}
return result.value;
}
return nixValueToJson(result, strict, outContext, copyToStore, seen);
}
if ("outPath" in v) {
return nixValueToJson(v.outPath, strict, outContext, copyToStore,seen);
}
const result: Record<string, any> = {};
const keys = Object.keys(v).sort();
for (const key of keys) {
result[key] = nixValueToJson(v[key], strict, outContext, copyToStore, seen);
}
return result;
};

View File

@@ -1,106 +0,0 @@
export interface OutputInfo {
path: string;
hashAlgo: string;
hash: string;
}
export interface DerivationData {
name: string;
outputs: Map<string, OutputInfo>;
inputDrvs: Map<string, Set<string>>;
inputSrcs: Set<string>;
platform: string;
builder: string;
args: string[];
env: Map<string, string>;
}
export const escapeString = (s: string): string => {
let result = "";
for (const char of s) {
switch (char) {
case '"':
result += '\\"';
break;
case "\\":
result += "\\\\";
break;
case "\n":
result += "\\n";
break;
case "\r":
result += "\\r";
break;
case "\t":
result += "\\t";
break;
default:
result += char;
}
}
return `"${result}"`;
};
const quoteString = (s: string): string => `"${s}"`;
const cmpByKey = <T>(a: [string, T], b: [string, T]): number => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
export const generateAterm = (drv: DerivationData): string => {
const outputEntries: string[] = [];
const sortedOutputs = Array.from(drv.outputs.entries()).sort(cmpByKey);
for (const [name, info] of sortedOutputs) {
outputEntries.push(
`(${quoteString(name)},${quoteString(info.path)},${quoteString(info.hashAlgo)},${quoteString(info.hash)})`,
);
}
const outputs = outputEntries.join(",");
const inputDrvEntries: string[] = [];
const sortedInputDrvs = Array.from(drv.inputDrvs.entries()).sort(cmpByKey);
for (const [drvPath, outputs] of sortedInputDrvs) {
const sortedOuts = Array.from(outputs).sort();
const outList = `[${sortedOuts.map(quoteString).join(",")}]`;
inputDrvEntries.push(`(${quoteString(drvPath)},${outList})`);
}
const inputDrvs = inputDrvEntries.join(",");
const sortedInputSrcs = Array.from(drv.inputSrcs).sort();
const inputSrcs = sortedInputSrcs.map(quoteString).join(",");
const args = drv.args.map(escapeString).join(",");
const envs = Array.from(drv.env.entries())
.sort(cmpByKey)
.map(([k, v]) => `(${escapeString(k)},${escapeString(v)})`);
return `Derive([${outputs}],[${inputDrvs}],[${inputSrcs}],${quoteString(drv.platform)},${escapeString(drv.builder)},[${args}],[${envs}])`;
};
export const generateAtermModulo = (drv: DerivationData, inputDrvHashes: Map<string, string>): string => {
const outputEntries: string[] = [];
const sortedOutputs = Array.from(drv.outputs.entries()).sort(cmpByKey);
for (const [name, info] of sortedOutputs) {
outputEntries.push(
`(${quoteString(name)},${quoteString(info.path)},${quoteString(info.hashAlgo)},${quoteString(info.hash)})`,
);
}
const outputs = outputEntries.join(",");
const inputDrvEntries: string[] = [];
const sortedInputDrvHashes = Array.from(inputDrvHashes.entries()).sort(cmpByKey);
for (const [drvHash, outputs] of sortedInputDrvHashes) {
const sortedOuts = outputs.split(",").sort();
const outList = `[${sortedOuts.map(quoteString).join(",")}]`;
inputDrvEntries.push(`(${quoteString(drvHash)},${outList})`);
}
const inputDrvs = inputDrvEntries.join(",");
const sortedInputSrcs = Array.from(drv.inputSrcs).sort();
const inputSrcs = sortedInputSrcs.map(quoteString).join(",");
const args = drv.args.map(escapeString).join(",");
const envs = Array.from(drv.env.entries())
.sort(cmpByKey)
.map(([k, v]) => `(${escapeString(k)},${escapeString(v)})`);
return `Derive([${outputs}],[${inputDrvs}],[${inputSrcs}],${quoteString(drv.platform)},${escapeString(drv.builder)},[${args}],[${envs}])`;
};

View File

@@ -1,14 +1,10 @@
/**
* Helper functions for nix-js runtime
*/
import type { NixValue, NixAttrs, NixBool, NixString, NixPath } from "./types";
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";
import { isAttrs, typeOf } from "./builtins/type-check";
import { mkPath } from "./path";
import { isStringWithContext, mkStringWithContext, type NixStringContext } from "./string-context";
import { force } from "./thunk";
import { forceAttrs, forceBool, forceFunction, forceStringValue } from "./type-assert";
import type { NixAttrs, NixBool, NixPath, NixString, NixValue } from "./types";
import { CatchableError, isNixPath } from "./types";
interface StackFrame {
@@ -36,34 +32,17 @@ function enrichError(error: unknown): Error {
return err;
}
export const getTos = (): string => {
const tos = callStack[callStack.length - 2];
const { file, line, column } = Deno.core.ops.op_decode_span(tos.span);
return `${tos.message} at ${file}:${line}:${column}`;
};
/**
* Push an error context onto the stack
* Used for tracking evaluation context (e.g., "while evaluating the condition")
*/
export const pushContext = (message: string, span: string): void => {
const pushContext = (message: string, span: string): void => {
if (callStack.length >= MAX_STACK_DEPTH) {
callStack.shift();
}
callStack.push({ span, message });
};
/**
* Pop an error context from the stack
*/
export const popContext = (): void => {
const popContext = (): void => {
callStack.pop();
};
/**
* Execute a function with error context tracking
* Automatically pushes context before execution and pops after
*/
export const withContext = <T>(message: string, span: string, fn: () => T): T => {
pushContext(message, span);
try {
@@ -149,13 +128,6 @@ export const concatStringsWithContext = (parts: NixValue[], forceString: boolean
return mkStringWithContext(value, context);
};
/**
* Resolve a path (handles both absolute and relative paths)
* For relative paths, resolves against current import stack
*
* @param path - Path string (may be relative or absolute)
* @returns NixPath object with absolute path
*/
export const resolvePath = (currentDir: string, path: NixValue): NixPath => {
const forced = force(path);
let pathStr: string;
@@ -181,18 +153,18 @@ export const select = (obj: NixValue, attrpath: NixValue[], span?: string): NixV
}
callStack.push({ span, message });
try {
return select_impl(obj, attrpath);
return selectImpl(obj, attrpath);
} catch (error) {
throw enrichError(error);
} finally {
callStack.pop();
}
} else {
return select_impl(obj, attrpath);
return selectImpl(obj, attrpath);
}
};
function select_impl(obj: NixValue, attrpath: NixValue[]): NixValue {
function selectImpl(obj: NixValue, attrpath: NixValue[]): NixValue {
let attrs = forceAttrs(obj);
for (const attr of attrpath.slice(0, -1)) {
@@ -214,7 +186,7 @@ function select_impl(obj: NixValue, attrpath: NixValue[]): NixValue {
export const selectWithDefault = (
obj: NixValue,
attrpath: NixValue[],
default_val: NixValue,
defaultVal: NixValue,
span?: string,
): NixValue => {
if (span) {
@@ -227,18 +199,18 @@ export const selectWithDefault = (
}
callStack.push({ span, message });
try {
return selectWithDefault_impl(obj, attrpath, default_val);
return selectWithDefaultImpl(obj, attrpath, defaultVal);
} catch (error) {
throw enrichError(error);
} finally {
callStack.pop();
}
} else {
return selectWithDefault_impl(obj, attrpath, default_val);
return selectWithDefaultImpl(obj, attrpath, defaultVal);
}
};
function selectWithDefault_impl(obj: NixValue, attrpath: NixValue[], defaultVal: NixValue): NixValue {
function selectWithDefaultImpl(obj: NixValue, attrpath: NixValue[], defaultVal: NixValue): NixValue {
let attrs = force(obj);
if (!isAttrs(attrs)) {
return defaultVal;
@@ -281,49 +253,6 @@ export const hasAttr = (obj: NixValue, attrpath: NixValue[]): NixBool => {
return forceStringValue(attrpath[attrpath.length - 1]) in attrs;
};
/**
* Validate function parameters
* Used for pattern matching in function parameters
*
* Example: { a, b ? 1, ... }: ...
* - required: ["a"]
* - allowed: ["a", "b"] (or null if ellipsis "..." present)
*
* @param arg - Argument object to validate
* @param required - Array of required parameter names (or null)
* @param allowed - Array of allowed parameter names (or null for ellipsis)
* @returns The forced argument object
* @throws Error if required param missing or unexpected param present
*/
export const validateParams = (
arg: NixValue,
required: string[] | null,
allowed: string[] | null,
): NixAttrs => {
const forced_arg = forceAttrs(arg);
// Check required parameters
if (required) {
for (const key of required) {
if (!Object.hasOwn(forced_arg, key)) {
throw new Error(`Function called without required argument '${key}'`);
}
}
}
// Check allowed parameters (if not using ellipsis)
if (allowed) {
const allowed_set = new Set(allowed);
for (const key in forced_arg) {
if (!allowed_set.has(key)) {
throw new Error(`Function called with unexpected argument '${key}'`);
}
}
}
return forced_arg;
};
export const call = (func: NixValue, arg: NixValue, span?: string): NixValue => {
if (span) {
if (callStack.length >= MAX_STACK_DEPTH) {
@@ -331,18 +260,18 @@ export const call = (func: NixValue, arg: NixValue, span?: string): NixValue =>
}
callStack.push({ span, message: "from call site" });
try {
return call_impl(func, arg);
return callImpl(func, arg);
} catch (error) {
throw enrichError(error);
} finally {
callStack.pop();
}
} else {
return call_impl(func, arg);
return callImpl(func, arg);
}
};
function call_impl(func: NixValue, arg: NixValue): NixValue {
function callImpl(func: NixValue, arg: NixValue): NixValue {
const forcedFunc = force(func);
if (typeof forcedFunc === "function") {
forcedFunc.args?.check(arg);

View File

@@ -4,45 +4,39 @@
* All functionality is exported via the global `Nix` object
*/
import {
createThunk,
force,
isThunk,
IS_THUNK,
DEBUG_THUNKS,
forceDeep,
IS_CYCLE,
forceShallow,
} from "./thunk";
import {
select,
selectWithDefault,
validateParams,
resolvePath,
hasAttr,
concatStringsWithContext,
call,
assert,
pushContext,
popContext,
withContext,
mkPos,
lookupWith,
} from "./helpers";
import { op } from "./operators";
import { builtins, PRIMOP_METADATA } from "./builtins";
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
import {
assert,
call,
concatStringsWithContext,
hasAttr,
lookupWith,
mkPos,
resolvePath,
select,
selectWithDefault,
withContext,
} from "./helpers";
import { op } from "./operators";
import { HAS_CONTEXT } from "./string-context";
import { IS_PATH, mkAttrs, mkFunction, mkAttrsWithPos, ATTR_POSITIONS, NixValue } from "./types";
import {
createThunk,
DEBUG_THUNKS,
force,
forceDeep,
forceShallow,
IS_CYCLE,
IS_THUNK,
isThunk,
} from "./thunk";
import { forceBool } from "./type-assert";
import { ATTR_POSITIONS, IS_PATH, mkAttrs, mkAttrsWithPos, mkFunction, type NixValue } from "./types";
export type NixRuntime = typeof Nix;
const replBindings: Record<string, NixValue> = {};
/**
* The global Nix runtime object
*/
export const Nix = {
createThunk,
force,
@@ -62,7 +56,6 @@ export const Nix = {
select,
selectWithDefault,
lookupWith,
validateParams,
resolvePath,
coerceToString,
concatStringsWithContext,
@@ -73,8 +66,6 @@ export const Nix = {
mkPos,
ATTR_POSITIONS,
pushContext,
popContext,
withContext,
op,

View File

@@ -1,22 +1,17 @@
/**
* Nix operators module
* Implements all binary and unary operators used by codegen
*/
import type { NixValue, NixList, NixAttrs, NixString, NixPath } from "./types";
import { isNixPath } from "./types";
import { force } from "./thunk";
import { forceNumeric, forceList, forceAttrs, coerceNumeric } from "./type-assert";
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
import { isNixString, typeOf } from "./builtins/type-check";
import { mkPath } from "./path";
import {
type NixStringContext,
getStringValue,
getStringContext,
getStringValue,
mergeContexts,
mkStringWithContext,
type NixStringContext,
} from "./string-context";
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
import { mkPath } from "./path";
import { typeOf, isNixString } from "./builtins/type-check";
import { force } from "./thunk";
import { coerceNumeric, forceAttrs, forceBool, forceList, forceNumeric } from "./type-assert";
import type { NixAttrs, NixList, NixPath, NixString, NixValue } from "./types";
import { isNixPath } from "./types";
const canCoerceToString = (v: NixValue): boolean => {
const forced = force(v);
@@ -27,11 +22,6 @@ 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);
@@ -54,32 +44,29 @@ export const compareValues = (a: NixValue, b: NixValue): -1 | 0 | 1 => {
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;
return (av as never) < (bv as never) ? -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++) {
// Equal if same length, else aList > bList
if (i === bList.length) {
return i === aList.length ? 0 : 1; // Equal if same length, else aList > bList
return i === aList.length ? 0 : 1;
} else if (i === aList.length) {
return -1; // aList < bList
} else if (!op.eq(aList[i], bList[i])) {
@@ -94,10 +81,6 @@ export const compareValues = (a: NixValue, b: NixValue): -1 | 0 | 1 => {
);
};
/**
* Operator object exported as Nix.op
* All operators referenced by codegen (e.g., Nix.op.add, Nix.op.eq)
*/
export const op = {
add: (a: NixValue, b: NixValue): bigint | number | NixString | NixPath => {
const av = force(a);
@@ -109,15 +92,14 @@ export const op = {
const strB = getStringValue(bv);
const ctxB = getStringContext(bv);
// Lix constraint: cannot append string with store context to path
if (ctxB.size > 0) {
throw new TypeError("a string that refers to a store path cannot be appended to a path");
}
// Concatenate paths
return mkPath(av.value + strB);
}
// FIXME: handle corepkgs
// path + path: concatenate
if (isNixPath(bv)) {
return mkPath(av.value + bv.value);
@@ -138,6 +120,7 @@ export const op = {
// String concatenation
if (isNixString(av) && isNixString(bv)) {
// Merge string context
const strA = getStringValue(av);
const strB = getStringValue(bv);
const ctxA = getStringContext(av);
@@ -162,19 +145,19 @@ export const op = {
return mkStringWithContext(result, context);
}
// Numeric addition
// Perform numeric addition otherwise
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (numA as any) + (numB as any);
return (numA as never) + (numB as never);
},
sub: (a: NixValue, b: NixValue): bigint | number => {
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as any) - (bv as any);
return (av as never) - (bv as never);
},
mul: (a: NixValue, b: NixValue): bigint | number => {
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as any) * (bv as any);
return (av as never) * (bv as never);
},
div: (a: NixValue, b: NixValue): bigint | number => {
@@ -184,7 +167,7 @@ export const op = {
throw new RangeError("Division by zero");
}
return (av as any) / (bv as any);
return (av as never) / (bv as never);
},
eq: (a: NixValue, b: NixValue): boolean => {
@@ -202,7 +185,6 @@ export const op = {
return av === Number(bv);
}
// Get type names for comparison (skip if already handled above)
const typeA = typeOf(av);
const typeB = typeOf(bv);
@@ -223,10 +205,12 @@ export const op = {
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++) {
if (!op.eq(av[i], bv[i])) return false;
if (typeA === "list") {
const aList = av as NixList;
const bList = bv as NixList;
if (aList.length !== bList.length) return false;
for (let i = 0; i < aList.length; i++) {
if (!op.eq(aList[i], bList[i])) return false;
}
return true;
}
@@ -261,11 +245,7 @@ export const op = {
return true;
}
// Functions are incomparable
if (typeof av === "function") {
return false;
}
// Other types are incomparable
return false;
},
neq: (a: NixValue, b: NixValue): boolean => {
@@ -284,10 +264,10 @@ export const op = {
return compareValues(a, b) >= 0;
},
bnot: (a: NixValue): boolean => !force(a),
bnot: (a: NixValue): boolean => !forceBool(a),
concat: (a: NixValue, b: NixValue): NixList => {
return Array.prototype.concat.call(forceList(a), forceList(b));
return forceList(a).concat(forceList(b));
},
update: (a: NixValue, b: NixValue): NixAttrs => ({ ...forceAttrs(a), ...forceAttrs(b) }),

View File

@@ -15,7 +15,6 @@ const canonicalizePath = (path: string): string => {
i = j;
if (component === ".") {
continue;
} else if (component === "..") {
if (parts.length > 0) {
parts.pop();
@@ -28,7 +27,7 @@ const canonicalizePath = (path: string): string => {
if (parts.length === 0) {
return "/";
}
return "/" + parts.join("/");
return `/${parts.join("/")}`;
};
export const mkPath = (value: string): NixPath => {

View File

@@ -1,7 +1,7 @@
import { isThunk, IS_CYCLE } from "./thunk";
import { getPrimopMetadata, isPrimop } from "./builtins/index";
import { isStringWithContext } from "./string-context";
import { IS_CYCLE, isThunk } from "./thunk";
import { isNixPath, type NixValue } from "./types";
import { is_primop, get_primop_metadata } from "./builtins/index";
export const printValue = (value: NixValue, seen: WeakSet<object> = new WeakSet()): string => {
if (isThunk(value)) {
@@ -29,8 +29,8 @@ export const printValue = (value: NixValue, seen: WeakSet<object> = new WeakSet(
}
if (typeof value === "function") {
if (is_primop(value)) {
const meta = get_primop_metadata(value);
if (isPrimop(value)) {
const meta = getPrimopMetadata(value);
if (meta && meta.applied > 0) {
return "<PRIMOP-APP>";
}
@@ -40,7 +40,7 @@ export const printValue = (value: NixValue, seen: WeakSet<object> = new WeakSet(
}
if (typeof value === "object") {
if (IS_CYCLE in value && (value as any)[IS_CYCLE] === true) {
if (IS_CYCLE in value && (value as Record<symbol, unknown>)[IS_CYCLE] === true) {
return "«repeated»";
}
@@ -94,7 +94,7 @@ const printString = (s: string): string => {
result += c;
}
}
return result + '"';
return `${result}"`;
};
const SYMBOL_REGEX = /^[a-zA-Z_][a-zA-Z0-9_'-]*$/;

View File

@@ -1,29 +1,4 @@
/**
* String Context System for Nix
*
* String context tracks references to store paths and derivations within strings.
* This is critical for Nix's dependency tracking - when a string containing a
* store path is used in a derivation, that store path becomes a build dependency.
*
* Context Elements (encoded as strings):
* - Opaque: Plain store path reference
* Format: "/nix/store/..."
* Example: "/nix/store/abc123-hello"
*
* - DrvDeep: Derivation with all outputs
* Format: "=/nix/store/...drv"
* Example: "=/nix/store/xyz789-hello.drv"
* Meaning: All outputs of this derivation and its closure
*
* - Built: Specific derivation output
* Format: "!<output>!/nix/store/...drv"
* Example: "!out!/nix/store/xyz789-hello.drv"
* Meaning: Specific output (e.g., "out", "dev", "lib") of this derivation
*
* This implementation matches Lix's NixStringContext system.
*/
import { NixStrictValue } from "./types";
import type { NixStrictValue } from "./types";
export const HAS_CONTEXT = Symbol("HAS_CONTEXT");
@@ -172,22 +147,6 @@ export const parseContextToInfoMap = (context: NixStringContext): Map<string, Pa
return result;
};
/**
* Extract input derivations and source paths from context
*
* IMPORTANT: Used by derivation builder to determine build dependencies.
*
* Returns:
* - inputDrvs: Map of derivation paths to their required output names
* - inputSrcs: Set of plain store paths (opaque) and drvDeep references
*
* Context type handling:
* - Opaque: Added to inputSrcs
* - DrvDeep: Computes FS closure (like Nix's computeFSClosure) - adds all paths
* in the dependency graph to inputSrcs, and all derivations with their
* outputs to inputDrvs
* - Built: Added to inputDrvs with specific output name
*/
export const extractInputDrvsAndSrcs = (
context: NixStringContext,
): { inputDrvs: Map<string, Set<string>>; inputSrcs: Set<string> } => {

View File

@@ -1,17 +1,8 @@
/**
* Lazy evaluation system for nix-js
* Implements thunks for lazy evaluation of Nix expressions
*/
import type { NixValue, NixThunkInterface, NixStrictValue } from "./types";
import { HAS_CONTEXT } from "./string-context";
import { IS_PATH } from "./types";
import { isAttrs, isList } from "./builtins/type-check";
import { HAS_CONTEXT } from "./string-context";
import type { NixStrictValue, NixThunkInterface, NixValue } from "./types";
import { IS_PATH } from "./types";
/**
* Symbol used to mark objects as thunks
* This is exported to Rust via Nix.IS_THUNK
*/
export const IS_THUNK = Symbol("is_thunk");
const forceStack: NixThunk[] = [];
@@ -31,7 +22,7 @@ export const DEBUG_THUNKS = { enabled: true };
* - Evaluated: func is undefined, result is defined
*/
export class NixThunk implements NixThunkInterface {
[key: symbol]: any;
[key: symbol]: unknown;
readonly [IS_THUNK] = true as const;
func: (() => NixValue) | undefined;
result: NixStrictValue | undefined;
@@ -51,11 +42,6 @@ export class NixThunk implements NixThunkInterface {
}
}
/**
* Type guard to check if a value is a thunk
* @param value - Value to check
* @returns true if value is a NixThunk
*/
export const isThunk = (value: NixValue): value is NixThunkInterface => {
return value !== null && typeof value === "object" && IS_THUNK in value && value[IS_THUNK] === true;
};
@@ -96,7 +82,7 @@ export const force = (value: NixValue): NixStrictValue => {
}
const thunk = value as NixThunk;
const func = thunk.func!;
const func = thunk.func as () => NixValue;
thunk.func = undefined;
if (DEBUG_THUNKS.enabled) {
@@ -126,24 +112,11 @@ export const force = (value: NixValue): NixStrictValue => {
}
};
/**
* Create a new thunk from a function
* @param func - Function that produces a value when called
* @param label - Optional label for debugging
* @returns A new NixThunk wrapping the function
*/
export const createThunk = (func: () => NixValue, label?: string): NixThunkInterface => {
return new NixThunk(func, label);
};
/**
* Symbol to mark cyclic references detected during deep forcing
*/
export const IS_CYCLE = Symbol("is_cycle");
/**
* Marker object for cyclic references
*/
export const CYCLE_MARKER = { [IS_CYCLE]: true };
/**

View File

@@ -1,27 +1,18 @@
/**
* Type assertion helpers for runtime type checking
* These functions force evaluation and verify the type, throwing errors on mismatch
*/
import { isAttrs, isFunction, typeOf } from "./builtins/type-check";
import { force } from "./thunk";
import type {
NixValue,
NixList,
NixAttrs,
NixFloat,
NixFunction,
NixInt,
NixFloat,
NixList,
NixNumber,
NixString,
NixPath,
NixString,
NixValue,
} from "./types";
import { isStringWithContext, isNixPath } from "./types";
import { force } from "./thunk";
import { isAttrs, isFunction, typeOf } from "./builtins/type-check";
import { isNixPath, isStringWithContext } from "./types";
/**
* Force a value and assert it's a list
* @throws TypeError if value is not a list after forcing
*/
export const forceList = (value: NixValue): NixList => {
const forced = force(value);
if (!Array.isArray(forced)) {
@@ -30,10 +21,6 @@ export const forceList = (value: NixValue): NixList => {
return forced;
};
/**
* Force a value and assert it's a function or functor
* @throws TypeError if value is not a function or functor after forcing
*/
export const forceFunction = (value: NixValue): NixFunction => {
const forced = force(value);
if (isFunction(forced)) {
@@ -47,10 +34,6 @@ export const forceFunction = (value: NixValue): NixFunction => {
throw new TypeError(`Expected function, got ${typeOf(forced)}`);
};
/**
* Force a value and assert it's an attribute set
* @throws TypeError if value is not an attribute set after forcing
*/
export const forceAttrs = (value: NixValue): NixAttrs => {
const forced = force(value);
if (!isAttrs(forced)) {
@@ -59,10 +42,6 @@ export const forceAttrs = (value: NixValue): NixAttrs => {
return forced;
};
/**
* 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 forceStringValue = (value: NixValue): string => {
const forced = force(value);
if (typeof forced === "string") {
@@ -74,10 +53,6 @@ export const forceStringValue = (value: NixValue): string => {
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 forceString = (value: NixValue): NixString => {
const forced = force(value);
if (typeof forced === "string") {
@@ -100,10 +75,6 @@ export const forceStringNoCtx = (value: NixValue): string => {
throw new TypeError(`Expected string, got ${typeOf(forced)}`);
};
/**
* Force a value and assert it's a boolean
* @throws TypeError if value is not a boolean after forcing
*/
export const forceBool = (value: NixValue): boolean => {
const forced = force(value);
if (typeof forced !== "boolean") {
@@ -112,10 +83,6 @@ export const forceBool = (value: NixValue): boolean => {
return forced;
};
/**
* Force a value and extract int value
* @throws TypeError if value is not an int
*/
export const forceInt = (value: NixValue): NixInt => {
const forced = force(value);
if (typeof forced === "bigint") {
@@ -124,10 +91,6 @@ export const forceInt = (value: NixValue): NixInt => {
throw new TypeError(`Expected int, got ${typeOf(forced)}`);
};
/**
* Force a value and extract float value
* @throws TypeError if value is not a float
*/
export const forceFloat = (value: NixValue): NixFloat => {
const forced = force(value);
if (typeof forced === "number") {
@@ -136,10 +99,6 @@ export const forceFloat = (value: NixValue): NixFloat => {
throw new TypeError(`Expected float, got ${typeOf(forced)}`);
};
/**
* Force a value and extract numeric value (int or float)
* @throws TypeError if value is not a numeric type
*/
export const forceNumeric = (value: NixValue): NixNumber => {
const forced = force(value);
if (typeof forced === "bigint" || typeof forced === "number") {
@@ -148,28 +107,17 @@ export const forceNumeric = (value: NixValue): NixNumber => {
throw new TypeError(`Expected numeric type, got ${typeOf(forced)}`);
};
/**
* Coerce two numeric values to a common type for arithmetic
* Rule: If either is float, convert both to float; otherwise keep as bigint
* @returns [a, b] tuple of coerced values
*/
export const coerceNumeric = (a: NixNumber, b: NixNumber): [NixFloat, NixFloat] | [NixInt, NixInt] => {
const aIsInt = typeof a === "bigint";
const bIsInt = typeof b === "bigint";
// If either is float, convert both to float
if (!aIsInt || !bIsInt) {
return [Number(a), Number(b)];
}
// Both are integers
return [a, b];
};
/**
* Force a value and assert it's a path
* @throws TypeError if value is not a path after forcing
*/
export const forceNixPath = (value: NixValue): NixPath => {
const forced = force(value);
if (isNixPath(forced)) {

View File

@@ -1,12 +1,6 @@
/**
* Core TypeScript type definitions for nix-js runtime
*/
import { HAS_CONTEXT, isStringWithContext, type StringWithContext } from "./string-context";
import { force, IS_THUNK } from "./thunk";
import { type StringWithContext, HAS_CONTEXT, isStringWithContext, getStringContext } from "./string-context";
import { op } from "./operators";
import { forceAttrs, forceStringNoCtx } from "./type-assert";
import { isString, typeOf } from "./builtins/type-check";
export { HAS_CONTEXT, isStringWithContext };
export type { StringWithContext };
@@ -21,7 +15,6 @@ export const isNixPath = (v: NixStrictValue): v is NixPath => {
return typeof v === "object" && v !== null && IS_PATH in v;
};
// Nix primitive types
export type NixInt = bigint;
export type NixFloat = number;
export type NixNumber = NixInt | NixFloat;
@@ -29,7 +22,6 @@ export type NixBool = boolean;
export type NixString = string | StringWithContext;
export type NixNull = null;
// Nix composite types
export type NixList = NixValue[];
// FIXME: reject contextful string
export type NixAttrs = { [key: string]: NixValue };
@@ -72,7 +64,7 @@ export const mkFunction = (
positions: Record<string, string>,
ellipsis: boolean,
): NixFunction => {
const func = f as NixFunction;
const func: NixFunction = f;
func.args = new NixArgs(required, optional, positions, ellipsis);
return func;
};
@@ -90,7 +82,7 @@ export const mkAttrs = (attrs: NixAttrs, keys: NixValue[], values: NixValue[]):
return attrs;
};
const ATTR_POSITIONS = Symbol("attrPositions");
export const ATTR_POSITIONS = Symbol("attrPositions");
export const mkAttrsWithPos = (
attrs: NixAttrs,
@@ -121,46 +113,14 @@ export const mkAttrsWithPos = (
return attrs;
};
export { ATTR_POSITIONS };
/**
* Interface for lazy thunk values
* Thunks delay evaluation until forced
*/
export interface NixThunkInterface {
readonly [IS_THUNK]: true;
func: (() => NixValue) | undefined;
result: NixStrictValue | undefined;
}
// Union of all Nix primitive types
export type NixPrimitive = NixNull | NixBool | NixInt | NixFloat | NixString;
/**
* NixValue: Union type representing any possible Nix value
* This is the core type used throughout the runtime
*/
export type NixValue = NixPrimitive | NixPath | NixList | NixAttrs | NixFunction | NixThunkInterface;
export type NixStrictValue = Exclude<NixValue, NixThunkInterface>;
/**
* CatchableError: Error type thrown by `builtins.throw`
* This can be caught by `builtins.tryEval`
*/
export class CatchableError extends Error {}
// Operator function signatures
export type BinaryOp<T = NixValue, U = NixValue, R = NixValue> = (a: T, b: U) => R;
export type UnaryOp<T = NixValue, R = NixValue> = (a: T) => R;
/**
* Curried function types - All Nix builtins must be curried!
*
* Examples:
* - add: Curried2<number, number, number> = (a) => (b) => a + b
* - map: Curried2<NixFunction, NixList, NixList> = (f) => (list) => list.map(f)
*/
export type Curried2<A, B, R> = (a: A) => (b: B) => R;
export type Curried3<A, B, C, R> = (a: A) => (b: B) => (c: C) => R;
export type Curried4<A, B, C, D, R> = (a: A) => (b: B) => (c: C) => (d: D) => R;

View File

@@ -1,5 +1,5 @@
import type { NixRuntime } from "..";
import type { FetchTarballResult, FetchUrlResult, FetchGitResult, FetchHgResult } from "../builtins/io";
import type { FetchTarballResult, FetchUrlResult, FetchGitResult } from "../builtins/io";
declare global {
var Nix: NixRuntime;
@@ -20,17 +20,17 @@ declare global {
line: number | null;
column: number | null;
};
function op_make_store_path(ty: string, hash_hex: string, name: string): string;
function op_parse_hash(hash_str: string, algo: string | null): { hex: string; algo: string };
function op_make_store_path(ty: string, hashHex: string, name: string): string;
function op_parse_hash(hashStr: string, algo: string | null): { hex: string; algo: string };
function op_make_fixed_output_path(
hash_algo: string,
hashAlgo: string,
hash: string,
hash_mode: string,
hashMode: string,
name: string,
): string;
function op_fetch_url(
url: string,
expected_hash: string | null,
expectedHash: string | null,
name: string | null,
executable: boolean,
): FetchUrlResult;
@@ -45,7 +45,7 @@ declare global {
rev: string | null,
shallow: boolean,
submodules: boolean,
all_refs: boolean,
allRefs: boolean,
name: string | null,
): FetchGitResult;
function op_add_path(
@@ -56,9 +56,9 @@ declare global {
): string;
function op_store_path(path: string): string;
function op_to_file(name: string, contents: string, references: string[]): string;
function op_write_derivation(drv_name: string, aterm: string, references: string[]): string;
function op_read_derivation_outputs(drv_path: string): string[];
function op_compute_fs_closure(drv_path: string): {
function op_write_derivation(drvName: string, aterm: string, references: string[]): string;
function op_read_derivation_outputs(drvPath: string): string[];
function op_compute_fs_closure(drvPath: string): {
input_drvs: [string, string[]][];
input_srcs: string[];
};
@@ -70,12 +70,12 @@ declare global {
name: string | null,
recursive: boolean,
sha256: string | null,
include_paths: string[],
includePaths: string[],
): string;
function op_match(regex: string, text: string): (string | null)[] | null;
function op_split(regex: string, text: string): (string | (string | null)[])[];
function op_from_json(json: string): any;
function op_from_toml(toml: string): any;
function op_from_json(json: string): unknown;
function op_from_toml(toml: string): unknown;
}
}
}