fix: lazy select_with_default

This commit is contained in:
2026-01-11 16:08:02 +08:00
parent 5b1750b1ba
commit 158784cbe8
6 changed files with 56 additions and 10 deletions

View File

@@ -249,7 +249,7 @@ export const builtins: any = {
builtins: createThunk(() => builtins),
currentSystem: createThunk(() => {
throw new Error("Not implemented: currentSystem");
return "x86_64-linux"
}),
currentTime: createThunk(() => Date.now()),

View File

@@ -4,7 +4,8 @@
import { force } from "../thunk";
import { CatchableError } from "../types";
import type { NixBool, NixStrictValue, NixValue, NixString } from "../types";
import type { NixBool, NixStrictValue, NixValue } from "../types";
import { forceList, forceAttrs, forceFunction } from "../type-assert";
import * as context from "./context";
export const addErrorContext =
@@ -138,6 +139,34 @@ export const tryEval = (e: NixValue): { success: NixBool; value: NixStrictValue
export const zipAttrsWith =
(f: NixValue) =>
(list: NixValue): never => {
throw new Error("Not implemented: zipAttrsWith");
(list: NixValue): NixValue => {
const listForced = forceList(list);
// Map to collect all values for each attribute name
const attrMap = new Map<string, NixValue[]>();
// Iterate through each attribute set in the list
for (const item of listForced) {
const attrs = forceAttrs(force(item) as NixValue);
// Collect all attribute names and their values
for (const [key, value] of Object.entries(attrs)) {
if (!attrMap.has(key)) {
attrMap.set(key, []);
}
attrMap.get(key)!.push(value);
}
}
// Build the result attribute set
const result: Record<string, NixValue> = {};
for (const [name, values] of attrMap.entries()) {
// Apply f to name and values list
// f is curried: f name values
const fWithName = forceFunction(f)(name);
result[name] = forceFunction(fWithName)(values);
}
return result;
};

View File

@@ -72,15 +72,15 @@ export const select = (obj: NixValue, key: NixValue): NixValue => {
*
* @param obj - Attribute set to select from
* @param key - Key to select
* @param default_val - Value to return if key not found
* @returns obj[key] if exists, otherwise default_val
* @param default_val - Value to return if key not found (will be forced if it's a thunk)
* @returns obj[key] if exists, otherwise force(default_val)
*/
export const selectWithDefault = (obj: NixValue, key: NixValue, default_val: NixValue): NixValue => {
const attrs = forceAttrs(obj);
const forced_key = forceString(key);
if (!(forced_key in attrs)) {
return default_val;
return force(default_val);
}
return attrs[forced_key];

View File

@@ -8,8 +8,6 @@ mod nar;
pub use cache::FetcherCache;
pub use download::Downloader;
use std::path::PathBuf;
use deno_core::op2;
use serde::Serialize;

View File

@@ -245,7 +245,8 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Select {
let expr = self.expr().unwrap().downgrade(ctx)?;
let attrpath = downgrade_attrpath(self.attrpath().unwrap(), ctx)?;
let default = if let Some(default) = self.default_expr() {
Some(default.downgrade(ctx)?)
let default_expr = default.downgrade(ctx)?;
Some(ctx.new_expr(Ir::Thunk(default_expr)))
} else {
None
};

View File

@@ -99,3 +99,21 @@ fn logical_not() {
assert_eq!(eval("!true"), Value::Bool(false));
assert_eq!(eval("!false"), Value::Bool(true));
}
#[test]
fn select_with_default_lazy_evaluation() {
assert_eq!(eval("{ a = 1; }.a or (1 / 0)"), Value::Int(1));
}
#[test]
fn select_with_default_nested_lazy() {
assert_eq!(
eval("{ a.b = 42; }.a.b or (builtins.abort \"should not evaluate\")"),
Value::Int(42)
);
}
#[test]
fn select_with_default_fallback() {
assert_eq!(eval("{ a = 1; }.b or 999"), Value::Int(999));
}