feat: string context
This commit is contained in:
@@ -11,16 +11,33 @@ import {
|
||||
parseContextToInfoMap,
|
||||
} 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 => {
|
||||
const s = forceNixString(value);
|
||||
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 => {
|
||||
const s = forceNixString(value);
|
||||
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 => {
|
||||
const s = forceNixString(value);
|
||||
const strValue = getStringValue(s);
|
||||
@@ -47,6 +64,12 @@ export const unsafeDiscardOutputDependency = (value: NixValue): NixString => {
|
||||
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 => {
|
||||
const s = forceNixString(value);
|
||||
const strValue = getStringValue(s);
|
||||
@@ -77,6 +100,14 @@ export const addDrvOutputDependencies = (value: NixValue): NixString => {
|
||||
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 => {
|
||||
const s = forceNixString(value);
|
||||
const context = getStringContext(s);
|
||||
@@ -101,6 +132,18 @@ export const getContext = (value: NixValue): NixAttrs => {
|
||||
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 =
|
||||
(strValue: NixValue) =>
|
||||
(ctxValue: NixValue): NixString => {
|
||||
|
||||
@@ -110,6 +110,12 @@ export interface CoerceResult {
|
||||
* Coerce a Nix value to a string according to the specified mode.
|
||||
* 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 mode - The coercion mode (controls which types are allowed)
|
||||
* @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 { isNixPath } from "../types";
|
||||
import { force } from "../thunk";
|
||||
import { coerceToPath, coerceToString, StringCoercionMode } from "./conversion";
|
||||
import { coerceToPath } from "./conversion";
|
||||
import { getPathValue } from "../path";
|
||||
|
||||
// Declare Deno.core.ops global (provided by deno_core runtime)
|
||||
import type { NixStringContext } from "../string-context";
|
||||
|
||||
export const importFunc = (path: NixValue): NixValue => {
|
||||
// TODO: context?
|
||||
const pathStr = coerceToString(path, StringCoercionMode.Base);
|
||||
const context: NixStringContext = new Set();
|
||||
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
|
||||
const code = Deno.core.ops.op_import(pathStr);
|
||||
|
||||
@@ -2,29 +2,83 @@
|
||||
* String operation builtin functions
|
||||
*/
|
||||
|
||||
import type { NixInt, NixValue } from "../types";
|
||||
import { forceString, forceList, forceInt } from "../type-assert";
|
||||
import type { NixInt, NixValue, NixString } from "../types";
|
||||
import { forceString, forceList, forceInt, forceNixString } from "../type-assert";
|
||||
import { coerceToString, StringCoercionMode } from "./conversion";
|
||||
import {
|
||||
type NixStringContext,
|
||||
getStringValue,
|
||||
getStringContext,
|
||||
mkStringWithContext,
|
||||
} from "../string-context";
|
||||
|
||||
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 =
|
||||
(start: NixValue) =>
|
||||
(len: NixValue) =>
|
||||
(s: NixValue): string => {
|
||||
const str = forceString(s);
|
||||
(s: NixValue): NixString => {
|
||||
const startPos = Number(forceInt(start));
|
||||
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 =
|
||||
(sep: NixValue) =>
|
||||
(list: NixValue): string =>
|
||||
// FIXME: context?
|
||||
forceList(list)
|
||||
.map((elem) => coerceToString(elem, StringCoercionMode.Interpolation))
|
||||
.join(forceString(sep));
|
||||
(list: NixValue): NixString => {
|
||||
const context: NixStringContext = new Set();
|
||||
const separator = coerceToString(sep, StringCoercionMode.Interpolation, false, context);
|
||||
|
||||
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 => {
|
||||
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 { isAttrs } from "./builtins/type-check";
|
||||
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||
import {
|
||||
type NixStringContext,
|
||||
mkStringWithContext,
|
||||
isStringWithContext,
|
||||
} from "./string-context";
|
||||
import { type NixStringContext, mkStringWithContext, isStringWithContext } from "./string-context";
|
||||
import { force } from "./thunk";
|
||||
import { mkPath } from "./path";
|
||||
import { CatchableError, isNixPath } from "./types";
|
||||
@@ -18,6 +14,12 @@ import { CatchableError, isNixPath } from "./types";
|
||||
/**
|
||||
* Concatenate multiple values into a string or path with context
|
||||
* 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)
|
||||
*
|
||||
* @param parts - Array of values to concatenate
|
||||
@@ -230,5 +232,5 @@ export const assert = (assertion: NixValue, expr: NixValue, assertionRaw: string
|
||||
if (forceBool(assertion)) {
|
||||
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 interface StringContextOpaque {
|
||||
@@ -143,6 +168,20 @@ export const parseContextToInfoMap = (context: NixStringContext): Map<string, Pa
|
||||
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 = (
|
||||
context: NixStringContext,
|
||||
): { inputDrvs: Map<string, Set<string>>; inputSrcs: Set<string> } => {
|
||||
|
||||
@@ -288,3 +288,92 @@ fn interpolation_derivation_equals_tostring() {
|
||||
);
|
||||
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