Compare commits

..

9 Commits

40 changed files with 1017 additions and 711 deletions

View File

@@ -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
View File

@@ -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": {

View File

@@ -37,6 +37,8 @@
biome biome
claude-code claude-code
codex
opencode
]; ];
}; };
} }

View File

@@ -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",

View File

@@ -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"
} }
} }

View File

@@ -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);

View File

@@ -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);
} }

View File

@@ -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}`);
} }
} }

View File

@@ -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`);
}; };
/** /**

View File

@@ -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')`);

View File

@@ -37,10 +37,12 @@ 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;
}; };
export const warn = export const warn =
(e1: NixValue) => (e1: NixValue) =>

View File

@@ -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",
}; };

View File

@@ -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));
}; };

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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;
}; };
/** /**

View File

@@ -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}`);

View File

@@ -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}`);
}; };

View File

@@ -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 => {

View File

@@ -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,

View File

@@ -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),

View File

@@ -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 => {

View File

@@ -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)}`);
}; };

View File

@@ -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

View File

@@ -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;
} }
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -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)),
} }
} }
} }

View File

@@ -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

View File

@@ -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)]

View File

@@ -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
}

View File

@@ -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(

View File

@@ -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>>,
} }

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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")?;

View File

@@ -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>,
} }
@@ -118,26 +118,21 @@ impl Debug for AttrSet {
impl Display for AttrSet { 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, " {k} = ")?;
write!(f, "; ")?;
}
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>,
} }

View File

@@ -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))
])))
);
}

View File

@@ -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));
}

View File

@@ -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
View File

@@ -0,0 +1,8 @@
[files]
extend-exclude = [
"nix-js/tests/regex.rs"
]
[default.extend-words]
contextful = "contextful"
contextfull = "contextful"