Skip to content

Commit 8f7e2e1

Browse files
committed
feat(vm): generic operation/scope consumption for invocation, run, reset and pool
1 parent 77c3b87 commit 8f7e2e1

56 files changed

Lines changed: 5587 additions & 442 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,18 @@ fn main() {
166166
catalog.retain(|entry| !entry.source_name.starts_with("sqlite::"));
167167
}
168168

169-
let host_sources = [SourceSpec {
170-
path: "src/builtins/runtime/host.rs".to_string(),
171-
module: "host".to_string(),
172-
category: SourceCategory::DefaultHost,
173-
}];
169+
let host_sources = vec![
170+
SourceSpec {
171+
path: "src/builtins/runtime/host.rs".to_string(),
172+
module: "host".to_string(),
173+
category: SourceCategory::DefaultHost,
174+
},
175+
SourceSpec {
176+
path: "src/builtins/runtime/context_host.rs".to_string(),
177+
module: "context_host".to_string(),
178+
category: SourceCategory::DefaultHost,
179+
},
180+
];
174181
let async_enabled = env::var_os("CARGO_FEATURE_ASYNC").is_some();
175182
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("missing target architecture");
176183
let builtin_sources = builtin_source_specs(&namespaces, async_enabled, &target_arch);

crates/rustscript/tests/alias_smoke.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,31 @@ fn alias_exports_op_code() {
2121
let _ = rustscript::OpCode::Nop;
2222
let _ = rustscript::OpCode::Add;
2323
}
24+
25+
#[cfg(feature = "runtime")]
26+
#[test]
27+
fn alias_exports_public_invocation_stream_contract() {
28+
fn accept_item(_item: rustscript::InvocationItem) {}
29+
30+
accept_item(rustscript::InvocationItem::Complete(
31+
rustscript::Value::Null,
32+
));
33+
accept_item(rustscript::InvocationItem::Event(rustscript::Value::Bool(
34+
true,
35+
)));
36+
37+
fn accept_poll(_poll: rustscript::InvocationPoll) {}
38+
accept_poll(rustscript::InvocationPoll::Pending);
39+
accept_poll(rustscript::InvocationPoll::Ready(None));
40+
accept_poll(rustscript::InvocationPoll::Ready(Some(Ok(
41+
rustscript::InvocationItem::Complete(rustscript::Value::Null),
42+
))));
43+
44+
fn accept_error(_error: rustscript::InvocationError) {}
45+
accept_error(rustscript::InvocationError::Cancelled(
46+
rustscript::operation::OperationCancelReason::Requested,
47+
));
48+
accept_error(rustscript::InvocationError::Host {
49+
message: "boom".to_string(),
50+
});
51+
}

docs/callable-runtime.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ Reset clears Program runtime values and rebinds root function items from Program
4343

4444
PDRC recordings preserve full execution-frame metadata. Callable environments use identity-table encoding, so aliases still share one environment after decode.
4545

46+
## Invocation item stream
47+
48+
`Vm::start_invocation` starts one exported callable with ordinary `Value` arguments and returns an `Invocation` handle that behaves like a fused `Stream<Item = Result<InvocationItem, InvocationError>>`:
49+
50+
- `InvocationItem::Event(value)` items arrive in order for each `stream::emit(value)` call; `stream::emit` still evaluates to `()` inside RSS.
51+
- exactly one `InvocationItem::Complete(value)` carries the callable return value; events never replace it;
52+
- cancellation, fuel exhaustion, epoch deadline expiry, runtime capability failures (including event payload bound violations), and host failures each produce exactly one typed `InvocationError` item;
53+
- every poll after `Complete` or the error item returns `Ready(None)` (fused end of stream);
54+
- `InvocationPoll::Pending` means the VM is paused on an outstanding host operation; drive it through the embedding-owned async bridge and poll again.
55+
56+
Polling drives execution and provides backpressure: at most one event item is buffered between polls, and the VM does not produce items while the consumer is not polling. `stream::emit` validates only the configured per-item value bound (payload bytes and nesting depth); sequence assignment, receipts, persistence, and delivery policy belong to the embedding. At most one invocation is active per VM, `Invocation::cancel(reason)` cancels with a typed `OperationCancelReason`, dropping the handle retires the invocation synchronously for immediate VM reuse, and the low-level `Vm::run` pump is unchanged for custom drivers. VM reset uses the generic execution-scope close boundary; a pending close keeps the old scope installed and blocks reuse until `poll_reset_for_reuse` reports quiescence.
57+
4658
## Optimized backends
4759

4860
Whole-program AOT and Trace JIT use the same builtin call path (static catalog IDs) for environment binding and native frame dispatch for `callvalue`. Script-frame entry and return preserve frame-relative locals and typed continuations.

examples/collection_rebind_bench.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ fn measure(
222222
let mut samples = Vec::with_capacity(config.samples);
223223
let mut generic_builtin_calls = 0u64;
224224
for _ in 0..config.samples {
225-
vm.reset_for_reuse();
225+
let _ = vm.reset_for_reuse();
226226
let native_execs_before_sample = vm.jit_native_exec_count();
227227
let started = Instant::now();
228228
let status = vm

examples/mini_bench.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ fn measure_runtime_mode(
569569
let mut vm = Vm::new(program.clone());
570570
configure_vm_for_mode(&mut vm, mode);
571571
warm_vm_for_mode(&mut vm, mode, expected_stack)?;
572-
vm.reset_for_reuse();
572+
let _ = vm.reset_for_reuse();
573573
let started = Instant::now();
574574
let status = vm
575575
.run()

pd-vm-wasm/src/runtime.rs

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ use std::time::Instant;
1111

1212
use serde::Deserialize;
1313
use vm::{
14-
CallOutcome, CallReturn, FunctionDecl, HostAsyncBridge, HostFunction, HostOpId, LocalInfo,
15-
SourceFlavor, SourcePathError, Value, Vm, VmError, VmResult, VmStatus, VmYieldReason,
16-
compile_source_with_flavor_and_options, format_value, render_vm_error,
14+
CallOutcome, CallReturn, FunctionDecl, HostAsyncBridge, HostAsyncOpTerminal, HostFunction,
15+
HostOpId, LocalInfo, SourceFlavor, SourcePathError, Value, Vm, VmError, VmResult, VmStatus,
16+
VmYieldReason, compile_source_with_flavor_and_options, format_value, render_vm_error,
1717
};
1818

1919
use crate::analyzer::{LintDiagnostic, lint_source_with_flavor, lint_success_diagnostics};
@@ -294,6 +294,34 @@ impl HostAsyncBridge for BrowserAsyncBridge {
294294
Poll::Pending
295295
}
296296
}
297+
298+
fn request_cancel_op(
299+
&mut self,
300+
op_id: HostOpId,
301+
_reason: vm::operation::OperationCancelReason,
302+
) -> VmResult<()> {
303+
let Ok(mut state) = self.state.lock() else {
304+
return Err(VmError::HostError(
305+
"browser async bridge state is unavailable".to_string(),
306+
));
307+
};
308+
state.deadlines_ms.remove(&op_id);
309+
Ok(())
310+
}
311+
312+
fn poll_cancel_op(&mut self, _op_id: HostOpId, _cx: &mut Context<'_>) -> Poll<VmResult<()>> {
313+
Poll::Ready(Ok(()))
314+
}
315+
316+
fn cleanup_op(&mut self, op_id: HostOpId, _terminal: HostAsyncOpTerminal) -> VmResult<()> {
317+
let Ok(mut state) = self.state.lock() else {
318+
return Err(VmError::HostError(
319+
"browser async bridge state is unavailable".to_string(),
320+
));
321+
};
322+
state.deadlines_ms.remove(&op_id);
323+
Ok(())
324+
}
297325
}
298326

299327
struct PlaygroundRuntimeSleepHostFunction {
@@ -1323,7 +1351,8 @@ fn register_functions(
13231351
.any(|decl| decl.name == "runtime::sleep")
13241352
.then(|| {
13251353
let state = Arc::new(Mutex::new(BrowserAsyncState::default()));
1326-
vm.set_async_bridge(Box::new(BrowserAsyncBridge::new(Arc::clone(&state))));
1354+
vm.set_async_bridge(Box::new(BrowserAsyncBridge::new(Arc::clone(&state))))
1355+
.expect("browser async bridge should install");
13271356
state
13281357
});
13291358
for decl in functions {

src/builtins/runtime/context.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
//! Compatibility re-exports for the adapter runtime context.
2+
3+
pub(crate) use crate::vm::runtime::{RuntimeContext, RuntimeContextConfig, STREAM_EMIT_NAME};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use pd_host_function::pd_host_function;
2+
3+
use super::AnyValue;
4+
use crate::vm::{CallOutcome, Vm, VmResult};
5+
6+
/// Places one bounded event item on the active invocation stream and yields
7+
/// control to the invocation poller. `stream::emit` still evaluates to `()`
8+
/// inside RSS.
9+
#[pd_host_function(name = "stream::emit")]
10+
fn stream_emit_impl(vm: &mut Vm, value: AnyValue) -> VmResult<CallOutcome> {
11+
vm.emit_stream_item(value)
12+
}

src/builtins/runtime/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
//! Compatibility re-exports for the adapter runtime error API.
2+
3+
pub use crate::vm::runtime::{RuntimeError, RuntimeErrorCode, RuntimeResult};

src/builtins/runtime/event.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//! Compatibility re-exports for invocation event validation.
2+
3+
#[allow(unused_imports)]
4+
pub use crate::vm::runtime::{
5+
DEFAULT_MAX_EVENT_DEPTH, DEFAULT_MAX_EVENT_PAYLOAD_BYTES, EventLimits, EventPayload,
6+
estimate_value_size,
7+
};

0 commit comments

Comments
 (0)