Skip to content

Commit 0c85b5c

Browse files
authored
feat: register_external_functions() — set ext funcs without clearing state (#7)
feat: register_external_functions() — set ext funcs without clearing state
2 parents 488d8a9 + 3625abd commit 0c85b5c

File tree

10 files changed

+181
-35
lines changed

10 files changed

+181
-35
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ default-members = ["crates/ouros-cli"]
1313

1414
[workspace.package]
1515
edition = "2024"
16-
version = "0.0.4"
16+
version = "0.0.6"
1717
rust-version = "1.90"
1818
license = "MIT"
1919
authors = ["parcadei <227596144+parcadei@users.noreply.github.com>"]

crates/ouros-js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ouros",
3-
"version": "0.0.4",
3+
"version": "0.0.6",
44
"type": "module",
55
"description": "Sandboxed Python interpreter for JavaScript/TypeScript",
66
"main": "wrapper.js",

crates/ouros-python/python/ouros/session.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ def reset(self, *, external_functions: list[str] | None = None) -> None:
126126
"""Reset this session to a fresh state."""
127127
self._manager._native.reset(session_id=self._id, external_functions=external_functions)
128128

129+
def register_external_functions(self, external_functions: list[str]) -> list[str]:
130+
"""Register additional external functions without clearing session state.
131+
132+
Returns names skipped due to collision with existing user variables.
133+
"""
134+
return self._manager.register_external_functions(external_functions, session_id=self._id)
135+
129136
def __repr__(self) -> str:
130137
return f'Session(id={self._id!r})'
131138

@@ -264,6 +271,18 @@ def reset(self, *, session_id: str | None = None, external_functions: list[str]
264271
"""Reset a session to a fresh state."""
265272
self._native.reset(session_id=session_id, external_functions=external_functions)
266273

274+
def register_external_functions(
275+
self,
276+
external_functions: list[str],
277+
*,
278+
session_id: str | None = None,
279+
) -> list[str]:
280+
"""Register additional external functions without clearing session state.
281+
282+
Returns names skipped due to collision with existing user variables.
283+
"""
284+
return self._native.register_external_functions(external_functions, session_id=session_id)
285+
267286
# -- Cross-session pipeline ------------------------------------------------
268287

269288
def call_session(

crates/ouros-python/src/session_manager.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,21 @@ impl PySessionManager {
337337
Ok(())
338338
}
339339

340+
/// Register additional external functions without clearing session state.
341+
///
342+
/// Functions already registered are silently skipped. Names that collide
343+
/// with existing user variables are skipped and returned.
344+
#[pyo3(signature = (external_functions, *, session_id=None))]
345+
fn register_external_functions(
346+
&mut self,
347+
external_functions: Vec<String>,
348+
session_id: Option<&str>,
349+
) -> PyResult<Vec<String>> {
350+
self.inner
351+
.register_external_functions(session_id, external_functions)
352+
.map_err(session_err_to_py)
353+
}
354+
340355
// -------------------------------------------------------------------------
341356
// Persistence
342357
// -------------------------------------------------------------------------
@@ -542,22 +557,44 @@ fn progress_to_dict<'py>(py: Python<'py>, progress: &ReplProgress) -> PyResult<B
542557
dict.set_item("result", json_to_py(py, &json_val)?)?;
543558
}
544559
ReplProgress::FunctionCall {
545-
function_name, call_id, ..
560+
function_name,
561+
call_id,
562+
args,
563+
kwargs,
546564
} => {
547565
dict.set_item("status", "function_call")?;
548566
dict.set_item("function_name", function_name)?;
549567
dict.set_item("call_id", call_id)?;
568+
let py_args: PyResult<Vec<Py<PyAny>>> = args.iter().map(|a| json_to_py(py, &a.to_json_value())).collect();
569+
dict.set_item("args", PyList::new(py, py_args?)?)?;
570+
let kw_dict = PyDict::new(py);
571+
for (k, v) in kwargs {
572+
let key = json_to_py(py, &k.to_json_value())?;
573+
let val = json_to_py(py, &v.to_json_value())?;
574+
kw_dict.set_item(key, val)?;
575+
}
576+
dict.set_item("kwargs", kw_dict)?;
550577
}
551578
ReplProgress::ProxyCall {
552579
proxy_id,
553580
method,
554581
call_id,
555-
..
582+
args,
583+
kwargs,
556584
} => {
557585
dict.set_item("status", "proxy_call")?;
558586
dict.set_item("proxy_id", proxy_id)?;
559587
dict.set_item("method", method)?;
560588
dict.set_item("call_id", call_id)?;
589+
let py_args: PyResult<Vec<Py<PyAny>>> = args.iter().map(|a| json_to_py(py, &a.to_json_value())).collect();
590+
dict.set_item("args", PyList::new(py, py_args?)?)?;
591+
let kw_dict = PyDict::new(py);
592+
for (k, v) in kwargs {
593+
let key = json_to_py(py, &k.to_json_value())?;
594+
let val = json_to_py(py, &v.to_json_value())?;
595+
kw_dict.set_item(key, val)?;
596+
}
597+
dict.set_item("kwargs", kw_dict)?;
561598
}
562599
ReplProgress::ResolveFutures { pending_call_ids, .. } => {
563600
dict.set_item("status", "resolve_futures")?;

crates/ouros/src/modules/codecs_mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2235,7 +2235,7 @@ fn decode_utf16_ex(data: &[u8], errors: &str, byteorder: i64, final_flag: bool)
22352235
}
22362236

22372237
let mut out = String::new();
2238-
for decoded in char::decode_utf16(units.into_iter()) {
2238+
for decoded in char::decode_utf16(units) {
22392239
match decoded {
22402240
Ok(ch) => out.push(ch),
22412241
Err(_) => match errors {
@@ -2430,7 +2430,7 @@ fn decode_utf7(data: &[u8], errors: &str, _final_flag: bool) -> RunResult<(Strin
24302430
units.push(u16::from_be_bytes([decoded_bytes[j], decoded_bytes[j + 1]]));
24312431
j += 2;
24322432
}
2433-
for ch in char::decode_utf16(units.into_iter()) {
2433+
for ch in char::decode_utf16(units) {
24342434
match ch {
24352435
Ok(ch) => out.push(ch),
24362436
Err(_) => match errors {

crates/ouros/src/modules/itertools.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ fn itertools_chain(heap: &mut Heap<impl ResourceTracker>, interns: &Interns, arg
350350
iter.drop_with_heap(heap);
351351

352352
// items contains owned values - transfer ownership to result
353-
result.extend(items.into_iter());
353+
result.extend(items);
354354
}
355355
// pos is fully consumed above
356356

crates/ouros/src/repl.rs

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
use std::time::Instant;
77

8-
use ahash::AHashMap;
8+
use ahash::{AHashMap, AHashSet};
99
use num_bigint::BigInt;
1010

1111
use crate::{
@@ -181,8 +181,6 @@ pub struct ReplSession {
181181
name_map: AHashMap<String, NamespaceId>,
182182
/// Current size of the global namespace.
183183
namespace_size: usize,
184-
/// Number of external function slots at the start of the namespace.
185-
external_function_count: usize,
186184
/// Script name used in parse/runtime error reporting.
187185
script_name: String,
188186
/// VM snapshot waiting to be resumed after an interactive yield.
@@ -205,6 +203,8 @@ pub struct ReplSession {
205203
/// `ResolveFutures` is returned to the host, and stored in
206204
/// `PendingFuturesState` for incremental resolution.
207205
future_metadata: AHashMap<u32, PendingFutureInfo>,
206+
/// O(1) membership set mirroring `external_functions` for name checks.
207+
external_function_set: AHashSet<String>,
208208
}
209209

210210
impl ReplSession {
@@ -238,6 +238,7 @@ impl ReplSession {
238238
}
239239

240240
let namespace_size = namespace_values.len();
241+
let external_function_set: AHashSet<String> = external_functions.iter().cloned().collect();
241242

242243
Self {
243244
interner: InternerBuilder::new(""),
@@ -247,13 +248,13 @@ impl ReplSession {
247248
namespaces: Namespaces::new(namespace_values),
248249
name_map,
249250
namespace_size,
250-
external_function_count: external_function_ids.len(),
251251
script_name: script_name.to_string(),
252252
pending_snapshot: None,
253253
pending_resume_state: None,
254254
pending_futures_state: None,
255255
capabilities: None,
256256
future_metadata: AHashMap::new(),
257+
external_function_set,
257258
}
258259
}
259260

@@ -268,6 +269,64 @@ impl ReplSession {
268269
self.capabilities = capabilities;
269270
}
270271

272+
/// Returns `true` if `name` is a registered external function.
273+
fn is_external_function_name(&self, name: &str) -> bool {
274+
self.external_function_set.contains(name)
275+
}
276+
277+
/// Returns the list of registered external function names.
278+
#[must_use]
279+
pub fn external_function_names(&self) -> &[String] {
280+
&self.external_functions
281+
}
282+
283+
/// Registers additional external functions on an existing session without
284+
/// clearing state.
285+
///
286+
/// Functions that are already registered as external functions are silently
287+
/// skipped. Names that collide with existing user variables are also skipped
288+
/// to avoid destroying session state.
289+
///
290+
/// New functions are added at the end of the namespace and are immediately
291+
/// callable from subsequent `execute()` calls.
292+
///
293+
/// Returns the names that were skipped due to collision with user variables.
294+
pub fn register_external_functions(&mut self, new_functions: Vec<String>) -> Result<Vec<String>, ReplError> {
295+
self.ensure_not_waiting_for_resume()?;
296+
let mut collisions = Vec::new();
297+
for function_name in new_functions {
298+
if self.external_function_set.contains(&function_name) {
299+
continue;
300+
}
301+
302+
// Check if a name_map entry exists.
303+
if let Some(&existing_slot) = self.name_map.get(&function_name) {
304+
let value = self.namespaces.get(GLOBAL_NS_IDX).get(existing_slot);
305+
if !matches!(value, Value::Undefined) {
306+
// Live user variable -- reject to preserve state.
307+
collisions.push(function_name);
308+
continue;
309+
}
310+
// Tombstoned slot (Value::Undefined) -- reuse it.
311+
let ext_func_id = ExtFunctionId::new(self.external_functions.len());
312+
self.external_functions.push(function_name.clone());
313+
self.external_function_set.insert(function_name);
314+
*self.namespaces.get_mut(GLOBAL_NS_IDX).get_mut(existing_slot) = Value::ExtFunction(ext_func_id);
315+
} else {
316+
let ext_func_id = ExtFunctionId::new(self.external_functions.len());
317+
self.external_functions.push(function_name.clone());
318+
self.external_function_set.insert(function_name.clone());
319+
320+
let slot = NamespaceId::new(self.namespace_size);
321+
self.namespace_size += 1;
322+
self.namespaces.grow_global(self.namespace_size);
323+
self.name_map.insert(function_name, slot);
324+
*self.namespaces.get_mut(GLOBAL_NS_IDX).get_mut(slot) = Value::ExtFunction(ext_func_id);
325+
}
326+
}
327+
Ok(collisions)
328+
}
329+
271330
/// Returns the current capability set, if any.
272331
#[must_use]
273332
pub fn capabilities(&self) -> Option<&CapabilitySet> {
@@ -303,13 +362,13 @@ impl ReplSession {
303362
namespaces: self.namespaces.deep_clone(),
304363
name_map: self.name_map.clone(),
305364
namespace_size: self.namespace_size,
306-
external_function_count: self.external_function_count,
307365
script_name: self.script_name.clone(),
308366
pending_snapshot: None,
309367
pending_resume_state: None,
310368
pending_futures_state: None,
311369
capabilities: self.capabilities.clone(),
312370
future_metadata: AHashMap::new(),
371+
external_function_set: self.external_function_set.clone(),
313372
}
314373
}
315374

@@ -346,7 +405,7 @@ impl ReplSession {
346405
namespaces_bytes,
347406
name_map: self.name_map.iter().map(|(k, v)| (k.clone(), *v)).collect(),
348407
namespace_size: self.namespace_size,
349-
external_function_count: self.external_function_count,
408+
external_function_count: self.external_functions.len(),
350409
script_name: self.script_name.clone(),
351410
};
352411

@@ -383,6 +442,7 @@ impl ReplSession {
383442
);
384443

385444
let name_map: AHashMap<String, NamespaceId> = snapshot.name_map.into_iter().collect();
445+
let external_function_set: AHashSet<String> = snapshot.external_functions.iter().cloned().collect();
386446

387447
Ok(Self {
388448
interner,
@@ -392,13 +452,13 @@ impl ReplSession {
392452
namespaces,
393453
name_map,
394454
namespace_size: snapshot.namespace_size,
395-
external_function_count: snapshot.external_function_count,
396455
script_name: snapshot.script_name,
397456
pending_snapshot: None,
398457
pending_resume_state: None,
399458
pending_futures_state: None,
400459
capabilities: None,
401460
future_metadata: AHashMap::new(),
461+
external_function_set,
402462
})
403463
}
404464

@@ -671,7 +731,7 @@ impl ReplSession {
671731
let mut vars = Vec::new();
672732

673733
for (name, &slot) in &self.name_map {
674-
if slot.index() < self.external_function_count {
734+
if self.is_external_function_name(name) {
675735
continue;
676736
}
677737
let value = global.get(slot);
@@ -691,7 +751,7 @@ impl ReplSession {
691751
#[must_use]
692752
pub fn get_variable(&self, name: &str) -> Option<Object> {
693753
let &slot = self.name_map.get(name)?;
694-
if slot.index() < self.external_function_count {
754+
if self.is_external_function_name(name) {
695755
return None;
696756
}
697757

@@ -719,7 +779,7 @@ impl ReplSession {
719779

720780
// First check if the variable exists
721781
let &slot = self.name_map.get(name)?;
722-
if slot.index() < self.external_function_count {
782+
if self.is_external_function_name(name) {
723783
return None;
724784
}
725785

@@ -791,7 +851,7 @@ impl ReplSession {
791851
let new_value = value.to_value(&mut self.heap, &interns)?;
792852

793853
if let Some(&existing_slot) = self.name_map.get(name) {
794-
if existing_slot.index() < self.external_function_count {
854+
if self.is_external_function_name(name) {
795855
new_value.drop_with_heap(&mut self.heap);
796856
return Err(InvalidInputError::invalid_type("cannot overwrite external function"));
797857
}
@@ -834,7 +894,7 @@ impl ReplSession {
834894
return Ok(false);
835895
};
836896

837-
if slot.index() < self.external_function_count {
897+
if self.is_external_function_name(name) {
838898
return Err(InvalidInputError::invalid_type("cannot delete external function"));
839899
}
840900

crates/ouros/src/session_manager.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,39 @@ impl SessionManager {
951951
entry.history.clear();
952952
Ok(())
953953
}
954+
955+
/// Registers additional external functions on an existing session without
956+
/// clearing state.
957+
///
958+
/// Functions already registered are silently skipped. Names that collide
959+
/// with existing user variables are skipped (returned in the result vec).
960+
/// New functions are appended and become immediately callable. Existing
961+
/// variables and execution history are preserved.
962+
///
963+
/// Returns the names that were skipped due to collision with user variables.
964+
///
965+
/// # Errors
966+
///
967+
/// Returns `SessionError::NotFound` if the session does not exist, or
968+
/// `SessionError::InvalidState` if the session has a pending external call.
969+
pub fn register_external_functions(
970+
&mut self,
971+
session_id: Option<&str>,
972+
external_functions: Vec<String>,
973+
) -> Result<Vec<String>, SessionError> {
974+
let sid = resolve_session_id(session_id);
975+
let entry = self.get_session_mut(sid)?;
976+
977+
if entry.pending_call_id.is_some() {
978+
return Err(SessionError::InvalidState(
979+
"cannot register external functions while a call is pending".to_owned(),
980+
));
981+
}
982+
983+
let collisions = entry.session.register_external_functions(external_functions)?;
984+
entry.external_functions = entry.session.external_function_names().to_vec();
985+
Ok(collisions)
986+
}
954987
}
955988

956989
// =============================================================================

0 commit comments

Comments
 (0)