From 4d6fd6d6143f59e9fc5eee6603521a86d6dc941e Mon Sep 17 00:00:00 2001 From: imxyy_soope_ Date: Sat, 31 Jan 2026 22:08:32 +0800 Subject: [PATCH] feat: eval_shallow & eval_deep --- .gitignore | 2 ++ nix-js/benches/utils.rs | 6 ++-- nix-js/runtime-ts/src/index.ts | 5 +-- nix-js/runtime-ts/src/thunk.ts | 37 ++++++++++++++++++-- nix-js/src/bin/eval.rs | 2 +- nix-js/src/bin/repl.rs | 2 +- nix-js/src/context.rs | 36 +++++++++++-------- nix-js/src/value.rs | 52 ++++++++++++++++++++++----- nix-js/tests/derivation.rs | 64 +++++++++++++++++----------------- nix-js/tests/io_operations.rs | 6 ++-- nix-js/tests/lang.rs | 2 +- nix-js/tests/string_context.rs | 6 ++-- nix-js/tests/utils.rs | 17 +++++++-- 13 files changed, 164 insertions(+), 73 deletions(-) diff --git a/.gitignore b/.gitignore index 0626d56..b10ea74 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ target/ # Profiling flamegraph*.svg perf.data* +profile.json.gz +prof.json diff --git a/nix-js/benches/utils.rs b/nix-js/benches/utils.rs index 13e03d5..5fe4173 100644 --- a/nix-js/benches/utils.rs +++ b/nix-js/benches/utils.rs @@ -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 { 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() } diff --git a/nix-js/runtime-ts/src/index.ts b/nix-js/runtime-ts/src/index.ts index 1d5706a..88441c4 100644 --- a/nix-js/runtime-ts/src/index.ts +++ b/nix-js/runtime-ts/src/index.ts @@ -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, diff --git a/nix-js/runtime-ts/src/thunk.ts b/nix-js/runtime-ts/src/thunk.ts index 304164d..020726e 100644 --- a/nix-js/runtime-ts/src/thunk.ts +++ b/nix-js/runtime-ts/src/thunk.ts @@ -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 = new WeakSet()): NixStrictValue => { +export const forceDeep = (value: NixValue, seen: WeakSet = new WeakSet()): NixStrictValue => { const forced = force(value); if (forced === null || typeof forced !== "object") { @@ -171,13 +172,43 @@ export const forceDeepSafe = (value: NixValue, seen: WeakSet = 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 = {}; 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 = {}; + for (const [key, val] of Object.entries(forced)) { + const forcedVal = force(val); + result[key] = forcedVal === forced ? CYCLE_MARKER : forcedVal; } return result; } diff --git a/nix-js/src/bin/eval.rs b/nix-js/src/bin/eval.rs index 4835091..a544cca 100644 --- a/nix-js/src/bin/eval.rs +++ b/nix-js/src/bin/eval.rs @@ -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(()) diff --git a/nix-js/src/bin/repl.rs b/nix-js/src/bin/repl.rs index 61a554e..2217df8 100644 --- a/nix-js/src/bin/repl.rs +++ b/nix-js/src/bin/repl.rs @@ -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)), } diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index 383e7d0..dc0eb2b 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -21,6 +21,21 @@ pub struct Context { runtime: Runtime, } +macro_rules! eval { + ($name:ident, $wrapper:literal) => { + pub fn $name(&mut self, source: Source) -> Result { + 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 { let ctx = Ctx::new()?; @@ -29,19 +44,12 @@ impl Context { Ok(Self { ctx, runtime }) } - pub fn eval_code(&mut self, source: Source) -> Result { - 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 { - self.ctx.compile_code(source) + pub fn compile(&mut self, source: Source) -> Result { + 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 { + fn compile(&mut self, source: Source) -> Result { 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 { - self.compile_code(source) + self.compile(source) } fn get_source(&self, id: usize) -> Source { self.get_source(id) diff --git a/nix-js/src/value.rs b/nix-js/src/value.rs index 25f49b8..12cdf67 100644 --- a/nix-js/src/value.rs +++ b/nix-js/src/value.rs @@ -117,11 +117,30 @@ impl Debug for AttrSet { impl Display for AttrSet { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "{{")?; - for (k, v) in self.data.iter() { - write!(f, " {k} = {v};")?; + use Value::*; + if self.data.len() > 1 { + writeln!(f, "{{")?; + for (k, v) in self.data.iter() { + 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, " }}") } - write!(f, " }}") } } @@ -163,11 +182,28 @@ impl DerefMut for List { impl Display for List { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "[ ")?; - for v in self.data.iter() { - write!(f, "{v} ")?; + use Value::*; + if self.data.len() > 1 { + writeln!(f, "[")?; + for v in self.data.iter() { + 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, "]") } - write!(f, "]") } } diff --git a/nix-js/tests/derivation.rs b/nix-js/tests/derivation.rs index b5891ef..6742679 100644 --- a/nix-js/tests/derivation.rs +++ b/nix-js/tests/derivation.rs @@ -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"; diff --git a/nix-js/tests/io_operations.rs b/nix-js/tests/io_operations.rs index c5a5106..31fbf94 100644 --- a/nix-js/tests/io_operations.rs +++ b/nix-js/tests/io_operations.rs @@ -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())); diff --git a/nix-js/tests/lang.rs b/nix-js/tests/lang.rs index 8c152b7..0118ea1 100644 --- a/nix-js/tests/lang.rs +++ b/nix-js/tests/lang.rs @@ -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()) } diff --git a/nix-js/tests/string_context.rs b/nix-js/tests/string_context.rs index 3730958..edf1d85 100644 --- a/nix-js/tests/string_context.rs +++ b/nix-js/tests/string_context.rs @@ -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"; }; diff --git a/nix-js/tests/utils.rs b/nix-js/tests/utils.rs index 1501a22..94b1f18 100644 --- a/nix-js/tests/utils.rs +++ b/nix-js/tests/utils.rs @@ -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 { + Context::new() + .unwrap() + .eval_deep(Source::new_eval(expr.into()).unwrap()) +} + pub fn eval_result(expr: &str) -> Result { Context::new() .unwrap() - .eval_code(Source::new_eval(expr.into()).unwrap()) + .eval(Source::new_eval(expr.into()).unwrap()) }