@@ -17,15 +17,34 @@ use serde::{Deserialize, Serialize};
1717
1818const MAX_PAGE_SIZE : usize = 16 * 1024 ;
1919
20+ /// Evaluates Nushell code in isolated contexts for MCP.
21+ ///
22+ /// # Architecture
23+ ///
24+ /// The evaluator maintains a pristine `EngineState` template. Each evaluation:
25+ /// 1. Clones the engine state (cheap due to internal `Arc`s)
26+ /// 2. Parses code into a `Block` and gets a `StateDelta` via `working_set.render()`
27+ /// 3. **Merges the delta** via `engine_state.merge_delta()` to register blocks
28+ /// 4. Evaluates the block with the merged state
29+ ///
30+ /// Step 3 is critical: parsed blocks (including closures) are only stored in the
31+ /// `StateWorkingSet` initially. Without merging, `eval_block()` will panic with
32+ /// "missing block" when it tries to execute closures or other block references.
33+ ///
34+ /// # Isolation
35+ ///
36+ /// Each evaluation gets its own cloned state, so variables/definitions from one
37+ /// evaluation don't persist to the next.
38+ ///
39+ /// This architecture also enables future parallel evaluation of multiple pipelines.
2040pub struct Evaluator {
21- engine_state : Arc < EngineState > ,
41+ engine_state : EngineState ,
2242 cache : Cache < String , Arc < PipelineBuffer > > ,
2343}
2444
2545impl Evaluator {
26- pub fn new ( engine_state : Arc < EngineState > ) -> Self {
46+ pub fn new ( mut engine_state : EngineState ) -> Self {
2747 // Disable ANSI coloring for MCP - it's a computer-to-computer protocol
28- let mut engine_state = Arc :: unwrap_or_clone ( engine_state) ;
2948 let mut config = Config :: clone ( engine_state. get_config ( ) ) ;
3049 config. use_ansi_coloring = UseAnsiColoring :: False ;
3150 engine_state. set_config ( config) ;
@@ -35,7 +54,7 @@ impl Evaluator {
3554 . time_to_live ( std:: time:: Duration :: from_secs ( 300 ) )
3655 . build ( ) ;
3756 Self {
38- engine_state : Arc :: new ( engine_state ) ,
57+ engine_state,
3958 cache,
4059 }
4160 }
@@ -45,28 +64,39 @@ impl Evaluator {
4564 {
4665 pipeline_buffer
4766 } else {
48- let engine_state = Arc :: clone ( & self . engine_state ) ;
49- let mut working_set = StateWorkingSet :: new ( & engine_state) ;
50-
51- // Parse the source code
52- let block = parse ( & mut working_set, None , nu_source. as_bytes ( ) , false ) ;
53-
54- // Check for parse errors
55- if let Some ( err) = working_set. parse_errors . first ( ) {
56- return Err ( McpError :: internal_error (
57- nu_protocol:: format_cli_error ( & working_set, err, None ) ,
58- None ,
59- ) ) ;
60- }
67+ // Clone the pristine engine state for this evaluation
68+ let mut engine_state = self . engine_state . clone ( ) ;
6169
62- // Check for compile errors (IR compilation errors)
63- // These are caught during the parse/compile phase, before evaluation
64- if let Some ( err) = working_set. compile_errors . first ( ) {
65- return Err ( McpError :: internal_error (
66- nu_protocol:: format_cli_error ( & working_set, err, None ) ,
67- None ,
68- ) ) ;
69- }
70+ let ( block, delta) = {
71+ let mut working_set = StateWorkingSet :: new ( & engine_state) ;
72+
73+ // Parse the source code
74+ let block = parse ( & mut working_set, None , nu_source. as_bytes ( ) , false ) ;
75+
76+ // Check for parse errors
77+ if let Some ( err) = working_set. parse_errors . first ( ) {
78+ return Err ( McpError :: internal_error (
79+ nu_protocol:: format_cli_error ( & working_set, err, None ) ,
80+ None ,
81+ ) ) ;
82+ }
83+
84+ // Check for compile errors (IR compilation errors)
85+ // These are caught during the parse/compile phase, before evaluation
86+ if let Some ( err) = working_set. compile_errors . first ( ) {
87+ return Err ( McpError :: internal_error (
88+ nu_protocol:: format_cli_error ( & working_set, err, None ) ,
89+ None ,
90+ ) ) ;
91+ }
92+
93+ ( block, working_set. render ( ) )
94+ } ;
95+
96+ // Merge the parsed blocks into the engine state so they're available during eval
97+ engine_state
98+ . merge_delta ( delta)
99+ . map_err ( |e| shell_error_to_mcp_error ( e, & engine_state) ) ?;
70100
71101 // Eval the block with the input
72102 let mut stack = Stack :: new ( ) . collect_value ( ) ;
@@ -78,7 +108,7 @@ impl Evaluator {
78108 )
79109 . map_err ( |e| shell_error_to_mcp_error ( e, & engine_state) ) ?;
80110
81- let r = Arc :: new ( self . process_pipeline ( output) ?) ;
111+ let r = Arc :: new ( self . process_pipeline ( output, & engine_state ) ?) ;
82112 self . cache . insert ( nu_source. to_string ( ) , Arc :: clone ( & r) ) ;
83113 r
84114 } ;
@@ -104,8 +134,8 @@ impl Evaluator {
104134 fn process_pipeline (
105135 & self ,
106136 pipeline_execution_data : PipelineExecutionData ,
137+ engine_state : & EngineState ,
107138 ) -> Result < PipelineBuffer , McpError > {
108- let engine_state = Arc :: clone ( & self . engine_state ) ;
109139 let span = pipeline_execution_data. span ( ) ;
110140 // todo - this bystream use case won't work
111141 if let PipelineData :: ByteStream ( _stream, ..) = pipeline_execution_data. body {
@@ -128,7 +158,7 @@ impl Evaluator {
128158 let mut last_page_index = 0 ;
129159 for item in pipeline_execution_data. body {
130160 let out = if let Value :: Error { error, .. } = item {
131- return Err ( shell_error_to_mcp_error ( * error, & engine_state) ) ;
161+ return Err ( shell_error_to_mcp_error ( * error, engine_state) ) ;
132162 } else {
133163 item. to_expanded_string ( "\n " , config) + "\n "
134164 } ;
@@ -147,7 +177,7 @@ impl Evaluator {
147177 last_index = buffer. len ( ) ;
148178
149179 write_all_and_flush ( out, & mut buffer, "mcp_output" , span, engine_state. signals ( ) )
150- . map_err ( |e| shell_error_to_mcp_error ( e, & engine_state) ) ?;
180+ . map_err ( |e| shell_error_to_mcp_error ( e, engine_state) ) ?;
151181 }
152182 if pages. is_empty ( ) {
153183 pages. push ( Page :: new ( 0 ..buffer. len ( ) , page_total) ) ;
@@ -238,7 +268,7 @@ mod tests {
238268 Some ( Span :: test_data ( ) ) ,
239269 false ,
240270 ) ?;
241- let evaluator = Evaluator :: new ( Arc :: new ( engine_state) ) ;
271+ let evaluator = Evaluator :: new ( engine_state) ;
242272 let result = evaluator. eval ( & nuon_values, None ) ?;
243273 assert_eq ! ( result. summary. total, 3 ) ;
244274 assert ! ( result. summary. next_cursor. is_some( ) ) ;
@@ -248,7 +278,7 @@ mod tests {
248278 #[ test]
249279 fn test_evaluator_parse_error_message ( ) {
250280 let engine_state = create_default_context ( ) ;
251- let evaluator = Evaluator :: new ( Arc :: new ( engine_state) ) ;
281+ let evaluator = Evaluator :: new ( engine_state) ;
252282
253283 // Invalid syntax - missing closing bracket
254284 let result = evaluator. eval ( "let x = [1, 2, 3" , None ) ;
@@ -285,7 +315,7 @@ mod tests {
285315 #[ test]
286316 fn test_evaluator_compile_error_message ( ) {
287317 let engine_state = create_default_context ( ) ;
288- let evaluator = Evaluator :: new ( Arc :: new ( engine_state) ) ;
318+ let evaluator = Evaluator :: new ( engine_state) ;
289319
290320 // This will trigger a compile error (IR compilation)
291321 // because create_default_context doesn't fully compile blocks for pipelines
@@ -311,7 +341,7 @@ mod tests {
311341 #[ test]
312342 fn test_evaluator_runtime_error_message ( ) {
313343 let engine_state = create_default_context ( ) ;
314- let evaluator = Evaluator :: new ( Arc :: new ( engine_state) ) ;
344+ let evaluator = Evaluator :: new ( engine_state) ;
315345
316346 // Use error make to create a runtime error with custom message and labels
317347 let result = evaluator. eval (
@@ -332,4 +362,27 @@ mod tests {
332362 "Error message should contain rich formatting and custom error message, but got: {err_msg}"
333363 ) ;
334364 }
365+
366+ #[ test]
367+ fn test_closure_in_pipeline ( ) {
368+ // Use add_default_context which includes basic language commands
369+ let engine_state = {
370+ let engine_state = nu_protocol:: engine:: EngineState :: new ( ) ;
371+ nu_cmd_lang:: add_default_context ( engine_state)
372+ } ;
373+ let evaluator = Evaluator :: new ( engine_state) ;
374+
375+ // Test with a simple closure using 'do' which is a lang command
376+ // The closure { ... } creates a block that must be available when eval_block runs
377+ // This tests that merge_delta properly registers blocks in the engine_state
378+ let result = evaluator. eval ( r#"do { |x| $x + 1 } 41"# , None ) ;
379+
380+ assert ! (
381+ result. is_ok( ) ,
382+ "Pipeline with closure should succeed: {:?}" ,
383+ result. err( )
384+ ) ;
385+ let output = result. unwrap ( ) ;
386+ assert_eq ! ( output. summary. total, 1 , "Should have 1 result" ) ;
387+ }
335388}
0 commit comments