diff --git a/biome.json b/biome.json index c2bc1e8..3ca5744 100644 --- a/biome.json +++ b/biome.json @@ -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", diff --git a/nix-js/runtime-ts/src/builtins/arithmetic.ts b/nix-js/runtime-ts/src/builtins/arithmetic.ts index b54159b..42d3f63 100644 --- a/nix-js/runtime-ts/src/builtins/arithmetic.ts +++ b/nix-js/runtime-ts/src/builtins/arithmetic.ts @@ -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 => { diff --git a/nix-js/runtime-ts/src/builtins/attrs.ts b/nix-js/runtime-ts/src/builtins/attrs.ts index 548b544..8e4934d 100644 --- a/nix-js/runtime-ts/src/builtins/attrs.ts +++ b/nix-js/runtime-ts/src/builtins/attrs.ts @@ -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(); - // 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 = {}; 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)[Nix.ATTR_POSITIONS] as + | Record + | undefined; if (!positions || !(name in positions)) { return null; } diff --git a/nix-js/runtime-ts/src/builtins/context.ts b/nix-js/runtime-ts/src/builtins/context.ts index 62aad33..f106417 100644 --- a/nix-js/runtime-ts/src/builtins/context.ts +++ b/nix-js/runtime-ts/src/builtins/context.ts @@ -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`, diff --git a/nix-js/runtime-ts/src/builtins/conversion.ts b/nix-js/runtime-ts/src/builtins/conversion.ts index 0cc4060..9ecd2de 100644 --- a/nix-js/runtime-ts/src/builtins/conversion.ts +++ b/nix-js/runtime-ts/src/builtins/conversion.ts @@ -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 = 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 = {}; + const keys = Object.keys(v).sort(); + for (const key of keys) { + result[key] = nixValueToJson(v[key], strict, outContext, copyToStore, seen); + } + return result; +}; diff --git a/nix-js/runtime-ts/src/builtins/derivation.ts b/nix-js/runtime-ts/src/builtins/derivation.ts index 944c3d9..4fa1ee9 100644 --- a/nix-js/runtime-ts/src/builtins/derivation.ts +++ b/nix-js/runtime-ts/src/builtins/derivation.ts @@ -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(); -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; + inputDrvs: Map>; + inputSrcs: Set; + platform: string; + builder: string; + args: string[]; + env: Map; +} + +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 = (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 => { + 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 => { +const sortedJsonStringify = (obj: Record): string => { const sortedKeys = Object.keys(obj).sort(); - const sortedObj: Record = {}; + const sortedObj: Record = {}; for (const key of sortedKeys) { sortedObj[key] = obj[key]; } @@ -143,7 +226,7 @@ const extractEnv = ( const env = new Map(); if (structuredAttrs) { - const jsonAttrs: Record = {}; + const jsonAttrs: Record = {}; 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"); }; diff --git a/nix-js/runtime-ts/src/builtins/flake.ts b/nix-js/runtime-ts/src/builtins/flake.ts new file mode 100644 index 0000000..d3f7480 --- /dev/null +++ b/nix-js/runtime-ts/src/builtins/flake.ts @@ -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"); +}; diff --git a/nix-js/runtime-ts/src/builtins/functional.ts b/nix-js/runtime-ts/src/builtins/functional.ts index bbcbd30..18be3a2 100644 --- a/nix-js/runtime-ts/src/builtins/functional.ts +++ b/nix-js/runtime-ts/src/builtins/functional.ts @@ -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 = diff --git a/nix-js/runtime-ts/src/builtins/hash.ts b/nix-js/runtime-ts/src/builtins/hash.ts new file mode 100644 index 0000000..dabeff5 --- /dev/null +++ b/nix-js/runtime-ts/src/builtins/hash.ts @@ -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"); +}; diff --git a/nix-js/runtime-ts/src/builtins/index.ts b/nix-js/runtime-ts/src/builtins/index.ts index 81c62ba..6492032 100644 --- a/nix-js/runtime-ts/src/builtins/index.ts +++ b/nix-js/runtime-ts/src/builtins/index.ts @@ -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)[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)[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 = { 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"); + }), }; diff --git a/nix-js/runtime-ts/src/builtins/io.ts b/nix-js/runtime-ts/src/builtins/io.ts index 40b7dc6..1319ec5 100644 --- a/nix-js/runtime-ts/src/builtins/io.ts +++ b/nix-js/runtime-ts/src/builtins/io.ts @@ -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(); @@ -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 => { - throw new Error("Not implemented: filterSource"); -}; +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; diff --git a/nix-js/runtime-ts/src/builtins/list.ts b/nix-js/runtime-ts/src/builtins/list.ts index d8c99ef..c986e00 100644 --- a/nix-js/runtime-ts/src/builtins/list.ts +++ b/nix-js/runtime-ts/src/builtins/list.ts @@ -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); diff --git a/nix-js/runtime-ts/src/builtins/math.ts b/nix-js/runtime-ts/src/builtins/math.ts index f8c09d4..4408a4d 100644 --- a/nix-js/runtime-ts/src/builtins/math.ts +++ b/nix-js/runtime-ts/src/builtins/math.ts @@ -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); diff --git a/nix-js/runtime-ts/src/builtins/misc.ts b/nix-js/runtime-ts/src/builtins/misc.ts index de2bdff..40b4c9b 100644 --- a/nix-js/runtime-ts/src/builtins/misc.ts +++ b/nix-js/runtime-ts/src/builtins/misc.ts @@ -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"); }; diff --git a/nix-js/runtime-ts/src/builtins/path.ts b/nix-js/runtime-ts/src/builtins/path.ts index f46db90..78ee51f 100644 --- a/nix-js/runtime-ts/src/builtins/path.ts +++ b/nix-js/runtime-ts/src/builtins/path.ts @@ -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 diff --git a/nix-js/runtime-ts/src/builtins/string.ts b/nix-js/runtime-ts/src/builtins/string.ts index 9556ddb..2d2282b 100644 --- a/nix-js/runtime-ts/src/builtins/string.ts +++ b/nix-js/runtime-ts/src/builtins/string.ts @@ -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 => { diff --git a/nix-js/runtime-ts/src/builtins/type-check.ts b/nix-js/runtime-ts/src/builtins/type-check.ts index 99f2f83..568e97b 100644 --- a/nix-js/runtime-ts/src/builtins/type-check.ts +++ b/nix-js/runtime-ts/src/builtins/type-check.ts @@ -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); }; diff --git a/nix-js/runtime-ts/src/conversion.ts b/nix-js/runtime-ts/src/conversion.ts deleted file mode 100644 index 435516a..0000000 --- a/nix-js/runtime-ts/src/conversion.ts +++ /dev/null @@ -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 = 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 = {}; - const keys = Object.keys(v).sort(); - for (const key of keys) { - result[key] = nixValueToJson(v[key], strict, outContext, copyToStore, seen); - } - return result; -}; diff --git a/nix-js/runtime-ts/src/derivation-helpers.ts b/nix-js/runtime-ts/src/derivation-helpers.ts deleted file mode 100644 index daebaea..0000000 --- a/nix-js/runtime-ts/src/derivation-helpers.ts +++ /dev/null @@ -1,106 +0,0 @@ -export interface OutputInfo { - path: string; - hashAlgo: string; - hash: string; -} - -export interface DerivationData { - name: string; - outputs: Map; - inputDrvs: Map>; - inputSrcs: Set; - platform: string; - builder: string; - args: string[]; - env: Map; -} - -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 = (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 => { - 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}])`; -}; diff --git a/nix-js/runtime-ts/src/helpers.ts b/nix-js/runtime-ts/src/helpers.ts index 849fc99..28aa3c3 100644 --- a/nix-js/runtime-ts/src/helpers.ts +++ b/nix-js/runtime-ts/src/helpers.ts @@ -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 = (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); diff --git a/nix-js/runtime-ts/src/index.ts b/nix-js/runtime-ts/src/index.ts index 3f6565f..e177c1d 100644 --- a/nix-js/runtime-ts/src/index.ts +++ b/nix-js/runtime-ts/src/index.ts @@ -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 = {}; -/** - * 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, diff --git a/nix-js/runtime-ts/src/operators.ts b/nix-js/runtime-ts/src/operators.ts index 87b089c..26ad183 100644 --- a/nix-js/runtime-ts/src/operators.ts +++ b/nix-js/runtime-ts/src/operators.ts @@ -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) }), diff --git a/nix-js/runtime-ts/src/path.ts b/nix-js/runtime-ts/src/path.ts index 583850d..fd69745 100644 --- a/nix-js/runtime-ts/src/path.ts +++ b/nix-js/runtime-ts/src/path.ts @@ -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 => { diff --git a/nix-js/runtime-ts/src/print.ts b/nix-js/runtime-ts/src/print.ts index 1a7018d..7aef6af 100644 --- a/nix-js/runtime-ts/src/print.ts +++ b/nix-js/runtime-ts/src/print.ts @@ -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 = new WeakSet()): string => { if (isThunk(value)) { @@ -29,8 +29,8 @@ export const printValue = (value: NixValue, seen: WeakSet = 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 ""; } @@ -40,7 +40,7 @@ export const printValue = (value: NixValue, seen: WeakSet = 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)[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_'-]*$/; diff --git a/nix-js/runtime-ts/src/string-context.ts b/nix-js/runtime-ts/src/string-context.ts index abdbada..c488bc7 100644 --- a/nix-js/runtime-ts/src/string-context.ts +++ b/nix-js/runtime-ts/src/string-context.ts @@ -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: "!!/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>; inputSrcs: Set } => { diff --git a/nix-js/runtime-ts/src/thunk.ts b/nix-js/runtime-ts/src/thunk.ts index 6253624..df6d354 100644 --- a/nix-js/runtime-ts/src/thunk.ts +++ b/nix-js/runtime-ts/src/thunk.ts @@ -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 }; /** diff --git a/nix-js/runtime-ts/src/type-assert.ts b/nix-js/runtime-ts/src/type-assert.ts index ea5a2ef..79afe7d 100644 --- a/nix-js/runtime-ts/src/type-assert.ts +++ b/nix-js/runtime-ts/src/type-assert.ts @@ -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)) { diff --git a/nix-js/runtime-ts/src/types.ts b/nix-js/runtime-ts/src/types.ts index 2cc6297..327ddf5 100644 --- a/nix-js/runtime-ts/src/types.ts +++ b/nix-js/runtime-ts/src/types.ts @@ -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, 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; -/** - * 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 = (a: T, b: U) => R; -export type UnaryOp = (a: T) => R; - -/** - * Curried function types - All Nix builtins must be curried! - * - * Examples: - * - add: Curried2 = (a) => (b) => a + b - * - map: Curried2 = (f) => (list) => list.map(f) - */ -export type Curried2 = (a: A) => (b: B) => R; -export type Curried3 = (a: A) => (b: B) => (c: C) => R; -export type Curried4 = (a: A) => (b: B) => (c: C) => (d: D) => R; diff --git a/nix-js/runtime-ts/src/types/global.d.ts b/nix-js/runtime-ts/src/types/global.d.ts index 45e6125..1d45ec6 100644 --- a/nix-js/runtime-ts/src/types/global.d.ts +++ b/nix-js/runtime-ts/src/types/global.d.ts @@ -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; } } }