Skip to content

Commit a68c8cf

Browse files
committed
Avoid cloning REPL global name maps
1 parent bf7c7ef commit a68c8cf

3 files changed

Lines changed: 116 additions & 43 deletions

File tree

crates/monty/src/prepare.rs

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::hash_map::Entry;
1+
use std::{collections::hash_map::Entry, mem};
22

33
use ahash::{AHashMap, AHashSet};
44

@@ -30,7 +30,7 @@ pub struct PrepareResult {
3030
///
3131
/// This map is used by:
3232
/// - ref-count tests for looking up variables by name
33-
/// - REPL incremental compilation to preserve stable global slot IDs across snippets
33+
/// - initial REPL compilation to seed stable global slot IDs across later snippets
3434
pub name_map: AHashMap<String, NamespaceId>,
3535
/// The prepared AST nodes with all names resolved to namespace indices.
3636
/// Function definitions are inline as `PreparedFunctionDef` variants.
@@ -39,6 +39,20 @@ pub struct PrepareResult {
3939
pub interner: InternerBuilder,
4040
}
4141

42+
/// Result of preparing a REPL snippet against an existing mutable global name map.
43+
///
44+
/// Unlike [`PrepareResult`], this does not return the module name map because the
45+
/// caller-owned map is mutated in place during preparation. On failure, any newly
46+
/// reserved global slots remain in that map so later REPL snippets can reuse them.
47+
pub struct PrepareExistingNamesResult {
48+
/// Number of items in the namespace (at module level, this IS the global namespace)
49+
pub namespace_size: usize,
50+
/// The prepared AST nodes with all names resolved to namespace indices.
51+
pub nodes: Vec<PreparedNode>,
52+
/// The string interner containing all interned identifiers and filenames.
53+
pub interner: InternerBuilder,
54+
}
55+
4256
/// Prepares parsed nodes for compilation by resolving names and building the initial namespace.
4357
///
4458
/// The namespace will be converted to runtime Objects when execution begins and the heap is available.
@@ -69,15 +83,22 @@ pub(crate) fn prepare(parse_result: ParseResult, input_names: Vec<String>) -> Re
6983

7084
/// Prepares parsed nodes for REPL-style incremental compilation using an existing global namespace map.
7185
///
72-
/// Existing bindings keep their original namespace slots; any new names are appended with new slots.
73-
/// This ensures snippets can be compiled independently while sharing one persistent global namespace.
86+
/// Existing bindings keep their original namespace slots; any new names are appended with new slots
87+
/// directly in `existing_name_map`. This ensures snippets can be compiled independently while sharing
88+
/// one persistent global namespace.
89+
///
90+
/// If preparation fails, any newly reserved slots stay in `existing_name_map`. That is intentional for
91+
/// REPL compilation: the corresponding namespace entries remain `Undefined`, so preserving the slots is
92+
/// harmless and avoids cloning the full map before each feed.
7493
pub(crate) fn prepare_with_existing_names(
7594
parse_result: ParseResult,
76-
existing_name_map: AHashMap<String, NamespaceId>,
77-
) -> Result<PrepareResult, ParseError> {
95+
existing_name_map: &mut AHashMap<String, NamespaceId>,
96+
) -> Result<PrepareExistingNamesResult, ParseError> {
7897
let ParseResult { nodes, interner } = parse_result;
7998
let mut p = Prepare::new_module_with_name_map(existing_name_map, &interner);
80-
let mut prepared_nodes = p.prepare_nodes(nodes)?;
99+
let prepare_result = p.prepare_nodes(nodes);
100+
p.write_back_module_name_map(existing_name_map);
101+
let mut prepared_nodes = prepare_result?;
81102

82103
// In the root frame, the last expression is implicitly returned to match REPL behavior.
83104
if let Some(Node::Expr(expr_loc)) = prepared_nodes.last()
@@ -88,9 +109,8 @@ pub(crate) fn prepare_with_existing_names(
88109
prepared_nodes.push(Node::Return(new_expr_loc));
89110
}
90111

91-
Ok(PrepareResult {
112+
Ok(PrepareExistingNamesResult {
92113
namespace_size: p.namespace_size,
93-
name_map: p.name_map,
94114
nodes: prepared_nodes,
95115
interner,
96116
})
@@ -184,10 +204,13 @@ impl<'i> Prepare<'i> {
184204
}
185205
}
186206

187-
/// Creates a module-scope Prepare instance from an existing global name map.
207+
/// Creates a module-scope Prepare instance from an existing mutable global name map.
188208
///
189209
/// Used by incremental REPL compilation to keep stable slot assignments across snippets.
190-
fn new_module_with_name_map(name_map: AHashMap<String, NamespaceId>, interner: &'i InternerBuilder) -> Self {
210+
/// The caller's map is moved into this preparer and must be written back with
211+
/// [`Self::write_back_module_name_map`] before returning.
212+
fn new_module_with_name_map(name_map: &mut AHashMap<String, NamespaceId>, interner: &'i InternerBuilder) -> Self {
213+
let name_map = mem::take(name_map);
191214
let namespace_size = name_map
192215
.values()
193216
.map(|id| id.index())
@@ -210,6 +233,19 @@ impl<'i> Prepare<'i> {
210233
}
211234
}
212235

236+
/// Writes the mutated module-level name map back to the caller-owned REPL state.
237+
///
238+
/// REPL preparation takes ownership of the map so new slots can be appended without cloning.
239+
/// Call this before returning from the incremental prepare path, even on errors, so newly
240+
/// reserved slots remain visible to later snippets.
241+
fn write_back_module_name_map(&mut self, target_name_map: &mut AHashMap<String, NamespaceId>) {
242+
debug_assert!(
243+
self.is_module_scope,
244+
"module name maps can only be written back from module scope"
245+
);
246+
mem::swap(target_name_map, &mut self.name_map);
247+
}
248+
213249
/// Creates a new Prepare instance for function-level code.
214250
///
215251
/// Pre-populates `free_var_map` with nonlocal declarations and implicit captures,

crates/monty/src/repl.rs

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,6 @@ use crate::{
3737
struct ReplExecutor {
3838
/// Number of slots needed in the global namespace.
3939
namespace_size: usize,
40-
/// Maps variable names to their indices in the namespace.
41-
///
42-
/// Stable slot assignment is required across snippets so previously created
43-
/// objects continue to resolve names correctly.
44-
name_map: AHashMap<String, NamespaceId>,
4540
/// Compiled bytecode for the snippet/module.
4641
module_code: Code,
4742
/// Interned strings and compiled functions for this snippet/module.
@@ -55,7 +50,11 @@ impl ReplExecutor {
5550
///
5651
/// This is equivalent to normal module compilation but scoped to REPL
5752
/// infrastructure so `run.rs` can remain unchanged.
58-
fn new(code: String, script_name: &str, input_names: Vec<String>) -> Result<Self, MontyException> {
53+
fn new(
54+
code: String,
55+
script_name: &str,
56+
input_names: Vec<String>,
57+
) -> Result<(Self, AHashMap<String, NamespaceId>), MontyException> {
5958
let parse_result = parse(&code, script_name).map_err(|e| e.into_python_exc(script_name, &code))?;
6059
let prepared = prepare(parse_result, input_names).map_err(|e| e.into_python_exc(script_name, &code))?;
6160

@@ -65,13 +64,15 @@ impl ReplExecutor {
6564
.map_err(|e| e.into_python_exc(script_name, &code))?;
6665
interns.set_functions(compile_result.functions);
6766

68-
Ok(Self {
69-
namespace_size: prepared.namespace_size,
70-
name_map: prepared.name_map,
71-
module_code: compile_result.code,
72-
interns,
73-
code,
74-
})
67+
Ok((
68+
Self {
69+
namespace_size: prepared.namespace_size,
70+
module_code: compile_result.code,
71+
interns,
72+
code,
73+
},
74+
prepared.name_map,
75+
))
7576
}
7677

7778
/// Compiles one incremental REPL snippet against existing session metadata.
@@ -80,11 +81,15 @@ impl ReplExecutor {
8081
/// no-replay execution:
8182
/// - Seeds parsing from `existing_interns` so old `StringId` values stay stable.
8283
/// - Seeds compilation with existing functions so old `FunctionId` values remain valid.
83-
/// - Reuses `existing_name_map` and appends new global names only.
84+
/// - Reuses `existing_name_map` in place and appends new global names only.
85+
///
86+
/// If snippet preparation fails, newly reserved global slots remain in the
87+
/// caller-owned name map. That is harmless because the corresponding runtime
88+
/// namespace entries stay `Undefined` until a later successful assignment.
8489
fn new_repl_snippet(
8590
code: String,
8691
script_name: &str,
87-
existing_name_map: AHashMap<String, NamespaceId>,
92+
existing_name_map: &mut AHashMap<String, NamespaceId>,
8893
existing_interns: &Interns,
8994
) -> Result<Self, MontyException> {
9095
let seeded_interner = InternerBuilder::from_interns(existing_interns, &code);
@@ -103,7 +108,6 @@ impl ReplExecutor {
103108

104109
Ok(Self {
105110
namespace_size: prepared.namespace_size,
106-
name_map: prepared.name_map,
107111
module_code: compile_result.code,
108112
interns,
109113
code,
@@ -282,7 +286,7 @@ impl<T: ResourceTracker> MontyRepl<T> {
282286
resource_tracker: T,
283287
print: &mut PrintWriter<'_>,
284288
) -> Result<(Self, MontyObject), MontyException> {
285-
let executor = ReplExecutor::new(code, script_name, input_names)?;
289+
let (executor, global_name_map) = ReplExecutor::new(code, script_name, input_names)?;
286290

287291
let mut heap = Heap::new(executor.namespace_size, resource_tracker);
288292
let mut namespaces = executor.prepare_namespaces(inputs, &mut heap)?;
@@ -306,7 +310,7 @@ impl<T: ResourceTracker> MontyRepl<T> {
306310
let repl = Self {
307311
script_name: script_name.to_owned(),
308312
next_input_id: 0,
309-
global_name_map: executor.name_map,
313+
global_name_map,
310314
interns: executor.interns,
311315
heap,
312316
namespaces,
@@ -345,7 +349,7 @@ impl<T: ResourceTracker> MontyRepl<T> {
345349
let executor = match ReplExecutor::new_repl_snippet(
346350
code.to_owned(),
347351
&input_script_name,
348-
this.global_name_map.clone(),
352+
&mut this.global_name_map,
349353
&this.interns,
350354
) {
351355
Ok(exec) => exec,
@@ -377,6 +381,9 @@ impl<T: ResourceTracker> MontyRepl<T> {
377381
/// partially mutating globals, those mutations remain visible in later feeds,
378382
/// matching Python REPL semantics.
379383
///
384+
/// Compile-time failures may still reserve new global slots in the REPL map.
385+
/// Those slots remain `Undefined` until a later successful snippet writes them.
386+
///
380387
/// # Errors
381388
/// Returns `MontyException` for syntax/compile/runtime failures.
382389
pub fn feed(&mut self, code: &str, print: &mut PrintWriter<'_>) -> Result<MontyObject, MontyException> {
@@ -388,13 +395,12 @@ impl<T: ResourceTracker> MontyRepl<T> {
388395
let executor = ReplExecutor::new_repl_snippet(
389396
code.to_owned(),
390397
&input_script_name,
391-
self.global_name_map.clone(),
398+
&mut self.global_name_map,
392399
&self.interns,
393400
)?;
394401

395402
let ReplExecutor {
396403
namespace_size,
397-
name_map,
398404
module_code,
399405
interns,
400406
code,
@@ -417,10 +423,8 @@ impl<T: ResourceTracker> MontyRepl<T> {
417423

418424
vm.cleanup();
419425

420-
// Commit compiler metadata even on runtime errors.
421-
// Snippets can mutate globals before raising, and those values may contain
422-
// FunctionId/StringId values that must be interpreted with the updated tables.
423-
self.global_name_map = name_map;
426+
// Commit intern/function metadata even on runtime errors. The REPL global
427+
// name map was already updated in place during preparation.
424428
self.interns = interns;
425429

426430
frame_exit_to_object(frame_exit_result, &mut self.heap, &self.interns)
@@ -976,8 +980,7 @@ fn handle_repl_vm_result<T: ResourceTracker>(
976980
match result {
977981
Ok(FrameExit::Return(value)) => {
978982
let output = MontyObject::new(value, &mut repl.heap, &executor.interns);
979-
let ReplExecutor { name_map, interns, .. } = executor;
980-
repl.global_name_map = name_map;
983+
let ReplExecutor { interns, .. } = executor;
981984
repl.interns = interns;
982985
Ok(ReplProgress::Complete { repl, value: output })
983986
}
@@ -1055,11 +1058,9 @@ fn handle_repl_vm_result<T: ResourceTracker>(
10551058
}
10561059
Err(err) => {
10571060
let error = err.into_python_exception(&executor.interns, &executor.code);
1058-
// Commit compiler metadata even on runtime errors, matching feed() behavior.
1059-
// Snippets can create new variables or functions before raising, and those
1060-
// values may reference FunctionId/StringId values from the new tables.
1061-
let ReplExecutor { name_map, interns, .. } = executor;
1062-
repl.global_name_map = name_map;
1061+
// Commit intern/function metadata even on runtime errors, matching feed() behavior.
1062+
// The global name map was already updated in place during preparation.
1063+
let ReplExecutor { interns, .. } = executor;
10631064
repl.interns = interns;
10641065
Err(Box::new(ReplStartError { repl, error }))
10651066
}

crates/monty/tests/repl.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,24 @@ fn repl_runtime_error_keeps_partial_state_consistent() {
8282
assert_eq!(repl.feed_no_print("x").unwrap(), MontyObject::Int(1));
8383
}
8484

85+
#[test]
86+
fn repl_feed_compile_error_keeps_reserved_global_slot() {
87+
let (mut repl, init_output) = init_repl("");
88+
assert_eq!(init_output, MontyObject::None);
89+
90+
let error = repl
91+
.feed_no_print("reserved_name = 1\nnonlocal missing_name")
92+
.expect_err("snippet should raise a prepare-time SyntaxError");
93+
assert_eq!(error.exc_type(), monty::ExcType::SyntaxError);
94+
assert_eq!(
95+
error.message(),
96+
Some("nonlocal declaration not allowed at module level")
97+
);
98+
99+
assert_eq!(repl.feed_no_print("reserved_name = 42").unwrap(), MontyObject::None);
100+
assert_eq!(repl.feed_no_print("reserved_name").unwrap(), MontyObject::Int(42));
101+
}
102+
85103
#[test]
86104
fn repl_heap_mutations_are_not_replayed() {
87105
let (mut repl, _) = init_repl("items = []");
@@ -255,6 +273,24 @@ fn repl_start_runtime_error_preserves_repl_state() {
255273
assert_eq!(repl.feed_no_print("x + y + 12").unwrap(), MontyObject::Int(42));
256274
}
257275

276+
#[test]
277+
fn repl_start_compile_error_keeps_reserved_global_slot() {
278+
let (repl, _) = init_repl("");
279+
280+
let err = repl
281+
.start("reserved_name = 1\nnonlocal missing_name", &mut PrintWriter::Stdout)
282+
.expect_err("expected ReplStartError");
283+
let ReplStartError { mut repl, error } = *err;
284+
assert_eq!(error.exc_type(), monty::ExcType::SyntaxError);
285+
assert_eq!(
286+
error.message(),
287+
Some("nonlocal declaration not allowed at module level")
288+
);
289+
290+
assert_eq!(repl.feed_no_print("reserved_name = 42").unwrap(), MontyObject::None);
291+
assert_eq!(repl.feed_no_print("reserved_name").unwrap(), MontyObject::Int(42));
292+
}
293+
258294
#[test]
259295
fn repl_start_runtime_error_during_external_call_preserves_repl_state() {
260296
// An external function returns an error, which should come back as ReplStartError

0 commit comments

Comments
 (0)