feat: initial string context implementation
This commit is contained in:
161
nix-js/runtime-ts/src/builtins/context.ts
Normal file
161
nix-js/runtime-ts/src/builtins/context.ts
Normal 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);
|
||||||
|
};
|
||||||
@@ -2,8 +2,15 @@
|
|||||||
* Conversion and serialization builtin functions
|
* 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 { force } from "../thunk";
|
||||||
|
import {
|
||||||
|
type NixStringContext,
|
||||||
|
mkStringWithContext,
|
||||||
|
addBuiltContext,
|
||||||
|
} from "../string-context";
|
||||||
|
import { forceFunction } from "../type-assert";
|
||||||
|
|
||||||
export const fromJSON = (e: NixValue): never => {
|
export const fromJSON = (e: NixValue): never => {
|
||||||
throw new Error("Not implemented: fromJSON");
|
throw new Error("Not implemented: fromJSON");
|
||||||
@@ -44,6 +51,7 @@ const typeName = (value: NixValue): string => {
|
|||||||
if (typeof val === "number") return "float";
|
if (typeof val === "number") return "float";
|
||||||
if (typeof val === "boolean") return "boolean";
|
if (typeof val === "boolean") return "boolean";
|
||||||
if (typeof val === "string") return "string";
|
if (typeof val === "string") return "string";
|
||||||
|
if (isStringWithContext(val)) return "string";
|
||||||
if (val === null) return "null";
|
if (val === null) return "null";
|
||||||
if (Array.isArray(val)) return "list";
|
if (Array.isArray(val)) return "list";
|
||||||
if (typeof val === "function") return "lambda";
|
if (typeof val === "function") return "lambda";
|
||||||
@@ -52,6 +60,11 @@ const typeName = (value: NixValue): string => {
|
|||||||
return `unknown type`;
|
return `unknown type`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface CoerceResult {
|
||||||
|
value: string;
|
||||||
|
context: NixStringContext;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coerce a Nix value to a string according to the specified mode.
|
* Coerce a Nix value to a string according to the specified mode.
|
||||||
* This implements the same behavior as Lix's EvalState::coerceToString.
|
* 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 value - The value to coerce
|
||||||
* @param mode - The coercion mode (controls which types are allowed)
|
* @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 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
|
* @returns The string representation of the value
|
||||||
* @throws TypeError if the value cannot be coerced in the given mode
|
* @throws TypeError if the value cannot be coerced in the given mode
|
||||||
*
|
*
|
||||||
@@ -77,6 +91,7 @@ export const coerceToString = (
|
|||||||
value: NixValue,
|
value: NixValue,
|
||||||
mode: StringCoercionMode = StringCoercionMode.ToString,
|
mode: StringCoercionMode = StringCoercionMode.ToString,
|
||||||
copyToStore: boolean = false,
|
copyToStore: boolean = false,
|
||||||
|
outContext?: NixStringContext,
|
||||||
): string => {
|
): string => {
|
||||||
const v = force(value);
|
const v = force(value);
|
||||||
|
|
||||||
@@ -85,29 +100,39 @@ export const coerceToString = (
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attribute sets can define custom string conversion via __toString method
|
if (isStringWithContext(v)) {
|
||||||
// or may have an outPath attribute (for derivations and paths)
|
if (outContext) {
|
||||||
|
for (const elem of v.context) {
|
||||||
|
outContext.add(elem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v.value;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
||||||
// First, try the __toString method if present
|
// First, try the __toString method if present
|
||||||
// This allows custom types to define their own string representation
|
// This allows custom types to define their own string representation
|
||||||
if ("__toString" in v) {
|
if ("__toString" in v) {
|
||||||
// Force the method in case it's a thunk
|
// Force the method in case it's a thunk
|
||||||
const toStringMethod = force(v["__toString"]);
|
const toStringMethod = forceFunction(v.__toString);
|
||||||
if (typeof toStringMethod === "function") {
|
|
||||||
// Call the method with self as argument
|
|
||||||
const result = force(toStringMethod(v));
|
const result = force(toStringMethod(v));
|
||||||
if (typeof result !== "string") {
|
// Recursively coerceToString
|
||||||
throw new TypeError(`__toString returned ${typeName(result)} instead of string`);
|
return coerceToString(result, mode, copyToStore, outContext);
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no __toString, try outPath (used for derivations and store paths)
|
// If no __toString, try outPath (used for derivations and store paths)
|
||||||
// This allows derivation objects like { outPath = "/nix/store/..."; } to be coerced
|
// This allows derivation objects like { outPath = "/nix/store/..."; } to be coerced
|
||||||
if ("outPath" in v) {
|
if ("outPath" in v) {
|
||||||
// Recursively coerce the outPath value (it might itself be an attrs with __toString)
|
// Recursively coerce the outPath value
|
||||||
return coerceToString(v["outPath"], mode, copyToStore);
|
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
|
// Attribute sets without __toString or outPath cannot be coerced
|
||||||
@@ -157,7 +182,7 @@ export const coerceToString = (
|
|||||||
for (let i = 0; i < v.length; i++) {
|
for (let i = 0; i < v.length; i++) {
|
||||||
const item = v[i];
|
const item = v[i];
|
||||||
// Recursively convert element to string
|
// Recursively convert element to string
|
||||||
const str = coerceToString(item, mode, copyToStore);
|
const str = coerceToString(item, mode, copyToStore, outContext);
|
||||||
result += str;
|
result += str;
|
||||||
|
|
||||||
// Add space after this element if:
|
// Add space after this element if:
|
||||||
@@ -182,6 +207,23 @@ export const coerceToString = (
|
|||||||
throw new TypeError(`cannot coerce ${typeName(v)} to a string`);
|
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
|
* builtins.toString - Convert a value to a string
|
||||||
*
|
*
|
||||||
@@ -191,6 +233,6 @@ export const coerceToString = (
|
|||||||
* @param value - The value to convert to a string
|
* @param value - The value to convert to a string
|
||||||
* @returns The string representation
|
* @returns The string representation
|
||||||
*/
|
*/
|
||||||
export const toStringFunc = (value: NixValue): string => {
|
export const toStringFunc = (value: NixValue): NixString => {
|
||||||
return coerceToString(value, StringCoercionMode.ToString, false);
|
return coerceToStringWithContext(value, StringCoercionMode.ToString, false);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import type { NixValue, NixAttrs } from "../types";
|
import type { NixValue, NixAttrs } from "../types";
|
||||||
import { forceString, forceList } from "../type-assert";
|
import { forceString, forceList, forceNixString } 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";
|
||||||
|
import {
|
||||||
|
type NixStringContext,
|
||||||
|
extractInputDrvsAndSrcs,
|
||||||
|
isStringWithContext,
|
||||||
|
HAS_CONTEXT,
|
||||||
|
} from "../string-context";
|
||||||
|
|
||||||
const forceAttrs = (value: NixValue): NixAttrs => {
|
const forceAttrs = (value: NixValue): NixAttrs => {
|
||||||
const forced = force(value);
|
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}`);
|
throw new TypeError(`Expected attribute set for derivation, got ${typeof forced}`);
|
||||||
}
|
}
|
||||||
return forced as NixAttrs;
|
return forced;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateName = (attrs: NixAttrs): string => {
|
const validateName = (attrs: NixAttrs): string => {
|
||||||
@@ -26,11 +32,11 @@ const validateName = (attrs: NixAttrs): string => {
|
|||||||
return name;
|
return name;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateBuilder = (attrs: NixAttrs): string => {
|
const validateBuilder = (attrs: NixAttrs, outContext: NixStringContext): string => {
|
||||||
if (!("builder" in attrs)) {
|
if (!("builder" in attrs)) {
|
||||||
throw new Error("derivation: missing required attribute 'builder'");
|
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 => {
|
const validateSystem = (attrs: NixAttrs): string => {
|
||||||
@@ -66,15 +72,19 @@ const extractOutputs = (attrs: NixAttrs): string[] => {
|
|||||||
return outputs;
|
return outputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractArgs = (attrs: NixAttrs): string[] => {
|
const extractArgs = (attrs: NixAttrs, outContext: NixStringContext): string[] => {
|
||||||
if (!("args" in attrs)) {
|
if (!("args" in attrs)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const argsList = forceList(attrs.args);
|
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);
|
const v = force(value);
|
||||||
|
|
||||||
if (v === null) return null;
|
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 === "string") return v;
|
||||||
if (typeof v === "number") 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") {
|
if (typeof v === "bigint") {
|
||||||
const num = Number(v);
|
const num = Number(v);
|
||||||
if (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER) {
|
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)) {
|
if (Array.isArray(v)) {
|
||||||
return v.map((item) => nixValueToJson(item, seen));
|
return v.map((item) => nixValueToJson(item, seen, outContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof v === "object") {
|
if (typeof v === "object") {
|
||||||
const result: Record<string, any> = {};
|
const result: Record<string, any> = {};
|
||||||
for (const [key, val] of Object.entries(v)) {
|
for (const [key, val] of Object.entries(v)) {
|
||||||
result[key] = nixValueToJson(val, seen);
|
result[key] = nixValueToJson(val, seen, outContext);
|
||||||
}
|
}
|
||||||
return result;
|
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`);
|
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([
|
const specialAttrs = new Set([
|
||||||
"name",
|
"name",
|
||||||
"builder",
|
"builder",
|
||||||
@@ -139,7 +163,7 @@ const extractEnv = (attrs: NixAttrs, structuredAttrs: boolean, ignoreNulls: bool
|
|||||||
if (ignoreNulls && forcedValue === null) {
|
if (ignoreNulls && forcedValue === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
jsonAttrs[key] = nixValueToJson(value);
|
jsonAttrs[key] = nixValueToJson(value, new Set(), outContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
env.set("__json", JSON.stringify(jsonAttrs));
|
env.set("__json", JSON.stringify(jsonAttrs));
|
||||||
@@ -150,7 +174,7 @@ const extractEnv = (attrs: NixAttrs, structuredAttrs: boolean, ignoreNulls: bool
|
|||||||
if (ignoreNulls && forcedValue === null) {
|
if (ignoreNulls && forcedValue === null) {
|
||||||
continue;
|
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 attrs = forceAttrs(args);
|
||||||
|
|
||||||
const drvName = validateName(attrs);
|
const drvName = validateName(attrs);
|
||||||
const builder = validateBuilder(attrs);
|
const collectedContext: NixStringContext = new Set();
|
||||||
|
const builder = validateBuilder(attrs, collectedContext);
|
||||||
const platform = validateSystem(attrs);
|
const platform = validateSystem(attrs);
|
||||||
|
|
||||||
const outputs = extractOutputs(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 ignoreNulls = "__ignoreNulls" in attrs ? force(attrs.__ignoreNulls) === true : false;
|
||||||
|
|
||||||
const drvArgs = extractArgs(attrs);
|
const drvArgs = extractArgs(attrs, collectedContext);
|
||||||
const env = extractEnv(attrs, structuredAttrs, ignoreNulls);
|
const env = extractEnv(attrs, structuredAttrs, ignoreNulls, collectedContext);
|
||||||
|
|
||||||
env.set("name", drvName);
|
env.set("name", drvName);
|
||||||
env.set("builder", builder);
|
env.set("builder", builder);
|
||||||
@@ -211,6 +236,8 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
|
|||||||
env.set("outputs", outputs.join(" "));
|
env.set("outputs", outputs.join(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { inputDrvs, inputSrcs } = extractInputDrvsAndSrcs(collectedContext);
|
||||||
|
|
||||||
let outputInfos: Map<string, OutputInfo>;
|
let outputInfos: Map<string, OutputInfo>;
|
||||||
let drvPath: string;
|
let drvPath: string;
|
||||||
|
|
||||||
@@ -239,8 +266,8 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
|
|||||||
const finalDrv: DerivationData = {
|
const finalDrv: DerivationData = {
|
||||||
name: drvName,
|
name: drvName,
|
||||||
outputs: outputInfos,
|
outputs: outputInfos,
|
||||||
inputDrvs: new Map(),
|
inputDrvs,
|
||||||
inputSrcs: new Set(),
|
inputSrcs,
|
||||||
platform,
|
platform,
|
||||||
builder,
|
builder,
|
||||||
args: drvArgs,
|
args: drvArgs,
|
||||||
@@ -268,8 +295,8 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
|
|||||||
const maskedDrv: DerivationData = {
|
const maskedDrv: DerivationData = {
|
||||||
name: drvName,
|
name: drvName,
|
||||||
outputs: maskedOutputs,
|
outputs: maskedOutputs,
|
||||||
inputDrvs: new Map(),
|
inputDrvs,
|
||||||
inputSrcs: new Set(),
|
inputSrcs,
|
||||||
platform,
|
platform,
|
||||||
builder,
|
builder,
|
||||||
args: drvArgs,
|
args: drvArgs,
|
||||||
|
|||||||
@@ -87,15 +87,15 @@ export const partition =
|
|||||||
(list: NixValue): NixAttrs => {
|
(list: NixValue): NixAttrs => {
|
||||||
const forced_list = forceList(list);
|
const forced_list = forceList(list);
|
||||||
const forced_pred = forceFunction(pred);
|
const forced_pred = forceFunction(pred);
|
||||||
const attrs: NixAttrs = {
|
const attrs = {
|
||||||
right: [],
|
right: [] as NixList,
|
||||||
wrong: [],
|
wrong: [] as NixList,
|
||||||
};
|
};
|
||||||
for (const elem of forced_list) {
|
for (const elem of forced_list) {
|
||||||
if (force(forced_pred(elem))) {
|
if (force(forced_pred(elem))) {
|
||||||
(attrs.right as NixList).push(elem);
|
attrs.right.push(elem);
|
||||||
} else {
|
} else {
|
||||||
(attrs.wrong as NixList).push(elem);
|
attrs.wrong.push(elem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return attrs;
|
return attrs;
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
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 { NixBool, NixStrictValue, NixValue, NixString } from "../types";
|
||||||
|
import * as context from "./context";
|
||||||
|
|
||||||
export const addErrorContext =
|
export const addErrorContext =
|
||||||
(e1: NixValue) =>
|
(e1: NixValue) =>
|
||||||
@@ -12,19 +13,11 @@ export const addErrorContext =
|
|||||||
throw new Error("Not implemented: addErrorContext");
|
throw new Error("Not implemented: addErrorContext");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appendContext =
|
export const appendContext = context.appendContext;
|
||||||
(e1: NixValue) =>
|
|
||||||
(e2: NixValue): never => {
|
|
||||||
throw new Error("Not implemented: appendContext");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getContext = (s: NixValue): never => {
|
export const getContext = context.getContext;
|
||||||
throw new Error("Not implemented: getContext");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasContext = (s: NixValue): never => {
|
export const hasContext = context.hasContext;
|
||||||
throw new Error("Not implemented: hasContext");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hashFile =
|
export const hashFile =
|
||||||
(type: NixValue) =>
|
(type: NixValue) =>
|
||||||
@@ -42,21 +35,15 @@ export const convertHash = (args: NixValue): never => {
|
|||||||
throw new Error("Not implemented: convertHash");
|
throw new Error("Not implemented: convertHash");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const unsafeDiscardOutputDependency = (s: NixValue): never => {
|
export const unsafeDiscardOutputDependency = context.unsafeDiscardOutputDependency;
|
||||||
throw new Error("Not implemented: unsafeDiscardOutputDependency");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const unsafeDiscardStringContext = (s: NixValue): never => {
|
export const unsafeDiscardStringContext = context.unsafeDiscardStringContext;
|
||||||
throw new Error("Not implemented: unsafeDiscardStringContext");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const unsafeGetAttrPos = (s: NixValue): never => {
|
export const unsafeGetAttrPos = (s: NixValue): never => {
|
||||||
throw new Error("Not implemented: unsafeGetAttrPos");
|
throw new Error("Not implemented: unsafeGetAttrPos");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addDrvOutputDependencies = (s: NixValue): never => {
|
export const addDrvOutputDependencies = context.addDrvOutputDependencies;
|
||||||
throw new Error("Not implemented: addDrvOutputDependencies");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const compareVersions =
|
export const compareVersions =
|
||||||
(s1: NixValue) =>
|
(s1: NixValue) =>
|
||||||
|
|||||||
@@ -2,9 +2,36 @@
|
|||||||
* Helper functions for nix-js runtime
|
* 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 { forceAttrs, forceString } from "./type-assert";
|
||||||
import { isAttrs } from "./builtins/type-check";
|
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)
|
* Resolve a path (handles both absolute and relative paths)
|
||||||
|
|||||||
@@ -5,10 +5,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createThunk, force, isThunk, IS_THUNK } from "./thunk";
|
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 { 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";
|
||||||
|
|
||||||
export type NixRuntime = typeof Nix;
|
export type NixRuntime = typeof Nix;
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ export const Nix = {
|
|||||||
force,
|
force,
|
||||||
isThunk,
|
isThunk,
|
||||||
IS_THUNK,
|
IS_THUNK,
|
||||||
|
HAS_CONTEXT,
|
||||||
|
|
||||||
hasAttr,
|
hasAttr,
|
||||||
select,
|
select,
|
||||||
@@ -27,6 +29,7 @@ export const Nix = {
|
|||||||
validateParams,
|
validateParams,
|
||||||
resolvePath,
|
resolvePath,
|
||||||
coerceToString,
|
coerceToString,
|
||||||
|
concatStringsWithContext,
|
||||||
StringCoercionMode,
|
StringCoercionMode,
|
||||||
|
|
||||||
op,
|
op,
|
||||||
|
|||||||
@@ -3,20 +3,45 @@
|
|||||||
* Implements all binary and unary operators used by codegen
|
* 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 { 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";
|
||||||
|
|
||||||
|
const isNixString = (v: unknown): v is NixString => {
|
||||||
|
return typeof v === "string" || isStringWithContext(v);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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)
|
||||||
*/
|
*/
|
||||||
export const op = {
|
export const op = {
|
||||||
// Arithmetic operators - preserve int/float distinction
|
add: (a: NixValue, b: NixValue): bigint | number | NixString => {
|
||||||
add: (a: NixValue, b: NixValue): bigint | number => {
|
const av = force(a);
|
||||||
// FIXME: String & Path
|
const bv = force(b);
|
||||||
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
|
||||||
return (av as any) + (bv as any);
|
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 => {
|
sub: (a: NixValue, b: NixValue): bigint | number => {
|
||||||
@@ -39,52 +64,77 @@ export const op = {
|
|||||||
return (av as any) / (bv as any);
|
return (av as any) / (bv as any);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Comparison operators (JavaScript natively supports bigint/number mixed comparison)
|
|
||||||
eq: (a: NixValue, b: NixValue): boolean => {
|
eq: (a: NixValue, b: NixValue): boolean => {
|
||||||
// FIXME: Int and Float
|
|
||||||
const av = force(a);
|
const av = force(a);
|
||||||
const bv = force(b);
|
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;
|
return av === bv;
|
||||||
},
|
},
|
||||||
neq: (a: NixValue, b: NixValue): boolean => {
|
neq: (a: NixValue, b: NixValue): boolean => {
|
||||||
// FIXME: Int and Float
|
return !op.eq(a, b);
|
||||||
const av = force(a);
|
|
||||||
const bv = force(b);
|
|
||||||
return av !== bv;
|
|
||||||
},
|
},
|
||||||
lt: (a: NixValue, b: NixValue): boolean => {
|
lt: (a: NixValue, b: NixValue): boolean => {
|
||||||
// FIXME: Non-numeric
|
const av = force(a);
|
||||||
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
const bv = force(b);
|
||||||
return (av as any) < (bv as any);
|
|
||||||
|
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 => {
|
lte: (a: NixValue, b: NixValue): boolean => {
|
||||||
// FIXME: Non-numeric
|
const av = force(a);
|
||||||
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
const bv = force(b);
|
||||||
return (av as any) <= (bv as any);
|
|
||||||
|
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 => {
|
gt: (a: NixValue, b: NixValue): boolean => {
|
||||||
// FIXME: Non-numeric
|
const av = force(a);
|
||||||
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
const bv = force(b);
|
||||||
return (av as any) > (bv as any);
|
|
||||||
|
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 => {
|
gte: (a: NixValue, b: NixValue): boolean => {
|
||||||
// FIXME: Non-numeric
|
const av = force(a);
|
||||||
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
const bv = force(b);
|
||||||
return (av as any) >= (bv as any);
|
|
||||||
|
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),
|
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 => {
|
concat: (a: NixValue, b: NixValue): NixList => {
|
||||||
return Array.prototype.concat.call(forceList(a), forceList(b));
|
return Array.prototype.concat.call(forceList(a), forceList(b));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Attribute set update (merge)
|
|
||||||
update: (a: NixValue, b: NixValue): NixAttrs => {
|
update: (a: NixValue, b: NixValue): NixAttrs => {
|
||||||
return { ...forceAttrs(a), ...forceAttrs(b) };
|
return { ...forceAttrs(a), ...forceAttrs(b) };
|
||||||
},
|
},
|
||||||
|
|||||||
194
nix-js/runtime-ts/src/string-context.ts
Normal file
194
nix-js/runtime-ts/src/string-context.ts
Normal 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 };
|
||||||
|
};
|
||||||
@@ -3,8 +3,10 @@
|
|||||||
* These functions force evaluation and verify the type, throwing errors on mismatch
|
* 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 { force } from "./thunk";
|
||||||
|
import { getStringValue } from "./string-context";
|
||||||
|
|
||||||
const typeName = (value: NixValue): string => {
|
const typeName = (value: NixValue): string => {
|
||||||
const val = force(value);
|
const val = force(value);
|
||||||
@@ -13,6 +15,7 @@ const typeName = (value: NixValue): string => {
|
|||||||
if (typeof val === "number") return "float";
|
if (typeof val === "number") return "float";
|
||||||
if (typeof val === "boolean") return "boolean";
|
if (typeof val === "boolean") return "boolean";
|
||||||
if (typeof val === "string") return "string";
|
if (typeof val === "string") return "string";
|
||||||
|
if (isStringWithContext(val)) return "string";
|
||||||
if (val === null) return "null";
|
if (val === null) return "null";
|
||||||
if (Array.isArray(val)) return "list";
|
if (Array.isArray(val)) return "list";
|
||||||
if (typeof val === "function") return "lambda";
|
if (typeof val === "function") return "lambda";
|
||||||
@@ -51,22 +54,47 @@ 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 (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)}`);
|
throw new TypeError(`Expected attribute set, got ${typeName(forced)}`);
|
||||||
}
|
}
|
||||||
return 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
|
* @throws TypeError if value is not a string after forcing
|
||||||
*/
|
*/
|
||||||
export const forceString = (value: NixValue): string => {
|
export const forceString = (value: NixValue): string => {
|
||||||
const forced = force(value);
|
const forced = force(value);
|
||||||
if (typeof forced !== "string") {
|
if (typeof forced === "string") {
|
||||||
throw new TypeError(`Expected string, got ${typeName(forced)}`);
|
|
||||||
}
|
|
||||||
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,13 +3,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IS_THUNK } from "./thunk";
|
import { IS_THUNK } from "./thunk";
|
||||||
|
import { type StringWithContext, HAS_CONTEXT, isStringWithContext } from "./string-context";
|
||||||
|
export { HAS_CONTEXT, isStringWithContext };
|
||||||
|
export type { StringWithContext };
|
||||||
|
|
||||||
// Nix primitive types
|
// Nix primitive types
|
||||||
export type NixInt = bigint;
|
export type NixInt = bigint;
|
||||||
export type NixFloat = number;
|
export type NixFloat = number;
|
||||||
export type NixNumber = NixInt | NixFloat;
|
export type NixNumber = NixInt | NixFloat;
|
||||||
export type NixBool = boolean;
|
export type NixBool = boolean;
|
||||||
export type NixString = string;
|
export type NixString = string | StringWithContext;
|
||||||
export type NixNull = null;
|
export type NixNull = null;
|
||||||
|
|
||||||
// Nix composite types
|
// Nix composite types
|
||||||
|
|||||||
@@ -281,23 +281,13 @@ impl<Ctx: CodegenContext> Compile<Ctx> for List {
|
|||||||
|
|
||||||
impl<Ctx: CodegenContext> Compile<Ctx> for ConcatStrings {
|
impl<Ctx: CodegenContext> Compile<Ctx> for ConcatStrings {
|
||||||
fn compile(&self, ctx: &Ctx) -> String {
|
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
|
let parts: Vec<String> = self
|
||||||
.parts
|
.parts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|part| {
|
.map(|part| ctx.get_ir(*part).compile(ctx))
|
||||||
let compiled = ctx.get_ir(*part).compile(ctx);
|
|
||||||
// TODO: copyToStore
|
|
||||||
format!(
|
|
||||||
"Nix.coerceToString({}, Nix.StringCoercionMode.Interpolation, false)",
|
|
||||||
compiled
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Use array join for concatenation
|
format!("Nix.concatStringsWithContext([{}])", parts.join(","))
|
||||||
format!("[{}].join('')", parts.join(","))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ mod private {
|
|||||||
use private::CtxPtr;
|
use private::CtxPtr;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SccInfo {
|
pub(crate) struct SccInfo {
|
||||||
/// list of SCCs (exprs, recursive)
|
/// list of SCCs (exprs, recursive)
|
||||||
pub sccs: Vec<(Vec<ExprId>, bool)>,
|
pub(crate) sccs: Vec<(Vec<ExprId>, bool)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
|
|||||||
@@ -329,9 +329,6 @@ pub struct Let {
|
|||||||
/// Describes the parameters of a function.
|
/// Describes the parameters of a function.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Param {
|
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.
|
/// The set of required parameter names for a pattern-matching function.
|
||||||
pub required: Option<Vec<SymId>>,
|
pub required: Option<Vec<SymId>>,
|
||||||
/// The set of all allowed parameter names for a non-ellipsis pattern-matching function.
|
/// The set of all allowed parameter names for a non-ellipsis pattern-matching function.
|
||||||
|
|||||||
@@ -121,8 +121,11 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Path {
|
|||||||
|
|
||||||
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Str {
|
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Str {
|
||||||
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
|
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
|
||||||
let parts = self
|
let normalized = self.normalized_parts();
|
||||||
.normalized_parts()
|
let is_single_literal = normalized.len() == 1
|
||||||
|
&& matches!(normalized.first(), Some(ast::InterpolPart::Literal(_)));
|
||||||
|
|
||||||
|
let parts = normalized
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|part| match part {
|
.map(|part| match part {
|
||||||
ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr(Str { val: lit }.to_ir())),
|
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<_>>>()?;
|
.collect::<Result<Vec<_>>>()?;
|
||||||
Ok(if parts.len() == 1 {
|
|
||||||
|
Ok(if is_single_literal {
|
||||||
parts.into_iter().next().unwrap()
|
parts.into_iter().next().unwrap()
|
||||||
} else {
|
} else {
|
||||||
ctx.new_expr(ConcatStrings { parts }.to_ir())
|
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> {
|
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
|
||||||
let arg = ctx.new_arg();
|
let arg = ctx.new_arg();
|
||||||
|
|
||||||
let ident;
|
|
||||||
let required;
|
let required;
|
||||||
let allowed;
|
let allowed;
|
||||||
let body;
|
let body;
|
||||||
@@ -323,7 +326,6 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
|
|||||||
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());
|
||||||
ident = Some(param_sym);
|
|
||||||
required = None;
|
required = None;
|
||||||
allowed = None;
|
allowed = None;
|
||||||
|
|
||||||
@@ -335,7 +337,6 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
|
|||||||
let alias = pattern
|
let alias = pattern
|
||||||
.pat_bind()
|
.pat_bind()
|
||||||
.map(|alias| ctx.new_sym(alias.ident().unwrap().to_string()));
|
.map(|alias| ctx.new_sym(alias.ident().unwrap().to_string()));
|
||||||
ident = alias;
|
|
||||||
|
|
||||||
let has_ellipsis = pattern.ellipsis_token().is_some();
|
let has_ellipsis = pattern.ellipsis_token().is_some();
|
||||||
let pat_entries = pattern.pat_entries();
|
let pat_entries = pattern.pat_entries();
|
||||||
@@ -367,11 +368,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let param = Param {
|
let param = Param { required, allowed };
|
||||||
ident,
|
|
||||||
required,
|
|
||||||
allowed,
|
|
||||||
};
|
|
||||||
// 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(Func { body, param, arg }.to_ir()))
|
Ok(ctx.new_expr(Func { body, param, arg }.to_ir()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
mod codegen;
|
mod codegen;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod ir;
|
mod ir;
|
||||||
mod nix_hash;
|
mod nix_hash;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
pub mod value;
|
pub mod value;
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ pub(crate) struct Runtime<Ctx: RuntimeCtx> {
|
|||||||
js_runtime: JsRuntime,
|
js_runtime: JsRuntime,
|
||||||
is_thunk_symbol: v8::Global<v8::Symbol>,
|
is_thunk_symbol: v8::Global<v8::Symbol>,
|
||||||
primop_metadata_symbol: v8::Global<v8::Symbol>,
|
primop_metadata_symbol: v8::Global<v8::Symbol>,
|
||||||
|
has_context_symbol: v8::Global<v8::Symbol>,
|
||||||
_marker: PhantomData<Ctx>,
|
_marker: PhantomData<Ctx>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +200,7 @@ impl<Ctx: RuntimeCtx> Runtime<Ctx> {
|
|||||||
..Default::default()
|
..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);
|
deno_core::scope!(scope, &mut js_runtime);
|
||||||
Self::get_symbols(scope)?
|
Self::get_symbols(scope)?
|
||||||
};
|
};
|
||||||
@@ -208,6 +209,7 @@ impl<Ctx: RuntimeCtx> Runtime<Ctx> {
|
|||||||
js_runtime,
|
js_runtime,
|
||||||
is_thunk_symbol,
|
is_thunk_symbol,
|
||||||
primop_metadata_symbol,
|
primop_metadata_symbol,
|
||||||
|
has_context_symbol,
|
||||||
_marker: PhantomData,
|
_marker: PhantomData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -225,17 +227,25 @@ impl<Ctx: RuntimeCtx> Runtime<Ctx> {
|
|||||||
let local_value = v8::Local::new(scope, &global_value);
|
let local_value = v8::Local::new(scope, &global_value);
|
||||||
let is_thunk_symbol = v8::Local::new(scope, &self.is_thunk_symbol);
|
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 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(
|
Ok(to_value(
|
||||||
local_value,
|
local_value,
|
||||||
scope,
|
scope,
|
||||||
is_thunk_symbol,
|
is_thunk_symbol,
|
||||||
primop_metadata_symbol,
|
primop_metadata_symbol,
|
||||||
|
has_context_symbol,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get (IS_THUNK, PRIMOP_METADATA)
|
/// get (IS_THUNK, PRIMOP_METADATA, HAS_CONTEXT)
|
||||||
fn get_symbols(scope: &ScopeRef) -> Result<(v8::Global<v8::Symbol>, v8::Global<v8::Symbol>)> {
|
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 global = scope.get_current_context().global(scope);
|
||||||
let nix_key = v8::String::new(scope, "Nix")
|
let nix_key = v8::String::new(scope, "Nix")
|
||||||
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
|
.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);
|
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, '_>,
|
scope: &ScopeRef<'a, '_>,
|
||||||
is_thunk_symbol: LocalSymbol<'a>,
|
is_thunk_symbol: LocalSymbol<'a>,
|
||||||
primop_metadata_symbol: LocalSymbol<'a>,
|
primop_metadata_symbol: LocalSymbol<'a>,
|
||||||
|
has_context_symbol: LocalSymbol<'a>,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
match () {
|
match () {
|
||||||
_ if val.is_big_int() => {
|
_ if val.is_big_int() => {
|
||||||
@@ -296,7 +319,6 @@ fn to_value<'a>(
|
|||||||
}
|
}
|
||||||
_ if val.is_number() => {
|
_ if val.is_number() => {
|
||||||
let val = val.to_number(scope).expect("infallible conversion").value();
|
let val = val.to_number(scope).expect("infallible conversion").value();
|
||||||
// number is always NixFloat
|
|
||||||
Value::Float(val)
|
Value::Float(val)
|
||||||
}
|
}
|
||||||
_ if val.is_true() => Value::Bool(true),
|
_ if val.is_true() => Value::Bool(true),
|
||||||
@@ -312,7 +334,13 @@ fn to_value<'a>(
|
|||||||
let list = (0..len)
|
let list = (0..len)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let val = val.get_index(scope, i).expect("infallible index operation");
|
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();
|
.collect();
|
||||||
Value::List(List::new(list))
|
Value::List(List::new(list))
|
||||||
@@ -329,6 +357,10 @@ fn to_value<'a>(
|
|||||||
return Value::Thunk;
|
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 val = val.to_object(scope).expect("infallible conversion");
|
||||||
let keys = val
|
let keys = val
|
||||||
.get_own_property_names(scope, v8::GetPropertyNamesArgsBuilder::new().build())
|
.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);
|
let key = key.to_rust_string_lossy(scope);
|
||||||
(
|
(
|
||||||
Symbol::new(key),
|
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();
|
.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())
|
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>(
|
fn to_primop<'a>(
|
||||||
val: LocalValue<'a>,
|
val: LocalValue<'a>,
|
||||||
scope: &ScopeRef<'a, '_>,
|
scope: &ScopeRef<'a, '_>,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use core::ops::Deref;
|
|||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::ops::DerefMut;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use derive_more::{Constructor, IsVariant, Unwrap};
|
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 {
|
impl Debug for AttrSet {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
use Value::*;
|
use Value::*;
|
||||||
@@ -129,15 +142,15 @@ pub struct List {
|
|||||||
data: Vec<Value>,
|
data: Vec<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl List {
|
impl Deref for List {
|
||||||
/// Returns the number of elements in the list.
|
type Target = Vec<Value>;
|
||||||
pub fn len(&self) -> usize {
|
fn deref(&self) -> &Self::Target {
|
||||||
self.data.len()
|
&self.data
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/// Returns true if the list is empty.
|
impl DerefMut for List {
|
||||||
pub fn is_empty(&self) -> bool {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
self.data.is_empty()
|
&mut self.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
290
nix-js/tests/string_context.rs
Normal file
290
nix-js/tests/string_context.rs
Normal 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));
|
||||||
|
}
|
||||||
@@ -225,8 +225,14 @@ fn function_to_string_fails() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn to_string_method_must_return_string() {
|
fn to_string_method_must_return_string() {
|
||||||
let result = utils::eval_result(r#"toString { __toString = self: 42; }"#);
|
assert_eq!(
|
||||||
assert!(result.is_err());
|
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]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user