fix: derivation semantic

This commit is contained in:
2026-01-29 18:11:20 +08:00
parent 9ee2dd5c08
commit 1f835e7b06
6 changed files with 122 additions and 62 deletions

View File

@@ -365,65 +365,52 @@ const specialAttrs = new Set([
export const derivation = (args: NixValue): NixAttrs => {
const attrs = forceAttrs(args);
const strict = derivationStrict(args);
const outputs: string[] = extractOutputs(attrs);
const drvName = validateName(attrs);
const collectedContext: NixStringContext = new Set();
const builder = validateBuilder(attrs, collectedContext);
const platform = validateSystem(attrs);
const structuredAttrs = "__structuredAttrs" in attrs ? force(attrs.__structuredAttrs) === true : false;
const ignoreNulls = "__ignoreNulls" in attrs ? force(attrs.__ignoreNulls) === true : false;
const drvArgs = extractArgs(attrs, collectedContext);
const baseAttrs: NixAttrs = {
const strictThunk = createThunk(() => derivationStrict(args), "derivationStrict");
const commonAttrs: NixAttrs = { ...attrs };
const outputToAttrListElement = (outputName: string): { name: string; value: NixAttrs } => {
const value: NixAttrs = {
...commonAttrs,
outPath: createThunk(() => (force(strictThunk) as NixAttrs)[outputName], `outPath_${outputName}`),
drvPath: createThunk(() => (force(strictThunk) as NixAttrs).drvPath, "drvPath"),
type: "derivation",
drvPath: strict.drvPath,
name: drvName,
builder,
system: platform,
};
if (drvArgs.length > 0) {
baseAttrs.args = drvArgs;
}
if (!structuredAttrs) {
for (const [key, value] of Object.entries(attrs)) {
if (!specialAttrs.has(key) && !outputs.includes(key)) {
const forcedValue = force(value);
if (!(ignoreNulls && forcedValue === null)) {
baseAttrs[key] = value;
}
}
}
}
const outputsList: NixAttrs[] = [];
for (const outputName of outputs) {
const outputObj: NixAttrs = {
...baseAttrs,
outPath: strict[outputName],
outputName,
};
outputsList.push(outputObj);
}
baseAttrs.drvAttrs = attrs;
for (const [i, outputName] of outputs.entries()) {
baseAttrs[outputName] = createThunk(() => outputsList[i], `output_${outputName}`);
}
baseAttrs.all = createThunk(() => outputsList, "all_outputs");
for (const outputObj of outputsList) {
outputObj.drvAttrs = attrs;
for (const [i, outputName] of outputs.entries()) {
outputObj[outputName] = createThunk(() => outputsList[i], `output_${outputName}`);
}
outputObj.all = createThunk(() => outputsList, "all_outputs");
}
return outputsList[0];
return { name: outputName, value };
};
const outputsList = outputs.map(outputToAttrListElement);
for (const { name: outputName, value } of outputsList) {
commonAttrs[outputName] = createThunk(
() => outputsList.find((o) => o.name === outputName)!.value,
`output_${outputName}`,
);
}
commonAttrs.all = createThunk(
() => outputsList.map((o) => o.value),
"all_outputs",
);
commonAttrs.drvAttrs = attrs;
for (const { value: outputObj } of outputsList) {
for (const { name: outputName } of outputsList) {
outputObj[outputName] = createThunk(
() => outputsList.find((o) => o.name === outputName)!.value,
`output_${outputName}`,
);
}
outputObj.all = createThunk(
() => outputsList.map((o) => o.value),
"all_outputs",
);
outputObj.drvAttrs = attrs;
}
return outputsList[0].value;
};

View File

@@ -4,7 +4,7 @@
* All functionality is exported via the global `Nix` object
*/
import { createThunk, force, isThunk, IS_THUNK, DEBUG_THUNKS } from "./thunk";
import { createThunk, force, isThunk, IS_THUNK, DEBUG_THUNKS, forceDeepSafe, IS_CYCLE } from "./thunk";
import {
select,
selectWithDefault,
@@ -34,9 +34,11 @@ export type NixRuntime = typeof Nix;
export const Nix = {
createThunk,
force,
forceDeepSafe,
forceBool,
isThunk,
IS_THUNK,
IS_CYCLE,
HAS_CONTEXT,
IS_PATH,
DEBUG_THUNKS,

View File

@@ -4,6 +4,8 @@
*/
import type { NixValue, NixThunkInterface, NixStrictValue } from "./types";
import { HAS_CONTEXT } from "./string-context";
import { IS_PATH } from "./types";
/**
* Symbol used to mark objects as thunks
@@ -132,3 +134,50 @@ export const force = (value: NixValue): NixStrictValue => {
export const createThunk = (func: () => NixValue, label?: string): NixThunkInterface => {
return new NixThunk(func, label);
};
/**
* Symbol to mark cyclic references detected during deep forcing
*/
export const IS_CYCLE = Symbol("is_cycle");
/**
* Marker object for cyclic references
*/
export const CYCLE_MARKER = { [IS_CYCLE]: true };
/**
* Deeply force a value, handling cycles by returning a special marker.
* Uses WeakSet to track seen objects and avoid infinite recursion.
* Returns a fully forced value where thunks are replaced with their results.
* Cyclic references are replaced with CYCLE_MARKER.
*/
export const forceDeepSafe = (value: NixValue, seen: WeakSet<object> = new WeakSet()): NixStrictValue => {
const forced = force(value);
if (forced === null || typeof forced !== "object") {
return forced;
}
if (seen.has(forced)) {
return CYCLE_MARKER;
}
seen.add(forced);
if (HAS_CONTEXT in forced || IS_PATH in forced) {
return forced;
}
if (Array.isArray(forced)) {
return forced.map((item) => forceDeepSafe(item, seen));
}
if (typeof forced === "object") {
const result: Record<string, NixValue> = {};
for (const [key, val] of Object.entries(forced)) {
result[key] = forceDeepSafe(val, seen);
}
return result;
}
return forced;
};

View File

@@ -37,7 +37,7 @@ impl Context {
tracing::debug!("Executing JavaScript");
self.runtime
.eval(format!("Nix.force({code})"), &mut self.ctx)
.eval(format!("Nix.forceDeepSafe({code})"), &mut self.ctx)
}
pub fn compile_code(&mut self, source: Source) -> Result<String> {

View File

@@ -552,6 +552,7 @@ pub(crate) struct Runtime<Ctx: RuntimeContext> {
primop_metadata_symbol: v8::Global<v8::Symbol>,
has_context_symbol: v8::Global<v8::Symbol>,
is_path_symbol: v8::Global<v8::Symbol>,
is_cycle_symbol: v8::Global<v8::Symbol>,
_marker: PhantomData<Ctx>,
}
@@ -571,7 +572,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
..Default::default()
});
let (is_thunk_symbol, primop_metadata_symbol, has_context_symbol, is_path_symbol) = {
let (is_thunk_symbol, primop_metadata_symbol, has_context_symbol, is_path_symbol, is_cycle_symbol) = {
deno_core::scope!(scope, &mut js_runtime);
Self::get_symbols(scope)?
};
@@ -582,6 +583,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
_marker: PhantomData,
})
}
@@ -609,6 +611,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
let primop_metadata_symbol = v8::Local::new(scope, &self.primop_metadata_symbol);
let has_context_symbol = v8::Local::new(scope, &self.has_context_symbol);
let is_path_symbol = v8::Local::new(scope, &self.is_path_symbol);
let is_cycle_symbol = v8::Local::new(scope, &self.is_cycle_symbol);
Ok(to_value(
local_value,
@@ -617,10 +620,11 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
))
}
/// get (IS_THUNK, PRIMOP_METADATA, HAS_CONTEXT, IS_PATH)
/// get (IS_THUNK, PRIMOP_METADATA, HAS_CONTEXT, IS_PATH, IS_CYCLE)
#[allow(clippy::type_complexity)]
fn get_symbols(
scope: &ScopeRef,
@@ -629,6 +633,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
v8::Global<v8::Symbol>,
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")
@@ -659,8 +664,9 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
let primop_metadata = get_symbol("PRIMOP_METADATA")?;
let has_context = get_symbol("HAS_CONTEXT")?;
let is_path = get_symbol("IS_PATH")?;
let is_cycle = get_symbol("IS_CYCLE")?;
Ok((is_thunk, primop_metadata, has_context, is_path))
Ok((is_thunk, primop_metadata, has_context, is_path, is_cycle))
}
}
@@ -671,6 +677,7 @@ fn to_value<'a>(
primop_metadata_symbol: LocalSymbol<'a>,
has_context_symbol: LocalSymbol<'a>,
is_path_symbol: LocalSymbol<'a>,
is_cycle_symbol: LocalSymbol<'a>,
) -> Value {
match () {
_ if val.is_big_int() => {
@@ -707,6 +714,7 @@ fn to_value<'a>(
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
)
})
.collect();
@@ -724,6 +732,10 @@ fn to_value<'a>(
return Value::Thunk;
}
if is_cycle(val, scope, is_cycle_symbol) {
return Value::Thunk;
}
if let Some(path_val) = extract_path(val, scope, is_path_symbol) {
return Value::Path(path_val);
}
@@ -753,6 +765,7 @@ fn to_value<'a>(
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
),
)
})
@@ -772,6 +785,15 @@ 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 is_cycle<'a>(val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, symbol: LocalSymbol<'a>) -> bool {
if !val.is_object() {
return false;
}
let obj = val.to_object(scope).expect("infallible conversion");
matches!(obj.get(scope, symbol.into()), Some(v) if v.is_true())
}
fn extract_string_with_context<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,

View File

@@ -470,8 +470,8 @@ fn structured_attrs_basic() {
Value::AttrSet(attrs) => {
assert!(attrs.contains_key("drvPath"));
assert!(attrs.contains_key("outPath"));
assert!(!attrs.contains_key("foo"));
assert!(!attrs.contains_key("count"));
assert!(attrs.contains_key("foo"));
assert!(attrs.contains_key("count"));
}
_ => panic!("Expected AttrSet"),
}
@@ -492,7 +492,7 @@ fn structured_attrs_nested() {
match result {
Value::AttrSet(attrs) => {
assert!(attrs.contains_key("drvPath"));
assert!(!attrs.contains_key("data"));
assert!(attrs.contains_key("data"));
}
_ => panic!("Expected AttrSet"),
}
@@ -554,7 +554,7 @@ fn ignore_nulls_true() {
match result {
Value::AttrSet(attrs) => {
assert!(attrs.contains_key("foo"));
assert!(!attrs.contains_key("nullValue"));
assert!(attrs.contains_key("nullValue"));
}
_ => panic!("Expected AttrSet"),
}
@@ -600,8 +600,8 @@ fn ignore_nulls_with_structured_attrs() {
match result {
Value::AttrSet(attrs) => {
assert!(attrs.contains_key("drvPath"));
assert!(!attrs.contains_key("foo"));
assert!(!attrs.contains_key("nullValue"));
assert!(attrs.contains_key("foo"));
assert!(attrs.contains_key("nullValue"));
}
_ => panic!("Expected AttrSet"),
}
@@ -627,8 +627,8 @@ fn all_features_combined() {
assert!(attrs.contains_key("out"));
assert!(attrs.contains_key("dev"));
assert!(attrs.contains_key("outPath"));
assert!(!attrs.contains_key("data"));
assert!(!attrs.contains_key("nullValue"));
assert!(attrs.contains_key("data"));
assert!(attrs.contains_key("nullValue"));
}
_ => panic!("Expected AttrSet"),
}
@@ -651,7 +651,7 @@ fn fixed_output_with_structured_attrs() {
Value::AttrSet(attrs) => {
assert!(attrs.contains_key("outPath"));
assert!(attrs.contains_key("drvPath"));
assert!(!attrs.contains_key("data"));
assert!(attrs.contains_key("data"));
}
_ => panic!("Expected AttrSet"),
}