feat: initial string context implementation

This commit is contained in:
2026-01-11 10:09:18 +08:00
parent 95088103c8
commit c5240385ea
20 changed files with 1027 additions and 148 deletions

View File

@@ -0,0 +1,161 @@
import type { NixValue, NixAttrs, NixString } from "../types";
import { isStringWithContext } from "../types";
import { forceNixString, forceAttrs, forceBool, forceList, forceString } from "../type-assert";
import { force } from "../thunk";
import {
type NixStringContext,
getStringValue,
getStringContext,
mkStringWithContext,
decodeContextElem,
parseContextToInfoMap,
} from "../string-context";
export const hasContext = (value: NixValue): boolean => {
const s = forceNixString(value);
return isStringWithContext(s) && s.context.size > 0;
};
export const unsafeDiscardStringContext = (value: NixValue): string => {
const s = forceNixString(value);
return getStringValue(s);
};
export const unsafeDiscardOutputDependency = (value: NixValue): NixString => {
const s = forceNixString(value);
const strValue = getStringValue(s);
const context = getStringContext(s);
if (context.size === 0) {
return strValue;
}
const newContext: NixStringContext = new Set();
for (const encoded of context) {
const elem = decodeContextElem(encoded);
if (elem.type === "drvDeep") {
newContext.add(elem.drvPath);
} else {
newContext.add(encoded);
}
}
if (newContext.size === 0) {
return strValue;
}
return mkStringWithContext(strValue, newContext);
};
export const addDrvOutputDependencies = (value: NixValue): NixString => {
const s = forceNixString(value);
const strValue = getStringValue(s);
const context = getStringContext(s);
if (context.size !== 1) {
throw new Error(
`context of string '${strValue}' must have exactly one element, but has ${context.size}`,
);
}
const [encoded] = context;
const elem = decodeContextElem(encoded);
if (elem.type === "drvDeep") {
return s;
}
if (elem.type === "built") {
throw new Error(
`\`addDrvOutputDependencies\` can only act on derivations, not on a derivation output such as '${elem.output}'`,
);
}
if (!elem.path.endsWith(".drv")) {
throw new Error(`path '${elem.path}' is not a derivation`);
}
const newContext: NixStringContext = new Set([`=${elem.path}`]);
return mkStringWithContext(strValue, newContext);
};
export const getContext = (value: NixValue): NixAttrs => {
const s = forceNixString(value);
const context = getStringContext(s);
const infoMap = parseContextToInfoMap(context);
const result: NixAttrs = {};
for (const [path, info] of infoMap) {
const attrs: NixAttrs = {};
if (info.path) {
attrs["path"] = true;
}
if (info.allOutputs) {
attrs["allOutputs"] = true;
}
if (info.outputs.length > 0) {
attrs["outputs"] = info.outputs;
}
result[path] = attrs;
}
return result;
};
export const appendContext =
(strValue: NixValue) =>
(ctxValue: NixValue): NixString => {
const s = forceNixString(strValue);
const strVal = getStringValue(s);
const existingContext = getStringContext(s);
const ctxAttrs = forceAttrs(ctxValue);
const newContext: NixStringContext = new Set(existingContext);
for (const [path, infoVal] of Object.entries(ctxAttrs)) {
if (!path.startsWith("/nix/store/")) {
throw new Error(`context key '${path}' is not a store path`);
}
const info = forceAttrs(infoVal);
if ("path" in info) {
const pathVal = force(info["path"]);
if (pathVal === true) {
newContext.add(path);
}
}
if ("allOutputs" in info) {
const allOutputs = force(info["allOutputs"]);
if (allOutputs === true) {
if (!path.endsWith(".drv")) {
throw new Error(
`tried to add all-outputs context of ${path}, which is not a derivation, to a string`,
);
}
newContext.add(`=${path}`);
}
}
if ("outputs" in info) {
const outputs = forceList(info["outputs"]);
if (outputs.length > 0 && !path.endsWith(".drv")) {
throw new Error(
`tried to add derivation output context of ${path}, which is not a derivation, to a string`,
);
}
for (const output of outputs) {
const outputName = forceString(output);
newContext.add(`!${outputName}!${path}`);
}
}
}
if (newContext.size === 0) {
return strVal;
}
return mkStringWithContext(strVal, newContext);
};

View File

@@ -2,8 +2,15 @@
* Conversion and serialization builtin functions
*/
import type { NixValue } from "../types";
import type { NixValue, NixString } from "../types";
import { isStringWithContext } from "../types";
import { force } from "../thunk";
import {
type NixStringContext,
mkStringWithContext,
addBuiltContext,
} from "../string-context";
import { forceFunction } from "../type-assert";
export const fromJSON = (e: NixValue): never => {
throw new Error("Not implemented: fromJSON");
@@ -44,6 +51,7 @@ const typeName = (value: NixValue): string => {
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";
@@ -52,6 +60,11 @@ const typeName = (value: NixValue): string => {
return `unknown type`;
};
export interface CoerceResult {
value: string;
context: NixStringContext;
}
/**
* Coerce a Nix value to a string according to the specified mode.
* This implements the same behavior as Lix's EvalState::coerceToString.
@@ -59,6 +72,7 @@ const typeName = (value: NixValue): string => {
* @param value - The value to coerce
* @param mode - The coercion mode (controls which types are allowed)
* @param copyToStore - If true, paths should be copied to the Nix store (not implemented yet)
* @param outContext - Optional context set to collect string contexts
* @returns The string representation of the value
* @throws TypeError if the value cannot be coerced in the given mode
*
@@ -77,6 +91,7 @@ export const coerceToString = (
value: NixValue,
mode: StringCoercionMode = StringCoercionMode.ToString,
copyToStore: boolean = false,
outContext?: NixStringContext,
): string => {
const v = force(value);
@@ -85,29 +100,39 @@ export const coerceToString = (
return v;
}
// Attribute sets can define custom string conversion via __toString method
// or may have an outPath attribute (for derivations and paths)
if (isStringWithContext(v)) {
if (outContext) {
for (const elem of v.context) {
outContext.add(elem);
}
}
return v.value;
}
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
// First, try the __toString method if present
// This allows custom types to define their own string representation
if ("__toString" in v) {
// Force the method in case it's a thunk
const toStringMethod = force(v["__toString"]);
if (typeof toStringMethod === "function") {
// Call the method with self as argument
const result = force(toStringMethod(v));
if (typeof result !== "string") {
throw new TypeError(`__toString returned ${typeName(result)} instead of string`);
}
return result;
}
const toStringMethod = forceFunction(v.__toString);
const result = force(toStringMethod(v));
// Recursively coerceToString
return coerceToString(result, mode, copyToStore, outContext);
}
// If no __toString, try outPath (used for derivations and store paths)
// This allows derivation objects like { outPath = "/nix/store/..."; } to be coerced
if ("outPath" in v) {
// Recursively coerce the outPath value (it might itself be an attrs with __toString)
return coerceToString(v["outPath"], mode, copyToStore);
// Recursively coerce the outPath value
const outPath = coerceToString(v.outPath, mode, copyToStore, outContext);
if ("type" in v && v.type === "derivation" && "drvPath" in v) {
const drvPath = force(v.drvPath);
if (typeof drvPath === "string" && outContext) {
const outputName = "outputName" in v ? String(force(v.outputName)) : "out";
addBuiltContext(outContext, drvPath, outputName);
}
}
return outPath;
}
// Attribute sets without __toString or outPath cannot be coerced
@@ -157,7 +182,7 @@ export const coerceToString = (
for (let i = 0; i < v.length; i++) {
const item = v[i];
// Recursively convert element to string
const str = coerceToString(item, mode, copyToStore);
const str = coerceToString(item, mode, copyToStore, outContext);
result += str;
// Add space after this element if:
@@ -182,6 +207,23 @@ export const coerceToString = (
throw new TypeError(`cannot coerce ${typeName(v)} to a string`);
};
/**
* Coerce a Nix value to a string with context tracking
*/
export const coerceToStringWithContext = (
value: NixValue,
mode: StringCoercionMode = StringCoercionMode.ToString,
copyToStore: boolean = false,
): NixString => {
const context: NixStringContext = new Set();
const str = coerceToString(value, mode, copyToStore, context);
if (context.size === 0) {
return str;
}
return mkStringWithContext(str, context);
};
/**
* builtins.toString - Convert a value to a string
*
@@ -191,6 +233,6 @@ export const coerceToString = (
* @param value - The value to convert to a string
* @returns The string representation
*/
export const toStringFunc = (value: NixValue): string => {
return coerceToString(value, StringCoercionMode.ToString, false);
export const toStringFunc = (value: NixValue): NixString => {
return coerceToStringWithContext(value, StringCoercionMode.ToString, false);
};

View File

@@ -1,15 +1,21 @@
import type { NixValue, NixAttrs } from "../types";
import { forceString, forceList } from "../type-assert";
import { forceString, forceList, forceNixString } from "../type-assert";
import { force } from "../thunk";
import { type DerivationData, type OutputInfo, generateAterm } from "../derivation-helpers";
import { coerceToString, StringCoercionMode } from "./conversion";
import {
type NixStringContext,
extractInputDrvsAndSrcs,
isStringWithContext,
HAS_CONTEXT,
} from "../string-context";
const forceAttrs = (value: NixValue): NixAttrs => {
const forced = force(value);
if (typeof forced !== "object" || forced === null || Array.isArray(forced)) {
if (typeof forced !== "object" || forced === null || Array.isArray(forced) || isStringWithContext(forced)) {
throw new TypeError(`Expected attribute set for derivation, got ${typeof forced}`);
}
return forced as NixAttrs;
return forced;
};
const validateName = (attrs: NixAttrs): string => {
@@ -26,11 +32,11 @@ const validateName = (attrs: NixAttrs): string => {
return name;
};
const validateBuilder = (attrs: NixAttrs): string => {
const validateBuilder = (attrs: NixAttrs, outContext: NixStringContext): string => {
if (!("builder" in attrs)) {
throw new Error("derivation: missing required attribute 'builder'");
}
return forceString(attrs.builder);
return coerceToString(attrs.builder, StringCoercionMode.ToString, false, outContext);
};
const validateSystem = (attrs: NixAttrs): string => {
@@ -66,15 +72,19 @@ const extractOutputs = (attrs: NixAttrs): string[] => {
return outputs;
};
const extractArgs = (attrs: NixAttrs): string[] => {
const extractArgs = (attrs: NixAttrs, outContext: NixStringContext): string[] => {
if (!("args" in attrs)) {
return [];
}
const argsList = forceList(attrs.args);
return argsList.map((a) => coerceToString(a, StringCoercionMode.ToString, false));
return argsList.map((a) => coerceToString(a, StringCoercionMode.ToString, false, outContext));
};
const nixValueToJson = (value: NixValue, seen = new Set<object>()): any => {
const nixValueToJson = (
value: NixValue,
seen = new Set<object>(),
outContext?: NixStringContext,
): any => {
const v = force(value);
if (v === null) return null;
@@ -82,6 +92,15 @@ const nixValueToJson = (value: NixValue, seen = new Set<object>()): any => {
if (typeof v === "string") return v;
if (typeof v === "number") return v;
if (typeof v === "object" && HAS_CONTEXT in v && "context" in v) {
if (outContext) {
for (const elem of v.context) {
outContext.add(elem);
}
}
return v.value;
}
if (typeof v === "bigint") {
const num = Number(v);
if (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER) {
@@ -98,13 +117,13 @@ const nixValueToJson = (value: NixValue, seen = new Set<object>()): any => {
}
if (Array.isArray(v)) {
return v.map((item) => nixValueToJson(item, seen));
return v.map((item) => nixValueToJson(item, seen, outContext));
}
if (typeof v === "object") {
const result: Record<string, any> = {};
for (const [key, val] of Object.entries(v)) {
result[key] = nixValueToJson(val, seen);
result[key] = nixValueToJson(val, seen, outContext);
}
return result;
}
@@ -116,7 +135,12 @@ const nixValueToJson = (value: NixValue, seen = new Set<object>()): any => {
throw new Error(`derivation: cannot serialize ${typeof v} to JSON`);
};
const extractEnv = (attrs: NixAttrs, structuredAttrs: boolean, ignoreNulls: boolean): Map<string, string> => {
const extractEnv = (
attrs: NixAttrs,
structuredAttrs: boolean,
ignoreNulls: boolean,
outContext: NixStringContext,
): Map<string, string> => {
const specialAttrs = new Set([
"name",
"builder",
@@ -139,7 +163,7 @@ const extractEnv = (attrs: NixAttrs, structuredAttrs: boolean, ignoreNulls: bool
if (ignoreNulls && forcedValue === null) {
continue;
}
jsonAttrs[key] = nixValueToJson(value);
jsonAttrs[key] = nixValueToJson(value, new Set(), outContext);
}
}
env.set("__json", JSON.stringify(jsonAttrs));
@@ -150,7 +174,7 @@ const extractEnv = (attrs: NixAttrs, structuredAttrs: boolean, ignoreNulls: bool
if (ignoreNulls && forcedValue === null) {
continue;
}
env.set(key, coerceToString(value, StringCoercionMode.ToString, false));
env.set(key, coerceToString(value, StringCoercionMode.ToString, false, outContext));
}
}
}
@@ -190,7 +214,8 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
const attrs = forceAttrs(args);
const drvName = validateName(attrs);
const builder = validateBuilder(attrs);
const collectedContext: NixStringContext = new Set();
const builder = validateBuilder(attrs, collectedContext);
const platform = validateSystem(attrs);
const outputs = extractOutputs(attrs);
@@ -201,8 +226,8 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
const ignoreNulls = "__ignoreNulls" in attrs ? force(attrs.__ignoreNulls) === true : false;
const drvArgs = extractArgs(attrs);
const env = extractEnv(attrs, structuredAttrs, ignoreNulls);
const drvArgs = extractArgs(attrs, collectedContext);
const env = extractEnv(attrs, structuredAttrs, ignoreNulls, collectedContext);
env.set("name", drvName);
env.set("builder", builder);
@@ -211,6 +236,8 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
env.set("outputs", outputs.join(" "));
}
const { inputDrvs, inputSrcs } = extractInputDrvsAndSrcs(collectedContext);
let outputInfos: Map<string, OutputInfo>;
let drvPath: string;
@@ -239,8 +266,8 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
const finalDrv: DerivationData = {
name: drvName,
outputs: outputInfos,
inputDrvs: new Map(),
inputSrcs: new Set(),
inputDrvs,
inputSrcs,
platform,
builder,
args: drvArgs,
@@ -268,8 +295,8 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
const maskedDrv: DerivationData = {
name: drvName,
outputs: maskedOutputs,
inputDrvs: new Map(),
inputSrcs: new Set(),
inputDrvs,
inputSrcs,
platform,
builder,
args: drvArgs,

View File

@@ -87,15 +87,15 @@ export const partition =
(list: NixValue): NixAttrs => {
const forced_list = forceList(list);
const forced_pred = forceFunction(pred);
const attrs: NixAttrs = {
right: [],
wrong: [],
const attrs = {
right: [] as NixList,
wrong: [] as NixList,
};
for (const elem of forced_list) {
if (force(forced_pred(elem))) {
(attrs.right as NixList).push(elem);
attrs.right.push(elem);
} else {
(attrs.wrong as NixList).push(elem);
attrs.wrong.push(elem);
}
}
return attrs;

View File

@@ -4,7 +4,8 @@
import { force } from "../thunk";
import { CatchableError } from "../types";
import type { NixBool, NixStrictValue, NixValue } from "../types";
import type { NixBool, NixStrictValue, NixValue, NixString } from "../types";
import * as context from "./context";
export const addErrorContext =
(e1: NixValue) =>
@@ -12,19 +13,11 @@ export const addErrorContext =
throw new Error("Not implemented: addErrorContext");
};
export const appendContext =
(e1: NixValue) =>
(e2: NixValue): never => {
throw new Error("Not implemented: appendContext");
};
export const appendContext = context.appendContext;
export const getContext = (s: NixValue): never => {
throw new Error("Not implemented: getContext");
};
export const getContext = context.getContext;
export const hasContext = (s: NixValue): never => {
throw new Error("Not implemented: hasContext");
};
export const hasContext = context.hasContext;
export const hashFile =
(type: NixValue) =>
@@ -42,21 +35,15 @@ export const convertHash = (args: NixValue): never => {
throw new Error("Not implemented: convertHash");
};
export const unsafeDiscardOutputDependency = (s: NixValue): never => {
throw new Error("Not implemented: unsafeDiscardOutputDependency");
};
export const unsafeDiscardOutputDependency = context.unsafeDiscardOutputDependency;
export const unsafeDiscardStringContext = (s: NixValue): never => {
throw new Error("Not implemented: unsafeDiscardStringContext");
};
export const unsafeDiscardStringContext = context.unsafeDiscardStringContext;
export const unsafeGetAttrPos = (s: NixValue): never => {
throw new Error("Not implemented: unsafeGetAttrPos");
};
export const addDrvOutputDependencies = (s: NixValue): never => {
throw new Error("Not implemented: addDrvOutputDependencies");
};
export const addDrvOutputDependencies = context.addDrvOutputDependencies;
export const compareVersions =
(s1: NixValue) =>

View File

@@ -2,9 +2,36 @@
* Helper functions for nix-js runtime
*/
import type { NixValue, NixAttrs, NixBool } from "./types";
import type { NixValue, NixAttrs, NixBool, NixString } from "./types";
import { forceAttrs, forceString } from "./type-assert";
import { isAttrs } from "./builtins/type-check";
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
import { type NixStringContext, mkStringWithContext } from "./string-context";
/**
* Concatenate multiple values into a string with context
* This is used for string interpolation like "hello ${world}"
*
* @param parts - Array of values to concatenate
* @returns String with merged contexts from all parts
*/
export const concatStringsWithContext = (parts: NixValue[]): NixString => {
const context: NixStringContext = new Set();
const strParts: string[] = [];
for (const part of parts) {
const str = coerceToString(part, StringCoercionMode.Interpolation, false, context);
strParts.push(str);
}
const value = strParts.join("");
if (context.size === 0) {
return value;
}
return mkStringWithContext(value, context);
};
/**
* Resolve a path (handles both absolute and relative paths)

View File

@@ -5,10 +5,11 @@
*/
import { createThunk, force, isThunk, IS_THUNK } from "./thunk";
import { select, selectWithDefault, validateParams, resolvePath, hasAttr } from "./helpers";
import { select, selectWithDefault, validateParams, resolvePath, hasAttr, concatStringsWithContext } from "./helpers";
import { op } from "./operators";
import { builtins, PRIMOP_METADATA } from "./builtins";
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
import { HAS_CONTEXT } from "./string-context";
export type NixRuntime = typeof Nix;
@@ -20,6 +21,7 @@ export const Nix = {
force,
isThunk,
IS_THUNK,
HAS_CONTEXT,
hasAttr,
select,
@@ -27,6 +29,7 @@ export const Nix = {
validateParams,
resolvePath,
coerceToString,
concatStringsWithContext,
StringCoercionMode,
op,

View File

@@ -3,20 +3,45 @@
* Implements all binary and unary operators used by codegen
*/
import type { NixValue, NixList, NixAttrs } from "./types";
import type { NixValue, NixList, NixAttrs, NixString } from "./types";
import { isStringWithContext } from "./types";
import { force } from "./thunk";
import { forceNumeric, forceList, forceAttrs, coerceNumeric } from "./type-assert";
import {
getStringValue,
getStringContext,
mergeContexts,
mkStringWithContext,
} from "./string-context";
const isNixString = (v: unknown): v is NixString => {
return typeof v === "string" || isStringWithContext(v);
};
/**
* Operator object exported as Nix.op
* All operators referenced by codegen (e.g., Nix.op.add, Nix.op.eq)
*/
export const op = {
// Arithmetic operators - preserve int/float distinction
add: (a: NixValue, b: NixValue): bigint | number => {
// FIXME: String & Path
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as any) + (bv as any);
add: (a: NixValue, b: NixValue): bigint | number | NixString => {
const av = force(a);
const bv = force(b);
if (isNixString(av) && isNixString(bv)) {
const strA = getStringValue(av);
const strB = getStringValue(bv);
const ctxA = getStringContext(av);
const ctxB = getStringContext(bv);
if (ctxA.size === 0 && ctxB.size === 0) {
return strA + strB;
}
return mkStringWithContext(strA + strB, mergeContexts(ctxA, ctxB));
}
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (numA as any) + (numB as any);
},
sub: (a: NixValue, b: NixValue): bigint | number => {
@@ -39,52 +64,77 @@ export const op = {
return (av as any) / (bv as any);
},
// Comparison operators (JavaScript natively supports bigint/number mixed comparison)
eq: (a: NixValue, b: NixValue): boolean => {
// FIXME: Int and Float
const av = force(a);
const bv = force(b);
if (isNixString(av) && isNixString(bv)) {
return getStringValue(av) === getStringValue(bv);
}
if (typeof av === "bigint" && typeof bv === "number") {
return Number(av) === bv;
}
if (typeof av === "number" && typeof bv === "bigint") {
return av === Number(bv);
}
return av === bv;
},
neq: (a: NixValue, b: NixValue): boolean => {
// FIXME: Int and Float
const av = force(a);
const bv = force(b);
return av !== bv;
return !op.eq(a, b);
},
lt: (a: NixValue, b: NixValue): boolean => {
// FIXME: Non-numeric
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as any) < (bv as any);
const av = force(a);
const bv = force(b);
if (isNixString(av) && isNixString(bv)) {
return getStringValue(av) < getStringValue(bv);
}
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (numA as any) < (numB as any);
},
lte: (a: NixValue, b: NixValue): boolean => {
// FIXME: Non-numeric
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as any) <= (bv as any);
const av = force(a);
const bv = force(b);
if (isNixString(av) && isNixString(bv)) {
return getStringValue(av) <= getStringValue(bv);
}
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (numA as any) <= (numB as any);
},
gt: (a: NixValue, b: NixValue): boolean => {
// FIXME: Non-numeric
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as any) > (bv as any);
const av = force(a);
const bv = force(b);
if (isNixString(av) && isNixString(bv)) {
return getStringValue(av) > getStringValue(bv);
}
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (numA as any) > (numB as any);
},
gte: (a: NixValue, b: NixValue): boolean => {
// FIXME: Non-numeric
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as any) >= (bv as any);
const av = force(a);
const bv = force(b);
if (isNixString(av) && isNixString(bv)) {
return getStringValue(av) >= getStringValue(bv);
}
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (numA as any) >= (numB as any);
},
// Boolean operators
bnot: (a: NixValue): boolean => !force(a),
// Non-short-circuit
// band: (a: NixValue, b: NixValue): boolean => !!(force(a) && force(b)),
// bor: (a: NixValue, b: NixValue): boolean => !!(force(a) || force(b)),
// List concatenation
concat: (a: NixValue, b: NixValue): NixList => {
return Array.prototype.concat.call(forceList(a), forceList(b));
},
// Attribute set update (merge)
update: (a: NixValue, b: NixValue): NixAttrs => {
return { ...forceAttrs(a), ...forceAttrs(b) };
},

View File

@@ -0,0 +1,194 @@
export const HAS_CONTEXT = Symbol("HAS_CONTEXT");
export interface StringContextOpaque {
type: "opaque";
path: string;
}
export interface StringContextDrvDeep {
type: "drvDeep";
drvPath: string;
}
export interface StringContextBuilt {
type: "built";
drvPath: string;
output: string;
}
export type StringContextElem = StringContextOpaque | StringContextDrvDeep | StringContextBuilt;
export type NixStringContext = Set<string>;
export interface StringWithContext {
readonly [HAS_CONTEXT]: true;
value: string;
context: NixStringContext;
}
export const isStringWithContext = (v: unknown): v is StringWithContext => {
return typeof v === "object" && v !== null && HAS_CONTEXT in v && (v as StringWithContext)[HAS_CONTEXT] === true;
};
export const mkStringWithContext = (value: string, context: NixStringContext): StringWithContext => {
return { [HAS_CONTEXT]: true, value, context };
};
export const mkPlainString = (value: string): string => value;
export const getStringValue = (s: string | StringWithContext): string => {
if (isStringWithContext(s)) {
return s.value;
}
return s;
};
export const getStringContext = (s: string | StringWithContext): NixStringContext => {
if (isStringWithContext(s)) {
return s.context;
}
return new Set();
};
export const mergeContexts = (...contexts: NixStringContext[]): NixStringContext => {
const result = new Set<string>();
for (const ctx of contexts) {
for (const elem of ctx) {
result.add(elem);
}
}
return result;
};
export const concatStringsWithContext = (
strings: (string | StringWithContext)[],
): string | StringWithContext => {
const parts: string[] = [];
const contexts: NixStringContext[] = [];
for (const s of strings) {
parts.push(getStringValue(s));
const ctx = getStringContext(s);
if (ctx.size > 0) {
contexts.push(ctx);
}
}
const value = parts.join("");
if (contexts.length === 0) {
return value;
}
return mkStringWithContext(value, mergeContexts(...contexts));
};
export const encodeContextElem = (elem: StringContextElem): string => {
switch (elem.type) {
case "opaque":
return elem.path;
case "drvDeep":
return `=${elem.drvPath}`;
case "built":
return `!${elem.output}!${elem.drvPath}`;
}
};
export const decodeContextElem = (encoded: string): StringContextElem => {
if (encoded.startsWith("=")) {
return { type: "drvDeep", drvPath: encoded.slice(1) };
} else if (encoded.startsWith("!")) {
const secondBang = encoded.indexOf("!", 1);
if (secondBang === -1) {
throw new Error(`Invalid context element: ${encoded}`);
}
return {
type: "built",
output: encoded.slice(1, secondBang),
drvPath: encoded.slice(secondBang + 1),
};
} else {
return { type: "opaque", path: encoded };
}
};
export const addContextElem = (context: NixStringContext, elem: StringContextElem): void => {
context.add(encodeContextElem(elem));
};
export const addOpaqueContext = (context: NixStringContext, path: string): void => {
context.add(path);
};
export const addDrvDeepContext = (context: NixStringContext, drvPath: string): void => {
context.add(`=${drvPath}`);
};
export const addBuiltContext = (context: NixStringContext, drvPath: string, output: string): void => {
context.add(`!${output}!${drvPath}`);
};
export interface ParsedContextInfo {
path: boolean;
allOutputs: boolean;
outputs: string[];
}
export const parseContextToInfoMap = (context: NixStringContext): Map<string, ParsedContextInfo> => {
const result = new Map<string, ParsedContextInfo>();
const getOrCreate = (path: string): ParsedContextInfo => {
let info = result.get(path);
if (!info) {
info = { path: false, allOutputs: false, outputs: [] };
result.set(path, info);
}
return info;
};
for (const encoded of context) {
const elem = decodeContextElem(encoded);
switch (elem.type) {
case "opaque":
getOrCreate(elem.path).path = true;
break;
case "drvDeep":
getOrCreate(elem.drvPath).allOutputs = true;
break;
case "built":
getOrCreate(elem.drvPath).outputs.push(elem.output);
break;
}
}
return result;
};
export const extractInputDrvsAndSrcs = (
context: NixStringContext,
): { inputDrvs: Map<string, Set<string>>; inputSrcs: Set<string> } => {
const inputDrvs = new Map<string, Set<string>>();
const inputSrcs = new Set<string>();
for (const encoded of context) {
const elem = decodeContextElem(encoded);
switch (elem.type) {
case "opaque":
inputSrcs.add(elem.path);
break;
case "drvDeep":
inputSrcs.add(elem.drvPath);
break;
case "built": {
let outputs = inputDrvs.get(elem.drvPath);
if (!outputs) {
outputs = new Set<string>();
inputDrvs.set(elem.drvPath, outputs);
}
outputs.add(elem.output);
break;
}
}
}
return { inputDrvs, inputSrcs };
};

View File

@@ -3,8 +3,10 @@
* These functions force evaluation and verify the type, throwing errors on mismatch
*/
import type { NixValue, NixList, NixAttrs, NixFunction, NixInt, NixFloat, NixNumber } from "./types";
import type { NixValue, NixList, NixAttrs, NixFunction, NixInt, NixFloat, NixNumber, NixString } from "./types";
import { isStringWithContext } from "./types";
import { force } from "./thunk";
import { getStringValue } from "./string-context";
const typeName = (value: NixValue): string => {
const val = force(value);
@@ -13,6 +15,7 @@ const typeName = (value: NixValue): string => {
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";
@@ -51,22 +54,47 @@ export const forceFunction = (value: NixValue): NixFunction => {
*/
export const forceAttrs = (value: NixValue): NixAttrs => {
const forced = force(value);
if (typeof forced !== "object" || Array.isArray(forced) || forced === null) {
if (typeof forced !== "object" || Array.isArray(forced) || forced === null || isStringWithContext(forced)) {
throw new TypeError(`Expected attribute set, got ${typeName(forced)}`);
}
return forced;
};
/**
* Force a value and assert it's a string
* Force a value and assert it's a string (plain or with context)
* @throws TypeError if value is not a string after forcing
*/
export const forceString = (value: NixValue): string => {
const forced = force(value);
if (typeof forced !== "string") {
throw new TypeError(`Expected string, got ${typeName(forced)}`);
if (typeof forced === "string") {
return forced;
}
return forced;
if (isStringWithContext(forced)) {
return forced.value;
}
throw new TypeError(`Expected string, got ${typeName(forced)}`);
};
/**
* Force a value and assert it's a string, returning NixString (preserving context)
* @throws TypeError if value is not a string after forcing
*/
export const forceNixString = (value: NixValue): NixString => {
const forced = force(value);
if (typeof forced === "string") {
return forced;
}
if (isStringWithContext(forced)) {
return forced;
}
throw new TypeError(`Expected string, got ${typeName(forced)}`);
};
/**
* Get the plain string value from any NixString
*/
export const nixStringValue = (s: NixString): string => {
return getStringValue(s);
};
/**

View File

@@ -3,13 +3,16 @@
*/
import { IS_THUNK } from "./thunk";
import { type StringWithContext, HAS_CONTEXT, isStringWithContext } from "./string-context";
export { HAS_CONTEXT, isStringWithContext };
export type { StringWithContext };
// Nix primitive types
export type NixInt = bigint;
export type NixFloat = number;
export type NixNumber = NixInt | NixFloat;
export type NixBool = boolean;
export type NixString = string;
export type NixString = string | StringWithContext;
export type NixNull = null;
// Nix composite types

View File

@@ -281,23 +281,13 @@ impl<Ctx: CodegenContext> Compile<Ctx> for List {
impl<Ctx: CodegenContext> Compile<Ctx> for ConcatStrings {
fn compile(&self, ctx: &Ctx) -> String {
// Concatenate all parts into a single string
// Use JavaScript template string or array join
let parts: Vec<String> = self
.parts
.iter()
.map(|part| {
let compiled = ctx.get_ir(*part).compile(ctx);
// TODO: copyToStore
format!(
"Nix.coerceToString({}, Nix.StringCoercionMode.Interpolation, false)",
compiled
)
})
.map(|part| ctx.get_ir(*part).compile(ctx))
.collect();
// Use array join for concatenation
format!("[{}].join('')", parts.join(","))
format!("Nix.concatStringsWithContext([{}])", parts.join(","))
}
}

View File

@@ -57,9 +57,9 @@ mod private {
use private::CtxPtr;
#[derive(Debug)]
pub struct SccInfo {
pub(crate) struct SccInfo {
/// list of SCCs (exprs, recursive)
pub sccs: Vec<(Vec<ExprId>, bool)>,
pub(crate) sccs: Vec<(Vec<ExprId>, bool)>,
}
pub struct Context {

View File

@@ -329,9 +329,6 @@ pub struct Let {
/// Describes the parameters of a function.
#[derive(Debug)]
pub struct Param {
/// The name of the argument if it's a simple identifier (e.g., `x: ...`).
/// Also used for the alias in a pattern (e.g., `args @ { ... }`).
pub ident: Option<SymId>,
/// The set of required parameter names for a pattern-matching function.
pub required: Option<Vec<SymId>>,
/// The set of all allowed parameter names for a non-ellipsis pattern-matching function.

View File

@@ -121,8 +121,11 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Path {
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Str {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let parts = self
.normalized_parts()
let normalized = self.normalized_parts();
let is_single_literal = normalized.len() == 1
&& matches!(normalized.first(), Some(ast::InterpolPart::Literal(_)));
let parts = normalized
.into_iter()
.map(|part| match part {
ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr(Str { val: lit }.to_ir())),
@@ -131,7 +134,8 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Str {
}
})
.collect::<Result<Vec<_>>>()?;
Ok(if parts.len() == 1 {
Ok(if is_single_literal {
parts.into_iter().next().unwrap()
} else {
ctx.new_expr(ConcatStrings { parts }.to_ir())
@@ -314,7 +318,6 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let arg = ctx.new_arg();
let ident;
let required;
let allowed;
let body;
@@ -323,7 +326,6 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
ast::Param::IdentParam(id) => {
// Simple case: `x: body`
let param_sym = ctx.new_sym(id.to_string());
ident = Some(param_sym);
required = None;
allowed = None;
@@ -335,7 +337,6 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
let alias = pattern
.pat_bind()
.map(|alias| ctx.new_sym(alias.ident().unwrap().to_string()));
ident = alias;
let has_ellipsis = pattern.ellipsis_token().is_some();
let pat_entries = pattern.pat_entries();
@@ -367,11 +368,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
}
}
let param = Param {
ident,
required,
allowed,
};
let param = Param { required, allowed };
// The function's body and parameters are now stored directly in the `Func` node.
Ok(ctx.new_expr(Func { body, param, arg }.to_ir()))
}

View File

@@ -3,7 +3,7 @@
mod codegen;
pub mod context;
pub mod error;
pub mod ir;
mod ir;
mod nix_hash;
mod runtime;
pub mod value;

View File

@@ -180,6 +180,7 @@ pub(crate) struct Runtime<Ctx: RuntimeCtx> {
js_runtime: JsRuntime,
is_thunk_symbol: v8::Global<v8::Symbol>,
primop_metadata_symbol: v8::Global<v8::Symbol>,
has_context_symbol: v8::Global<v8::Symbol>,
_marker: PhantomData<Ctx>,
}
@@ -199,7 +200,7 @@ impl<Ctx: RuntimeCtx> Runtime<Ctx> {
..Default::default()
});
let (is_thunk_symbol, primop_metadata_symbol) = {
let (is_thunk_symbol, primop_metadata_symbol, has_context_symbol) = {
deno_core::scope!(scope, &mut js_runtime);
Self::get_symbols(scope)?
};
@@ -208,6 +209,7 @@ impl<Ctx: RuntimeCtx> Runtime<Ctx> {
js_runtime,
is_thunk_symbol,
primop_metadata_symbol,
has_context_symbol,
_marker: PhantomData,
})
}
@@ -225,17 +227,25 @@ impl<Ctx: RuntimeCtx> Runtime<Ctx> {
let local_value = v8::Local::new(scope, &global_value);
let is_thunk_symbol = v8::Local::new(scope, &self.is_thunk_symbol);
let primop_metadata_symbol = v8::Local::new(scope, &self.primop_metadata_symbol);
let has_context_symbol = v8::Local::new(scope, &self.has_context_symbol);
Ok(to_value(
local_value,
scope,
is_thunk_symbol,
primop_metadata_symbol,
has_context_symbol,
))
}
/// get (IS_THUNK, PRIMOP_METADATA)
fn get_symbols(scope: &ScopeRef) -> Result<(v8::Global<v8::Symbol>, v8::Global<v8::Symbol>)> {
/// get (IS_THUNK, PRIMOP_METADATA, HAS_CONTEXT)
fn get_symbols(
scope: &ScopeRef,
) -> Result<(
v8::Global<v8::Symbol>,
v8::Global<v8::Symbol>,
v8::Global<v8::Symbol>,
)> {
let global = scope.get_current_context().global(scope);
let nix_key = v8::String::new(scope, "Nix")
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
@@ -273,7 +283,19 @@ impl<Ctx: RuntimeCtx> Runtime<Ctx> {
})?;
let primop_metadata = v8::Global::new(scope, primop_metadata);
Ok((is_thunk, primop_metadata))
let has_context_sym_key = v8::String::new(scope, "HAS_CONTEXT")
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
let has_context_sym = nix_obj
.get(scope, has_context_sym_key.into())
.ok_or_else(|| Error::internal("failed to get HAS_CONTEXT Symbol".into()))?;
let has_context = has_context_sym.try_cast::<v8::Symbol>().map_err(|err| {
Error::internal(format!(
"failed to convert HAS_CONTEXT Value to Symbol ({err})"
))
})?;
let has_context = v8::Global::new(scope, has_context);
Ok((is_thunk, primop_metadata, has_context))
}
}
@@ -282,6 +304,7 @@ fn to_value<'a>(
scope: &ScopeRef<'a, '_>,
is_thunk_symbol: LocalSymbol<'a>,
primop_metadata_symbol: LocalSymbol<'a>,
has_context_symbol: LocalSymbol<'a>,
) -> Value {
match () {
_ if val.is_big_int() => {
@@ -296,7 +319,6 @@ fn to_value<'a>(
}
_ if val.is_number() => {
let val = val.to_number(scope).expect("infallible conversion").value();
// number is always NixFloat
Value::Float(val)
}
_ if val.is_true() => Value::Bool(true),
@@ -312,7 +334,13 @@ fn to_value<'a>(
let list = (0..len)
.map(|i| {
let val = val.get_index(scope, i).expect("infallible index operation");
to_value(val, scope, is_thunk_symbol, primop_metadata_symbol)
to_value(
val,
scope,
is_thunk_symbol,
primop_metadata_symbol,
has_context_symbol,
)
})
.collect();
Value::List(List::new(list))
@@ -329,6 +357,10 @@ fn to_value<'a>(
return Value::Thunk;
}
if let Some(string_val) = extract_string_with_context(val, scope, has_context_symbol) {
return Value::String(string_val);
}
let val = val.to_object(scope).expect("infallible conversion");
let keys = val
.get_own_property_names(scope, v8::GetPropertyNamesArgsBuilder::new().build())
@@ -343,7 +375,13 @@ fn to_value<'a>(
let key = key.to_rust_string_lossy(scope);
(
Symbol::new(key),
to_value(val, scope, is_thunk_symbol, primop_metadata_symbol),
to_value(
val,
scope,
is_thunk_symbol,
primop_metadata_symbol,
has_context_symbol,
),
)
})
.collect();
@@ -362,6 +400,32 @@ fn is_thunk<'a>(val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, symbol: LocalSymb
matches!(obj.get(scope, symbol.into()), Some(v) if v.is_true())
}
fn extract_string_with_context<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,
symbol: LocalSymbol<'a>,
) -> Option<String> {
if !val.is_object() {
return None;
}
let obj = val.to_object(scope).expect("infallible conversion");
let has_context = obj.get(scope, symbol.into())?;
if !has_context.is_true() {
return None;
}
let value_key = v8::String::new(scope, "value")?;
let value = obj.get(scope, value_key.into())?;
if value.is_string() {
Some(value.to_rust_string_lossy(scope))
} else {
None
}
}
fn to_primop<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,

View File

@@ -4,6 +4,7 @@ use core::ops::Deref;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::ops::DerefMut;
use std::sync::LazyLock;
use derive_more::{Constructor, IsVariant, Unwrap};
@@ -86,6 +87,18 @@ impl AttrSet {
}
}
impl Deref for AttrSet {
type Target = BTreeMap<Symbol, Value>;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl DerefMut for AttrSet {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.data
}
}
impl Debug for AttrSet {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
use Value::*;
@@ -129,15 +142,15 @@ pub struct List {
data: Vec<Value>,
}
impl List {
/// Returns the number of elements in the list.
pub fn len(&self) -> usize {
self.data.len()
impl Deref for List {
type Target = Vec<Value>;
fn deref(&self) -> &Self::Target {
&self.data
}
/// Returns true if the list is empty.
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
impl DerefMut for List {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.data
}
}

View File

@@ -0,0 +1,290 @@
use nix_js::context::Context;
use nix_js::value::Value;
fn eval(expr: &str) -> Value {
let mut ctx = Context::new().unwrap();
ctx.eval_code(expr).unwrap_or_else(|e| panic!("{}", e))
}
#[test]
fn hascontext_plain_string() {
let result = eval(r#"builtins.hasContext "hello""#);
assert_eq!(result, Value::Bool(false));
}
#[test]
fn hascontext_derivation_output() {
let result = eval(
r#"
let
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
in builtins.hasContext (builtins.toString drv)
"#,
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn getcontext_plain_string() {
let result = eval(r#"builtins.getContext "hello""#);
match result {
Value::AttrSet(attrs) => {
assert!(attrs.is_empty(), "Plain string should have empty context");
}
_ => panic!("Expected AttrSet, got {:?}", result),
}
}
#[test]
fn getcontext_derivation_output() {
let result = eval(
r#"
let
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
str = builtins.toString drv;
ctx = builtins.getContext str;
in builtins.attrNames ctx
"#,
);
match result {
Value::List(list) => {
assert_eq!(list.len(), 1, "Should have exactly one context entry");
match list.first().unwrap() {
Value::String(s) => {
assert!(s.ends_with(".drv"), "Context key should be a .drv path");
}
other => panic!("Expected String, got {:?}", other),
}
}
_ => panic!("Expected List, got {:?}", result),
}
}
#[test]
fn unsafediscardstringcontext() {
let result = eval(
r#"
let
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
strWithContext = builtins.toString drv;
strWithoutContext = builtins.unsafeDiscardStringContext strWithContext;
in builtins.hasContext strWithoutContext
"#,
);
assert_eq!(result, Value::Bool(false));
}
#[test]
fn unsafediscardstringcontext_preserves_value() {
let result = eval(
r#"
let
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
strWithContext = builtins.toString drv;
strWithoutContext = builtins.unsafeDiscardStringContext strWithContext;
in strWithContext == strWithoutContext
"#,
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn appendcontext_basic() {
let result = eval(
r#"
let
str = builtins.appendContext "hello" {
"/nix/store/0000000000000000000000000000000-test.drv" = { outputs = ["out"]; };
};
in builtins.hasContext str
"#,
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn appendcontext_preserves_value() {
let result = eval(
r#"
let
str = builtins.appendContext "hello" {
"/nix/store/0000000000000000000000000000000-test.drv" = { outputs = ["out"]; };
};
in str == "hello"
"#,
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn string_concat_merges_context() {
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"; };
str1 = builtins.toString drv1;
str2 = builtins.toString drv2;
combined = str1 + " " + str2;
ctx = builtins.getContext combined;
in builtins.length (builtins.attrNames ctx)
"#,
);
assert_eq!(result, Value::Int(2));
}
#[test]
fn string_add_merges_context() {
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"; };
str1 = builtins.toString drv1;
str2 = builtins.toString drv2;
combined = str1 + " " + str2;
ctx = builtins.getContext combined;
in builtins.length (builtins.attrNames ctx)
"#,
);
assert_eq!(result, Value::Int(2));
}
#[test]
fn context_in_derivation_args() {
let result = eval(
r#"
let
dep = derivation { name = "dep"; builder = "/bin/sh"; system = "x86_64-linux"; };
drv = derivation {
name = "test";
builder = "/bin/sh";
system = "x86_64-linux";
args = [ ((builtins.toString dep) + "/bin/run") ];
};
in drv.drvPath
"#,
);
match result {
Value::String(s) => {
assert!(s.starts_with("/nix/store/"), "Should be a store path");
assert!(s.ends_with(".drv"), "Should be a .drv file");
}
_ => panic!("Expected String, got {:?}", result),
}
}
#[test]
fn context_in_derivation_env() {
let result = eval(
r#"
let
dep = derivation { name = "dep"; builder = "/bin/sh"; system = "x86_64-linux"; };
drv = derivation {
name = "test";
builder = "/bin/sh";
system = "x86_64-linux";
myDep = builtins.toString dep;
};
in drv.drvPath
"#,
);
match result {
Value::String(s) => {
assert!(s.starts_with("/nix/store/"), "Should be a store path");
assert!(s.ends_with(".drv"), "Should be a .drv file");
}
_ => panic!("Expected String, got {:?}", result),
}
}
#[test]
fn tostring_preserves_context() {
let result = eval(
r#"
let
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
str = builtins.toString drv;
in builtins.hasContext str
"#,
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn interpolation_derivation_returns_outpath() {
let result = eval(
r#"
let
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
in "${drv}"
"#,
);
match result {
Value::String(s) => {
assert!(s.starts_with("/nix/store/"), "Should be a store path");
assert!(s.ends_with("-test"), "Should end with derivation name");
}
_ => panic!("Expected String, got {:?}", result),
}
}
#[test]
fn interpolation_derivation_has_context() {
let result = eval(
r#"
let
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
in builtins.hasContext "${drv}"
"#,
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn interpolation_derivation_context_correct() {
let result = eval(
r#"
let
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
ctx = builtins.getContext "${drv}";
keys = builtins.attrNames ctx;
drvPath = builtins.head keys;
in ctx.${drvPath}.outputs
"#,
);
match result {
Value::List(list) => {
assert_eq!(list.len(), 1);
assert_eq!(list.first().unwrap(), &Value::String("out".to_string()));
}
_ => panic!("Expected List with ['out'], got {:?}", result),
}
}
#[test]
fn interpolation_multiple_derivations() {
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"; };
combined = "prefix-${drv1}-middle-${drv2}-suffix";
ctx = builtins.getContext combined;
in builtins.length (builtins.attrNames ctx)
"#,
);
assert_eq!(result, Value::Int(2));
}
#[test]
fn interpolation_derivation_equals_tostring() {
let result = eval(
r#"
let
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
in "${drv}" == builtins.toString drv
"#,
);
assert_eq!(result, Value::Bool(true));
}

View File

@@ -225,8 +225,14 @@ fn function_to_string_fails() {
#[test]
fn to_string_method_must_return_string() {
let result = utils::eval_result(r#"toString { __toString = self: 42; }"#);
assert!(result.is_err());
assert_eq!(
utils::eval(r#"toString { __toString = self: 42; }"#),
Value::String("42".into())
);
assert_eq!(
utils::eval(r#"toString { __toString = self: true; }"#),
Value::String("1".into())
);
}
#[test]