use std::borrow::Cow;
use monty::{
Dump, MontyRepl, MontyRun, ReplProgress, ReplStartError, RunProgress, Session, SessionRef, dump,
};
use monty_types::{
CompileOptions, ExcType, ExtFunctionResult, MontyException, MontyObject, NameLookupResult,
PrintWriter, PrintWriterCallback, ResourceTracker,
};
use serde_json::{Map, Value};
use tracing::debug;
use super::convert::{json_to_monty, monty_key_string, monty_to_json};
use super::host_fn::{FunctionRegistry, INPUT_BINDING};
use super::os_access::OsAccess;
use crate::ExecutionError;
pub(crate) type Tracker = ResourceTracker;
#[derive(Debug)]
pub(crate) struct CappedStdout {
buf: String,
cap: usize,
truncated: bool,
}
impl CappedStdout {
pub(crate) fn new(cap: usize) -> Self {
Self { buf: String::new(), cap, truncated: false }
}
pub(crate) fn into_parts(self) -> (String, bool) {
(self.buf, self.truncated)
}
fn append(&mut self, text: &str) {
if self.truncated {
return;
}
let remaining = self.cap - self.buf.len();
if text.len() <= remaining {
self.buf.push_str(text);
return;
}
let mut end = remaining;
while end > 0 && !text.is_char_boundary(end) {
end -= 1;
}
self.buf.push_str(&text[..end]);
self.truncated = true;
}
}
impl PrintWriterCallback for CappedStdout {
fn stdout_write(&mut self, output: Cow<'_, str>) -> Result<(), MontyException> {
self.append(&output);
Ok(())
}
fn stdout_push(&mut self, end: char) -> Result<(), MontyException> {
let mut bytes = [0u8; 4];
self.append(end.encode_utf8(&mut bytes));
Ok(())
}
}
#[derive(Debug)]
pub(crate) struct PausedCall {
pub(crate) name: String,
pub(crate) args: Vec<Value>,
pub(crate) kwargs: Map<String, Value>,
pub(crate) progress_bytes: Vec<u8>,
}
#[derive(Debug)]
pub(crate) struct DriveEnd {
pub(crate) value: Option<Value>,
pub(crate) error: Option<String>,
pub(crate) timed_out: bool,
}
impl DriveEnd {
fn complete(value: &MontyObject) -> Self {
Self { value: Some(monty_to_json(value)), error: None, timed_out: false }
}
fn raised(exc: &MontyException) -> Self {
Self {
value: None,
error: Some(exc.to_string()),
timed_out: exc.exc_type() == ExcType::TimeoutError,
}
}
}
#[derive(Debug)]
pub(crate) enum RunSegment {
Finished(DriveEnd),
Paused(PausedCall),
}
#[derive(Debug)]
pub(crate) enum ReplSegment {
Finished { end: DriveEnd, repl_bytes: Vec<u8> },
Paused(PausedCall),
}
fn internal(context: &str, err: impl std::fmt::Display) -> ExecutionError {
ExecutionError::InternalError(format!("{context}: {err}"))
}
fn runtime_error(message: &str) -> MontyException {
MontyException::new(ExcType::RuntimeError, Some(message.to_string()))
}
fn host_result(outcome: Result<Value, String>) -> ExtFunctionResult {
match outcome {
Ok(value) => ExtFunctionResult::Return(json_to_monty(value)),
Err(message) => ExtFunctionResult::Error(runtime_error(&message)),
}
}
fn lookup_result(registry: &FunctionRegistry, name: &str) -> NameLookupResult {
match registry.get(name) {
Some(function) => NameLookupResult::Value(MontyObject::Function {
name: name.to_string(),
docstring: Some(function.description().to_string()),
}),
None => NameLookupResult::Undefined,
}
}
fn kwargs_to_json(kwargs: &[(MontyObject, MontyObject)]) -> Map<String, Value> {
let mut map = Map::new();
for (key, value) in kwargs {
map.insert(monty_key_string(key), monty_to_json(value));
}
map
}
const AWAIT_DENIED: &str = "asynchronous external calls are not supported; call host functions synchronously, \
without `await`";
fn input_bindings(input: Option<Value>) -> Vec<(String, MontyObject)> {
input.map(|value| vec![(INPUT_BINDING.to_string(), json_to_monty(value))]).unwrap_or_default()
}
pub(crate) fn start_run(
code: &str,
script_name: &str,
input: Option<Value>,
tracker: Tracker,
os: &OsAccess,
registry: &FunctionRegistry,
stdout: &mut CappedStdout,
) -> Result<RunSegment, ExecutionError> {
let (input_names, inputs): (Vec<String>, Vec<MontyObject>) =
input_bindings(input).into_iter().unzip();
let run = match MontyRun::new(
code.to_string(),
script_name,
input_names,
CompileOptions::default(),
) {
Ok(run) => run,
Err(exc) => return Ok(RunSegment::Finished(DriveEnd::raised(&exc))),
};
match run.start(inputs, tracker, PrintWriter::Callback(stdout)) {
Ok(progress) => drive_run(progress, script_name, os, registry, stdout),
Err(exc) => Ok(RunSegment::Finished(DriveEnd::raised(&exc))),
}
}
pub(crate) fn resume_run(
progress_bytes: &[u8],
outcome: Result<Value, String>,
os: &OsAccess,
registry: &FunctionRegistry,
stdout: &mut CappedStdout,
) -> Result<RunSegment, ExecutionError> {
let restored = Dump::load(progress_bytes)
.map_err(|err| internal("failed to deserialize paused run progress", err))?;
let script_name = restored.script_name;
let Session::Running(progress) = restored.state else {
return Err(ExecutionError::InternalError(
"paused run dump does not contain run progress".to_string(),
));
};
let progress = *progress;
let Some(call) = progress.into_function_call() else {
return Err(ExecutionError::InternalError(
"paused run progress is not a function call".to_string(),
));
};
match call.resume(host_result(outcome), PrintWriter::Callback(stdout)) {
Ok(progress) => drive_run(progress, &script_name, os, registry, stdout),
Err(exc) => Ok(RunSegment::Finished(DriveEnd::raised(&exc))),
}
}
fn drive_run(
mut progress: RunProgress,
script_name: &str,
os: &OsAccess,
registry: &FunctionRegistry,
stdout: &mut CappedStdout,
) -> Result<RunSegment, ExecutionError> {
let mut mounts = os.build_mount_table()?;
loop {
match progress {
RunProgress::Complete(value) => {
return Ok(RunSegment::Finished(DriveEnd::complete(&value)));
}
RunProgress::FunctionCall(call) => {
if !call.method_call && registry.contains(&call.function_name) {
let name = call.function_name.clone();
let args = call.args.iter().map(monty_to_json).collect();
let kwargs = kwargs_to_json(&call.kwargs);
let paused = RunProgress::FunctionCall(call);
let progress_bytes = dump(script_name, None, SessionRef::Running(&paused))
.map_err(|err| internal("failed to serialize paused run progress", err))?;
debug!(
host_fn.name = %name,
progress.bytes = progress_bytes.len(),
"pausing run at host function call"
);
return Ok(RunSegment::Paused(PausedCall {
name,
args,
kwargs,
progress_bytes,
}));
}
let message = registry.call_failure_message(&call.function_name, call.method_call);
progress = match call.resume(
ExtFunctionResult::Error(runtime_error(&message)),
PrintWriter::Callback(stdout),
) {
Ok(next) => next,
Err(exc) => return Ok(RunSegment::Finished(DriveEnd::raised(&exc))),
};
}
RunProgress::OsCall(call) => {
progress = match call.resume_with(PrintWriter::Callback(stdout), |call| {
os.resolve(call, &mut mounts)
}) {
Ok(next) => next,
Err(exc) => return Ok(RunSegment::Finished(DriveEnd::raised(&exc))),
};
}
RunProgress::NameLookup(lookup) => {
let result = lookup_result(registry, &lookup.name);
progress = match lookup.resume(result, PrintWriter::Callback(stdout)) {
Ok(next) => next,
Err(exc) => return Ok(RunSegment::Finished(DriveEnd::raised(&exc))),
};
}
RunProgress::ResolveFutures(futures) => {
let denied: Vec<(u32, ExtFunctionResult)> = futures
.pending_call_ids()
.iter()
.map(|id| (*id, ExtFunctionResult::Error(runtime_error(AWAIT_DENIED))))
.collect();
progress = match futures.resume(denied, PrintWriter::Callback(stdout)) {
Ok(next) => next,
Err(exc) => return Ok(RunSegment::Finished(DriveEnd::raised(&exc))),
};
}
}
}
}
pub(crate) fn fresh_repl_bytes(
script_name: &str,
tracker: Tracker,
) -> Result<Vec<u8>, ExecutionError> {
let repl = MontyRepl::new(script_name, tracker, CompileOptions::default());
dump(script_name, None, SessionRef::Idle(&repl))
.map_err(|err| internal("failed to serialize fresh REPL state", err))
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn feed_repl(
repl_bytes: Option<&[u8]>,
script_name: &str,
tracker: Tracker,
timeout: std::time::Duration,
code: &str,
input: Option<Value>,
os: &OsAccess,
registry: &FunctionRegistry,
stdout: &mut CappedStdout,
) -> Result<ReplSegment, ExecutionError> {
let (mut repl, active_script_name) = match repl_bytes {
Some(bytes) => {
let restored = Dump::load(bytes)
.map_err(|err| internal("failed to deserialize REPL session state", err))?;
let Session::Idle(repl) = restored.state else {
return Err(ExecutionError::InternalError(
"REPL session dump is not idle".to_string(),
));
};
(*repl, restored.script_name)
}
None => (
MontyRepl::new(script_name, tracker, CompileOptions::default()),
script_name.to_string(),
),
};
repl.tracker_mut().set_max_duration(timeout);
match repl.feed_start(code, input_bindings(input), PrintWriter::Callback(stdout)) {
Ok(progress) => drive_repl(progress, &active_script_name, os, registry, stdout),
Err(err) => repl_raised(*err, &active_script_name),
}
}
pub(crate) fn resume_repl(
progress_bytes: &[u8],
outcome: Result<Value, String>,
os: &OsAccess,
registry: &FunctionRegistry,
stdout: &mut CappedStdout,
) -> Result<ReplSegment, ExecutionError> {
let restored = Dump::load(progress_bytes)
.map_err(|err| internal("failed to deserialize paused REPL progress", err))?;
let script_name = restored.script_name;
let Session::Suspended(progress) = restored.state else {
return Err(ExecutionError::InternalError(
"paused REPL dump does not contain suspended progress".to_string(),
));
};
let progress = *progress;
let Some(call) = progress.into_function_call() else {
return Err(ExecutionError::InternalError(
"paused REPL progress is not a function call".to_string(),
));
};
match call.resume(host_result(outcome), PrintWriter::Callback(stdout)) {
Ok(progress) => drive_repl(progress, &script_name, os, registry, stdout),
Err(err) => repl_raised(*err, &script_name),
}
}
fn repl_raised(err: ReplStartError, script_name: &str) -> Result<ReplSegment, ExecutionError> {
let repl_bytes = dump(script_name, None, SessionRef::Idle(&err.repl))
.map_err(|err| internal("failed to serialize REPL session state", err))?;
Ok(ReplSegment::Finished { end: DriveEnd::raised(&err.error), repl_bytes })
}
fn drive_repl(
mut progress: ReplProgress,
script_name: &str,
os: &OsAccess,
registry: &FunctionRegistry,
stdout: &mut CappedStdout,
) -> Result<ReplSegment, ExecutionError> {
let mut mounts = os.build_mount_table()?;
loop {
match progress {
ReplProgress::Complete { repl, value } => {
let repl_bytes = dump(script_name, None, SessionRef::Idle(&repl))
.map_err(|err| internal("failed to serialize REPL session state", err))?;
debug!(repl.state_bytes = repl_bytes.len(), "repl snippet complete");
return Ok(ReplSegment::Finished { end: DriveEnd::complete(&value), repl_bytes });
}
ReplProgress::FunctionCall(call) => {
if !call.method_call && registry.contains(&call.function_name) {
let name = call.function_name.clone();
let args = call.args.iter().map(monty_to_json).collect();
let kwargs = kwargs_to_json(&call.kwargs);
let paused = ReplProgress::FunctionCall(call);
let progress_bytes = dump(script_name, None, SessionRef::Suspended(&paused))
.map_err(|err| internal("failed to serialize paused REPL progress", err))?;
debug!(
host_fn.name = %name,
progress.bytes = progress_bytes.len(),
"pausing repl at host function call"
);
return Ok(ReplSegment::Paused(PausedCall {
name,
args,
kwargs,
progress_bytes,
}));
}
let message = registry.call_failure_message(&call.function_name, call.method_call);
progress = match call.resume(
ExtFunctionResult::Error(runtime_error(&message)),
PrintWriter::Callback(stdout),
) {
Ok(next) => next,
Err(err) => return repl_raised(*err, script_name),
};
}
ReplProgress::OsCall(call) => {
progress = match call.resume_with(PrintWriter::Callback(stdout), |call| {
os.resolve(call, &mut mounts)
}) {
Ok(next) => next,
Err(err) => return repl_raised(*err, script_name),
};
}
ReplProgress::NameLookup(lookup) => {
let result = lookup_result(registry, &lookup.name);
progress = match lookup.resume(result, PrintWriter::Callback(stdout)) {
Ok(next) => next,
Err(err) => return repl_raised(*err, script_name),
};
}
ReplProgress::ResolveFutures(futures) => {
let denied: Vec<(u32, ExtFunctionResult)> = futures
.pending_call_ids()
.iter()
.map(|id| (*id, ExtFunctionResult::Error(runtime_error(AWAIT_DENIED))))
.collect();
progress = match futures.resume(denied, PrintWriter::Callback(stdout)) {
Ok(next) => next,
Err(err) => return repl_raised(*err, script_name),
};
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use monty_types::ResourceLimits;
use serde_json::json;
use super::super::host_fn::{ClosureHostFunction, HostFunction};
use super::*;
fn tracker() -> Tracker {
Tracker::new(ResourceLimits::default())
}
fn registry_with(name: &str) -> FunctionRegistry {
let function: Arc<dyn HostFunction> =
Arc::new(ClosureHostFunction::new(name, "test function", |_args, _kwargs| async {
Ok(json!(null))
}));
FunctionRegistry::build(vec![function]).unwrap()
}
#[test]
fn capped_stdout_collects_within_the_cap() {
let mut stdout = CappedStdout::new(16);
stdout.stdout_write("hello".into()).unwrap();
stdout.stdout_push('\n').unwrap();
assert_eq!(stdout.into_parts(), ("hello\n".to_string(), false));
}
#[test]
fn capped_stdout_cuts_on_a_char_boundary_and_discards_the_rest() {
let mut stdout = CappedStdout::new(4);
stdout.stdout_write("ab€".into()).unwrap();
stdout.stdout_write("more".into()).unwrap();
stdout.stdout_push('x').unwrap();
assert_eq!(stdout.into_parts(), ("ab".to_string(), true));
}
#[test]
fn capped_stdout_fills_to_exactly_the_cap_without_truncation() {
let mut stdout = CappedStdout::new(5);
stdout.stdout_write("ab€".into()).unwrap();
assert_eq!(stdout.into_parts(), ("ab€".to_string(), false));
}
#[test]
fn capped_stdout_push_beyond_the_cap_truncates() {
let mut stdout = CappedStdout::new(1);
stdout.stdout_push('a').unwrap();
stdout.stdout_push('b').unwrap();
assert_eq!(stdout.into_parts(), ("a".to_string(), true));
}
#[test]
fn kwargs_with_a_non_string_key_degrade_to_repr() {
let kwargs = vec![(MontyObject::Int(1), MontyObject::Bool(true))];
let map = kwargs_to_json(&kwargs);
assert_eq!(serde_json::Value::Object(map), json!({"1": true}));
}
#[test]
fn lookup_resolves_registered_names_and_leaves_the_rest_undefined() {
let registry = registry_with("fetch");
match lookup_result(®istry, "fetch") {
NameLookupResult::Value(MontyObject::Function { name, docstring }) => {
assert_eq!(name, "fetch");
assert_eq!(docstring.as_deref(), Some("test function"));
}
other => panic!("expected a Function value, got {other:?}"),
}
assert!(matches!(lookup_result(®istry, "missing"), NameLookupResult::Undefined));
}
#[test]
fn a_timeout_exception_classifies_the_drive_end_as_timed_out() {
let end = DriveEnd::raised(&MontyException::new(
ExcType::TimeoutError,
Some("time budget exceeded".to_string()),
));
assert!(end.timed_out);
assert!(end.error.unwrap().contains("time budget exceeded"));
let end = DriveEnd::raised(&MontyException::new(ExcType::ValueError, None));
assert!(!end.timed_out);
}
#[test]
fn corrupted_run_progress_bytes_are_an_internal_error() {
let registry = FunctionRegistry::default();
let os = OsAccess::default();
let mut stdout = CappedStdout::new(1024);
let err =
resume_run(b"not postcard", Ok(json!(1)), &os, ®istry, &mut stdout).unwrap_err();
match err {
ExecutionError::InternalError(msg) => assert!(msg.contains("deserialize")),
other => panic!("expected InternalError, got {other:?}"),
}
}
#[test]
fn corrupted_repl_progress_bytes_are_an_internal_error() {
let registry = FunctionRegistry::default();
let os = OsAccess::default();
let mut stdout = CappedStdout::new(1024);
let err =
resume_repl(b"not postcard", Ok(json!(1)), &os, ®istry, &mut stdout).unwrap_err();
match err {
ExecutionError::InternalError(msg) => assert!(msg.contains("deserialize")),
other => panic!("expected InternalError, got {other:?}"),
}
}
#[test]
fn resuming_a_non_function_call_run_progress_is_an_internal_error() {
let registry = FunctionRegistry::default();
let os = OsAccess::default();
let mut stdout = CappedStdout::new(1024);
let run = MontyRun::new("1 + 1".to_string(), "test", Vec::new(), CompileOptions::default())
.unwrap();
let progress =
run.start(Vec::new(), tracker(), PrintWriter::Callback(&mut stdout)).unwrap();
let bytes = dump("test", None, SessionRef::Running(&progress)).unwrap();
let err = resume_run(&bytes, Ok(json!(1)), &os, ®istry, &mut stdout)
.expect_err("a completed progress must not resume");
match err {
ExecutionError::InternalError(msg) => assert!(msg.contains("not a function call")),
other => panic!("expected InternalError, got {other:?}"),
}
}
#[test]
fn resuming_a_non_function_call_repl_progress_is_an_internal_error() {
let registry = FunctionRegistry::default();
let os = OsAccess::default();
let mut stdout = CappedStdout::new(1024);
let repl = MontyRepl::new("test", tracker(), CompileOptions::default());
let progress =
repl.feed_start("1 + 1", Vec::new(), PrintWriter::Callback(&mut stdout)).unwrap();
let bytes = dump("test", None, SessionRef::Suspended(&progress)).unwrap();
let err = resume_repl(&bytes, Ok(json!(1)), &os, ®istry, &mut stdout)
.expect_err("a completed progress must not resume");
match err {
ExecutionError::InternalError(msg) => assert!(msg.contains("not a function call")),
other => panic!("expected InternalError, got {other:?}"),
}
}
#[test]
fn pending_futures_are_denied_with_the_await_message() {
let registry = registry_with("fetch");
let os = OsAccess::default();
let mut stdout = CappedStdout::new(1024);
let run = MontyRun::new(
"await fetch()".to_string(),
"test",
Vec::new(),
CompileOptions::default(),
)
.unwrap();
let mut progress =
run.start(Vec::new(), tracker(), PrintWriter::Callback(&mut stdout)).unwrap();
while !matches!(progress, RunProgress::ResolveFutures(_)) {
progress = match progress {
RunProgress::NameLookup(lookup) => {
let result = lookup_result(®istry, &lookup.name);
lookup.resume(result, PrintWriter::Callback(&mut stdout)).unwrap()
}
RunProgress::FunctionCall(call) => {
call.resume_pending(PrintWriter::Callback(&mut stdout)).unwrap()
}
other => panic!("unexpected progress while steering: {other:?}"),
};
}
let segment = drive_run(progress, "test", &os, ®istry, &mut stdout).unwrap();
match segment {
RunSegment::Finished(end) => {
let error = end.error.expect("the denied await raises");
assert!(
error.contains("asynchronous external calls are not supported"),
"error: {error}"
);
}
RunSegment::Paused(_) => panic!("expected the denied await to finish the run"),
}
}
}