fix: derivation semantic
This commit is contained in:
@@ -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}`);
|
for (const { name: outputName, value } of outputsList) {
|
||||||
}
|
commonAttrs[outputName] = createThunk(
|
||||||
baseAttrs.all = createThunk(() => outputsList, "all_outputs");
|
() => outputsList.find((o) => o.name === outputName)!.value,
|
||||||
|
`output_${outputName}`,
|
||||||
for (const outputObj of outputsList) {
|
);
|
||||||
outputObj.drvAttrs = attrs;
|
}
|
||||||
for (const [i, outputName] of outputs.entries()) {
|
commonAttrs.all = createThunk(
|
||||||
outputObj[outputName] = createThunk(() => outputsList[i], `output_${outputName}`);
|
() => outputsList.map((o) => o.value),
|
||||||
}
|
"all_outputs",
|
||||||
outputObj.all = createThunk(() => outputsList, "all_outputs");
|
);
|
||||||
}
|
commonAttrs.drvAttrs = attrs;
|
||||||
|
|
||||||
return outputsList[0];
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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, '_>,
|
||||||
|
|||||||
@@ -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"),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user