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