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 => { export const derivation = (args: NixValue): NixAttrs => {
const attrs = forceAttrs(args); const attrs = forceAttrs(args);
const strict = derivationStrict(args);
const outputs: string[] = extractOutputs(attrs); 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", 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, outputName,
}; };
outputsList.push(outputObj); return { name: outputName, value };
} };
baseAttrs.drvAttrs = attrs; const outputsList = outputs.map(outputToAttrListElement);
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) { 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; 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 outputsList[0].value;
}; };

View File

@@ -4,7 +4,7 @@
* All functionality is exported via the global `Nix` object * 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 { import {
select, select,
selectWithDefault, selectWithDefault,
@@ -34,9 +34,11 @@ export type NixRuntime = typeof Nix;
export const Nix = { export const Nix = {
createThunk, createThunk,
force, force,
forceDeepSafe,
forceBool, forceBool,
isThunk, isThunk,
IS_THUNK, IS_THUNK,
IS_CYCLE,
HAS_CONTEXT, HAS_CONTEXT,
IS_PATH, IS_PATH,
DEBUG_THUNKS, DEBUG_THUNKS,

View File

@@ -4,6 +4,8 @@
*/ */
import type { NixValue, NixThunkInterface, NixStrictValue } from "./types"; 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 * 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 => { export const createThunk = (func: () => NixValue, label?: string): NixThunkInterface => {
return new NixThunk(func, label); 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"); tracing::debug!("Executing JavaScript");
self.runtime 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> { 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>, primop_metadata_symbol: v8::Global<v8::Symbol>,
has_context_symbol: v8::Global<v8::Symbol>, has_context_symbol: v8::Global<v8::Symbol>,
is_path_symbol: v8::Global<v8::Symbol>, is_path_symbol: v8::Global<v8::Symbol>,
is_cycle_symbol: v8::Global<v8::Symbol>,
_marker: PhantomData<Ctx>, _marker: PhantomData<Ctx>,
} }
@@ -571,7 +572,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
..Default::default() ..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); deno_core::scope!(scope, &mut js_runtime);
Self::get_symbols(scope)? Self::get_symbols(scope)?
}; };
@@ -582,6 +583,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
primop_metadata_symbol, primop_metadata_symbol,
has_context_symbol, has_context_symbol,
is_path_symbol, is_path_symbol,
is_cycle_symbol,
_marker: PhantomData, _marker: PhantomData,
}) })
} }
@@ -609,6 +611,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
let primop_metadata_symbol = v8::Local::new(scope, &self.primop_metadata_symbol); let primop_metadata_symbol = v8::Local::new(scope, &self.primop_metadata_symbol);
let has_context_symbol = v8::Local::new(scope, &self.has_context_symbol); 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_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( Ok(to_value(
local_value, local_value,
@@ -617,10 +620,11 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
primop_metadata_symbol, primop_metadata_symbol,
has_context_symbol, has_context_symbol,
is_path_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)] #[allow(clippy::type_complexity)]
fn get_symbols( fn get_symbols(
scope: &ScopeRef, 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>,
v8::Global<v8::Symbol>, v8::Global<v8::Symbol>,
v8::Global<v8::Symbol>,
)> { )> {
let global = scope.get_current_context().global(scope); let global = scope.get_current_context().global(scope);
let nix_key = v8::String::new(scope, "Nix") let nix_key = v8::String::new(scope, "Nix")
@@ -659,8 +664,9 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
let primop_metadata = get_symbol("PRIMOP_METADATA")?; let primop_metadata = get_symbol("PRIMOP_METADATA")?;
let has_context = get_symbol("HAS_CONTEXT")?; let has_context = get_symbol("HAS_CONTEXT")?;
let is_path = get_symbol("IS_PATH")?; 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>, primop_metadata_symbol: LocalSymbol<'a>,
has_context_symbol: LocalSymbol<'a>, has_context_symbol: LocalSymbol<'a>,
is_path_symbol: LocalSymbol<'a>, is_path_symbol: LocalSymbol<'a>,
is_cycle_symbol: LocalSymbol<'a>,
) -> Value { ) -> Value {
match () { match () {
_ if val.is_big_int() => { _ if val.is_big_int() => {
@@ -707,6 +714,7 @@ fn to_value<'a>(
primop_metadata_symbol, primop_metadata_symbol,
has_context_symbol, has_context_symbol,
is_path_symbol, is_path_symbol,
is_cycle_symbol,
) )
}) })
.collect(); .collect();
@@ -724,6 +732,10 @@ fn to_value<'a>(
return Value::Thunk; 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) { if let Some(path_val) = extract_path(val, scope, is_path_symbol) {
return Value::Path(path_val); return Value::Path(path_val);
} }
@@ -753,6 +765,7 @@ fn to_value<'a>(
primop_metadata_symbol, primop_metadata_symbol,
has_context_symbol, has_context_symbol,
is_path_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()) 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>( fn extract_string_with_context<'a>(
val: LocalValue<'a>, val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>, scope: &ScopeRef<'a, '_>,

View File

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