Skip to content

Commit 3c4d4e9

Browse files
committed
Avoid cloning REPL global name maps
1 parent e59c8fa commit 3c4d4e9

3 files changed

Lines changed: 123 additions & 51 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: 29 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ impl<T: ResourceTracker> MontyRepl<T> {
113113
let executor = match ReplExecutor::new_repl_snippet(
114114
code.to_owned(),
115115
&input_script_name,
116-
this.global_name_map.clone(),
116+
&mut this.global_name_map,
117117
&this.interns,
118118
input_names,
119119
) {
@@ -154,6 +154,9 @@ impl<T: ResourceTracker> MontyRepl<T> {
154154
/// partially mutating globals, those mutations remain visible in later feeds,
155155
/// matching Python REPL semantics.
156156
///
157+
/// Compile-time failures may still reserve new global slots in the REPL map.
158+
/// Those slots remain `Undefined` until a later successful snippet writes them.
159+
///
157160
/// # Errors
158161
/// Returns `MontyException` for syntax/compile/runtime failures.
159162
pub fn feed_run(
@@ -172,7 +175,7 @@ impl<T: ResourceTracker> MontyRepl<T> {
172175
let executor = ReplExecutor::new_repl_snippet(
173176
code.to_owned(),
174177
&input_script_name,
175-
self.global_name_map.clone(),
178+
&mut self.global_name_map,
176179
&self.interns,
177180
input_names,
178181
)?;
@@ -205,16 +208,9 @@ impl<T: ResourceTracker> MontyRepl<T> {
205208
self.globals = vm.take_globals();
206209
vm.cleanup();
207210

208-
// Commit compiler metadata even on runtime errors.
209-
// Snippets can mutate globals before raising, and those values may contain
210-
// FunctionId/StringId values that must be interpreted with the updated tables.
211-
let ReplExecutor {
212-
name_map,
213-
interns,
214-
code,
215-
..
216-
} = executor;
217-
self.global_name_map = name_map;
211+
// Commit intern/function metadata even on runtime errors. The REPL global
212+
// name map was already updated in place during preparation.
213+
let ReplExecutor { interns, code, .. } = executor;
218214
self.interns = interns;
219215

220216
result.map_err(|e| e.into_python_exception(&self.interns, &code))
@@ -803,22 +799,18 @@ pub fn detect_repl_continuation_mode(source: &str) -> ReplContinuationMode {
803799
struct ReplExecutor {
804800
/// Number of slots needed in the global namespace.
805801
namespace_size: usize,
806-
/// Maps variable names to their indices in the namespace.
807-
///
808-
/// Stable slot assignment is required across snippets so previously created
809-
/// objects continue to resolve names correctly.
810-
name_map: AHashMap<String, NamespaceId>,
811802
/// Compiled bytecode for the snippet.
812803
module_code: Code,
813804
/// Interned strings and compiled functions for this snippet.
814805
interns: Interns,
815806
/// Source code used for traceback/error rendering.
816807
code: String,
817-
/// Input variable names that were injected for this snippet.
808+
/// Global slots assigned to inputs for this snippet.
818809
///
819-
/// Stored so that `inject_inputs` can look up their namespace slots
820-
/// after compilation assigns them.
821-
input_names: Vec<String>,
810+
/// Inputs are pre-registered in the REPL-global name map before preparation.
811+
/// We store only the assigned slots here so execution can inject values without
812+
/// needing a cloned copy of the full global name map.
813+
input_slots: Vec<usize>,
822814
}
823815

824816
impl ReplExecutor {
@@ -828,23 +820,29 @@ impl ReplExecutor {
828820
/// no-replay execution:
829821
/// - Seeds parsing from `existing_interns` so old `StringId` values stay stable.
830822
/// - Seeds compilation with existing functions so old `FunctionId` values remain valid.
831-
/// - Reuses `existing_name_map` and appends new global names only.
823+
/// - Reuses `existing_name_map` in place and appends new global names only.
832824
///
833-
/// `input_names` are pre-registered in the name map before preparation so they
825+
/// `input_names` are pre-registered in the REPL-global name map before preparation so they
834826
/// receive stable namespace slots that `inject_inputs` can use to store values.
827+
///
828+
/// If snippet preparation fails, newly reserved global slots remain in the
829+
/// caller-owned name map. That is harmless because the corresponding runtime
830+
/// global entries stay `Undefined` until a later successful assignment.
835831
fn new_repl_snippet(
836832
code: String,
837833
script_name: &str,
838-
mut existing_name_map: AHashMap<String, NamespaceId>,
834+
existing_name_map: &mut AHashMap<String, NamespaceId>,
839835
existing_interns: &Interns,
840836
input_names: Vec<String>,
841837
) -> Result<Self, MontyException> {
842838
// Pre-register input names so they get stable slots before preparation.
839+
let mut input_slots = Vec::with_capacity(input_names.len());
843840
for name in &input_names {
844841
let next_slot = existing_name_map.len();
845-
existing_name_map
842+
let slot = *existing_name_map
846843
.entry(name.clone())
847844
.or_insert_with(|| NamespaceId::new(next_slot));
845+
input_slots.push(slot.index());
848846
}
849847

850848
let seeded_interner = InternerBuilder::from_interns(existing_interns, &code);
@@ -863,11 +861,10 @@ impl ReplExecutor {
863861

864862
Ok(Self {
865863
namespace_size: prepared.namespace_size,
866-
name_map: prepared.name_map,
867864
module_code: compile_result.code,
868865
interns,
869866
code,
870-
input_names,
867+
input_slots,
871868
})
872869
}
873870
}
@@ -965,12 +962,7 @@ fn inject_inputs_into_vm(
965962
input_values: Vec<MontyObject>,
966963
vm: &mut VM<'_, '_, impl ResourceTracker>,
967964
) -> Result<(), MontyException> {
968-
for (name, obj) in executor.input_names.iter().zip(input_values) {
969-
let slot = executor
970-
.name_map
971-
.get(name)
972-
.expect("input name should have a namespace slot")
973-
.index();
965+
for (slot, obj) in executor.input_slots.iter().copied().zip(input_values) {
974966
let value = obj
975967
.to_value(vm)
976968
.map_err(|e| MontyException::runtime_error(format!("invalid input type: {e}")))?;
@@ -1048,8 +1040,7 @@ fn build_repl_progress<T: ResourceTracker>(
10481040

10491041
match converted {
10501042
ConvertedExit::Complete(obj) => {
1051-
let ReplExecutor { name_map, interns, .. } = executor;
1052-
repl.global_name_map = name_map;
1043+
let ReplExecutor { interns, .. } = executor;
10531044
repl.interns = interns;
10541045
Ok(ReplProgress::Complete { repl, value: obj })
10551046
}
@@ -1097,11 +1088,9 @@ fn build_repl_progress<T: ResourceTracker>(
10971088
})),
10981089
ConvertedExit::Error(err) => {
10991090
let error = err.into_python_exception(&executor.interns, &executor.code);
1100-
// Commit compiler metadata even on runtime errors, matching feed() behavior.
1101-
// Snippets can create new variables or functions before raising, and those
1102-
// values may reference FunctionId/StringId values from the new tables.
1103-
let ReplExecutor { name_map, interns, .. } = executor;
1104-
repl.global_name_map = name_map;
1091+
// Commit intern/function metadata even on runtime errors, matching feed() behavior.
1092+
// The global name map was already updated in place during preparation.
1093+
let ReplExecutor { interns, .. } = executor;
11051094
repl.interns = interns;
11061095
Err(Box::new(ReplStartError { repl, error }))
11071096
}

crates/monty/tests/repl.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,29 @@ fn repl_runtime_error_keeps_partial_state_consistent() {
8181
assert_eq!(feed_run_print(&mut repl, "x").unwrap(), MontyObject::Int(1));
8282
}
8383

84+
#[test]
85+
fn repl_feed_compile_error_keeps_reserved_global_slot() {
86+
let (mut repl, init_output) = init_repl("");
87+
assert_eq!(init_output, MontyObject::None);
88+
89+
let error = feed_run_print(&mut repl, "reserved_name = 1\nnonlocal missing_name")
90+
.expect_err("snippet should raise a prepare-time SyntaxError");
91+
assert_eq!(error.exc_type(), monty::ExcType::SyntaxError);
92+
assert_eq!(
93+
error.message(),
94+
Some("nonlocal declaration not allowed at module level")
95+
);
96+
97+
assert_eq!(
98+
feed_run_print(&mut repl, "reserved_name = 42").unwrap(),
99+
MontyObject::None
100+
);
101+
assert_eq!(
102+
feed_run_print(&mut repl, "reserved_name").unwrap(),
103+
MontyObject::Int(42)
104+
);
105+
}
106+
84107
#[test]
85108
fn repl_heap_mutations_are_not_replayed() {
86109
let (mut repl, _) = init_repl("items = []");
@@ -260,6 +283,30 @@ fn repl_start_runtime_error_preserves_repl_state() {
260283
assert_eq!(feed_run_print(&mut repl, "x + y + 12").unwrap(), MontyObject::Int(42));
261284
}
262285

286+
#[test]
287+
fn repl_start_compile_error_keeps_reserved_global_slot() {
288+
let (repl, _) = init_repl("");
289+
290+
let err = repl
291+
.feed_start("reserved_name = 1\nnonlocal missing_name", vec![], PrintWriter::Stdout)
292+
.expect_err("expected ReplStartError");
293+
let ReplStartError { mut repl, error } = *err;
294+
assert_eq!(error.exc_type(), monty::ExcType::SyntaxError);
295+
assert_eq!(
296+
error.message(),
297+
Some("nonlocal declaration not allowed at module level")
298+
);
299+
300+
assert_eq!(
301+
feed_run_print(&mut repl, "reserved_name = 42").unwrap(),
302+
MontyObject::None
303+
);
304+
assert_eq!(
305+
feed_run_print(&mut repl, "reserved_name").unwrap(),
306+
MontyObject::Int(42)
307+
);
308+
}
309+
263310
#[test]
264311
fn repl_start_runtime_error_during_external_call_preserves_repl_state() {
265312
// An external function returns an error, which should come back as ReplStartError

0 commit comments

Comments
 (0)