feat: eval_shallow & eval_deep

This commit is contained in:
2026-01-31 22:08:32 +08:00
parent b7f4ece472
commit 4d6fd6d614
13 changed files with 164 additions and 73 deletions

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@ target/
# Profiling
flamegraph*.svg
perf.data*
profile.json.gz
prof.json

View File

@@ -7,19 +7,19 @@ use nix_js::value::Value;
pub fn eval(expr: &str) -> Value {
Context::new()
.unwrap()
.eval_code(Source::new_eval(expr.into()).unwrap())
.eval(Source::new_eval(expr.into()).unwrap())
.unwrap()
}
pub fn eval_result(expr: &str) -> Result<Value> {
Context::new()
.unwrap()
.eval_code(Source::new_eval(expr.into()).unwrap())
.eval(Source::new_eval(expr.into()).unwrap())
}
pub fn compile(expr: &str) -> String {
Context::new()
.unwrap()
.compile_code(Source::new_eval(expr.into()).unwrap())
.compile(Source::new_eval(expr.into()).unwrap())
.unwrap()
}

View File

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

View File

@@ -6,6 +6,7 @@
import type { NixValue, NixThunkInterface, NixStrictValue } from "./types";
import { HAS_CONTEXT } from "./string-context";
import { IS_PATH } from "./types";
import { isAttrs } from "./builtins/type-check";
/**
* Symbol used to mark objects as thunks
@@ -151,7 +152,7 @@ export const CYCLE_MARKER = { [IS_CYCLE]: true };
* Returns a fully forced value where thunks are replaced with their results.
* Cyclic references are replaced with CYCLE_MARKER, preserving the container type.
*/
export const forceDeepSafe = (value: NixValue, seen: WeakSet<object> = new WeakSet()): NixStrictValue => {
export const forceDeep = (value: NixValue, seen: WeakSet<object> = new WeakSet()): NixStrictValue => {
const forced = force(value);
if (forced === null || typeof forced !== "object") {
@@ -171,13 +172,43 @@ export const forceDeepSafe = (value: NixValue, seen: WeakSet<object> = new WeakS
}
if (Array.isArray(forced)) {
return forced.map((item) => forceDeepSafe(item, seen));
return forced.map((item) => forceDeep(item, seen));
}
if (typeof forced === "object") {
const result: Record<string, NixValue> = {};
for (const [key, val] of Object.entries(forced)) {
result[key] = forceDeepSafe(val, seen);
result[key] = forceDeep(val, seen);
}
return result;
}
return forced;
};
export const forceShallow = (value: NixValue): NixStrictValue => {
const forced = force(value);
if (forced === null || typeof forced !== "object") {
return forced;
}
if (Array.isArray(forced)) {
return forced.map((item) => {
const forcedItem = force(item);
if (typeof forcedItem === "object" && forcedItem === forced) {
return CYCLE_MARKER
} else {
return forcedItem
}
});
}
if (isAttrs(forced)) {
const result: Record<string, NixValue> = {};
for (const [key, val] of Object.entries(forced)) {
const forcedVal = force(val);
result[key] = forcedVal === forced ? CYCLE_MARKER : forcedVal;
}
return result;
}

View File

@@ -13,7 +13,7 @@ fn main() -> Result<()> {
args.next();
let expr = args.next().unwrap();
let src = Source::new_eval(expr)?;
match Context::new()?.eval_code(src) {
match Context::new()?.eval(src) {
Ok(value) => {
println!("{value}");
Ok(())

View File

@@ -33,7 +33,7 @@ fn main() -> Result<()> {
} */
} else {
let src = Source::new_repl(line)?;
match context.eval_code(src) {
match context.eval_shallow(src) {
Ok(value) => println!("{value}"),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
}

View File

@@ -21,6 +21,21 @@ pub struct Context {
runtime: Runtime<Ctx>,
}
macro_rules! eval {
($name:ident, $wrapper:literal) => {
pub fn $name(&mut self, source: Source) -> Result<Value> {
tracing::info!("Starting evaluation");
tracing::debug!("Compiling code");
let code = self.compile(source)?;
tracing::debug!("Executing JavaScript");
self.runtime
.eval(format!($wrapper, code), &mut self.ctx)
}
};
}
impl Context {
pub fn new() -> Result<Self> {
let ctx = Ctx::new()?;
@@ -29,19 +44,12 @@ impl Context {
Ok(Self { ctx, runtime })
}
pub fn eval_code(&mut self, source: Source) -> Result<Value> {
tracing::info!("Starting evaluation");
eval!(eval, "Nix.force({})");
eval!(eval_shallow, "Nix.forceShallow({})");
eval!(eval_deep, "Nix.forceDeep({})");
tracing::debug!("Compiling code");
let code = self.compile_code(source)?;
tracing::debug!("Executing JavaScript");
self.runtime
.eval(format!("Nix.forceDeepSafe({code})"), &mut self.ctx)
}
pub fn compile_code(&mut self, source: Source) -> Result<String> {
self.ctx.compile_code(source)
pub fn compile(&mut self, source: Source) -> Result<String> {
self.ctx.compile(source)
}
#[allow(dead_code)]
@@ -183,7 +191,7 @@ impl Ctx {
self.sources.get(id).expect("source not found").clone()
}
fn compile_code(&mut self, source: Source) -> Result<String> {
fn compile(&mut self, source: Source) -> Result<String> {
tracing::debug!("Parsing Nix expression");
self.sources.push(source.clone());
@@ -239,7 +247,7 @@ impl RuntimeContext for Ctx {
self.sources.push(source);
}
fn compile_code(&mut self, source: Source) -> Result<String> {
self.compile_code(source)
self.compile(source)
}
fn get_source(&self, id: usize) -> Source {
self.get_source(id)

View File

@@ -117,11 +117,30 @@ impl Debug for AttrSet {
impl Display for AttrSet {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{{")?;
use Value::*;
if self.data.len() > 1 {
writeln!(f, "{{")?;
for (k, v) in self.data.iter() {
write!(f, " {k} = {v};")?;
write!(f, " {k} = ")?;
match v {
List(_) => writeln!(f, "[ ... ];")?,
AttrSet(_) => writeln!(f, "{{ ... }};")?,
v => writeln!(f, "{v};")?,
}
}
write!(f, "}}")
} else {
write!(f, "{{")?;
for (k, v) in self.data.iter() {
write!(f, " {k} = ")?;
match v {
List(_) => write!(f, "[ ... ];")?,
AttrSet(_) => write!(f, "{{ ... }};")?,
v => write!(f, "{v};")?,
}
}
write!(f, " }}")
}
}
}
@@ -163,11 +182,28 @@ impl DerefMut for List {
impl Display for List {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "[ ")?;
use Value::*;
if self.data.len() > 1 {
writeln!(f, "[")?;
for v in self.data.iter() {
write!(f, "{v} ")?;
match v {
List(_) => writeln!(f, " [ ... ]")?,
AttrSet(_) => writeln!(f, " {{ ... }}")?,
v => writeln!(f, " {v}")?,
}
}
write!(f, "]")
} else {
write!(f, "[ ")?;
for v in self.data.iter() {
match v {
List(_) => write!(f, "[ ... ] ")?,
AttrSet(_) => write!(f, "{{ ... }} ")?,
v => write!(f, "{v} ")?,
}
}
write!(f, "]")
}
}
}

View File

@@ -3,12 +3,12 @@
mod utils;
use nix_js::value::Value;
use utils::{eval, eval_result};
use utils::{eval_deep, eval_deep_result};
#[test]
fn derivation_minimal() {
let result =
eval(r#"derivation { name = "hello"; builder = "/bin/sh"; system = "x86_64-linux"; }"#);
eval_deep(r#"derivation { name = "hello"; builder = "/bin/sh"; system = "x86_64-linux"; }"#);
match result {
Value::AttrSet(attrs) => {
@@ -44,7 +44,7 @@ fn derivation_minimal() {
#[test]
fn derivation_with_args() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "test";
builder = "/bin/sh";
@@ -66,7 +66,7 @@ fn derivation_with_args() {
#[test]
fn derivation_to_string() {
let result = eval(
let result = eval_deep(
r#"toString (derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; })"#,
);
@@ -78,7 +78,7 @@ fn derivation_to_string() {
#[test]
fn derivation_missing_name() {
let result = eval_result(r#"derivation { builder = "/bin/sh"; system = "x86_64-linux"; }"#);
let result = eval_deep_result(r#"derivation { builder = "/bin/sh"; system = "x86_64-linux"; }"#);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
@@ -87,7 +87,7 @@ fn derivation_missing_name() {
#[test]
fn derivation_invalid_name_with_drv_suffix() {
let result = eval_result(
let result = eval_deep_result(
r#"derivation { name = "foo.drv"; builder = "/bin/sh"; system = "x86_64-linux"; }"#,
);
@@ -98,7 +98,7 @@ fn derivation_invalid_name_with_drv_suffix() {
#[test]
fn derivation_missing_builder() {
let result = eval_result(r#"derivation { name = "test"; system = "x86_64-linux"; }"#);
let result = eval_deep_result(r#"derivation { name = "test"; system = "x86_64-linux"; }"#);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
@@ -107,7 +107,7 @@ fn derivation_missing_builder() {
#[test]
fn derivation_missing_system() {
let result = eval_result(r#"derivation { name = "test"; builder = "/bin/sh"; }"#);
let result = eval_deep_result(r#"derivation { name = "test"; builder = "/bin/sh"; }"#);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
@@ -116,7 +116,7 @@ fn derivation_missing_system() {
#[test]
fn derivation_with_env_vars() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "test";
builder = "/bin/sh";
@@ -137,7 +137,7 @@ fn derivation_with_env_vars() {
#[test]
fn derivation_strict() {
let result = eval(
let result = eval_deep(
r#"builtins.derivationStrict { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; }"#,
);
@@ -156,8 +156,8 @@ fn derivation_strict() {
fn derivation_deterministic_paths() {
let expr = r#"derivation { name = "hello"; builder = "/bin/sh"; system = "x86_64-linux"; }"#;
let result1 = eval(expr);
let result2 = eval(expr);
let result1 = eval_deep(expr);
let result2 = eval_deep(expr);
match (result1, result2) {
(Value::AttrSet(attrs1), Value::AttrSet(attrs2)) => {
@@ -170,7 +170,7 @@ fn derivation_deterministic_paths() {
#[test]
fn derivation_escaping_in_aterm() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "test";
builder = "/bin/sh";
@@ -190,7 +190,7 @@ fn derivation_escaping_in_aterm() {
#[test]
fn multi_output_two_outputs() {
let drv = eval(
let drv = eval_deep(
r#"derivation {
name = "multi";
builder = "/bin/sh";
@@ -233,7 +233,7 @@ fn multi_output_two_outputs() {
#[test]
fn multi_output_three_outputs() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "three";
builder = "/bin/sh";
@@ -281,7 +281,7 @@ fn multi_output_three_outputs() {
#[test]
fn multi_output_backward_compat() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "compat";
builder = "/bin/sh";
@@ -307,7 +307,7 @@ fn multi_output_backward_compat() {
#[test]
fn multi_output_deterministic() {
let result1 = eval(
let result1 = eval_deep(
r#"derivation {
name = "determ";
builder = "/bin/sh";
@@ -316,7 +316,7 @@ fn multi_output_deterministic() {
}"#,
);
let result2 = eval(
let result2 = eval_deep(
r#"derivation {
name = "determ";
builder = "/bin/sh";
@@ -330,7 +330,7 @@ fn multi_output_deterministic() {
#[test]
fn fixed_output_sha256_flat() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "fixed";
builder = "/bin/sh";
@@ -367,7 +367,7 @@ fn fixed_output_sha256_flat() {
#[test]
fn fixed_output_default_algo() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "default";
builder = "/bin/sh";
@@ -390,7 +390,7 @@ fn fixed_output_default_algo() {
#[test]
fn fixed_output_recursive_mode() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "recursive";
builder = "/bin/sh";
@@ -420,7 +420,7 @@ fn fixed_output_recursive_mode() {
#[test]
fn fixed_output_rejects_multi_output() {
let result = eval_result(
let result = eval_deep_result(
r#"derivation {
name = "invalid";
builder = "/bin/sh";
@@ -437,7 +437,7 @@ fn fixed_output_rejects_multi_output() {
#[test]
fn fixed_output_invalid_hash_mode() {
let result = eval_result(
let result = eval_deep_result(
r#"derivation {
name = "invalid";
builder = "/bin/sh";
@@ -454,7 +454,7 @@ fn fixed_output_invalid_hash_mode() {
#[test]
fn structured_attrs_basic() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "struct";
builder = "/bin/sh";
@@ -479,7 +479,7 @@ fn structured_attrs_basic() {
#[test]
fn structured_attrs_nested() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "nested";
builder = "/bin/sh";
@@ -500,7 +500,7 @@ fn structured_attrs_nested() {
#[test]
fn structured_attrs_rejects_functions() {
let result = eval_result(
let result = eval_deep_result(
r#"derivation {
name = "invalid";
builder = "/bin/sh";
@@ -517,7 +517,7 @@ fn structured_attrs_rejects_functions() {
#[test]
fn structured_attrs_false() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "normal";
builder = "/bin/sh";
@@ -540,7 +540,7 @@ fn structured_attrs_false() {
#[test]
fn ignore_nulls_true() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "ignore";
builder = "/bin/sh";
@@ -562,7 +562,7 @@ fn ignore_nulls_true() {
#[test]
fn ignore_nulls_false() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "keep";
builder = "/bin/sh";
@@ -585,7 +585,7 @@ fn ignore_nulls_false() {
#[test]
fn ignore_nulls_with_structured_attrs() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "combined";
builder = "/bin/sh";
@@ -609,7 +609,7 @@ fn ignore_nulls_with_structured_attrs() {
#[test]
fn all_features_combined() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "all";
builder = "/bin/sh";
@@ -636,7 +636,7 @@ fn all_features_combined() {
#[test]
fn fixed_output_with_structured_attrs() {
let result = eval(
let result = eval_deep(
r#"derivation {
name = "fixstruct";
builder = "/bin/sh";

View File

@@ -105,7 +105,7 @@ fn path_with_file() {
std::fs::write(&test_file, "Hello, World!").unwrap();
let expr = format!(r#"builtins.path {{ path = {}; }}"#, test_file.display());
let result = ctx.eval_code(Source::new_eval(expr).unwrap()).unwrap();
let result = ctx.eval(Source::new_eval(expr).unwrap()).unwrap();
// Should return a store path string
if let Value::String(store_path) = result {
@@ -149,7 +149,7 @@ fn path_with_directory_recursive() {
r#"builtins.path {{ path = {}; recursive = true; }}"#,
test_dir.display()
);
let result = ctx.eval_code(Source::new_eval(expr).unwrap()).unwrap();
let result = ctx.eval(Source::new_eval(expr).unwrap()).unwrap();
if let Value::String(store_path) = result {
assert!(store_path.starts_with(ctx.get_store_dir()));
@@ -170,7 +170,7 @@ fn path_flat_with_file() {
r#"builtins.path {{ path = {}; recursive = false; }}"#,
test_file.display()
);
let result = ctx.eval_code(Source::new_eval(expr).unwrap()).unwrap();
let result = ctx.eval(Source::new_eval(expr).unwrap()).unwrap();
if let Value::String(store_path) = result {
assert!(store_path.starts_with(ctx.get_store_dir()));

View File

@@ -23,7 +23,7 @@ fn eval_file(name: &str) -> Result<(Value, Source), String> {
ty: nix_js::error::SourceType::File(nix_path.into()),
src: expr.into(),
};
ctx.eval_code(source.clone())
ctx.eval_deep(source.clone())
.map(|val| (val, source))
.map_err(|e| e.to_string())
}

View File

@@ -156,7 +156,7 @@ fn string_add_merges_context() {
fn context_in_derivation_args() {
let mut ctx = Context::new().unwrap();
let result = ctx
.eval_code(
.eval(
r#"
let
dep = derivation { name = "dep"; builder = "/bin/sh"; system = "x86_64-linux"; };
@@ -185,7 +185,7 @@ fn context_in_derivation_args() {
fn context_in_derivation_env() {
let mut ctx = Context::new().unwrap();
let result = ctx
.eval_code(
.eval(
r#"
let
dep = derivation { name = "dep"; builder = "/bin/sh"; system = "x86_64-linux"; };
@@ -227,7 +227,7 @@ fn tostring_preserves_context() {
fn interpolation_derivation_returns_outpath() {
let mut ctx = Context::new().unwrap();
let result = ctx
.eval_code(
.eval(
r#"
let
drv = derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; };

View File

@@ -7,12 +7,25 @@ use nix_js::value::Value;
pub fn eval(expr: &str) -> Value {
Context::new()
.unwrap()
.eval_code(Source::new_eval(expr.into()).unwrap())
.eval(Source::new_eval(expr.into()).unwrap())
.unwrap()
}
pub fn eval_deep(expr: &str) -> Value {
Context::new()
.unwrap()
.eval_deep(Source::new_eval(expr.into()).unwrap())
.unwrap()
}
pub fn eval_deep_result(expr: &str) -> Result<Value> {
Context::new()
.unwrap()
.eval_deep(Source::new_eval(expr.into()).unwrap())
}
pub fn eval_result(expr: &str) -> Result<Value> {
Context::new()
.unwrap()
.eval_code(Source::new_eval(expr.into()).unwrap())
.eval(Source::new_eval(expr.into()).unwrap())
}