feat: string context
This commit is contained in:
@@ -11,16 +11,33 @@ import {
|
|||||||
parseContextToInfoMap,
|
parseContextToInfoMap,
|
||||||
} from "../string-context";
|
} from "../string-context";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.hasContext - Check if string has context
|
||||||
|
*
|
||||||
|
* Returns true if the string has any store path references.
|
||||||
|
*/
|
||||||
export const hasContext = (value: NixValue): boolean => {
|
export const hasContext = (value: NixValue): boolean => {
|
||||||
const s = forceNixString(value);
|
const s = forceNixString(value);
|
||||||
return isStringWithContext(s) && s.context.size > 0;
|
return isStringWithContext(s) && s.context.size > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.unsafeDiscardStringContext - Remove all context from string
|
||||||
|
*
|
||||||
|
* IMPORTANT: This discards string context, returning only the string value.
|
||||||
|
* Use with caution as it removes derivation dependencies.
|
||||||
|
*/
|
||||||
export const unsafeDiscardStringContext = (value: NixValue): string => {
|
export const unsafeDiscardStringContext = (value: NixValue): string => {
|
||||||
const s = forceNixString(value);
|
const s = forceNixString(value);
|
||||||
return getStringValue(s);
|
return getStringValue(s);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.unsafeDiscardOutputDependency - Convert DrvDeep to Opaque context
|
||||||
|
*
|
||||||
|
* IMPORTANT: Transforms "all outputs" references (=) to plain path references.
|
||||||
|
* Preserves other context types unchanged.
|
||||||
|
*/
|
||||||
export const unsafeDiscardOutputDependency = (value: NixValue): NixString => {
|
export const unsafeDiscardOutputDependency = (value: NixValue): NixString => {
|
||||||
const s = forceNixString(value);
|
const s = forceNixString(value);
|
||||||
const strValue = getStringValue(s);
|
const strValue = getStringValue(s);
|
||||||
@@ -47,6 +64,12 @@ export const unsafeDiscardOutputDependency = (value: NixValue): NixString => {
|
|||||||
return mkStringWithContext(strValue, newContext);
|
return mkStringWithContext(strValue, newContext);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.addDrvOutputDependencies - Convert Opaque to DrvDeep context
|
||||||
|
*
|
||||||
|
* IMPORTANT: Transforms plain derivation path references to "all outputs" references (=).
|
||||||
|
* The string must have exactly one context element which must be a .drv path.
|
||||||
|
*/
|
||||||
export const addDrvOutputDependencies = (value: NixValue): NixString => {
|
export const addDrvOutputDependencies = (value: NixValue): NixString => {
|
||||||
const s = forceNixString(value);
|
const s = forceNixString(value);
|
||||||
const strValue = getStringValue(s);
|
const strValue = getStringValue(s);
|
||||||
@@ -77,6 +100,14 @@ export const addDrvOutputDependencies = (value: NixValue): NixString => {
|
|||||||
return mkStringWithContext(strValue, newContext);
|
return mkStringWithContext(strValue, newContext);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.getContext - Extract context as structured attribute set
|
||||||
|
*
|
||||||
|
* Returns an attribute set mapping store paths to their context info:
|
||||||
|
* - path: true if it's a plain store path reference (opaque)
|
||||||
|
* - allOutputs: true if it references all derivation outputs (drvDeep, encoded as =path)
|
||||||
|
* - outputs: list of specific output names (built, encoded as !output!path)
|
||||||
|
*/
|
||||||
export const getContext = (value: NixValue): NixAttrs => {
|
export const getContext = (value: NixValue): NixAttrs => {
|
||||||
const s = forceNixString(value);
|
const s = forceNixString(value);
|
||||||
const context = getStringContext(s);
|
const context = getStringContext(s);
|
||||||
@@ -101,6 +132,18 @@ export const getContext = (value: NixValue): NixAttrs => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.appendContext - Add context to a string
|
||||||
|
*
|
||||||
|
* IMPORTANT: Merges the provided context attribute set with any existing context
|
||||||
|
* from the input string. Used to manually construct strings with specific
|
||||||
|
* derivation dependencies.
|
||||||
|
*
|
||||||
|
* Context format matches getContext output:
|
||||||
|
* - path: boolean - add as opaque reference
|
||||||
|
* - allOutputs: boolean - add as drvDeep reference (=)
|
||||||
|
* - outputs: [string] - add as built references (!output!)
|
||||||
|
*/
|
||||||
export const appendContext =
|
export const appendContext =
|
||||||
(strValue: NixValue) =>
|
(strValue: NixValue) =>
|
||||||
(ctxValue: NixValue): NixString => {
|
(ctxValue: NixValue): NixString => {
|
||||||
|
|||||||
@@ -110,6 +110,12 @@ export interface CoerceResult {
|
|||||||
* 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.
|
||||||
*
|
*
|
||||||
|
* IMPORTANT: String context preservation rules:
|
||||||
|
* - StringWithContext: Context is collected in outContext parameter
|
||||||
|
* - Derivations (with outPath): Built context is added for the drvPath/outputName
|
||||||
|
* - Lists (ToString mode): Context from all elements is merged
|
||||||
|
* - All other coercions: No context added
|
||||||
|
*
|
||||||
* @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)
|
||||||
|
|||||||
@@ -7,14 +7,25 @@ import { forceAttrs, forceBool, forceString } from "../type-assert";
|
|||||||
import type { NixValue, NixAttrs } from "../types";
|
import type { NixValue, NixAttrs } from "../types";
|
||||||
import { isNixPath } from "../types";
|
import { isNixPath } from "../types";
|
||||||
import { force } from "../thunk";
|
import { force } from "../thunk";
|
||||||
import { coerceToPath, coerceToString, StringCoercionMode } from "./conversion";
|
import { coerceToPath } from "./conversion";
|
||||||
import { getPathValue } from "../path";
|
import { getPathValue } from "../path";
|
||||||
|
import type { NixStringContext } from "../string-context";
|
||||||
// Declare Deno.core.ops global (provided by deno_core runtime)
|
|
||||||
|
|
||||||
export const importFunc = (path: NixValue): NixValue => {
|
export const importFunc = (path: NixValue): NixValue => {
|
||||||
// TODO: context?
|
const context: NixStringContext = new Set();
|
||||||
const pathStr = coerceToString(path, StringCoercionMode.Base);
|
const pathStr = coerceToPath(path, context);
|
||||||
|
|
||||||
|
// FIXME: Context collected but not yet propagated to build system
|
||||||
|
// This means derivation dependencies from imported paths are not
|
||||||
|
// currently tracked. This will cause issues when:
|
||||||
|
// 1. Importing from derivation outputs: import "${drv}/file.nix"
|
||||||
|
// 2. Building packages that depend on imported configurations
|
||||||
|
if (context.size > 0) {
|
||||||
|
console.warn(
|
||||||
|
`[WARN] import: Path has string context which is not yet fully tracked.
|
||||||
|
Dependency tracking for imported derivations may be incomplete.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Call Rust op - returns JS code string
|
// Call Rust op - returns JS code string
|
||||||
const code = Deno.core.ops.op_import(pathStr);
|
const code = Deno.core.ops.op_import(pathStr);
|
||||||
|
|||||||
@@ -2,29 +2,83 @@
|
|||||||
* String operation builtin functions
|
* String operation builtin functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NixInt, NixValue } from "../types";
|
import type { NixInt, NixValue, NixString } from "../types";
|
||||||
import { forceString, forceList, forceInt } from "../type-assert";
|
import { forceString, forceList, forceInt, forceNixString } from "../type-assert";
|
||||||
import { coerceToString, StringCoercionMode } from "./conversion";
|
import { coerceToString, StringCoercionMode } from "./conversion";
|
||||||
|
import {
|
||||||
|
type NixStringContext,
|
||||||
|
getStringValue,
|
||||||
|
getStringContext,
|
||||||
|
mkStringWithContext,
|
||||||
|
} from "../string-context";
|
||||||
|
|
||||||
export const stringLength = (e: NixValue): NixInt => BigInt(forceString(e).length);
|
export const stringLength = (e: NixValue): NixInt => BigInt(forceString(e).length);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.substring - Extract substring while preserving string context
|
||||||
|
*
|
||||||
|
* IMPORTANT: String context must be preserved from the source string.
|
||||||
|
* This matches Lix behavior where substring operations maintain references
|
||||||
|
* to store paths and derivations.
|
||||||
|
*
|
||||||
|
* Special case: substring 0 0 str can be used idiomatically to capture
|
||||||
|
* string context efficiently without copying the string value.
|
||||||
|
*/
|
||||||
export const substring =
|
export const substring =
|
||||||
(start: NixValue) =>
|
(start: NixValue) =>
|
||||||
(len: NixValue) =>
|
(len: NixValue) =>
|
||||||
(s: NixValue): string => {
|
(s: NixValue): NixString => {
|
||||||
const str = forceString(s);
|
|
||||||
const startPos = Number(forceInt(start));
|
const startPos = Number(forceInt(start));
|
||||||
const length = Number(forceInt(len));
|
const length = Number(forceInt(len));
|
||||||
return str.substring(startPos, startPos + length);
|
|
||||||
|
if (startPos < 0) {
|
||||||
|
throw new Error("negative start position in 'substring'");
|
||||||
|
}
|
||||||
|
|
||||||
|
const str = forceNixString(s);
|
||||||
|
const strValue = getStringValue(str);
|
||||||
|
const context = getStringContext(str);
|
||||||
|
|
||||||
|
if (length === 0) {
|
||||||
|
if (context.size === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return mkStringWithContext("", context);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualLength = length < 0 ? Number.MAX_SAFE_INTEGER : length;
|
||||||
|
const result = startPos >= strValue.length ? "" : strValue.substring(startPos, startPos + actualLength);
|
||||||
|
|
||||||
|
if (context.size === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return mkStringWithContext(result, context);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.concatStringsSep - Concatenate strings with separator, merging contexts
|
||||||
|
*
|
||||||
|
* IMPORTANT: String context must be collected from both the separator and all
|
||||||
|
* list elements, then merged into the result. This ensures that store path
|
||||||
|
* references are preserved when building paths like "/nix/store/xxx/bin:/nix/store/yyy/bin".
|
||||||
|
*/
|
||||||
export const concatStringsSep =
|
export const concatStringsSep =
|
||||||
(sep: NixValue) =>
|
(sep: NixValue) =>
|
||||||
(list: NixValue): string =>
|
(list: NixValue): NixString => {
|
||||||
// FIXME: context?
|
const context: NixStringContext = new Set();
|
||||||
forceList(list)
|
const separator = coerceToString(sep, StringCoercionMode.Interpolation, false, context);
|
||||||
.map((elem) => coerceToString(elem, StringCoercionMode.Interpolation))
|
|
||||||
.join(forceString(sep));
|
const parts = forceList(list).map((elem) =>
|
||||||
|
coerceToString(elem, StringCoercionMode.Interpolation, false, context),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = parts.join(separator);
|
||||||
|
|
||||||
|
if (context.size === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return mkStringWithContext(result, context);
|
||||||
|
};
|
||||||
|
|
||||||
export const baseNameOf = (x: NixValue): string => {
|
export const baseNameOf = (x: NixValue): string => {
|
||||||
const str = forceString(x);
|
const str = forceString(x);
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ import type { NixValue, NixAttrs, NixBool, NixString, NixPath } from "./types";
|
|||||||
import { forceAttrs, forceBool, forceFunction, forceString, typeName } from "./type-assert";
|
import { forceAttrs, forceBool, forceFunction, forceString, typeName } from "./type-assert";
|
||||||
import { isAttrs } from "./builtins/type-check";
|
import { isAttrs } from "./builtins/type-check";
|
||||||
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||||
import {
|
import { type NixStringContext, mkStringWithContext, isStringWithContext } from "./string-context";
|
||||||
type NixStringContext,
|
|
||||||
mkStringWithContext,
|
|
||||||
isStringWithContext,
|
|
||||||
} from "./string-context";
|
|
||||||
import { force } from "./thunk";
|
import { force } from "./thunk";
|
||||||
import { mkPath } from "./path";
|
import { mkPath } from "./path";
|
||||||
import { CatchableError, isNixPath } from "./types";
|
import { CatchableError, isNixPath } from "./types";
|
||||||
@@ -18,6 +14,12 @@ import { CatchableError, isNixPath } from "./types";
|
|||||||
/**
|
/**
|
||||||
* Concatenate multiple values into a string or path with context
|
* Concatenate multiple values into a string or path with context
|
||||||
* This is used for string interpolation like "hello ${world}"
|
* This is used for string interpolation like "hello ${world}"
|
||||||
|
*
|
||||||
|
* IMPORTANT: String context handling:
|
||||||
|
* - All contexts from interpolated values are merged into the result
|
||||||
|
* - Path mode: Store contexts are forbidden (will throw error)
|
||||||
|
* - String mode: All contexts are preserved and merged
|
||||||
|
*
|
||||||
* If first element is a path, result is a path (with constraint: no store context allowed)
|
* If first element is a path, result is a path (with constraint: no store context allowed)
|
||||||
*
|
*
|
||||||
* @param parts - Array of values to concatenate
|
* @param parts - Array of values to concatenate
|
||||||
@@ -230,5 +232,5 @@ export const assert = (assertion: NixValue, expr: NixValue, assertionRaw: string
|
|||||||
if (forceBool(assertion)) {
|
if (forceBool(assertion)) {
|
||||||
return expr;
|
return expr;
|
||||||
}
|
}
|
||||||
throw new CatchableError(`assertion '${assertionRaw}' failed`)
|
throw new CatchableError(`assertion '${assertionRaw}' failed`);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,3 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* String Context System for Nix
|
||||||
|
*
|
||||||
|
* String context tracks references to store paths and derivations within strings.
|
||||||
|
* This is critical for Nix's dependency tracking - when a string containing a
|
||||||
|
* store path is used in a derivation, that store path becomes a build dependency.
|
||||||
|
*
|
||||||
|
* Context Elements (encoded as strings):
|
||||||
|
* - Opaque: Plain store path reference
|
||||||
|
* Format: "/nix/store/..."
|
||||||
|
* Example: "/nix/store/abc123-hello"
|
||||||
|
*
|
||||||
|
* - DrvDeep: Derivation with all outputs
|
||||||
|
* Format: "=/nix/store/...drv"
|
||||||
|
* Example: "=/nix/store/xyz789-hello.drv"
|
||||||
|
* Meaning: All outputs of this derivation and its closure
|
||||||
|
*
|
||||||
|
* - Built: Specific derivation output
|
||||||
|
* Format: "!<output>!/nix/store/...drv"
|
||||||
|
* Example: "!out!/nix/store/xyz789-hello.drv"
|
||||||
|
* Meaning: Specific output (e.g., "out", "dev", "lib") of this derivation
|
||||||
|
*
|
||||||
|
* This implementation matches Lix's NixStringContext system.
|
||||||
|
*/
|
||||||
|
|
||||||
export const HAS_CONTEXT = Symbol("HAS_CONTEXT");
|
export const HAS_CONTEXT = Symbol("HAS_CONTEXT");
|
||||||
|
|
||||||
export interface StringContextOpaque {
|
export interface StringContextOpaque {
|
||||||
@@ -143,6 +168,20 @@ export const parseContextToInfoMap = (context: NixStringContext): Map<string, Pa
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract input derivations and source paths from context
|
||||||
|
*
|
||||||
|
* IMPORTANT: Used by derivation builder to determine build dependencies.
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* - inputDrvs: Map of derivation paths to their required output names
|
||||||
|
* - inputSrcs: Set of plain store paths (opaque) and drvDeep references
|
||||||
|
*
|
||||||
|
* Context type handling:
|
||||||
|
* - Opaque: Added to inputSrcs
|
||||||
|
* - DrvDeep: Added to inputSrcs (entire derivation + all outputs)
|
||||||
|
* - Built: Added to inputDrvs with specific output name
|
||||||
|
*/
|
||||||
export const extractInputDrvsAndSrcs = (
|
export const extractInputDrvsAndSrcs = (
|
||||||
context: NixStringContext,
|
context: NixStringContext,
|
||||||
): { inputDrvs: Map<string, Set<string>>; inputSrcs: Set<string> } => {
|
): { inputDrvs: Map<string, Set<string>>; inputSrcs: Set<string> } => {
|
||||||
|
|||||||
@@ -288,3 +288,92 @@ fn interpolation_derivation_equals_tostring() {
|
|||||||
);
|
);
|
||||||
assert_eq!(result, Value::Bool(true));
|
assert_eq!(result, Value::Bool(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn substring_preserves_context() {
|
||||||
|
let result = eval(
|
||||||
|
r#"
|
||||||
|
let
|
||||||
|
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
|
||||||
|
str = builtins.toString drv;
|
||||||
|
sub = builtins.substring 0 10 str;
|
||||||
|
in builtins.hasContext sub
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert_eq!(result, Value::Bool(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn substring_zero_length_preserves_context() {
|
||||||
|
let result = eval(
|
||||||
|
r#"
|
||||||
|
let
|
||||||
|
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
|
||||||
|
str = builtins.toString drv;
|
||||||
|
empty = builtins.substring 0 0 str;
|
||||||
|
in builtins.hasContext empty
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert_eq!(result, Value::Bool(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn substring_zero_length_empty_value() {
|
||||||
|
let result = eval(
|
||||||
|
r#"
|
||||||
|
let
|
||||||
|
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
|
||||||
|
str = builtins.toString drv;
|
||||||
|
empty = builtins.substring 0 0 str;
|
||||||
|
in empty == ""
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert_eq!(result, Value::Bool(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn concatStringsSep_preserves_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 = builtins.concatStringsSep ":" [str1 str2];
|
||||||
|
in builtins.hasContext combined
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert_eq!(result, Value::Bool(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn concatStringsSep_merges_contexts() {
|
||||||
|
let result = eval(
|
||||||
|
r#"
|
||||||
|
let
|
||||||
|
drv1 = derivation { name = "test1"; builder = "/bin/sh"; system = "x86_64-linux"; };
|
||||||
|
drv2 = derivation { name = "test2"; builder = "/bin/sh"; system = "x86_64-linux"; };
|
||||||
|
str1 = builtins.toString drv1;
|
||||||
|
str2 = builtins.toString drv2;
|
||||||
|
combined = builtins.concatStringsSep ":" [str1 str2];
|
||||||
|
ctx = builtins.getContext combined;
|
||||||
|
in builtins.length (builtins.attrNames ctx)
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert_eq!(result, Value::Int(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn concatStringsSep_separator_has_context() {
|
||||||
|
let result = eval(
|
||||||
|
r#"
|
||||||
|
let
|
||||||
|
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };
|
||||||
|
sep = builtins.toString drv;
|
||||||
|
combined = builtins.concatStringsSep sep ["a" "b"];
|
||||||
|
in builtins.hasContext combined
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
assert_eq!(result, Value::Bool(true));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user