Compare commits
9 Commits
2cb85529c9
...
33775092ee
| Author | SHA1 | Date | |
|---|---|---|---|
|
33775092ee
|
|||
|
ef5d8c3b29
|
|||
|
56a8ba9475
|
|||
|
58c3e67409
|
|||
|
7679a3b67f
|
|||
|
95faa7b35f
|
|||
|
43b8959842
|
|||
|
041d7b7dd2
|
|||
|
15c4159dcc
|
4
Justfile
4
Justfile
@@ -8,8 +8,8 @@
|
||||
|
||||
[no-exit-message]
|
||||
@replr:
|
||||
cargo run --bin repl --release
|
||||
RUST_LOG=info cargo run --bin repl --release
|
||||
|
||||
[no-exit-message]
|
||||
@evalr expr:
|
||||
cargo run --bin eval --release -- '{{expr}}'
|
||||
RUST_LOG=info cargo run --bin eval --release -- '{{expr}}'
|
||||
|
||||
18
flake.lock
generated
18
flake.lock
generated
@@ -8,11 +8,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1767250179,
|
||||
"narHash": "sha256-PnQdWvPZqHp+7yaHWDFX3NYSKaOy0fjkwpR+rIQC7AY=",
|
||||
"lastModified": 1768892055,
|
||||
"narHash": "sha256-zatCoDgFd0C8YEOztMeBcom6cka0GqJGfc0aAXvpktc=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "a3eaf682db8800962943a77ab77c0aae966f9825",
|
||||
"rev": "81d6a7547e090f7e760b95b9cc534461f6045e43",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -37,11 +37,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1767116409,
|
||||
"narHash": "sha256-5vKw92l1GyTnjoLzEagJy5V5mDFck72LiQWZSOnSicw=",
|
||||
"lastModified": 1768886240,
|
||||
"narHash": "sha256-C2TjvwYZ2VDxYWeqvvJ5XPPp6U7H66zeJlRaErJKoEM=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "cad22e7d996aea55ecab064e84834289143e44a0",
|
||||
"rev": "80e4adbcf8992d3fd27ad4964fbb84907f9478b0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -61,11 +61,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1767191410,
|
||||
"narHash": "sha256-cCZGjubgDWmstvFkS6eAw2qk2ihgWkycw55u2dtLd70=",
|
||||
"lastModified": 1768816483,
|
||||
"narHash": "sha256-bXeWgVkvxN76QEw12OaWFbRhO1yt+5QETz/BxBX4dk0=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "a9026e6d5068172bf5a0d52a260bb290961d1cb4",
|
||||
"rev": "1b8952b49fa10cae9020f0e46d0b8938563a6b64",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
13
nix-js/runtime-ts/package-lock.json
generated
13
nix-js/runtime-ts/package-lock.json
generated
@@ -7,6 +7,9 @@
|
||||
"": {
|
||||
"name": "nix-js-runtime",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"js-sdsl": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.24.2",
|
||||
"typescript": "^5.7.2"
|
||||
@@ -478,6 +481,16 @@
|
||||
"@esbuild/win32-x64": "0.24.2"
|
||||
}
|
||||
},
|
||||
"node_modules/js-sdsl": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/js-sdsl/-/js-sdsl-4.4.2.tgz",
|
||||
"integrity": "sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
||||
|
||||
@@ -10,5 +10,8 @@
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.24.2",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"js-sdsl": "^4.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import type { NixBool, NixInt, NixNumber, NixValue } from "../types";
|
||||
import { forceNumeric, coerceNumeric, forceInt } from "../type-assert";
|
||||
import { op } from "../operators";
|
||||
|
||||
export const add =
|
||||
(a: NixValue) =>
|
||||
@@ -66,4 +67,4 @@ export const bitXor =
|
||||
export const lessThan =
|
||||
(a: NixValue) =>
|
||||
(b: NixValue): NixBool =>
|
||||
forceNumeric(a) < forceNumeric(b);
|
||||
op.lt(a, b);
|
||||
|
||||
@@ -3,22 +3,33 @@
|
||||
*/
|
||||
|
||||
import type { NixValue, NixAttrs, NixList } from "../types";
|
||||
import { forceAttrs, forceString, forceFunction, forceList } from "../type-assert";
|
||||
import { forceAttrs, forceStringValue, forceFunction, forceList } from "../type-assert";
|
||||
import { createThunk } from "../thunk";
|
||||
|
||||
export const attrNames = (set: NixValue): string[] => Object.keys(forceAttrs(set)).sort();
|
||||
|
||||
export const attrValues = (set: NixValue): NixValue[] => Object.values(forceAttrs(set));
|
||||
export const attrValues = (set: NixValue): NixValue[] =>
|
||||
Object.entries(forceAttrs(set))
|
||||
.sort(([a], [b]) => {
|
||||
if (a < b) {
|
||||
return -1;
|
||||
} else if (a === b) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
})
|
||||
.map(([_, val]) => val);
|
||||
|
||||
export const getAttr =
|
||||
(s: NixValue) =>
|
||||
(set: NixValue): NixValue =>
|
||||
forceAttrs(set)[forceString(s)];
|
||||
forceAttrs(set)[forceStringValue(s)];
|
||||
|
||||
export const hasAttr =
|
||||
(s: NixValue) =>
|
||||
(set: NixValue): boolean =>
|
||||
Object.hasOwn(forceAttrs(set), forceString(s));
|
||||
Object.hasOwn(forceAttrs(set), forceStringValue(s));
|
||||
|
||||
export const mapAttrs =
|
||||
(f: NixValue) =>
|
||||
@@ -52,7 +63,7 @@ export const listToAttrs = (e: NixValue): NixAttrs => {
|
||||
const forced_e = [...forceList(e)].reverse();
|
||||
for (const obj of forced_e) {
|
||||
const item = forceAttrs(obj);
|
||||
attrs[forceString(item.name)] = item.value;
|
||||
attrs[forceStringValue(item.name)] = item.value;
|
||||
}
|
||||
return attrs;
|
||||
};
|
||||
@@ -74,7 +85,7 @@ export const intersectAttrs =
|
||||
export const catAttrs =
|
||||
(attr: NixValue) =>
|
||||
(list: NixValue): NixList => {
|
||||
const key = forceString(attr);
|
||||
const key = forceStringValue(attr);
|
||||
return forceList(list)
|
||||
.map((set) => forceAttrs(set)[key])
|
||||
.filter((val) => val !== undefined);
|
||||
@@ -87,7 +98,7 @@ export const groupBy =
|
||||
const forced_f = forceFunction(f);
|
||||
const forced_list = forceList(list);
|
||||
for (const elem of forced_list) {
|
||||
const key = forceString(forced_f(elem));
|
||||
const key = forceStringValue(forced_f(elem));
|
||||
if (!attrs[key]) attrs[key] = [];
|
||||
(attrs[key] as NixList).push(elem);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NixValue, NixAttrs, NixString } from "../types";
|
||||
import { isStringWithContext } from "../types";
|
||||
import { forceNixString, forceAttrs, forceList, forceString } from "../type-assert";
|
||||
import { forceString, forceAttrs, forceList, forceStringValue } from "../type-assert";
|
||||
import { force } from "../thunk";
|
||||
import {
|
||||
type NixStringContext,
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
* Returns true if the string has any store path references.
|
||||
*/
|
||||
export const hasContext = (value: NixValue): boolean => {
|
||||
const s = forceNixString(value);
|
||||
const s = forceString(value);
|
||||
return isStringWithContext(s) && s.context.size > 0;
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ export const hasContext = (value: NixValue): boolean => {
|
||||
* Use with caution as it removes derivation dependencies.
|
||||
*/
|
||||
export const unsafeDiscardStringContext = (value: NixValue): string => {
|
||||
const s = forceNixString(value);
|
||||
const s = forceString(value);
|
||||
return getStringValue(s);
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export const unsafeDiscardStringContext = (value: NixValue): string => {
|
||||
* Preserves other context types unchanged.
|
||||
*/
|
||||
export const unsafeDiscardOutputDependency = (value: NixValue): NixString => {
|
||||
const s = forceNixString(value);
|
||||
const s = forceString(value);
|
||||
const strValue = getStringValue(s);
|
||||
const context = getStringContext(s);
|
||||
|
||||
@@ -71,7 +71,7 @@ export const unsafeDiscardOutputDependency = (value: NixValue): NixString => {
|
||||
* The string must have exactly one context element which must be a .drv path.
|
||||
*/
|
||||
export const addDrvOutputDependencies = (value: NixValue): NixString => {
|
||||
const s = forceNixString(value);
|
||||
const s = forceString(value);
|
||||
const strValue = getStringValue(s);
|
||||
const context = getStringContext(s);
|
||||
|
||||
@@ -109,7 +109,7 @@ export const addDrvOutputDependencies = (value: NixValue): NixString => {
|
||||
* - outputs: list of specific output names (built, encoded as !output!path)
|
||||
*/
|
||||
export const getContext = (value: NixValue): NixAttrs => {
|
||||
const s = forceNixString(value);
|
||||
const s = forceString(value);
|
||||
const context = getStringContext(s);
|
||||
|
||||
const infoMap = parseContextToInfoMap(context);
|
||||
@@ -147,7 +147,7 @@ export const getContext = (value: NixValue): NixAttrs => {
|
||||
export const appendContext =
|
||||
(strValue: NixValue) =>
|
||||
(ctxValue: NixValue): NixString => {
|
||||
const s = forceNixString(strValue);
|
||||
const s = forceString(strValue);
|
||||
const strVal = getStringValue(s);
|
||||
const existingContext = getStringContext(s);
|
||||
|
||||
@@ -188,7 +188,7 @@ export const appendContext =
|
||||
);
|
||||
}
|
||||
for (const output of outputs) {
|
||||
const outputName = forceString(output);
|
||||
const outputName = forceStringValue(output);
|
||||
newContext.add(`!${outputName}!${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { force } from "../thunk";
|
||||
import { type NixStringContext, mkStringWithContext, addBuiltContext } from "../string-context";
|
||||
import { forceFunction } from "../type-assert";
|
||||
import { nixValueToJson } from "../conversion";
|
||||
import { typeOf } from "./type-check";
|
||||
|
||||
const convertJsonToNix = (json: unknown): NixValue => {
|
||||
if (json === null) {
|
||||
@@ -41,7 +42,7 @@ const convertJsonToNix = (json: unknown): NixValue => {
|
||||
export const fromJSON = (e: NixValue): NixValue => {
|
||||
const str = force(e);
|
||||
if (typeof str !== "string" && !isStringWithContext(str)) {
|
||||
throw new TypeError(`builtins.fromJSON: expected a string, got ${typeName(str)}`);
|
||||
throw new TypeError(`builtins.fromJSON: expected a string, got ${typeOf(str)}`);
|
||||
}
|
||||
const jsonStr = isStringWithContext(str) ? str.value : str;
|
||||
try {
|
||||
@@ -82,25 +83,6 @@ export enum StringCoercionMode {
|
||||
ToString = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get human-readable type names for error messages
|
||||
*/
|
||||
const typeName = (value: NixValue): string => {
|
||||
const val = force(value);
|
||||
|
||||
if (typeof val === "bigint") return "int";
|
||||
if (typeof val === "number") return "float";
|
||||
if (typeof val === "boolean") return "boolean";
|
||||
if (typeof val === "string") return "string";
|
||||
if (isStringWithContext(val)) return "string";
|
||||
if (val === null) return "null";
|
||||
if (Array.isArray(val)) return "list";
|
||||
if (typeof val === "function") return "lambda";
|
||||
if (typeof val === "object") return "attribute set";
|
||||
|
||||
return `unknown type`;
|
||||
};
|
||||
|
||||
export interface CoerceResult {
|
||||
value: string;
|
||||
context: NixStringContext;
|
||||
@@ -196,7 +178,7 @@ export const coerceToString = (
|
||||
}
|
||||
|
||||
// Attribute sets without __toString or outPath cannot be coerced
|
||||
throw new TypeError(`cannot coerce ${typeName(v)} to a string`);
|
||||
throw new TypeError(`cannot coerce ${typeOf(v)} to a string`);
|
||||
}
|
||||
|
||||
// Integer coercion is allowed in Interpolation and ToString modes
|
||||
@@ -264,7 +246,7 @@ export const coerceToString = (
|
||||
}
|
||||
}
|
||||
|
||||
throw new TypeError(`cannot coerce ${typeName(v)} to a string`);
|
||||
throw new TypeError(`cannot coerce ${typeOf(v)} to a string`);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { NixValue, NixAttrs } from "../types";
|
||||
import { forceString, forceList } from "../type-assert";
|
||||
import { forceStringValue, forceList } from "../type-assert";
|
||||
import { force } from "../thunk";
|
||||
import { type DerivationData, type OutputInfo, generateAterm } from "../derivation-helpers";
|
||||
import { coerceToString, StringCoercionMode } from "./conversion";
|
||||
@@ -25,7 +25,7 @@ const validateName = (attrs: NixAttrs): string => {
|
||||
if (!("name" in attrs)) {
|
||||
throw new Error("derivation: missing required attribute 'name'");
|
||||
}
|
||||
const name = forceString(attrs.name);
|
||||
const name = forceStringValue(attrs.name);
|
||||
if (!name) {
|
||||
throw new Error("derivation: 'name' cannot be empty");
|
||||
}
|
||||
@@ -46,7 +46,7 @@ const validateSystem = (attrs: NixAttrs): string => {
|
||||
if (!("system" in attrs)) {
|
||||
throw new Error("derivation: missing required attribute 'system'");
|
||||
}
|
||||
return forceString(attrs.system);
|
||||
return forceStringValue(attrs.system);
|
||||
};
|
||||
|
||||
const extractOutputs = (attrs: NixAttrs): string[] => {
|
||||
@@ -54,7 +54,7 @@ const extractOutputs = (attrs: NixAttrs): string[] => {
|
||||
return ["out"];
|
||||
}
|
||||
const outputsList = forceList(attrs.outputs);
|
||||
const outputs = outputsList.map((o) => forceString(o));
|
||||
const outputs = outputsList.map((o) => forceStringValue(o));
|
||||
|
||||
if (outputs.length === 0) {
|
||||
throw new Error("derivation: outputs list cannot be empty");
|
||||
@@ -141,9 +141,9 @@ const extractFixedOutputInfo = (attrs: NixAttrs): FixedOutputInfo | null => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hash = forceString(attrs.outputHash);
|
||||
const hashAlgo = "outputHashAlgo" in attrs ? forceString(attrs.outputHashAlgo) : "sha256";
|
||||
const hashMode = "outputHashMode" in attrs ? forceString(attrs.outputHashMode) : "flat";
|
||||
const hash = forceStringValue(attrs.outputHash);
|
||||
const hashAlgo = "outputHashAlgo" in attrs ? forceStringValue(attrs.outputHashAlgo) : "sha256";
|
||||
const hashMode = "outputHashMode" in attrs ? forceStringValue(attrs.outputHashMode) : "flat";
|
||||
|
||||
if (hashMode !== "flat" && hashMode !== "recursive") {
|
||||
throw new Error(`derivation: invalid outputHashMode '${hashMode}' (must be 'flat' or 'recursive')`);
|
||||
|
||||
@@ -37,8 +37,10 @@ export const throwFunc = (s: NixValue): never => {
|
||||
throw new CatchableError(coerceToString(s, StringCoercionMode.Base));
|
||||
};
|
||||
|
||||
export const trace = (e1: NixValue, e2: NixValue): NixValue => {
|
||||
console.log(`trace: ${force(e1)}`);
|
||||
export const trace =
|
||||
(e1: NixValue) =>
|
||||
(e2: NixValue): NixValue => {
|
||||
console.log(`trace: ${coerceToString(e1, StringCoercionMode.Base)}`);
|
||||
return e2;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,22 @@
|
||||
* Combines all builtin function categories into the global `builtins` object
|
||||
*/
|
||||
|
||||
import { createThunk } from "../thunk";
|
||||
// Import all builtin categories
|
||||
import * as arithmetic from "./arithmetic";
|
||||
import * as math from "./math";
|
||||
import * as typeCheck from "./type-check";
|
||||
import * as list from "./list";
|
||||
import * as attrs from "./attrs";
|
||||
import * as string from "./string";
|
||||
import * as pathOps from "./path";
|
||||
import * as functional from "./functional";
|
||||
import * as io from "./io";
|
||||
import * as conversion from "./conversion";
|
||||
import * as misc from "./misc";
|
||||
import * as derivation from "./derivation";
|
||||
|
||||
import type { NixValue } from "../types";
|
||||
import { createThunk, force } from "../thunk";
|
||||
|
||||
/**
|
||||
* Symbol used to mark functions as primops (primitive operations)
|
||||
@@ -33,23 +48,23 @@ export interface PrimopMetadata {
|
||||
* @param applied - Number of arguments already applied (default: 0)
|
||||
* @returns The marked function
|
||||
*/
|
||||
export const mkPrimop = <T extends Function>(
|
||||
func: T,
|
||||
export const mkPrimop = (
|
||||
func: (...args: NixValue[]) => NixValue,
|
||||
name: string,
|
||||
arity: number,
|
||||
applied: number = 0,
|
||||
): T => {
|
||||
): Function => {
|
||||
// Mark this function as a primop
|
||||
(func as any)[PRIMOP_METADATA] = {
|
||||
name,
|
||||
arity,
|
||||
applied,
|
||||
} as PrimopMetadata;
|
||||
} satisfies PrimopMetadata;
|
||||
|
||||
// If this is a curried function and not fully applied,
|
||||
// wrap it to mark the next layer too
|
||||
if (applied < arity - 1) {
|
||||
const wrappedFunc = ((...args: any[]) => {
|
||||
const wrappedFunc = ((...args: NixValue[]) => {
|
||||
const result = func(...args);
|
||||
// If result is a function, mark it as the next layer
|
||||
if (typeof result === "function") {
|
||||
@@ -63,9 +78,9 @@ export const mkPrimop = <T extends Function>(
|
||||
name,
|
||||
arity,
|
||||
applied,
|
||||
} as PrimopMetadata;
|
||||
} satisfies PrimopMetadata;
|
||||
|
||||
return wrappedFunc as T;
|
||||
return wrappedFunc;
|
||||
}
|
||||
|
||||
return func;
|
||||
@@ -97,20 +112,6 @@ export const get_primop_metadata = (func: unknown): PrimopMetadata | undefined =
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Import all builtin categories
|
||||
import * as arithmetic from "./arithmetic";
|
||||
import * as math from "./math";
|
||||
import * as typeCheck from "./type-check";
|
||||
import * as list from "./list";
|
||||
import * as attrs from "./attrs";
|
||||
import * as string from "./string";
|
||||
import * as pathOps from "./path";
|
||||
import * as functional from "./functional";
|
||||
import * as io from "./io";
|
||||
import * as conversion from "./conversion";
|
||||
import * as misc from "./misc";
|
||||
import * as derivation from "./derivation";
|
||||
|
||||
/**
|
||||
* The global builtins object
|
||||
* Contains 80+ Nix builtin functions plus metadata
|
||||
@@ -134,16 +135,16 @@ export const builtins: any = {
|
||||
ceil: mkPrimop(math.ceil, "ceil", 1),
|
||||
floor: mkPrimop(math.floor, "floor", 1),
|
||||
|
||||
isAttrs: mkPrimop(typeCheck.isAttrs, "isAttrs", 1),
|
||||
isBool: mkPrimop(typeCheck.isBool, "isBool", 1),
|
||||
isFloat: mkPrimop(typeCheck.isFloat, "isFloat", 1),
|
||||
isFunction: mkPrimop(typeCheck.isFunction, "isFunction", 1),
|
||||
isInt: mkPrimop(typeCheck.isInt, "isInt", 1),
|
||||
isList: mkPrimop(typeCheck.isList, "isList", 1),
|
||||
isNull: mkPrimop(typeCheck.isNull, "isNull", 1),
|
||||
isPath: mkPrimop(typeCheck.isPath, "isPath", 1),
|
||||
isString: mkPrimop(typeCheck.isString, "isString", 1),
|
||||
typeOf: mkPrimop(typeCheck.typeOf, "typeOf", 1),
|
||||
isAttrs: mkPrimop((e: NixValue) => typeCheck.isAttrs(force(e)), "isAttrs", 1),
|
||||
isBool: mkPrimop((e: NixValue) => typeCheck.isBool(force(e)), "isBool", 1),
|
||||
isFloat: mkPrimop((e: NixValue) => typeCheck.isFloat(force(e)), "isFloat", 1),
|
||||
isFunction: mkPrimop((e: NixValue) => typeCheck.isFunction(force(e)), "isFunction", 1),
|
||||
isInt: mkPrimop((e: NixValue) => typeCheck.isInt(force(e)), "isInt", 1),
|
||||
isList: mkPrimop((e: NixValue) => typeCheck.isList(force(e)), "isList", 1),
|
||||
isNull: mkPrimop((e: NixValue) => typeCheck.isNull(force(e)), "isNull", 1),
|
||||
isPath: mkPrimop((e: NixValue) => typeCheck.isPath(force(e)), "isPath", 1),
|
||||
isString: mkPrimop((e: NixValue) => typeCheck.isString(force(e)), "isString", 1),
|
||||
typeOf: mkPrimop((e: NixValue) => typeCheck.typeOf(force(e)), "typeOf", 1),
|
||||
|
||||
map: mkPrimop(list.map, "map", 2),
|
||||
filter: mkPrimop(list.filter, "filter", 2),
|
||||
@@ -261,5 +262,5 @@ export const builtins: any = {
|
||||
langVersion: 6,
|
||||
nixPath: [],
|
||||
nixVersion: "2.31.2",
|
||||
storeDir: "/home/imxyy/.cache/nix-js/fetchers/store",
|
||||
storeDir: "INVALID_PATH",
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Implemented via Rust ops exposed through deno_core
|
||||
*/
|
||||
|
||||
import { forceAttrs, forceBool, forceString } from "../type-assert";
|
||||
import { forceAttrs, forceBool, forceStringValue } from "../type-assert";
|
||||
import type { NixValue, NixAttrs } from "../types";
|
||||
import { isNixPath } from "../types";
|
||||
import { force } from "../thunk";
|
||||
@@ -92,10 +92,14 @@ const normalizeUrlInput = (
|
||||
return { url: forced };
|
||||
}
|
||||
const attrs = forceAttrs(args);
|
||||
const url = forceString(attrs.url);
|
||||
const url = forceStringValue(attrs.url);
|
||||
const hash =
|
||||
"sha256" in attrs ? forceString(attrs.sha256) : "hash" in attrs ? forceString(attrs.hash) : undefined;
|
||||
const name = "name" in attrs ? forceString(attrs.name) : undefined;
|
||||
"sha256" in attrs
|
||||
? forceStringValue(attrs.sha256)
|
||||
: "hash" in attrs
|
||||
? forceStringValue(attrs.hash)
|
||||
: undefined;
|
||||
const name = "name" in attrs ? forceStringValue(attrs.name) : undefined;
|
||||
const executable = "executable" in attrs ? forceBool(attrs.executable) : false;
|
||||
return { url, hash, name, executable };
|
||||
};
|
||||
@@ -108,15 +112,15 @@ const normalizeTarballInput = (
|
||||
return { url: forced };
|
||||
}
|
||||
const attrs = forceAttrs(args);
|
||||
const url = forceString(attrs.url);
|
||||
const hash = "hash" in attrs ? forceString(attrs.hash) : undefined;
|
||||
const url = forceStringValue(attrs.url);
|
||||
const hash = "hash" in attrs ? forceStringValue(attrs.hash) : undefined;
|
||||
const narHash =
|
||||
"narHash" in attrs
|
||||
? forceString(attrs.narHash)
|
||||
? forceStringValue(attrs.narHash)
|
||||
: "sha256" in attrs
|
||||
? forceString(attrs.sha256)
|
||||
? forceStringValue(attrs.sha256)
|
||||
: undefined;
|
||||
const name = "name" in attrs ? forceString(attrs.name) : undefined;
|
||||
const name = "name" in attrs ? forceStringValue(attrs.name) : undefined;
|
||||
return { url, hash, narHash, name };
|
||||
};
|
||||
|
||||
@@ -159,13 +163,13 @@ export const fetchGit = (args: NixValue): NixAttrs => {
|
||||
};
|
||||
}
|
||||
const attrs = forceAttrs(args);
|
||||
const url = forceString(attrs.url);
|
||||
const gitRef = "ref" in attrs ? forceString(attrs.ref) : null;
|
||||
const rev = "rev" in attrs ? forceString(attrs.rev) : null;
|
||||
const url = forceStringValue(attrs.url);
|
||||
const gitRef = "ref" in attrs ? forceStringValue(attrs.ref) : null;
|
||||
const rev = "rev" in attrs ? forceStringValue(attrs.rev) : null;
|
||||
const shallow = "shallow" in attrs ? forceBool(attrs.shallow) : false;
|
||||
const submodules = "submodules" in attrs ? forceBool(attrs.submodules) : false;
|
||||
const allRefs = "allRefs" in attrs ? forceBool(attrs.allRefs) : false;
|
||||
const name = "name" in attrs ? forceString(attrs.name) : null;
|
||||
const name = "name" in attrs ? forceStringValue(attrs.name) : null;
|
||||
|
||||
const result: FetchGitResult = Deno.core.ops.op_fetch_git(
|
||||
url,
|
||||
@@ -191,9 +195,9 @@ export const fetchGit = (args: NixValue): NixAttrs => {
|
||||
|
||||
export const fetchMercurial = (args: NixValue): NixAttrs => {
|
||||
const attrs = forceAttrs(args);
|
||||
const url = forceString(attrs.url);
|
||||
const rev = "rev" in attrs ? forceString(attrs.rev) : null;
|
||||
const name = "name" in attrs ? forceString(attrs.name) : null;
|
||||
const url = forceStringValue(attrs.url);
|
||||
const rev = "rev" in attrs ? forceStringValue(attrs.rev) : null;
|
||||
const name = "name" in attrs ? forceStringValue(attrs.name) : null;
|
||||
|
||||
const result: FetchHgResult = Deno.core.ops.op_fetch_hg(url, rev, name);
|
||||
|
||||
@@ -208,7 +212,7 @@ export const fetchMercurial = (args: NixValue): NixAttrs => {
|
||||
|
||||
export const fetchTree = (args: NixValue): NixAttrs => {
|
||||
const attrs = forceAttrs(args);
|
||||
const type = "type" in attrs ? forceString(attrs.type) : "auto";
|
||||
const type = "type" in attrs ? forceStringValue(attrs.type) : "auto";
|
||||
|
||||
switch (type) {
|
||||
case "git":
|
||||
@@ -221,7 +225,7 @@ export const fetchTree = (args: NixValue): NixAttrs => {
|
||||
case "file":
|
||||
return { outPath: fetchurl(args) };
|
||||
case "path": {
|
||||
const path = forceString(attrs.path);
|
||||
const path = forceStringValue(attrs.path);
|
||||
return { outPath: path };
|
||||
}
|
||||
case "github":
|
||||
@@ -235,10 +239,11 @@ export const fetchTree = (args: NixValue): NixAttrs => {
|
||||
};
|
||||
|
||||
const fetchGitForge = (forge: string, attrs: NixAttrs): NixAttrs => {
|
||||
const owner = forceString(attrs.owner);
|
||||
const repo = forceString(attrs.repo);
|
||||
const rev = "rev" in attrs ? forceString(attrs.rev) : "ref" in attrs ? forceString(attrs.ref) : "HEAD";
|
||||
const host = "host" in attrs ? forceString(attrs.host) : undefined;
|
||||
const owner = forceStringValue(attrs.owner);
|
||||
const repo = forceStringValue(attrs.repo);
|
||||
const rev =
|
||||
"rev" in attrs ? forceStringValue(attrs.rev) : "ref" in attrs ? forceStringValue(attrs.ref) : "HEAD";
|
||||
const host = "host" in attrs ? forceStringValue(attrs.host) : undefined;
|
||||
|
||||
let tarballUrl: string;
|
||||
switch (forge) {
|
||||
@@ -271,7 +276,7 @@ const fetchGitForge = (forge: string, attrs: NixAttrs): NixAttrs => {
|
||||
};
|
||||
|
||||
const autoDetectAndFetch = (attrs: NixAttrs): NixAttrs => {
|
||||
const url = forceString(attrs.url);
|
||||
const url = forceStringValue(attrs.url);
|
||||
if (url.endsWith(".git") || url.includes("github.com") || url.includes("gitlab.com")) {
|
||||
return fetchGit(attrs);
|
||||
}
|
||||
@@ -332,17 +337,17 @@ export const path = (args: NixValue): string => {
|
||||
if (isNixPath(pathValue)) {
|
||||
pathStr = getPathValue(pathValue);
|
||||
} else {
|
||||
pathStr = forceString(pathValue);
|
||||
pathStr = forceStringValue(pathValue);
|
||||
}
|
||||
|
||||
// Optional: name parameter (defaults to basename in Rust)
|
||||
const name = "name" in attrs ? forceString(attrs.name) : null;
|
||||
const name = "name" in attrs ? forceStringValue(attrs.name) : null;
|
||||
|
||||
// Optional: recursive parameter (default: true)
|
||||
const recursive = "recursive" in attrs ? forceBool(attrs.recursive) : true;
|
||||
|
||||
// Optional: sha256 parameter
|
||||
const sha256 = "sha256" in attrs ? forceString(attrs.sha256) : null;
|
||||
const sha256 = "sha256" in attrs ? forceStringValue(attrs.sha256) : null;
|
||||
|
||||
// TODO: Handle filter parameter
|
||||
if ("filter" in attrs) {
|
||||
@@ -358,7 +363,7 @@ export const path = (args: NixValue): string => {
|
||||
export const toFile =
|
||||
(nameArg: NixValue) =>
|
||||
(contentsArg: NixValue): StringWithContext => {
|
||||
const name = forceString(nameArg);
|
||||
const name = forceStringValue(nameArg);
|
||||
|
||||
if (name.includes("/")) {
|
||||
throw new Error("builtins.toFile: name cannot contain '/'");
|
||||
@@ -391,6 +396,6 @@ export const findFile =
|
||||
throw new Error("Not implemented: findFile");
|
||||
};
|
||||
|
||||
export const getEnv = (s: NixValue): never => {
|
||||
throw new Error("Not implemented: getEnv");
|
||||
export const getEnv = (s: NixValue): string => {
|
||||
return Deno.core.ops.op_get_env(forceStringValue(s));
|
||||
};
|
||||
|
||||
@@ -5,17 +5,30 @@
|
||||
|
||||
import type { NixValue, NixList, NixAttrs } from "../types";
|
||||
import { force } from "../thunk";
|
||||
import { forceList, forceFunction, forceInt } from "../type-assert";
|
||||
import { forceList, forceFunction, forceInt, forceBool } from "../type-assert";
|
||||
import { op } from "../operators";
|
||||
|
||||
export const map =
|
||||
(f: NixValue) =>
|
||||
(list: NixValue): NixList =>
|
||||
forceList(list).map(forceFunction(f));
|
||||
(list: NixValue): NixList => {
|
||||
const forcedList = forceList(list);
|
||||
if (forcedList.length) {
|
||||
const func = forceFunction(f);
|
||||
return forcedList.map(func);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const filter =
|
||||
(f: NixValue) =>
|
||||
(list: NixValue): NixList =>
|
||||
forceList(list).filter(forceFunction(f));
|
||||
(list: NixValue): NixList => {
|
||||
const forcedList = forceList(list);
|
||||
if (forcedList.length) {
|
||||
const func = forceFunction(f);
|
||||
return forcedList.filter((e) => forceBool(func(e)));
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const length = (e: NixValue): bigint => {
|
||||
const forced = force(e);
|
||||
@@ -30,7 +43,7 @@ export const tail = (list: NixValue): NixList => forceList(list).slice(1);
|
||||
export const elem =
|
||||
(x: NixValue) =>
|
||||
(xs: NixValue): boolean =>
|
||||
forceList(xs).includes(force(x));
|
||||
forceList(xs).find((e) => op.eq(x, e)) !== undefined;
|
||||
|
||||
export const elemAt =
|
||||
(xs: NixValue) =>
|
||||
@@ -116,10 +129,22 @@ export const genList =
|
||||
|
||||
export const all =
|
||||
(pred: NixValue) =>
|
||||
(list: NixValue): boolean =>
|
||||
forceList(list).every(forceFunction(pred));
|
||||
(list: NixValue): boolean => {
|
||||
const forcedList = forceList(list);
|
||||
if (forcedList.length) {
|
||||
const f = forceFunction(pred);
|
||||
return forcedList.every((e) => forceBool(f(e)));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const any =
|
||||
(pred: NixValue) =>
|
||||
(list: NixValue): boolean =>
|
||||
forceList(list).some(forceFunction(pred));
|
||||
(list: NixValue): boolean => {
|
||||
const forcedList = forceList(list);
|
||||
if (forcedList.length) {
|
||||
const f = forceFunction(pred);
|
||||
return forcedList.some((e) => forceBool(f(e)));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -4,9 +4,19 @@
|
||||
|
||||
import { force } from "../thunk";
|
||||
import { CatchableError } from "../types";
|
||||
import type { NixBool, NixStrictValue, NixValue } from "../types";
|
||||
import { forceList, forceAttrs, forceFunction, forceString } from "../type-assert";
|
||||
import type { NixAttrs, NixBool, NixStrictValue, NixValue } from "../types";
|
||||
import { forceList, forceAttrs, forceFunction, forceStringValue, forceString } from "../type-assert";
|
||||
import * as context from "./context";
|
||||
import { compareValues, op } from "../operators";
|
||||
import { isBool, isFloat, isInt, isList, isString, typeOf } from "./type-check";
|
||||
import { OrderedSet } from "js-sdsl";
|
||||
import {
|
||||
type NixStringContext,
|
||||
getStringValue,
|
||||
getStringContext,
|
||||
mkStringWithContext,
|
||||
mergeContexts,
|
||||
} from "../string-context";
|
||||
|
||||
export const addErrorContext =
|
||||
(e1: NixValue) =>
|
||||
@@ -49,8 +59,8 @@ export const addDrvOutputDependencies = context.addDrvOutputDependencies;
|
||||
export const compareVersions =
|
||||
(s1: NixValue) =>
|
||||
(s2: NixValue): NixValue => {
|
||||
const str1 = forceString(s1);
|
||||
const str2 = forceString(s2);
|
||||
const str1 = forceStringValue(s1);
|
||||
const str2 = forceStringValue(s2);
|
||||
|
||||
let i1 = 0;
|
||||
let i2 = 0;
|
||||
@@ -148,12 +158,68 @@ export const flakeRefToString = (attrs: NixValue): never => {
|
||||
throw new Error("Not implemented: flakeRefToString");
|
||||
};
|
||||
|
||||
export const functionArgs = (f: NixValue): never => {
|
||||
throw new Error("Not implemented: functionArgs");
|
||||
export const functionArgs = (f: NixValue): NixAttrs => {
|
||||
const func = forceFunction(f);
|
||||
if (func.args) {
|
||||
const ret: NixAttrs = {};
|
||||
for (const key of func.args!.required) {
|
||||
ret[key] = false;
|
||||
}
|
||||
for (const key of func.args!.optional) {
|
||||
ret[key] = true;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const genericClosure = (args: NixValue): never => {
|
||||
throw new Error("Not implemented: genericClosure");
|
||||
const checkComparable = (value: NixStrictValue): void => {
|
||||
if (isString(value) || isInt(value) || isFloat(value) || isBool(value) || isList(value)) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`Unsupported key type for genericClosure: ${typeOf(value)}`);
|
||||
};
|
||||
|
||||
export const genericClosure = (args: NixValue): NixValue => {
|
||||
const forcedArgs = forceAttrs(args);
|
||||
const { startSet, operator } = forcedArgs;
|
||||
|
||||
const initialList = forceList(startSet);
|
||||
const opFunction = forceFunction(operator);
|
||||
|
||||
const resultSet = new OrderedSet<NixStrictValue>(undefined, compareValues);
|
||||
const resultList: NixStrictValue[] = [];
|
||||
const queue: NixStrictValue[] = [];
|
||||
|
||||
for (const item of initialList) {
|
||||
const itemAttrs = forceAttrs(item);
|
||||
const key = force(itemAttrs.key);
|
||||
checkComparable(key);
|
||||
if (resultSet.find(key).equals(resultSet.end())) {
|
||||
resultSet.insert(key);
|
||||
resultList.push(itemAttrs);
|
||||
queue.push(itemAttrs);
|
||||
}
|
||||
}
|
||||
|
||||
let head = 0;
|
||||
while (head < queue.length) {
|
||||
const currentItem = queue[head++];
|
||||
const newItems = forceList(opFunction(currentItem));
|
||||
|
||||
for (const newItem of newItems) {
|
||||
const newItemAttrs = forceAttrs(newItem);
|
||||
const key = force(newItemAttrs.key);
|
||||
checkComparable(key);
|
||||
if (resultSet.find(key).equals(resultSet.end())) {
|
||||
resultSet.insert(key);
|
||||
resultList.push(newItemAttrs);
|
||||
queue.push(newItemAttrs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultList;
|
||||
};
|
||||
|
||||
export const getFlake = (attrs: NixValue): never => {
|
||||
@@ -189,35 +255,45 @@ export const replaceStrings =
|
||||
const fromList = forceList(from);
|
||||
const toList = forceList(to);
|
||||
const inputStr = forceString(s);
|
||||
const inputStrValue = getStringValue(inputStr);
|
||||
const resultContext: NixStringContext = getStringContext(inputStr);
|
||||
|
||||
if (fromList.length !== toList.length) {
|
||||
throw new Error("'from' and 'to' arguments passed to builtins.replaceStrings have different lengths");
|
||||
}
|
||||
|
||||
const toCache = new Map<number, string>();
|
||||
const toContextCache = new Map<number, NixStringContext>();
|
||||
|
||||
let result = "";
|
||||
let pos = 0;
|
||||
|
||||
while (pos <= inputStr.length) {
|
||||
while (pos <= inputStrValue.length) {
|
||||
let found = false;
|
||||
|
||||
for (let i = 0; i < fromList.length; i++) {
|
||||
const pattern = forceString(fromList[i]);
|
||||
const pattern = forceStringValue(fromList[i]);
|
||||
|
||||
if (inputStr.substring(pos).startsWith(pattern)) {
|
||||
if (inputStrValue.substring(pos).startsWith(pattern)) {
|
||||
found = true;
|
||||
|
||||
if (!toCache.has(i)) {
|
||||
toCache.set(i, forceString(toList[i]));
|
||||
const replacementStr = forceString(toList[i]);
|
||||
const replacementValue = getStringValue(replacementStr);
|
||||
const replacementContext = getStringContext(replacementStr);
|
||||
toCache.set(i, replacementValue);
|
||||
toContextCache.set(i, replacementContext);
|
||||
for (const elem of replacementContext) {
|
||||
resultContext.add(elem);
|
||||
}
|
||||
}
|
||||
const replacement = toCache.get(i)!;
|
||||
|
||||
result += replacement;
|
||||
|
||||
if (pattern.length === 0) {
|
||||
if (pos < inputStr.length) {
|
||||
result += inputStr[pos];
|
||||
if (pos < inputStrValue.length) {
|
||||
result += inputStrValue[pos];
|
||||
}
|
||||
pos++;
|
||||
} else {
|
||||
@@ -228,18 +304,21 @@ export const replaceStrings =
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
if (pos < inputStr.length) {
|
||||
result += inputStr[pos];
|
||||
if (pos < inputStrValue.length) {
|
||||
result += inputStrValue[pos];
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
if (resultContext.size === 0) {
|
||||
return result;
|
||||
}
|
||||
return mkStringWithContext(result, resultContext);
|
||||
};
|
||||
|
||||
export const splitVersion = (s: NixValue): NixValue => {
|
||||
const version = forceString(s);
|
||||
const version = forceStringValue(s);
|
||||
const components: string[] = [];
|
||||
let idx = 0;
|
||||
|
||||
|
||||
@@ -13,28 +13,82 @@ import { mkStringWithContext, type NixStringContext } from "../string-context";
|
||||
* builtins.baseNameOf
|
||||
* Get the last component of a path or string
|
||||
* Always returns a string (coerces paths)
|
||||
* Preserves string context if present
|
||||
*
|
||||
* Implements Nix's legacyBaseNameOf logic:
|
||||
* - If string ends with '/', removes only the final slash
|
||||
* - Then returns everything after the last remaining '/'
|
||||
*
|
||||
* Examples:
|
||||
* - baseNameOf ./foo/bar → "bar"
|
||||
* - baseNameOf "/foo/bar/" → "bar"
|
||||
* - baseNameOf "/foo/bar/" → "bar" (trailing slash removed first)
|
||||
* - baseNameOf "foo" → "foo"
|
||||
*/
|
||||
export const baseNameOf = (s: NixValue): string => {
|
||||
export const baseNameOf = (s: NixValue): NixString => {
|
||||
const forced = force(s);
|
||||
|
||||
let pathStr: string;
|
||||
// Path input → string output (no context)
|
||||
if (isNixPath(forced)) {
|
||||
pathStr = forced.value;
|
||||
const pathStr = forced.value;
|
||||
|
||||
if (pathStr.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let last = pathStr.length - 1;
|
||||
if (pathStr[last] === "/" && last > 0) {
|
||||
last -= 1;
|
||||
}
|
||||
|
||||
let pos = last;
|
||||
while (pos >= 0 && pathStr[pos] !== "/") {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if (pos === -1) {
|
||||
pos = 0;
|
||||
} else {
|
||||
pathStr = coerceToString(s, StringCoercionMode.Base, false) as string;
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
const lastSlash = pathStr.lastIndexOf("/");
|
||||
if (lastSlash === -1) {
|
||||
return pathStr;
|
||||
return pathStr.substring(pos, last + 1);
|
||||
}
|
||||
|
||||
return pathStr.slice(lastSlash + 1);
|
||||
// String input → string output (preserve context)
|
||||
const context: NixStringContext = new Set();
|
||||
const pathStr = coerceToString(s, StringCoercionMode.Base, false, context);
|
||||
|
||||
if (pathStr.length === 0) {
|
||||
if (context.size === 0) {
|
||||
return "";
|
||||
}
|
||||
return mkStringWithContext("", context);
|
||||
}
|
||||
|
||||
let last = pathStr.length - 1;
|
||||
if (pathStr[last] === "/" && last > 0) {
|
||||
last -= 1;
|
||||
}
|
||||
|
||||
let pos = last;
|
||||
while (pos >= 0 && pathStr[pos] !== "/") {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if (pos === -1) {
|
||||
pos = 0;
|
||||
} else {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
const result = pathStr.substring(pos, last + 1);
|
||||
|
||||
// Preserve string context if present
|
||||
if (context.size > 0) {
|
||||
return mkStringWithContext(result, context);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import type { NixInt, NixValue, NixString } from "../types";
|
||||
import { forceString, forceList, forceInt, forceNixString } from "../type-assert";
|
||||
import { forceStringValue, forceList, forceInt, forceString } from "../type-assert";
|
||||
import { coerceToString, StringCoercionMode } from "./conversion";
|
||||
import {
|
||||
type NixStringContext,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
mkStringWithContext,
|
||||
} from "../string-context";
|
||||
|
||||
export const stringLength = (e: NixValue): NixInt => BigInt(forceString(e).length);
|
||||
export const stringLength = (e: NixValue): NixInt => BigInt(forceStringValue(e).length);
|
||||
|
||||
/**
|
||||
* builtins.substring - Extract substring while preserving string context
|
||||
@@ -35,7 +35,7 @@ export const substring =
|
||||
throw new Error("negative start position in 'substring'");
|
||||
}
|
||||
|
||||
const str = forceNixString(s);
|
||||
const str = forceString(s);
|
||||
const strValue = getStringValue(str);
|
||||
const context = getStringContext(str);
|
||||
|
||||
@@ -80,23 +80,6 @@ export const concatStringsSep =
|
||||
return mkStringWithContext(result, context);
|
||||
};
|
||||
|
||||
export const baseNameOf = (x: NixValue): string => {
|
||||
const str = forceString(x);
|
||||
if (str.length === 0) return "";
|
||||
|
||||
let last = str.length - 1;
|
||||
if (str[last] === "/" && last > 0) last -= 1;
|
||||
|
||||
let pos = last;
|
||||
while (pos >= 0 && str[pos] !== "/") pos -= 1;
|
||||
|
||||
if (pos !== 0 || (pos === 0 && str[pos] === "/")) {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
return str.substring(pos, last + 1);
|
||||
};
|
||||
|
||||
const POSIX_CLASSES: Record<string, string> = {
|
||||
alnum: "a-zA-Z0-9",
|
||||
alpha: "a-zA-Z",
|
||||
@@ -152,8 +135,8 @@ function posixToJsRegex(pattern: string, fullMatch: boolean = false): RegExp {
|
||||
export const match =
|
||||
(regex: NixValue) =>
|
||||
(str: NixValue): NixValue => {
|
||||
const regexStr = forceString(regex);
|
||||
const inputStr = forceString(str);
|
||||
const regexStr = forceStringValue(regex);
|
||||
const inputStr = forceStringValue(str);
|
||||
|
||||
try {
|
||||
const re = posixToJsRegex(regexStr, true);
|
||||
@@ -177,8 +160,9 @@ export const match =
|
||||
export const split =
|
||||
(regex: NixValue) =>
|
||||
(str: NixValue): NixValue => {
|
||||
const regexStr = forceString(regex);
|
||||
const regexStr = forceStringValue(regex);
|
||||
const inputStr = forceString(str);
|
||||
const inputStrValue = getStringValue(inputStr);
|
||||
|
||||
try {
|
||||
const re = posixToJsRegex(regexStr);
|
||||
@@ -188,8 +172,8 @@ export const split =
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = reGlobal.exec(inputStr)) !== null) {
|
||||
result.push(inputStr.substring(lastIndex, match.index));
|
||||
while ((match = reGlobal.exec(inputStrValue)) !== null) {
|
||||
result.push(inputStrValue.substring(lastIndex, match.index));
|
||||
|
||||
const groups: NixValue[] = [];
|
||||
for (let i = 1; i < match.length; i++) {
|
||||
@@ -208,7 +192,7 @@ export const split =
|
||||
return [inputStr];
|
||||
}
|
||||
|
||||
result.push(inputStr.substring(lastIndex));
|
||||
result.push(inputStrValue.substring(lastIndex));
|
||||
return result;
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid regular expression '${regexStr}': ${e}`);
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
import {
|
||||
HAS_CONTEXT,
|
||||
isNixPath,
|
||||
NixPath,
|
||||
isStringWithContext,
|
||||
type NixPath,
|
||||
type NixAttrs,
|
||||
type NixBool,
|
||||
type NixFloat,
|
||||
@@ -14,52 +15,61 @@ import {
|
||||
type NixList,
|
||||
type NixNull,
|
||||
type NixString,
|
||||
type NixValue,
|
||||
type NixStrictValue,
|
||||
} from "../types";
|
||||
import { force } from "../thunk";
|
||||
|
||||
export const isAttrs = (e: NixValue): e is NixAttrs => {
|
||||
const val = force(e);
|
||||
return typeof val === "object" && !Array.isArray(val) && val !== null && !(HAS_CONTEXT in val);
|
||||
/**
|
||||
* Check if a value is a Nix string (plain string or StringWithContext)
|
||||
* This works on already-forced values (NixStrictValue).
|
||||
*/
|
||||
export const isNixString = (v: NixStrictValue): v is NixString => {
|
||||
return typeof v === "string" || isStringWithContext(v);
|
||||
};
|
||||
|
||||
export const isBool = (e: NixValue): e is NixBool => typeof force(e) === "boolean";
|
||||
export const isAttrs = (e: NixStrictValue): e is NixAttrs => {
|
||||
const val = e;
|
||||
return (
|
||||
typeof val === "object" && !Array.isArray(val) && val !== null && !(HAS_CONTEXT in val) && !isPath(val)
|
||||
);
|
||||
};
|
||||
|
||||
export const isFloat = (e: NixValue): e is NixFloat => {
|
||||
const val = force(e);
|
||||
export const isBool = (e: NixStrictValue): e is NixBool => typeof e === "boolean";
|
||||
|
||||
export const isFloat = (e: NixStrictValue): e is NixFloat => {
|
||||
const val = e;
|
||||
return typeof val === "number"; // Only number is float
|
||||
};
|
||||
|
||||
export const isFunction = (e: NixValue): e is NixFunction => typeof force(e) === "function";
|
||||
export const isFunction = (e: NixStrictValue): e is NixFunction => typeof e === "function";
|
||||
|
||||
export const isInt = (e: NixValue): e is NixInt => {
|
||||
const val = force(e);
|
||||
export const isInt = (e: NixStrictValue): e is NixInt => {
|
||||
const val = e;
|
||||
return typeof val === "bigint"; // Only bigint is int
|
||||
};
|
||||
|
||||
export const isList = (e: NixValue): e is NixList => Array.isArray(force(e));
|
||||
export const isList = (e: NixStrictValue): e is NixList => Array.isArray(e);
|
||||
|
||||
export const isNull = (e: NixValue): e is NixNull => force(e) === null;
|
||||
export const isNull = (e: NixStrictValue): e is NixNull => e === null;
|
||||
|
||||
export const isPath = (e: NixValue): e is NixPath => {
|
||||
const val = force(e);
|
||||
export const isPath = (e: NixStrictValue): e is NixPath => {
|
||||
const val = e;
|
||||
return isNixPath(val);
|
||||
};
|
||||
|
||||
export const isString = (e: NixValue): e is NixString => typeof force(e) === "string";
|
||||
export const isString = (e: NixStrictValue): e is NixString =>
|
||||
typeof e === "string" || isStringWithContext(e);
|
||||
|
||||
export const typeOf = (e: NixValue): string => {
|
||||
const val = force(e);
|
||||
export type NixType = "int" | "float" | "bool" | "string" | "path" | "null" | "list" | "lambda" | "set";
|
||||
export const typeOf = (e: NixStrictValue): NixType => {
|
||||
if (typeof e === "bigint") return "int";
|
||||
if (typeof e === "number") return "float";
|
||||
if (typeof e === "boolean") return "bool";
|
||||
if (e === null) return "null";
|
||||
if (isNixString(e)) return "string";
|
||||
if (isNixPath(e)) return "path";
|
||||
if (Array.isArray(e)) return "list";
|
||||
if (typeof e === "object") return "set";
|
||||
if (typeof e === "function") return "lambda";
|
||||
|
||||
if (isNixPath(val)) return "path";
|
||||
if (typeof val === "bigint") return "int";
|
||||
if (typeof val === "number") return "float";
|
||||
if (typeof val === "boolean") return "bool";
|
||||
if (typeof val === "string") return "string";
|
||||
if (val === null) return "null";
|
||||
if (Array.isArray(val)) return "list";
|
||||
if (typeof val === "function") return "lambda";
|
||||
if (typeof val === "object") return "set";
|
||||
|
||||
throw new TypeError(`Unknown Nix type: ${typeof val}`);
|
||||
throw new TypeError(`Unknown Nix type: ${typeof e}`);
|
||||
};
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
*/
|
||||
|
||||
import type { NixValue, NixAttrs, NixBool, NixString, NixPath } from "./types";
|
||||
import { forceAttrs, forceBool, forceFunction, forceString, typeName } from "./type-assert";
|
||||
import { isAttrs } from "./builtins/type-check";
|
||||
import { forceAttrs, forceBool, forceFunction, forceStringValue } from "./type-assert";
|
||||
import { isAttrs, typeOf } from "./builtins/type-check";
|
||||
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||
import { type NixStringContext, mkStringWithContext, isStringWithContext } from "./string-context";
|
||||
import { force } from "./thunk";
|
||||
@@ -29,11 +29,11 @@ function enrichError(error: unknown): Error {
|
||||
}
|
||||
|
||||
const nixStackLines = callStack.map((frame) => {
|
||||
return `NIX_STACK_FRAME:context:${frame.span}:${frame.message}`;
|
||||
return `NIX_STACK_FRAME:${frame.span}:${frame.message}`;
|
||||
});
|
||||
|
||||
// Prepend stack frames to error stack
|
||||
err.stack = `${nixStackLines.join('\n')}\n${err.stack || ''}`;
|
||||
err.stack = `${nixStackLines.join("\n")}\n${err.stack || ""}`;
|
||||
|
||||
return err;
|
||||
}
|
||||
@@ -169,16 +169,16 @@ export const concatStringsWithContext = (parts: NixValue[]): NixString | NixPath
|
||||
* @returns NixPath object with absolute path
|
||||
*/
|
||||
export const resolvePath = (currentDir: string, path: NixValue): NixPath => {
|
||||
const pathStr = forceString(path);
|
||||
const pathStr = forceStringValue(path);
|
||||
const resolved = Deno.core.ops.op_resolve_path(currentDir, pathStr);
|
||||
return mkPath(resolved);
|
||||
};
|
||||
|
||||
export const select = (obj: NixValue, attrpath: NixValue[], span?: string): NixValue => {
|
||||
if (STACK_TRACE.enabled && span) {
|
||||
const pathStrings = attrpath.map(a => forceString(a));
|
||||
const path = pathStrings.join('.');
|
||||
const message = path ? `while selecting attribute [${path}]` : 'while selecting attribute';
|
||||
const pathStrings = attrpath.map((a) => forceStringValue(a));
|
||||
const path = pathStrings.join(".");
|
||||
const message = path ? `while selecting attribute [${path}]` : "while selecting attribute";
|
||||
|
||||
if (callStack.length >= MAX_STACK_DEPTH) {
|
||||
callStack.shift();
|
||||
@@ -200,26 +200,31 @@ function select_impl(obj: NixValue, attrpath: NixValue[]): NixValue {
|
||||
let attrs = forceAttrs(obj);
|
||||
|
||||
for (const attr of attrpath.slice(0, -1)) {
|
||||
const key = forceString(attr);
|
||||
const key = forceStringValue(attr);
|
||||
if (!(key in attrs)) {
|
||||
throw new Error(`Attribute '${key}' not found`);
|
||||
}
|
||||
const cur = forceAttrs(attrs[forceString(attr)]);
|
||||
const cur = forceAttrs(attrs[forceStringValue(attr)]);
|
||||
attrs = cur;
|
||||
}
|
||||
|
||||
const last = forceString(attrpath[attrpath.length - 1]);
|
||||
const last = forceStringValue(attrpath[attrpath.length - 1]);
|
||||
if (!(last in attrs)) {
|
||||
throw new Error(`Attribute '${last}' not found`);
|
||||
}
|
||||
return attrs[last];
|
||||
}
|
||||
|
||||
export const selectWithDefault = (obj: NixValue, attrpath: NixValue[], default_val: NixValue, span?: string): NixValue => {
|
||||
export const selectWithDefault = (
|
||||
obj: NixValue,
|
||||
attrpath: NixValue[],
|
||||
default_val: NixValue,
|
||||
span?: string,
|
||||
): NixValue => {
|
||||
if (STACK_TRACE.enabled && span) {
|
||||
const pathStrings = attrpath.map(a => forceString(a));
|
||||
const path = pathStrings.join('.');
|
||||
const message = path ? `while selecting attribute [${path}]` : 'while selecting attribute';
|
||||
const pathStrings = attrpath.map((a) => forceStringValue(a));
|
||||
const path = pathStrings.join(".");
|
||||
const message = path ? `while selecting attribute [${path}]` : "while selecting attribute";
|
||||
|
||||
if (callStack.length >= MAX_STACK_DEPTH) {
|
||||
callStack.shift();
|
||||
@@ -241,7 +246,7 @@ function selectWithDefault_impl(obj: NixValue, attrpath: NixValue[], default_val
|
||||
let attrs = forceAttrs(obj);
|
||||
|
||||
for (const attr of attrpath.slice(0, -1)) {
|
||||
const key = forceString(attr);
|
||||
const key = forceStringValue(attr);
|
||||
if (!(key in attrs)) {
|
||||
return default_val;
|
||||
}
|
||||
@@ -252,7 +257,7 @@ function selectWithDefault_impl(obj: NixValue, attrpath: NixValue[], default_val
|
||||
attrs = cur;
|
||||
}
|
||||
|
||||
const last = forceString(attrpath[attrpath.length - 1]);
|
||||
const last = forceStringValue(attrpath[attrpath.length - 1]);
|
||||
if (last in attrs) {
|
||||
return attrs[last];
|
||||
}
|
||||
@@ -260,20 +265,21 @@ function selectWithDefault_impl(obj: NixValue, attrpath: NixValue[], default_val
|
||||
}
|
||||
|
||||
export const hasAttr = (obj: NixValue, attrpath: NixValue[]): NixBool => {
|
||||
if (!isAttrs(obj)) {
|
||||
const forced = force(obj);
|
||||
if (!isAttrs(forced)) {
|
||||
return false;
|
||||
}
|
||||
let attrs = obj;
|
||||
let attrs = forced;
|
||||
|
||||
for (const attr of attrpath.slice(0, -1)) {
|
||||
const cur = force(attrs[forceString(attr)]);
|
||||
const cur = force(attrs[forceStringValue(attr)]);
|
||||
if (!isAttrs(cur)) {
|
||||
return false;
|
||||
}
|
||||
attrs = cur;
|
||||
}
|
||||
|
||||
return forceString(attrpath[attrpath.length - 1]) in attrs;
|
||||
return forceStringValue(attrpath[attrpath.length - 1]) in attrs;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -324,7 +330,7 @@ export const call = (func: NixValue, arg: NixValue, span?: string): NixValue =>
|
||||
if (callStack.length >= MAX_STACK_DEPTH) {
|
||||
callStack.shift();
|
||||
}
|
||||
callStack.push({ span, message: 'from call site' });
|
||||
callStack.push({ span, message: "from call site" });
|
||||
try {
|
||||
return call_impl(func, arg);
|
||||
} catch (error) {
|
||||
@@ -340,6 +346,7 @@ export const call = (func: NixValue, arg: NixValue, span?: string): NixValue =>
|
||||
function call_impl(func: NixValue, arg: NixValue): NixValue {
|
||||
const forcedFunc = force(func);
|
||||
if (typeof forcedFunc === "function") {
|
||||
forcedFunc.args?.check(arg);
|
||||
return forcedFunc(arg);
|
||||
}
|
||||
if (
|
||||
@@ -349,9 +356,9 @@ function call_impl(func: NixValue, arg: NixValue): NixValue {
|
||||
"__functor" in forcedFunc
|
||||
) {
|
||||
const functor = forceFunction(forcedFunc.__functor);
|
||||
return forceFunction(functor(forcedFunc))(arg);
|
||||
return call(functor(forcedFunc), arg);
|
||||
}
|
||||
throw new Error(`attempt to call something which is not a function but ${typeName(forcedFunc)}`);
|
||||
throw new Error(`attempt to call something which is not a function but ${typeOf(forcedFunc)}`);
|
||||
}
|
||||
|
||||
export const assert = (assertion: NixValue, expr: NixValue, assertionRaw: string, span: string): NixValue => {
|
||||
|
||||
@@ -23,7 +23,8 @@ import { op } from "./operators";
|
||||
import { builtins, PRIMOP_METADATA } from "./builtins";
|
||||
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||
import { HAS_CONTEXT } from "./string-context";
|
||||
import { IS_PATH } from "./types";
|
||||
import { IS_PATH, mkFunction } from "./types";
|
||||
import { forceBool } from "./type-assert";
|
||||
|
||||
export type NixRuntime = typeof Nix;
|
||||
|
||||
@@ -33,6 +34,7 @@ export type NixRuntime = typeof Nix;
|
||||
export const Nix = {
|
||||
createThunk,
|
||||
force,
|
||||
forceBool,
|
||||
isThunk,
|
||||
IS_THUNK,
|
||||
HAS_CONTEXT,
|
||||
@@ -50,6 +52,7 @@ export const Nix = {
|
||||
coerceToString,
|
||||
concatStringsWithContext,
|
||||
StringCoercionMode,
|
||||
mkFunction,
|
||||
|
||||
pushContext,
|
||||
popContext,
|
||||
|
||||
@@ -4,16 +4,13 @@
|
||||
*/
|
||||
|
||||
import type { NixValue, NixList, NixAttrs, NixString, NixPath } from "./types";
|
||||
import { isStringWithContext, isNixPath } from "./types";
|
||||
import { isNixPath } from "./types";
|
||||
import { force } from "./thunk";
|
||||
import { forceNumeric, forceList, forceAttrs, coerceNumeric } from "./type-assert";
|
||||
import { getStringValue, getStringContext, mergeContexts, mkStringWithContext } from "./string-context";
|
||||
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||
import { mkPath } from "./path";
|
||||
|
||||
const isNixString = (v: unknown): v is NixString => {
|
||||
return typeof v === "string" || isStringWithContext(v);
|
||||
};
|
||||
import { typeOf, isNixString } from "./builtins/type-check";
|
||||
|
||||
const canCoerceToString = (v: NixValue): boolean => {
|
||||
const forced = force(v);
|
||||
@@ -24,6 +21,73 @@ const canCoerceToString = (v: NixValue): boolean => {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare two values, similar to Nix's CompareValues.
|
||||
* Returns: -1 if a < b, 0 if a == b, 1 if a > b
|
||||
* Throws TypeError for incomparable types.
|
||||
*/
|
||||
export const compareValues = (a: NixValue, b: NixValue): -1 | 0 | 1 => {
|
||||
const av = force(a);
|
||||
const bv = force(b);
|
||||
|
||||
// Handle float/int mixed comparisons
|
||||
if (typeof av === "number" && typeof bv === "bigint") {
|
||||
const cmp = av - Number(bv);
|
||||
return cmp < 0 ? -1 : cmp > 0 ? 1 : 0;
|
||||
}
|
||||
if (typeof av === "bigint" && typeof bv === "number") {
|
||||
const cmp = Number(av) - bv;
|
||||
return cmp < 0 ? -1 : cmp > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
const typeA = typeOf(av);
|
||||
const typeB = typeOf(bv);
|
||||
|
||||
// Types must match (except float/int which is handled above)
|
||||
if (typeA !== typeB) {
|
||||
throw new TypeError(`cannot compare ${typeOf(av)} with ${typeOf(bv)}`);
|
||||
}
|
||||
|
||||
// Int and float comparison
|
||||
if (typeA === "int" || typeA === "float") {
|
||||
return av! < bv! ? -1 : av === bv ? 0 : 1;
|
||||
}
|
||||
|
||||
// String comparison (handles both plain strings and StringWithContext)
|
||||
if (typeA === "string") {
|
||||
const strA = getStringValue(av as NixString);
|
||||
const strB = getStringValue(bv as NixString);
|
||||
return strA < strB ? -1 : strA > strB ? 1 : 0;
|
||||
}
|
||||
|
||||
// Path comparison
|
||||
if (typeA === "path") {
|
||||
const aPath = av as NixPath;
|
||||
const bPath = bv as NixPath;
|
||||
return aPath.value < bPath.value ? -1 : aPath.value > bPath.value ? 1 : 0;
|
||||
}
|
||||
|
||||
// List comparison (lexicographic)
|
||||
if (typeA === "list") {
|
||||
const aList = av as NixList;
|
||||
const bList = bv as NixList;
|
||||
for (let i = 0; ; i++) {
|
||||
if (i === bList.length) {
|
||||
return i === aList.length ? 0 : 1; // Equal if same length, else aList > bList
|
||||
} else if (i === aList.length) {
|
||||
return -1; // aList < bList
|
||||
} else if (!op.eq(aList[i], bList[i])) {
|
||||
return compareValues(aList[i], bList[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Other types are incomparable
|
||||
throw new TypeError(
|
||||
`cannot compare ${typeOf(av)} with ${typeOf(bv)}; values of that type are incomparable`,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Operator object exported as Nix.op
|
||||
* All operators referenced by codegen (e.g., Nix.op.add, Nix.op.eq)
|
||||
@@ -116,17 +180,10 @@ export const op = {
|
||||
const av = force(a);
|
||||
const bv = force(b);
|
||||
|
||||
// Path comparison
|
||||
if (isNixPath(av) && isNixPath(bv)) {
|
||||
return av.value === bv.value;
|
||||
}
|
||||
// Pointer equality
|
||||
if (av === bv) return true;
|
||||
|
||||
// String comparison
|
||||
if (isNixString(av) && isNixString(bv)) {
|
||||
return getStringValue(av) === getStringValue(bv);
|
||||
}
|
||||
|
||||
// Numeric comparison with type coercion
|
||||
// Special case: int == float type compatibility
|
||||
if (typeof av === "bigint" && typeof bv === "number") {
|
||||
return Number(av) === bv;
|
||||
}
|
||||
@@ -134,7 +191,27 @@ export const op = {
|
||||
return av === Number(bv);
|
||||
}
|
||||
|
||||
// List comparison
|
||||
// Get type names for comparison (skip if already handled above)
|
||||
const typeA = typeOf(av);
|
||||
const typeB = typeOf(bv);
|
||||
|
||||
// All other types must match exactly
|
||||
if (typeA !== typeB) return false;
|
||||
|
||||
if (typeA === "int" || typeA === "float" || typeA === "bool" || typeA === "null") {
|
||||
return av === bv;
|
||||
}
|
||||
|
||||
// String comparison (handles both plain strings and StringWithContext)
|
||||
if (typeA === "string") {
|
||||
return getStringValue(av as NixString) === getStringValue(bv as NixString);
|
||||
}
|
||||
|
||||
// Path comparison
|
||||
if (typeA === "path") {
|
||||
return (av as NixPath).value === (bv as NixPath).value;
|
||||
}
|
||||
|
||||
if (Array.isArray(av) && Array.isArray(bv)) {
|
||||
if (av.length !== bv.length) return false;
|
||||
for (let i = 0; i < av.length; i++) {
|
||||
@@ -143,105 +220,55 @@ export const op = {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Attrset comparison
|
||||
if (
|
||||
typeof av === "object" &&
|
||||
av !== null &&
|
||||
!Array.isArray(av) &&
|
||||
typeof bv === "object" &&
|
||||
bv !== null &&
|
||||
!Array.isArray(bv) &&
|
||||
!isNixString(av) &&
|
||||
!isNixString(bv) &&
|
||||
!isNixPath(av) &&
|
||||
!isNixPath(bv)
|
||||
) {
|
||||
const keysA = Object.keys(av);
|
||||
const keysB = Object.keys(bv);
|
||||
if (keysA.length !== keysB.length) return false;
|
||||
for (const key of keysA) {
|
||||
if (!(key in bv)) return false;
|
||||
if (!op.eq((av as NixAttrs)[key], (bv as NixAttrs)[key])) return false;
|
||||
if (typeA === "set") {
|
||||
const attrsA = av as NixAttrs;
|
||||
const attrsB = bv as NixAttrs;
|
||||
|
||||
// If both denote a derivation (type = "derivation"), compare their outPaths
|
||||
const isDerivationA = "type" in attrsA && force(attrsA.type) === "derivation";
|
||||
const isDerivationB = "type" in attrsB && force(attrsB.type) === "derivation";
|
||||
|
||||
if (isDerivationA && isDerivationB) {
|
||||
if ("outPath" in attrsA && "outPath" in attrsB) {
|
||||
return op.eq(attrsA.outPath, attrsB.outPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, compare attributes one by one
|
||||
const keysA = Object.keys(attrsA).sort();
|
||||
const keysB = Object.keys(attrsB).sort();
|
||||
|
||||
if (keysA.length !== keysB.length) return false;
|
||||
|
||||
for (let i = 0; i < keysA.length; i++) {
|
||||
if (keysA[i] !== keysB[i]) return false;
|
||||
if (!op.eq(attrsA[keysA[i]], attrsB[keysB[i]])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return av === bv;
|
||||
// Functions are incomparable
|
||||
if (typeof av === "function") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
neq: (a: NixValue, b: NixValue): boolean => {
|
||||
return !op.eq(a, b);
|
||||
},
|
||||
lt: (a: NixValue, b: NixValue): boolean => {
|
||||
const av = force(a);
|
||||
const bv = force(b);
|
||||
|
||||
// Path comparison
|
||||
if (isNixPath(av) && isNixPath(bv)) {
|
||||
return av.value < bv.value;
|
||||
}
|
||||
|
||||
// String comparison
|
||||
if (isNixString(av) && isNixString(bv)) {
|
||||
return getStringValue(av) < getStringValue(bv);
|
||||
}
|
||||
|
||||
// Numeric comparison
|
||||
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||
return (numA as any) < (numB as any);
|
||||
return compareValues(a, b) < 0;
|
||||
},
|
||||
lte: (a: NixValue, b: NixValue): boolean => {
|
||||
const av = force(a);
|
||||
const bv = force(b);
|
||||
|
||||
// Path comparison
|
||||
if (isNixPath(av) && isNixPath(bv)) {
|
||||
return av.value <= bv.value;
|
||||
}
|
||||
|
||||
// String comparison
|
||||
if (isNixString(av) && isNixString(bv)) {
|
||||
return getStringValue(av) <= getStringValue(bv);
|
||||
}
|
||||
|
||||
// Numeric comparison
|
||||
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||
return (numA as any) <= (numB as any);
|
||||
return compareValues(a, b) <= 0;
|
||||
},
|
||||
gt: (a: NixValue, b: NixValue): boolean => {
|
||||
const av = force(a);
|
||||
const bv = force(b);
|
||||
|
||||
// Path comparison
|
||||
if (isNixPath(av) && isNixPath(bv)) {
|
||||
return av.value > bv.value;
|
||||
}
|
||||
|
||||
// String comparison
|
||||
if (isNixString(av) && isNixString(bv)) {
|
||||
return getStringValue(av) > getStringValue(bv);
|
||||
}
|
||||
|
||||
// Numeric comparison
|
||||
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||
return (numA as any) > (numB as any);
|
||||
return compareValues(a, b) > 0;
|
||||
},
|
||||
gte: (a: NixValue, b: NixValue): boolean => {
|
||||
const av = force(a);
|
||||
const bv = force(b);
|
||||
|
||||
// Path comparison
|
||||
if (isNixPath(av) && isNixPath(bv)) {
|
||||
return av.value >= bv.value;
|
||||
}
|
||||
|
||||
// String comparison
|
||||
if (isNixString(av) && isNixString(bv)) {
|
||||
return getStringValue(av) >= getStringValue(bv);
|
||||
}
|
||||
|
||||
// Numeric comparison
|
||||
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||
return (numA as any) >= (numB as any);
|
||||
return compareValues(a, b) >= 0;
|
||||
},
|
||||
|
||||
bnot: (a: NixValue): boolean => !force(a),
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
* This implementation matches Lix's NixStringContext system.
|
||||
*/
|
||||
|
||||
import { NixStrictValue } from "./types";
|
||||
|
||||
export const HAS_CONTEXT = Symbol("HAS_CONTEXT");
|
||||
|
||||
export interface StringContextOpaque {
|
||||
@@ -51,10 +53,8 @@ export interface StringWithContext {
|
||||
context: NixStringContext;
|
||||
}
|
||||
|
||||
export const isStringWithContext = (v: unknown): v is StringWithContext => {
|
||||
return (
|
||||
typeof v === "object" && v !== null && HAS_CONTEXT in v && (v as StringWithContext)[HAS_CONTEXT] === true
|
||||
);
|
||||
export const isStringWithContext = (v: NixStrictValue): v is StringWithContext => {
|
||||
return typeof v === "object" && v !== null && HAS_CONTEXT in v;
|
||||
};
|
||||
|
||||
export const mkStringWithContext = (value: string, context: NixStringContext): StringWithContext => {
|
||||
|
||||
@@ -17,23 +17,7 @@ import type {
|
||||
import { isStringWithContext, isNixPath } from "./types";
|
||||
import { force } from "./thunk";
|
||||
import { getStringValue } from "./string-context";
|
||||
|
||||
export const typeName = (value: NixValue): string => {
|
||||
const val = force(value);
|
||||
|
||||
if (isNixPath(val)) return "path";
|
||||
if (typeof val === "bigint") return "int";
|
||||
if (typeof val === "number") return "float";
|
||||
if (typeof val === "boolean") return "boolean";
|
||||
if (typeof val === "string") return "string";
|
||||
if (isStringWithContext(val)) return "string";
|
||||
if (val === null) return "null";
|
||||
if (Array.isArray(val)) return "list";
|
||||
if (typeof val === "function") return "lambda";
|
||||
if (typeof val === "object") return "attribute set";
|
||||
|
||||
throw new TypeError(`Unknown Nix type: ${typeof val}`);
|
||||
};
|
||||
import { isAttrs, isFunction, typeOf } from "./builtins/type-check";
|
||||
|
||||
/**
|
||||
* Force a value and assert it's a list
|
||||
@@ -42,7 +26,7 @@ export const typeName = (value: NixValue): string => {
|
||||
export const forceList = (value: NixValue): NixList => {
|
||||
const forced = force(value);
|
||||
if (!Array.isArray(forced)) {
|
||||
throw new TypeError(`Expected list, got ${typeName(forced)}`);
|
||||
throw new TypeError(`Expected list, got ${typeOf(forced)}`);
|
||||
}
|
||||
return forced;
|
||||
};
|
||||
@@ -53,8 +37,8 @@ export const forceList = (value: NixValue): NixList => {
|
||||
*/
|
||||
export const forceFunction = (value: NixValue): NixFunction => {
|
||||
const forced = force(value);
|
||||
if (typeof forced !== "function") {
|
||||
throw new TypeError(`Expected function, got ${typeName(forced)}`);
|
||||
if (!isFunction(forced)) {
|
||||
throw new TypeError(`Expected function, got ${typeOf(forced)}`);
|
||||
}
|
||||
return forced;
|
||||
};
|
||||
@@ -65,14 +49,8 @@ export const forceFunction = (value: NixValue): NixFunction => {
|
||||
*/
|
||||
export const forceAttrs = (value: NixValue): NixAttrs => {
|
||||
const forced = force(value);
|
||||
if (
|
||||
typeof forced !== "object" ||
|
||||
Array.isArray(forced) ||
|
||||
forced === null ||
|
||||
isStringWithContext(forced) ||
|
||||
isNixPath(forced)
|
||||
) {
|
||||
throw new TypeError(`Expected attribute set, got ${typeName(forced)}`);
|
||||
if (!isAttrs(forced)) {
|
||||
throw new TypeError(`Expected attribute set, got ${typeOf(forced)}`);
|
||||
}
|
||||
return forced;
|
||||
};
|
||||
@@ -81,7 +59,7 @@ export const forceAttrs = (value: NixValue): NixAttrs => {
|
||||
* Force a value and assert it's a string (plain or with context)
|
||||
* @throws TypeError if value is not a string after forcing
|
||||
*/
|
||||
export const forceString = (value: NixValue): string => {
|
||||
export const forceStringValue = (value: NixValue): string => {
|
||||
const forced = force(value);
|
||||
if (typeof forced === "string") {
|
||||
return forced;
|
||||
@@ -89,14 +67,14 @@ export const forceString = (value: NixValue): string => {
|
||||
if (isStringWithContext(forced)) {
|
||||
return forced.value;
|
||||
}
|
||||
throw new TypeError(`Expected string, got ${typeName(forced)}`);
|
||||
throw new TypeError(`Expected string, got ${typeOf(forced)}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Force a value and assert it's a string, returning NixString (preserving context)
|
||||
* @throws TypeError if value is not a string after forcing
|
||||
*/
|
||||
export const forceNixString = (value: NixValue): NixString => {
|
||||
export const forceString = (value: NixValue): NixString => {
|
||||
const forced = force(value);
|
||||
if (typeof forced === "string") {
|
||||
return forced;
|
||||
@@ -104,14 +82,7 @@ export const forceNixString = (value: NixValue): NixString => {
|
||||
if (isStringWithContext(forced)) {
|
||||
return forced;
|
||||
}
|
||||
throw new TypeError(`Expected string, got ${typeName(forced)}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the plain string value from any NixString
|
||||
*/
|
||||
export const nixStringValue = (s: NixString): string => {
|
||||
return getStringValue(s);
|
||||
throw new TypeError(`Expected string, got ${typeOf(forced)}`);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -121,7 +92,7 @@ export const nixStringValue = (s: NixString): string => {
|
||||
export const forceBool = (value: NixValue): boolean => {
|
||||
const forced = force(value);
|
||||
if (typeof forced !== "boolean") {
|
||||
throw new TypeError(`Expected boolean, got ${typeName(forced)}`);
|
||||
throw new TypeError(`Expected boolean, got ${typeOf(forced)}`);
|
||||
}
|
||||
return forced;
|
||||
};
|
||||
@@ -135,7 +106,7 @@ export const forceInt = (value: NixValue): NixInt => {
|
||||
if (typeof forced === "bigint") {
|
||||
return forced;
|
||||
}
|
||||
throw new TypeError(`Expected int, got ${typeName(forced)}`);
|
||||
throw new TypeError(`Expected int, got ${typeOf(forced)}`);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -147,7 +118,7 @@ export const forceFloat = (value: NixValue): NixFloat => {
|
||||
if (typeof forced === "number") {
|
||||
return forced;
|
||||
}
|
||||
throw new TypeError(`Expected float, got ${typeName(forced)}`);
|
||||
throw new TypeError(`Expected float, got ${typeOf(forced)}`);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -159,7 +130,7 @@ export const forceNumeric = (value: NixValue): NixNumber => {
|
||||
if (typeof forced === "bigint" || typeof forced === "number") {
|
||||
return forced;
|
||||
}
|
||||
throw new TypeError(`Expected numeric type, got ${typeName(forced)}`);
|
||||
throw new TypeError(`Expected numeric type, got ${typeOf(forced)}`);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -173,7 +144,7 @@ export const coerceNumeric = (a: NixNumber, b: NixNumber): [NixFloat, NixFloat]
|
||||
|
||||
// If either is float, convert both to float
|
||||
if (!aIsInt || !bIsInt) {
|
||||
return [aIsInt ? Number(a) : a, bIsInt ? Number(b) : b];
|
||||
return [Number(a), Number(b)];
|
||||
}
|
||||
|
||||
// Both are integers
|
||||
@@ -189,5 +160,5 @@ export const forceNixPath = (value: NixValue): NixPath => {
|
||||
if (isNixPath(forced)) {
|
||||
return forced;
|
||||
}
|
||||
throw new TypeError(`Expected path, got ${typeName(forced)}`);
|
||||
throw new TypeError(`Expected path, got ${typeOf(forced)}`);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
import { IS_THUNK } from "./thunk";
|
||||
import { type StringWithContext, HAS_CONTEXT, isStringWithContext } from "./string-context";
|
||||
import { op } from "./operators";
|
||||
import { forceAttrs } from "./type-assert";
|
||||
export { HAS_CONTEXT, isStringWithContext };
|
||||
export type { StringWithContext };
|
||||
|
||||
@@ -15,7 +17,7 @@ export interface NixPath {
|
||||
}
|
||||
|
||||
export const isNixPath = (v: NixStrictValue): v is NixPath => {
|
||||
return typeof v === "object" && v !== null && IS_PATH in v && (v as NixPath)[IS_PATH] === true;
|
||||
return typeof v === "object" && v !== null && IS_PATH in v;
|
||||
};
|
||||
|
||||
// Nix primitive types
|
||||
@@ -28,8 +30,43 @@ export type NixNull = null;
|
||||
|
||||
// Nix composite types
|
||||
export type NixList = NixValue[];
|
||||
// FIXME: reject contextful string
|
||||
export type NixAttrs = { [key: string]: NixValue };
|
||||
export type NixFunction = (arg: NixValue) => NixValue;
|
||||
export type NixFunction = ((arg: NixValue) => NixValue) & { args?: NixArgs };
|
||||
export class NixArgs {
|
||||
required: string[];
|
||||
optional: string[];
|
||||
allowed: Set<string>;
|
||||
ellipsis: boolean;
|
||||
constructor(required: string[], optional: string[], ellipsis: boolean) {
|
||||
this.required = required;
|
||||
this.optional = optional;
|
||||
this.ellipsis = ellipsis;
|
||||
this.allowed = new Set(required.concat(optional));
|
||||
}
|
||||
check(arg: NixValue) {
|
||||
const attrs = forceAttrs(arg);
|
||||
|
||||
for (const key of this.required) {
|
||||
if (!Object.hasOwn(attrs, key)) {
|
||||
throw new Error(`Function called without required argument '${key}'`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.ellipsis) {
|
||||
for (const key in attrs) {
|
||||
if (!this.allowed.has(key)) {
|
||||
throw new Error(`Function called with unexpected argument '${key}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export const mkFunction = (f: (arg: NixValue) => NixValue, required: string[], optional: string[], ellipsis: boolean): NixFunction => {
|
||||
const func = f as NixFunction;
|
||||
func.args = new NixArgs(required, optional, ellipsis);
|
||||
return func
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for lazy thunk values
|
||||
|
||||
1
nix-js/runtime-ts/src/types/global.d.ts
vendored
1
nix-js/runtime-ts/src/types/global.d.ts
vendored
@@ -79,6 +79,7 @@ declare global {
|
||||
function op_store_path(path: string): string;
|
||||
function op_to_file(name: string, contents: string, references: string[]): string;
|
||||
function op_copy_path_to_store(path: string): string;
|
||||
function op_get_env(key: string): string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{:?}", miette::Report::new(err));
|
||||
eprintln!("{:?}", miette::Report::new(*err));
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ fn main() -> Result<()> {
|
||||
let src = Source::new_repl(line)?;
|
||||
match context.eval_code(src) {
|
||||
Ok(value) => println!("{value}"),
|
||||
Err(err) => eprintln!("{:?}", miette::Report::new(err)),
|
||||
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,11 @@ pub(crate) fn compile(expr: &Ir, ctx: &impl CodegenContext) -> String {
|
||||
|
||||
let cur_dir = ctx.get_current_dir().display().to_string().escape_quote();
|
||||
format!(
|
||||
"(()=>{{{}const currentDir={};return {}}})()",
|
||||
debug_prefix, cur_dir, code
|
||||
"(()=>{{{}Nix.builtins.storeDir={};const currentDir={};return {}}})()",
|
||||
debug_prefix,
|
||||
ctx.get_store_dir().escape_quote(),
|
||||
cur_dir,
|
||||
code
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,6 +38,8 @@ pub(crate) trait CodegenContext {
|
||||
fn get_ir(&self, id: ExprId) -> &Ir;
|
||||
fn get_sym(&self, id: SymId) -> &str;
|
||||
fn get_current_dir(&self) -> &Path;
|
||||
fn get_store_dir(&self) -> &str;
|
||||
fn get_current_source_id(&self) -> usize;
|
||||
}
|
||||
|
||||
trait EscapeQuote {
|
||||
@@ -60,9 +65,10 @@ impl EscapeQuote for str {
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_span(span: rnix::TextRange) -> String {
|
||||
fn encode_span(span: rnix::TextRange, ctx: &impl CodegenContext) -> String {
|
||||
format!(
|
||||
"\"{}:{}\"",
|
||||
"\"{}:{}:{}\"",
|
||||
ctx.get_current_source_id(),
|
||||
usize::from(span.start()),
|
||||
usize::from(span.end())
|
||||
)
|
||||
@@ -93,13 +99,13 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
|
||||
|
||||
// Only add context tracking if STACK_TRACE is enabled
|
||||
if std::env::var("NIX_JS_STACK_TRACE").is_ok() {
|
||||
let cond_span = encode_span(ctx.get_ir(cond).span());
|
||||
let cond_span = encode_span(ctx.get_ir(cond).span(), ctx);
|
||||
format!(
|
||||
"(Nix.withContext(\"while evaluating a branch condition\",{},()=>({})))?({}):({})",
|
||||
"(Nix.withContext(\"while evaluating a branch condition\",{},()=>Nix.forceBool({})))?({}):({})",
|
||||
cond_span, cond_code, consq, alter
|
||||
)
|
||||
} else {
|
||||
format!("({cond_code})?({consq}):({alter})")
|
||||
format!("Nix.forceBool({cond_code})?({consq}):({alter})")
|
||||
}
|
||||
}
|
||||
Ir::BinOp(x) => x.compile(ctx),
|
||||
@@ -135,8 +141,8 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
|
||||
|
||||
// Only add context tracking if STACK_TRACE is enabled
|
||||
if std::env::var("NIX_JS_STACK_TRACE").is_ok() {
|
||||
let assertion_span = encode_span(ctx.get_ir(assertion).span());
|
||||
let span = encode_span(span);
|
||||
let assertion_span = encode_span(ctx.get_ir(assertion).span(), ctx);
|
||||
let span = encode_span(span, ctx);
|
||||
format!(
|
||||
"Nix.assert(Nix.withContext(\"while evaluating the condition of the assert statement\",{},()=>({})),{},{},{})",
|
||||
assertion_span,
|
||||
@@ -171,7 +177,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for BinOp {
|
||||
// Helper to wrap operation with context (only if enabled)
|
||||
let with_ctx = |op_name: &str, op_call: String| {
|
||||
if stack_trace_enabled {
|
||||
let span = encode_span(self.span);
|
||||
let span = encode_span(self.span, ctx);
|
||||
format!(
|
||||
"Nix.withContext(\"while evaluating the {} operator\",{},()=>({}))",
|
||||
op_name, span, op_call
|
||||
@@ -193,9 +199,18 @@ impl<Ctx: CodegenContext> Compile<Ctx> for BinOp {
|
||||
Leq => with_ctx("<=", format!("Nix.op.lte({},{})", lhs, rhs)),
|
||||
Geq => with_ctx(">=", format!("Nix.op.gte({},{})", lhs, rhs)),
|
||||
// Short-circuit operators: use JavaScript native && and ||
|
||||
And => with_ctx("&&", format!("Nix.force({})&&Nix.force({})", lhs, rhs)),
|
||||
Or => with_ctx("||", format!("Nix.force({})||Nix.force({})", lhs, rhs)),
|
||||
Impl => with_ctx("->", format!("(!Nix.force({})||Nix.force({}))", lhs, rhs)),
|
||||
And => with_ctx(
|
||||
"&&",
|
||||
format!("Nix.forceBool({})&&Nix.forceBool({})", lhs, rhs),
|
||||
),
|
||||
Or => with_ctx(
|
||||
"||",
|
||||
format!("Nix.forceBool({})||Nix.forceBool({})", lhs, rhs),
|
||||
),
|
||||
Impl => with_ctx(
|
||||
"->",
|
||||
format!("(!Nix.forceBool({})||Nix.forceBool({}))", lhs, rhs),
|
||||
),
|
||||
Con => with_ctx("++", format!("Nix.op.concat({},{})", lhs, rhs)),
|
||||
Upd => with_ctx("//", format!("Nix.op.update({},{})", lhs, rhs)),
|
||||
PipeL => format!("Nix.call({},{})", rhs, lhs),
|
||||
@@ -220,61 +235,28 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Func {
|
||||
let id = ctx.get_ir(self.arg).as_ref().unwrap_arg().inner.0;
|
||||
let body = ctx.get_ir(self.body).compile(ctx);
|
||||
|
||||
// Generate parameter validation code
|
||||
let param_check = self.generate_param_check(ctx);
|
||||
|
||||
if param_check.is_empty() {
|
||||
// Simple function without parameter validation
|
||||
if let Some(Param {
|
||||
required,
|
||||
optional,
|
||||
ellipsis,
|
||||
}) = &self.param
|
||||
{
|
||||
let mut required = required.iter().map(|&sym| ctx.get_sym(sym).escape_quote());
|
||||
let required = format!("[{}]", required.join(","));
|
||||
let mut optional = optional.iter().map(|&sym| ctx.get_sym(sym).escape_quote());
|
||||
let optional = format!("[{}]", optional.join(","));
|
||||
format!("Nix.mkFunction(arg{id}=>({body}),{required},{optional},{ellipsis})")
|
||||
} else {
|
||||
format!("arg{id}=>({body})")
|
||||
} else {
|
||||
// Function with parameter validation (use block statement, not object literal)
|
||||
format!("arg{id}=>{{{}return {}}}", param_check, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Func {
|
||||
fn generate_param_check<Ctx: CodegenContext>(&self, ctx: &Ctx) -> String {
|
||||
let has_checks = self.param.required.is_some() || self.param.allowed.is_some();
|
||||
|
||||
if !has_checks {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let id = ctx.get_ir(self.arg).as_ref().unwrap_arg().inner.0;
|
||||
|
||||
// Build required parameter array
|
||||
let required = if let Some(req) = &self.param.required {
|
||||
let keys: Vec<_> = req
|
||||
.iter()
|
||||
.map(|&sym| ctx.get_sym(sym).escape_quote())
|
||||
.collect();
|
||||
format!("[{}]", keys.join(","))
|
||||
} else {
|
||||
"null".to_string()
|
||||
};
|
||||
|
||||
// Build allowed parameter array
|
||||
let allowed = if let Some(allow) = &self.param.allowed {
|
||||
let keys: Vec<_> = allow
|
||||
.iter()
|
||||
.map(|&sym| ctx.get_sym(sym).escape_quote())
|
||||
.collect();
|
||||
format!("[{}]", keys.join(","))
|
||||
} else {
|
||||
"null".to_string()
|
||||
};
|
||||
|
||||
// Call Nix.validateParams and store the result
|
||||
format!("Nix.validateParams(arg{},{},{});", id, required, allowed)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ctx: CodegenContext> Compile<Ctx> for Call {
|
||||
fn compile(&self, ctx: &Ctx) -> String {
|
||||
let func = ctx.get_ir(self.func).compile(ctx);
|
||||
let arg = ctx.get_ir(self.arg).compile(ctx);
|
||||
let span_str = encode_span(self.span);
|
||||
let span_str = encode_span(self.span, ctx);
|
||||
format!("Nix.call({func},{arg},{span_str})")
|
||||
}
|
||||
}
|
||||
@@ -338,7 +320,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Select {
|
||||
Attr::Dynamic(expr_id) => ctx.get_ir(*expr_id).compile(ctx),
|
||||
})
|
||||
.join(",");
|
||||
let span_str = encode_span(self.span);
|
||||
let span_str = encode_span(self.span, ctx);
|
||||
if let Some(default) = self.default {
|
||||
format!(
|
||||
"Nix.selectWithDefault({lhs},[{attrpath}],{},{span_str})",
|
||||
@@ -360,7 +342,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for AttrSet {
|
||||
let value_code = ctx.get_ir(expr).compile(ctx);
|
||||
|
||||
let value = if stack_trace_enabled {
|
||||
let value_span = encode_span(ctx.get_ir(expr).span());
|
||||
let value_span = encode_span(ctx.get_ir(expr).span(), ctx);
|
||||
format!(
|
||||
"Nix.withContext(\"while evaluating the attribute '{}'\",{},()=>({}))",
|
||||
key, value_span, value_code
|
||||
@@ -377,7 +359,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for AttrSet {
|
||||
let value_code = ctx.get_ir(*value_expr).compile(ctx);
|
||||
|
||||
let value = if stack_trace_enabled {
|
||||
let value_span = encode_span(ctx.get_ir(*value_expr).span());
|
||||
let value_span = encode_span(ctx.get_ir(*value_expr).span(), ctx);
|
||||
format!(
|
||||
"Nix.withContext(\"while evaluating a dynamic attribute\",{},()=>({}))",
|
||||
value_span, value_code
|
||||
@@ -403,7 +385,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for List {
|
||||
.map(|(idx, item)| {
|
||||
let item_code = ctx.get_ir(*item).compile(ctx);
|
||||
if stack_trace_enabled {
|
||||
let item_span = encode_span(ctx.get_ir(*item).span());
|
||||
let item_span = encode_span(ctx.get_ir(*item).span(), ctx);
|
||||
format!(
|
||||
"Nix.withContext(\"while evaluating list element {}\",{},()=>({}))",
|
||||
idx, item_span, item_code
|
||||
@@ -427,7 +409,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for ConcatStrings {
|
||||
.map(|part| {
|
||||
let part_code = ctx.get_ir(*part).compile(ctx);
|
||||
if stack_trace_enabled {
|
||||
let part_span = encode_span(ctx.get_ir(*part).span());
|
||||
let part_span = encode_span(ctx.get_ir(*part).span(), ctx);
|
||||
format!(
|
||||
"Nix.withContext(\"while evaluating a path segment\",{},()=>({}))",
|
||||
part_span, part_code
|
||||
|
||||
@@ -41,15 +41,18 @@ mod private {
|
||||
fn get_current_dir(&self) -> &Path {
|
||||
self.as_ref().get_current_dir()
|
||||
}
|
||||
fn set_current_file(&mut self, source: Source) {
|
||||
self.as_mut().current_file = Some(source);
|
||||
fn add_source(&mut self, source: Source) {
|
||||
self.as_mut().sources.push(source);
|
||||
}
|
||||
fn compile_code(&mut self, source: Source) -> Result<String> {
|
||||
self.as_mut().compile_code(source)
|
||||
}
|
||||
fn get_current_source(&self) -> Option<Source> {
|
||||
fn get_current_source(&self) -> Source {
|
||||
self.as_ref().get_current_source()
|
||||
}
|
||||
fn get_source(&self, id: usize) -> Source {
|
||||
self.as_ref().get_source(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
use private::CtxPtr;
|
||||
@@ -63,32 +66,26 @@ pub(crate) struct SccInfo {
|
||||
pub struct Context {
|
||||
ctx: Ctx,
|
||||
runtime: Runtime<CtxPtr>,
|
||||
store: Arc<StoreBackend>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new() -> Result<Self> {
|
||||
let ctx = Ctx::new();
|
||||
let ctx = Ctx::new()?;
|
||||
let runtime = Runtime::new()?;
|
||||
|
||||
let config = StoreConfig::from_env();
|
||||
let store = Arc::new(StoreBackend::new(config)?);
|
||||
|
||||
Ok(Self {
|
||||
ctx,
|
||||
runtime,
|
||||
store,
|
||||
})
|
||||
Ok(Self { ctx, runtime })
|
||||
}
|
||||
|
||||
pub fn eval_code(&mut self, source: Source) -> Result<Value> {
|
||||
tracing::info!("Starting evaluation");
|
||||
self.ctx.current_file = Some(source.clone());
|
||||
|
||||
tracing::debug!("Compiling code");
|
||||
let code = self.compile_code(source)?;
|
||||
|
||||
self.runtime.op_state().borrow_mut().put(self.store.clone());
|
||||
self.runtime
|
||||
.op_state()
|
||||
.borrow_mut()
|
||||
.put(self.ctx.store.clone());
|
||||
|
||||
tracing::debug!("Executing JavaScript");
|
||||
self.runtime
|
||||
@@ -105,7 +102,7 @@ impl Context {
|
||||
}
|
||||
|
||||
pub fn get_store_dir(&self) -> &str {
|
||||
self.store.as_store().get_store_dir()
|
||||
self.ctx.get_store_dir()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,12 +110,12 @@ pub(crate) struct Ctx {
|
||||
irs: Vec<Ir>,
|
||||
symbols: DefaultStringInterner,
|
||||
global: NonNull<HashMap<SymId, ExprId>>,
|
||||
current_file: Option<Source>,
|
||||
current_source: Option<Source>,
|
||||
sources: Vec<Source>,
|
||||
store: Arc<StoreBackend>,
|
||||
}
|
||||
|
||||
impl Default for Ctx {
|
||||
fn default() -> Self {
|
||||
impl Ctx {
|
||||
fn new() -> Result<Self> {
|
||||
use crate::ir::{Builtins, ToIr as _};
|
||||
|
||||
let mut symbols = DefaultStringInterner::new();
|
||||
@@ -202,41 +199,48 @@ impl Default for Ctx {
|
||||
global.insert(name_sym, id);
|
||||
}
|
||||
|
||||
Self {
|
||||
let config = StoreConfig::from_env();
|
||||
let store = Arc::new(StoreBackend::new(config)?);
|
||||
|
||||
Ok(Self {
|
||||
symbols,
|
||||
irs,
|
||||
global: unsafe { NonNull::new_unchecked(Box::leak(Box::new(global))) },
|
||||
current_file: None,
|
||||
current_source: None,
|
||||
}
|
||||
sources: Vec::new(),
|
||||
store,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Ctx {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(crate) fn downgrade_ctx<'a>(&'a mut self) -> DowngradeCtx<'a> {
|
||||
let global_ref = unsafe { self.global.as_ref() };
|
||||
DowngradeCtx::new(self, global_ref)
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_dir(&self) -> &Path {
|
||||
self.current_file
|
||||
self.sources
|
||||
.last()
|
||||
.as_ref()
|
||||
.expect("current_file is not set")
|
||||
.expect("current_source is not set")
|
||||
.get_dir()
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_source(&self) -> Option<Source> {
|
||||
self.current_source.clone()
|
||||
pub(crate) fn get_current_source(&self) -> Source {
|
||||
self.sources
|
||||
.last()
|
||||
.expect("current_source is not set")
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn get_source(&self, id: usize) -> Source {
|
||||
self.sources.get(id).expect("source not found").clone()
|
||||
}
|
||||
|
||||
fn compile_code(&mut self, source: Source) -> Result<String> {
|
||||
tracing::debug!("Parsing Nix expression");
|
||||
|
||||
self.current_source = Some(source.clone());
|
||||
self.sources.push(source.clone());
|
||||
|
||||
let root = rnix::Root::parse(&source.src);
|
||||
if !root.errors().is_empty() {
|
||||
@@ -267,6 +271,15 @@ impl CodegenContext for Ctx {
|
||||
fn get_current_dir(&self) -> &std::path::Path {
|
||||
self.get_current_dir()
|
||||
}
|
||||
fn get_current_source_id(&self) -> usize {
|
||||
self.sources
|
||||
.len()
|
||||
.checked_sub(1)
|
||||
.expect("current_source not set")
|
||||
}
|
||||
fn get_store_dir(&self) -> &str {
|
||||
self.store.as_store().get_store_dir()
|
||||
}
|
||||
}
|
||||
|
||||
struct DependencyTracker {
|
||||
@@ -431,7 +444,7 @@ impl DowngradeContext for DowngradeCtx<'_> {
|
||||
result.ok_or_else(|| {
|
||||
Error::downgrade_error(format!("'{}' not found", self.get_sym(sym)))
|
||||
.with_span(span)
|
||||
.with_source(self.get_current_source().expect("no source set"))
|
||||
.with_source(self.get_current_source())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -453,17 +466,8 @@ impl DowngradeContext for DowngradeCtx<'_> {
|
||||
.insert(expr);
|
||||
}
|
||||
|
||||
fn get_span(&self, id: ExprId) -> rnix::TextRange {
|
||||
dbg!(id);
|
||||
if id.0 >= self.ctx.irs.len() {
|
||||
return self.ctx.irs.get(id.0).unwrap().span();
|
||||
}
|
||||
let local_id = id.0 - self.ctx.irs.len();
|
||||
self.irs.get(local_id).unwrap().as_ref().unwrap().span()
|
||||
}
|
||||
|
||||
fn get_current_source(&self) -> Option<Source> {
|
||||
self.ctx.current_source.clone()
|
||||
fn get_current_source(&self) -> Source {
|
||||
self.ctx.get_current_source()
|
||||
}
|
||||
|
||||
#[allow(refining_impl_trait)]
|
||||
|
||||
@@ -5,7 +5,9 @@ use std::{
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
use crate::{context::Ctx, runtime::RuntimeContext};
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Box<Error>>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SourceType {
|
||||
@@ -24,7 +26,7 @@ pub struct Source {
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Source {
|
||||
type Error = Error;
|
||||
type Error = Box<Error>;
|
||||
fn try_from(value: &str) -> Result<Self> {
|
||||
Source::new_eval(value.into())
|
||||
}
|
||||
@@ -66,7 +68,10 @@ impl Source {
|
||||
use SourceType::*;
|
||||
match &self.ty {
|
||||
Eval(dir) | Repl(dir) => dir.as_ref(),
|
||||
File(file) => file.as_path().parent().unwrap(),
|
||||
File(file) => file
|
||||
.as_path()
|
||||
.parent()
|
||||
.expect("source file must have a parent dir"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +106,7 @@ pub enum Error {
|
||||
#[label("error occurred here")]
|
||||
span: Option<SourceSpan>,
|
||||
message: String,
|
||||
// #[help]
|
||||
#[help]
|
||||
js_backtrace: Option<String>,
|
||||
},
|
||||
|
||||
@@ -119,91 +124,64 @@ pub enum Error {
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn parse_error(msg: String) -> Self {
|
||||
pub fn parse_error(msg: String) -> Box<Self> {
|
||||
Error::ParseError {
|
||||
src: None,
|
||||
span: None,
|
||||
message: msg,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn downgrade_error(msg: String) -> Self {
|
||||
pub fn downgrade_error(msg: String) -> Box<Self> {
|
||||
Error::DowngradeError {
|
||||
src: None,
|
||||
span: None,
|
||||
message: msg,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn eval_error(msg: String, backtrace: Option<String>) -> Self {
|
||||
pub fn eval_error(msg: String, backtrace: Option<String>) -> Box<Self> {
|
||||
Error::EvalError {
|
||||
src: None,
|
||||
span: None,
|
||||
message: msg,
|
||||
js_backtrace: backtrace,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn internal(msg: String) -> Self {
|
||||
Error::InternalError { message: msg }
|
||||
pub fn internal(msg: String) -> Box<Self> {
|
||||
Error::InternalError { message: msg }.into()
|
||||
}
|
||||
|
||||
pub fn catchable(msg: String) -> Self {
|
||||
Error::Catchable { message: msg }
|
||||
pub fn catchable(msg: String) -> Box<Self> {
|
||||
Error::Catchable { message: msg }.into()
|
||||
}
|
||||
|
||||
pub fn unknown() -> Self {
|
||||
Error::Unknown
|
||||
}
|
||||
|
||||
pub fn with_span(self, span: rnix::TextRange) -> Self {
|
||||
pub fn with_span(mut self: Box<Self>, span: rnix::TextRange) -> Box<Self> {
|
||||
use Error::*;
|
||||
let source_span = Some(text_range_to_source_span(span));
|
||||
match self {
|
||||
Error::ParseError { src, message, .. } => Error::ParseError {
|
||||
src,
|
||||
span: source_span,
|
||||
message,
|
||||
},
|
||||
Error::DowngradeError { src, message, .. } => Error::DowngradeError {
|
||||
src,
|
||||
span: source_span,
|
||||
message,
|
||||
},
|
||||
Error::EvalError {
|
||||
src,
|
||||
message,
|
||||
js_backtrace,
|
||||
..
|
||||
} => Error::EvalError {
|
||||
src,
|
||||
span: source_span,
|
||||
message,
|
||||
js_backtrace,
|
||||
},
|
||||
other => other,
|
||||
}
|
||||
let (ParseError { span, .. } | DowngradeError { span, .. } | EvalError { span, .. }) =
|
||||
self.as_mut()
|
||||
else {
|
||||
return self;
|
||||
};
|
||||
*span = source_span;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_source(self, source: Source) -> Self {
|
||||
let src = Some(source.into());
|
||||
match self {
|
||||
Error::ParseError { span, message, .. } => Error::ParseError { src, span, message },
|
||||
Error::DowngradeError { span, message, .. } => {
|
||||
Error::DowngradeError { src, span, message }
|
||||
}
|
||||
Error::EvalError {
|
||||
span,
|
||||
message,
|
||||
js_backtrace,
|
||||
..
|
||||
} => Error::EvalError {
|
||||
src,
|
||||
span,
|
||||
message,
|
||||
js_backtrace,
|
||||
},
|
||||
other => other,
|
||||
}
|
||||
pub fn with_source(mut self: Box<Self>, source: Source) -> Box<Self> {
|
||||
use Error::*;
|
||||
let new_src = Some(source.into());
|
||||
let (ParseError { src, .. } | DowngradeError { src, .. } | EvalError { src, .. }) =
|
||||
self.as_mut()
|
||||
else {
|
||||
return self;
|
||||
};
|
||||
*src = new_src;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,27 +196,27 @@ pub fn text_range_to_source_span(range: rnix::TextRange) -> SourceSpan {
|
||||
pub(crate) struct NixStackFrame {
|
||||
pub span: rnix::TextRange,
|
||||
pub message: String,
|
||||
pub source: Source,
|
||||
}
|
||||
|
||||
/// Parse Nix stack trace from V8 Error.stack
|
||||
/// Returns vector of stack frames (in order from oldest to newest)
|
||||
pub(crate) fn parse_nix_stack(stack: &str) -> Vec<NixStackFrame> {
|
||||
pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<NixStackFrame> {
|
||||
let mut frames = Vec::new();
|
||||
|
||||
for line in stack.lines() {
|
||||
if !line.starts_with("NIX_STACK_FRAME:") {
|
||||
// Format: NIX_STACK_FRAME:start:end[:extra_data]
|
||||
let Some(rest) = line.strip_prefix("NIX_STACK_FRAME:") else {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Format: NIX_STACK_FRAME:type:start:end[:extra_data]
|
||||
let rest = line.strip_prefix("NIX_STACK_FRAME:").unwrap();
|
||||
};
|
||||
let parts: Vec<&str> = rest.splitn(4, ':').collect();
|
||||
|
||||
if parts.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let frame_type = parts[0];
|
||||
let source = match parts[0].parse() {
|
||||
Ok(id) => ctx.get_source(id),
|
||||
Err(_) => continue,
|
||||
};
|
||||
let start: u32 = match parts[1].parse() {
|
||||
Ok(v) => v,
|
||||
Err(_) => continue,
|
||||
@@ -250,28 +228,19 @@ pub(crate) fn parse_nix_stack(stack: &str) -> Vec<NixStackFrame> {
|
||||
|
||||
let span = rnix::TextRange::new(rnix::TextSize::from(start), rnix::TextSize::from(end));
|
||||
|
||||
// Convert all frame types to context frames with descriptive messages
|
||||
let message = match frame_type {
|
||||
"call" => "from call site".to_string(),
|
||||
"select" => {
|
||||
let path = if parts.len() >= 4 { parts[3] } else { "" };
|
||||
if path.is_empty() {
|
||||
"while selecting attribute".to_string()
|
||||
} else {
|
||||
format!("while selecting attribute [{}]", path)
|
||||
}
|
||||
}
|
||||
"context" => {
|
||||
if parts.len() >= 4 {
|
||||
let message = {
|
||||
if parts.len() == 4 {
|
||||
parts[3].to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
frames.push(NixStackFrame { span, message });
|
||||
frames.push(NixStackFrame {
|
||||
span,
|
||||
message,
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
// Deduplicate consecutive identical frames
|
||||
@@ -279,20 +248,3 @@ pub(crate) fn parse_nix_stack(stack: &str) -> Vec<NixStackFrame> {
|
||||
|
||||
frames
|
||||
}
|
||||
|
||||
/// Format stack trace for display (reversed order, newest at bottom)
|
||||
pub(crate) fn format_stack_trace(frames: &[NixStackFrame]) -> Vec<String> {
|
||||
let mut lines = Vec::new();
|
||||
|
||||
// Reverse order: oldest first, newest last
|
||||
for frame in frames.iter().rev() {
|
||||
lines.push(format!(
|
||||
"{} at {}:{}",
|
||||
frame.message,
|
||||
usize::from(frame.span.start()),
|
||||
usize::from(frame.span.end())
|
||||
));
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ impl CacheEntry {
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.expect("Clock may have gone backwards")
|
||||
.as_secs();
|
||||
|
||||
now > self.timestamp + ttl_seconds
|
||||
@@ -180,7 +180,7 @@ impl MetadataCache {
|
||||
let info_str = serde_json::to_string(info)?;
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.expect("Clock may have gone backwards")
|
||||
.as_secs();
|
||||
|
||||
self.conn.execute(
|
||||
@@ -202,7 +202,7 @@ impl MetadataCache {
|
||||
let input_str = serde_json::to_string(input)?;
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.expect("Clock may have gone backwards")
|
||||
.as_secs();
|
||||
|
||||
self.conn.execute(
|
||||
|
||||
@@ -30,8 +30,7 @@ pub trait DowngradeContext {
|
||||
fn extract_expr(&mut self, id: ExprId) -> Ir;
|
||||
fn replace_expr(&mut self, id: ExprId, expr: Ir);
|
||||
fn reserve_slots(&mut self, slots: usize) -> impl Iterator<Item = ExprId> + Clone + use<Self>;
|
||||
fn get_span(&self, id: ExprId) -> TextRange;
|
||||
fn get_current_source(&self) -> Option<Source>;
|
||||
fn get_current_source(&self) -> Source;
|
||||
|
||||
fn with_param_scope<F, R>(&mut self, param: SymId, arg: ExprId, f: F) -> R
|
||||
where
|
||||
@@ -70,7 +69,7 @@ ir! {
|
||||
Assert { pub assertion: ExprId, pub expr: ExprId, pub assertion_raw: String },
|
||||
ConcatStrings { pub parts: Vec<ExprId> },
|
||||
Path { pub expr: ExprId },
|
||||
Func { pub body: ExprId, pub param: Param, pub arg: ExprId },
|
||||
Func { pub body: ExprId, pub param: Option<Param>, pub arg: ExprId },
|
||||
Let { pub binding_sccs: SccInfo, pub body: ExprId },
|
||||
Arg(ArgId),
|
||||
ExprRef(ExprId),
|
||||
@@ -297,9 +296,7 @@ impl From<ast::UnaryOpKind> for UnOpKind {
|
||||
/// Describes the parameters of a function.
|
||||
#[derive(Debug)]
|
||||
pub struct Param {
|
||||
/// The set of required parameter names for a pattern-matching function.
|
||||
pub required: Option<Vec<SymId>>,
|
||||
/// The set of all allowed parameter names for a non-ellipsis pattern-matching function.
|
||||
/// If `None`, any attribute is allowed (ellipsis `...` is present).
|
||||
pub allowed: Option<HashSet<SymId>>,
|
||||
pub required: Vec<SymId>,
|
||||
pub optional: Vec<SymId>,
|
||||
pub ellipsis: bool,
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for Expr {
|
||||
let span = error.syntax().text_range();
|
||||
Err(self::Error::downgrade_error(error.to_string())
|
||||
.with_span(span)
|
||||
.with_source(ctx.get_current_source().expect("no source set")))
|
||||
.with_source(ctx.get_current_source()))
|
||||
}
|
||||
IfElse(ifelse) => ifelse.downgrade(ctx),
|
||||
Select(select) => select.downgrade(ctx),
|
||||
@@ -310,7 +310,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::LegacyLet {
|
||||
attrs.stcs.insert(sym, expr);
|
||||
}
|
||||
|
||||
Ok(ctx.new_expr(attrs.to_ir()))
|
||||
Result::Ok(ctx.new_expr(attrs.to_ir()))
|
||||
})?;
|
||||
|
||||
let body_sym = ctx.new_sym("body".to_string());
|
||||
@@ -363,20 +363,18 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::With {
|
||||
/// This involves desugaring pattern-matching arguments into `let` bindings.
|
||||
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
|
||||
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
|
||||
let param = self.param().unwrap();
|
||||
let arg = ctx.new_arg(param.syntax().text_range());
|
||||
let raw_param = self.param().unwrap();
|
||||
let arg = ctx.new_arg(raw_param.syntax().text_range());
|
||||
|
||||
let required;
|
||||
let allowed;
|
||||
let param;
|
||||
let body;
|
||||
let span = self.body().unwrap().syntax().text_range();
|
||||
|
||||
match param {
|
||||
match raw_param {
|
||||
ast::Param::IdentParam(id) => {
|
||||
// Simple case: `x: body`
|
||||
let param_sym = ctx.new_sym(id.to_string());
|
||||
required = None;
|
||||
allowed = None;
|
||||
param = None;
|
||||
|
||||
// Downgrade body in Param scope
|
||||
body = ctx
|
||||
@@ -387,25 +385,28 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
|
||||
.pat_bind()
|
||||
.map(|alias| ctx.new_sym(alias.ident().unwrap().to_string()));
|
||||
|
||||
let has_ellipsis = pattern.ellipsis_token().is_some();
|
||||
let ellipsis = pattern.ellipsis_token().is_some();
|
||||
let pat_entries = pattern.pat_entries();
|
||||
|
||||
let PatternBindings {
|
||||
body: inner_body,
|
||||
scc_info,
|
||||
required_params,
|
||||
allowed_params,
|
||||
required,
|
||||
optional,
|
||||
} = downgrade_pattern_bindings(
|
||||
pat_entries,
|
||||
alias,
|
||||
arg,
|
||||
has_ellipsis,
|
||||
ellipsis,
|
||||
ctx,
|
||||
|ctx, _| self.body().unwrap().downgrade(ctx),
|
||||
)?;
|
||||
|
||||
required = Some(required_params);
|
||||
allowed = allowed_params;
|
||||
param = Some(Param {
|
||||
required,
|
||||
optional,
|
||||
ellipsis,
|
||||
});
|
||||
|
||||
body = ctx.new_expr(
|
||||
Let {
|
||||
@@ -418,7 +419,6 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
|
||||
}
|
||||
}
|
||||
|
||||
let param = Param { required, allowed };
|
||||
let span = self.syntax().text_range();
|
||||
// The function's body and parameters are now stored directly in the `Func` node.
|
||||
Ok(ctx.new_expr(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use hashbrown::hash_map::Entry;
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use itertools::Itertools as _;
|
||||
use rnix::ast;
|
||||
use rowan::ast::AstNode;
|
||||
|
||||
@@ -26,7 +27,7 @@ pub fn maybe_thunk(mut expr: ast::Expr, ctx: &mut impl DowngradeContext) -> Resu
|
||||
let span = error.syntax().text_range();
|
||||
return Err(self::Error::downgrade_error(error.to_string())
|
||||
.with_span(span)
|
||||
.with_source(ctx.get_current_source().expect("no source set")));
|
||||
.with_source(ctx.get_current_source()));
|
||||
}
|
||||
Ident(ident) => return ident.downgrade(ctx),
|
||||
Literal(lit) => return lit.downgrade(ctx),
|
||||
@@ -136,7 +137,7 @@ pub fn downgrade_inherit(
|
||||
"dynamic attributes not allowed in inherit".to_string(),
|
||||
)
|
||||
.with_span(span)
|
||||
.with_source(ctx.get_current_source().expect("no source set")));
|
||||
.with_source(ctx.get_current_source()));
|
||||
}
|
||||
};
|
||||
let expr = if let Some(expr) = from {
|
||||
@@ -166,7 +167,7 @@ pub fn downgrade_inherit(
|
||||
format_symbol(ctx.get_sym(*occupied.key()))
|
||||
))
|
||||
.with_span(span)
|
||||
.with_source(ctx.get_current_source().expect("no source set")));
|
||||
.with_source(ctx.get_current_source()));
|
||||
}
|
||||
Entry::Vacant(vacant) => vacant.insert(expr),
|
||||
};
|
||||
@@ -248,7 +249,7 @@ pub fn downgrade_static_attrpathvalue(
|
||||
"dynamic attributes not allowed in let bindings".to_string(),
|
||||
)
|
||||
.with_span(attrpath_node.syntax().text_range())
|
||||
.with_source(ctx.get_current_source().expect("no source set")));
|
||||
.with_source(ctx.get_current_source()));
|
||||
}
|
||||
let value = value.value().unwrap().downgrade(ctx)?;
|
||||
attrs.insert(path, value, ctx)
|
||||
@@ -257,8 +258,8 @@ pub fn downgrade_static_attrpathvalue(
|
||||
pub struct PatternBindings {
|
||||
pub body: ExprId,
|
||||
pub scc_info: SccInfo,
|
||||
pub required_params: Vec<SymId>,
|
||||
pub allowed_params: Option<HashSet<SymId>>,
|
||||
pub required: Vec<SymId>,
|
||||
pub optional: Vec<SymId>,
|
||||
}
|
||||
|
||||
/// Helper function for Lambda pattern parameters with SCC analysis.
|
||||
@@ -296,7 +297,7 @@ where
|
||||
format_symbol(ctx.get_sym(sym))
|
||||
))
|
||||
.with_span(span)
|
||||
.with_source(ctx.get_current_source().expect("no source set")));
|
||||
.with_source(ctx.get_current_source()));
|
||||
}
|
||||
|
||||
let default_ast = entry.default();
|
||||
@@ -310,17 +311,18 @@ where
|
||||
binding_keys.push(alias_sym);
|
||||
}
|
||||
|
||||
let required: Vec<SymId> = param_syms
|
||||
let (required, optional) =
|
||||
param_syms
|
||||
.iter()
|
||||
.zip(param_defaults.iter())
|
||||
.filter_map(|(&sym, default)| if default.is_none() { Some(sym) } else { None })
|
||||
.collect();
|
||||
|
||||
let allowed: Option<HashSet<SymId>> = if has_ellipsis {
|
||||
None
|
||||
.partition_map(|(&sym, default)| {
|
||||
use itertools::Either::*;
|
||||
if default.is_none() {
|
||||
Left(sym)
|
||||
} else {
|
||||
Some(param_syms.iter().copied().collect())
|
||||
};
|
||||
Right(sym)
|
||||
}
|
||||
});
|
||||
|
||||
// Get the owner from outer tracker's current_binding
|
||||
let owner = ctx.get_current_binding();
|
||||
@@ -371,8 +373,8 @@ where
|
||||
Ok(PatternBindings {
|
||||
body,
|
||||
scc_info,
|
||||
required_params: required,
|
||||
allowed_params: allowed,
|
||||
required,
|
||||
optional,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -447,7 +449,7 @@ where
|
||||
format_symbol(ctx.get_sym(sym))
|
||||
))
|
||||
.with_span(synthetic_span())
|
||||
.with_source(ctx.get_current_source().expect("no source set")));
|
||||
.with_source(ctx.get_current_source()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,7 +489,7 @@ where
|
||||
format_symbol(ctx.get_sym(sym))
|
||||
))
|
||||
.with_span(ident.syntax().text_range())
|
||||
.with_source(ctx.get_current_source().expect("no source set")));
|
||||
.with_source(ctx.get_current_source()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -507,7 +509,7 @@ where
|
||||
format_symbol(ctx.get_sym(sym))
|
||||
))
|
||||
.with_span(ident.syntax().text_range())
|
||||
.with_source(ctx.get_current_source().expect("no source set")));
|
||||
.with_source(ctx.get_current_source()));
|
||||
}
|
||||
}
|
||||
} else if attrs_vec.len() > 1 {
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::Once;
|
||||
|
||||
use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8};
|
||||
use deno_error::JsErrorClass;
|
||||
use itertools::Itertools as _;
|
||||
|
||||
use crate::error::{Error, Result, Source};
|
||||
use crate::value::{AttrSet, List, Symbol, Value};
|
||||
@@ -15,9 +16,10 @@ type LocalSymbol<'a> = v8::Local<'a, v8::Symbol>;
|
||||
|
||||
pub(crate) trait RuntimeContext: 'static {
|
||||
fn get_current_dir(&self) -> &Path;
|
||||
fn set_current_file(&mut self, path: Source);
|
||||
fn add_source(&mut self, path: Source);
|
||||
fn compile_code(&mut self, source: Source) -> Result<String>;
|
||||
fn get_current_source(&self) -> Option<Source>;
|
||||
fn get_current_source(&self) -> Source;
|
||||
fn get_source(&self, id: usize) -> Source;
|
||||
}
|
||||
|
||||
fn runtime_extension<Ctx: RuntimeContext>() -> Extension {
|
||||
@@ -36,6 +38,7 @@ fn runtime_extension<Ctx: RuntimeContext>() -> Extension {
|
||||
op_store_path(),
|
||||
op_to_file(),
|
||||
op_copy_path_to_store(),
|
||||
op_get_env(),
|
||||
];
|
||||
ops.extend(crate::fetcher::register_ops());
|
||||
|
||||
@@ -104,7 +107,7 @@ fn op_import<Ctx: RuntimeContext>(
|
||||
};
|
||||
|
||||
tracing::debug!("Compiling file");
|
||||
ctx.set_current_file(source.clone());
|
||||
ctx.add_source(source.clone());
|
||||
|
||||
Ok(ctx.compile_code(source).map_err(|err| err.to_string())?)
|
||||
}
|
||||
@@ -388,6 +391,12 @@ fn op_copy_path_to_store(
|
||||
Ok(store_path)
|
||||
}
|
||||
|
||||
#[deno_core::op2]
|
||||
#[string]
|
||||
fn op_get_env(#[string] key: String) -> std::result::Result<String, NixError> {
|
||||
Ok(std::env::var(key).map_err(|err| format!("Failed to read env var: {err}"))?)
|
||||
}
|
||||
|
||||
pub(crate) struct Runtime<Ctx: RuntimeContext> {
|
||||
js_runtime: JsRuntime,
|
||||
is_thunk_symbol: v8::Global<v8::Symbol>,
|
||||
@@ -439,45 +448,38 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
|
||||
.js_runtime
|
||||
.execute_script("<eval>", script)
|
||||
.map_err(|e| {
|
||||
let msg = format!("{}", e.get_message());
|
||||
let stack_str = e.stack.as_ref().map(|s| s.to_string());
|
||||
|
||||
let mut error = Error::eval_error(msg.clone(), None);
|
||||
|
||||
// Parse Nix stack trace frames
|
||||
if let Some(ref stack) = stack_str {
|
||||
let frames = crate::error::parse_nix_stack(stack);
|
||||
|
||||
if !frames.is_empty() {
|
||||
// Get the last frame (where error occurred) for span
|
||||
if let Some(last_frame) = frames.last() {
|
||||
let span = last_frame.span;
|
||||
error = error.with_span(span);
|
||||
}
|
||||
|
||||
// Format stack trace (reversed, newest at bottom)
|
||||
let trace_lines = crate::error::format_stack_trace(&frames);
|
||||
if !trace_lines.is_empty() {
|
||||
let formatted_trace = trace_lines.join("\n");
|
||||
error = Error::eval_error(msg, Some(formatted_trace));
|
||||
|
||||
// Re-apply span after recreating error
|
||||
if let Some(last_frame) = frames.last() {
|
||||
let span = last_frame.span;
|
||||
error = error.with_span(span);
|
||||
}
|
||||
}
|
||||
|
||||
// Get current source from Context
|
||||
let op_state = self.js_runtime.op_state();
|
||||
let op_state_borrow = op_state.borrow();
|
||||
if let Some(ctx) = op_state_borrow.try_borrow::<Ctx>()
|
||||
&& let Some(source) = ctx.get_current_source()
|
||||
{
|
||||
let ctx = op_state_borrow.borrow::<Ctx>();
|
||||
|
||||
let msg = e.get_message().to_string();
|
||||
let mut span = None;
|
||||
let mut source = None;
|
||||
|
||||
// Parse Nix stack trace frames
|
||||
if let Some(stack) = &e.stack {
|
||||
let frames = crate::error::parse_nix_stack(stack, ctx);
|
||||
|
||||
if let Some(last_frame) = frames.last() {
|
||||
span = Some(last_frame.span);
|
||||
source = Some(last_frame.source.clone())
|
||||
}
|
||||
}
|
||||
|
||||
let js_backtrace = e.stack.map(|stack| {
|
||||
stack
|
||||
.lines()
|
||||
.filter(|line| !line.starts_with("NIX_STACK_FRAME:"))
|
||||
.join("\n")
|
||||
});
|
||||
let mut error = Error::eval_error(msg.clone(), js_backtrace);
|
||||
if let Some(span) = span {
|
||||
error = error.with_span(span);
|
||||
}
|
||||
if let Some(source) = source {
|
||||
error = error.with_source(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error
|
||||
})?;
|
||||
@@ -532,7 +534,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
|
||||
"failed to convert {symbol} Value to Symbol ({err})"
|
||||
))
|
||||
})?;
|
||||
Ok(v8::Global::new(scope, sym))
|
||||
Result::Ok(v8::Global::new(scope, sym))
|
||||
};
|
||||
|
||||
let is_thunk = get_symbol("IS_THUNK")?;
|
||||
|
||||
@@ -70,7 +70,7 @@ impl Symbol {
|
||||
}
|
||||
|
||||
/// Represents a Nix attribute set, which is a map from symbols to values.
|
||||
#[derive(Constructor, Clone, PartialEq)]
|
||||
#[derive(Constructor, Default, Clone, PartialEq)]
|
||||
pub struct AttrSet {
|
||||
data: BTreeMap<Symbol, Value>,
|
||||
}
|
||||
@@ -119,25 +119,20 @@ impl Display for AttrSet {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
use Value::*;
|
||||
write!(f, "{{")?;
|
||||
let mut first = true;
|
||||
for (k, v) in self.data.iter() {
|
||||
if !first {
|
||||
write!(f, "; ")?;
|
||||
}
|
||||
write!(f, " {k} = ")?;
|
||||
match v {
|
||||
AttrSet(_) => write!(f, "{{ ... }}"),
|
||||
List(_) => write!(f, "[ ... ]"),
|
||||
v => write!(f, "{v}"),
|
||||
}?;
|
||||
first = false;
|
||||
List(_) => write!(f, "[ ... ];")?,
|
||||
AttrSet(_) => write!(f, "{{ ... }};")?,
|
||||
v => write!(f, "{v};")?,
|
||||
}
|
||||
}
|
||||
write!(f, " }}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a Nix list, which is a vector of values.
|
||||
#[derive(Constructor, Clone, Debug, PartialEq)]
|
||||
#[derive(Constructor, Default, Clone, Debug, PartialEq)]
|
||||
pub struct List {
|
||||
data: Vec<Value>,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
mod utils;
|
||||
|
||||
use nix_js::value::{List, Value};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use nix_js::value::{AttrSet, List, Value};
|
||||
use utils::eval;
|
||||
|
||||
#[test]
|
||||
@@ -260,3 +262,56 @@ fn builtins_compare_versions_complex() {
|
||||
Value::Int(-1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtins_generic_closure() {
|
||||
assert_eq!(
|
||||
eval(
|
||||
"with builtins; length (genericClosure { startSet = [ { key = 1; } ]; operator = { key }: [ { key = key / 1.; } ]; a = 1; })"
|
||||
),
|
||||
Value::Int(1),
|
||||
);
|
||||
assert_eq!(
|
||||
eval(
|
||||
"with builtins; (elemAt (genericClosure { startSet = [ { key = 1; } ]; operator = { key }: [ { key = key / 1.; } ]; a = 1; }) 0).key"
|
||||
),
|
||||
Value::Int(1),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtins_function_args() {
|
||||
assert_eq!(
|
||||
eval("builtins.functionArgs (x: 1)"),
|
||||
Value::AttrSet(AttrSet::default())
|
||||
);
|
||||
assert_eq!(
|
||||
eval("builtins.functionArgs ({}: 1)"),
|
||||
Value::AttrSet(AttrSet::default())
|
||||
);
|
||||
assert_eq!(
|
||||
eval("builtins.functionArgs ({...}: 1)"),
|
||||
Value::AttrSet(AttrSet::default())
|
||||
);
|
||||
assert_eq!(
|
||||
eval("builtins.functionArgs ({a}: 1)"),
|
||||
Value::AttrSet(AttrSet::new(BTreeMap::from([(
|
||||
"a".into(),
|
||||
Value::Bool(false)
|
||||
)])))
|
||||
);
|
||||
assert_eq!(
|
||||
eval("builtins.functionArgs ({a, b ? 1}: 1)"),
|
||||
Value::AttrSet(AttrSet::new(BTreeMap::from([
|
||||
("a".into(), Value::Bool(false)),
|
||||
("b".into(), Value::Bool(true))
|
||||
])))
|
||||
);
|
||||
assert_eq!(
|
||||
eval("builtins.functionArgs ({a, b ? 1, ...}: 1)"),
|
||||
Value::AttrSet(AttrSet::new(BTreeMap::from([
|
||||
("a".into(), Value::Bool(false)),
|
||||
("b".into(), Value::Bool(true))
|
||||
])))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ use nix_js::value::Value;
|
||||
use utils::eval_result;
|
||||
|
||||
fn eval(expr: &str) -> Value {
|
||||
let mut ctx = Context::new().unwrap();
|
||||
eval_result(expr).unwrap_or_else(|e| panic!("{}", e))
|
||||
}
|
||||
|
||||
@@ -398,3 +397,95 @@ fn concatStringsSep_separator_has_context() {
|
||||
);
|
||||
assert_eq!(result, Value::Bool(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn replaceStrings_input_context_preserved() {
|
||||
let result = eval(
|
||||
r#"
|
||||
let
|
||||
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
|
||||
str = builtins.toString drv;
|
||||
replaced = builtins.replaceStrings ["x"] ["y"] str;
|
||||
in builtins.hasContext replaced
|
||||
"#,
|
||||
);
|
||||
assert_eq!(result, Value::Bool(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn replaceStrings_replacement_context_collected() {
|
||||
let result = eval(
|
||||
r#"
|
||||
let
|
||||
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
|
||||
replacement = builtins.toString drv;
|
||||
replaced = builtins.replaceStrings ["foo"] [replacement] "hello foo world";
|
||||
in builtins.hasContext replaced
|
||||
"#,
|
||||
);
|
||||
assert_eq!(result, Value::Bool(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn replaceStrings_merges_contexts() {
|
||||
let result = eval(
|
||||
r#"
|
||||
let
|
||||
drv1 = derivation { name = "test1"; builder = "/bin/sh"; system = "x86_64-linux"; };
|
||||
drv2 = derivation { name = "test2"; builder = "/bin/sh"; system = "x86_64-linux"; };
|
||||
str = builtins.toString drv1;
|
||||
replacement = builtins.toString drv2;
|
||||
replaced = builtins.replaceStrings ["x"] [replacement] str;
|
||||
ctx = builtins.getContext replaced;
|
||||
in builtins.length (builtins.attrNames ctx)
|
||||
"#,
|
||||
);
|
||||
assert_eq!(result, Value::Int(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn replaceStrings_lazy_evaluation_context() {
|
||||
let result = eval(
|
||||
r#"
|
||||
let
|
||||
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
|
||||
replacement = builtins.toString drv;
|
||||
replaced = builtins.replaceStrings ["a" "b"] [replacement "unused"] "hello";
|
||||
in builtins.hasContext replaced
|
||||
"#,
|
||||
);
|
||||
assert_eq!(result, Value::Bool(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn baseNameOf_preserves_context() {
|
||||
let result = eval(
|
||||
r#"
|
||||
let
|
||||
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
|
||||
str = builtins.toString drv;
|
||||
base = builtins.baseNameOf str;
|
||||
in builtins.hasContext base
|
||||
"#,
|
||||
);
|
||||
assert_eq!(result, Value::Bool(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_no_match_preserves_context() {
|
||||
let result = eval(
|
||||
r#"
|
||||
let
|
||||
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
|
||||
str = builtins.toString drv;
|
||||
result = builtins.split "xyz" str;
|
||||
in builtins.hasContext (builtins.head result)
|
||||
"#,
|
||||
);
|
||||
assert_eq!(result, Value::Bool(true));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use nix_js::context::Context;
|
||||
use nix_js::error::Source;
|
||||
use nix_js::error::{Result, Source};
|
||||
use nix_js::value::Value;
|
||||
|
||||
pub fn eval(expr: &str) -> Value {
|
||||
@@ -11,7 +11,7 @@ pub fn eval(expr: &str) -> Value {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn eval_result(expr: &str) -> Result<Value, nix_js::error::Error> {
|
||||
pub fn eval_result(expr: &str) -> Result<Value> {
|
||||
Context::new()
|
||||
.unwrap()
|
||||
.eval_code(Source::new_eval(expr.into()).unwrap())
|
||||
|
||||
8
typos.toml
Normal file
8
typos.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[files]
|
||||
extend-exclude = [
|
||||
"nix-js/tests/regex.rs"
|
||||
]
|
||||
|
||||
[default.extend-words]
|
||||
contextful = "contextful"
|
||||
contextfull = "contextful"
|
||||
Reference in New Issue
Block a user