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
|
||||
*/
|
||||
|
||||
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 toStringMethod = forceFunction(v.__toString);
|
||||
const result = force(toStringMethod(v));
|
||||
if (typeof result !== "string") {
|
||||
throw new TypeError(`__toString returned ${typeName(result)} instead of string`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// 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);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) };
|
||||
},
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
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 { 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
|
||||
|
||||
@@ -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(","))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, '_>,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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]
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user