Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,16 @@ The `call` opcode pops its arguments, dispatches to a builtin or bound host func
| `Yield` | Retry next `run()` | **Rewound to `call` opcode** | **Args re-pushed** |
| `Pending(op_id)` | Async result pending | Advanced past `call` | Empty (result injected via `complete_host_op`) |

**Non-yielding static host calls** — native-trace opt-in:

- `Vm::bind_static_non_yielding_args_function` and
`Vm::register_static_non_yielding_args_function` let the trace JIT keep eligible host calls
inside native traces.
- The function must return exactly one value via `CallOutcome::Return(CallReturn::One(...))`.
Returning no value, `Halt`, `Yield`, or `Pending` is a contract violation and produces the same
`VmError::HostError` in interpreted and native execution.
- Use the ordinary static args APIs when the host function may suspend, halt, or return no value.

**`CallOutcome::Yield`** — cooperative "retry me later":

- The VM re-pushes the original args onto the stack and rewinds `self.ip` to the start of the
Expand Down
9 changes: 6 additions & 3 deletions src/builtins/runtime/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,10 @@ pub(super) fn builtin_array_push_typed_impl(
items
}

fn builtin_array_push_shared_impl(mut items: SharedArray, value: AnyValue) -> SharedArray {
pub(crate) fn builtin_array_push_shared_impl(
mut items: SharedArray,
value: AnyValue,
) -> SharedArray {
Arc::make_mut(&mut items).push(value);
items
}
Expand Down Expand Up @@ -632,7 +635,7 @@ pub(super) fn builtin_set_array_impl(
Ok(items)
}

fn builtin_set_array_shared_impl(
pub(crate) fn builtin_set_array_shared_impl(
mut items: SharedArray,
index: i64,
value: AnyValue,
Expand Down Expand Up @@ -673,7 +676,7 @@ pub(super) fn builtin_set_map_impl(
entries
}

fn builtin_set_map_shared_impl(
pub(crate) fn builtin_set_map_shared_impl(
mut entries: SharedMap,
key: AnyValue,
value: AnyValue,
Expand Down
102 changes: 97 additions & 5 deletions src/vm/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ pub(super) enum VmHostFunction {
StackStatic(StaticHostStackFunction),
ArgsDynamic(Box<dyn HostArgsFunction>),
ArgsStatic(StaticHostArgsFunction),
ArgsStaticNonYielding(StaticHostArgsFunction),
}

pub(super) enum HostCallExecOutcome {
Expand All @@ -438,6 +439,24 @@ pub(super) enum HostCallExecOutcome {
Pending(HostOpId),
}

pub(crate) fn require_non_yielding_host_value(outcome: CallOutcome) -> VmResult<Value> {
match outcome {
CallOutcome::Return(CallReturn::One(value)) => Ok(value),
CallOutcome::Return(CallReturn::None) => Err(VmError::HostError(
"non-yielding host function returned no value".to_string(),
)),
CallOutcome::Halt => Err(VmError::HostError(
"non-yielding host function returned halt".to_string(),
)),
CallOutcome::Yield => Err(VmError::HostError(
"non-yielding host function returned yield".to_string(),
)),
CallOutcome::Pending(_) => Err(VmError::HostError(
"non-yielding host function returned pending".to_string(),
)),
}
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct WaitingHostOp {
pub(super) op_id: HostOpId,
Expand Down Expand Up @@ -515,6 +534,20 @@ impl Vm {
index
}

/// Registers a static args-only host function that always returns one value synchronously.
///
/// Returning no value, `Halt`, `Yield`, or `Pending` violates the contract and is a host error.
pub fn register_static_non_yielding_args_function(
&mut self,
function: StaticHostArgsFunction,
) -> u16 {
let index = self.host_functions.len() as u16;
self.host_functions
.push(VmHostFunction::ArgsStaticNonYielding(function));
self.resolved_calls_dirty = true;
index
}

pub fn bind_function(&mut self, name: impl Into<String>, function: Box<dyn HostFunction>) {
let name = name.into();
if let Some(builtin) = builtin_for_binding_name(&name) {
Expand Down Expand Up @@ -650,6 +683,37 @@ impl Vm {
self.resolved_calls_dirty = true;
}

/// Binds a static args-only host function that always returns one value synchronously.
///
/// This is equivalent to [`Vm::bind_static_args_function`] except that the VM may keep
/// native JIT traces active across the call boundary. Returning no value, `Halt`, `Yield`,
/// or `Pending` violates the contract and is reported as a host error.
pub fn bind_static_non_yielding_args_function(
&mut self,
name: impl Into<String>,
function: StaticHostArgsFunction,
) {
let name = name.into();
if let Some(builtin) = builtin_for_binding_name(&name) {
self.bind_builtin_overrideslot(
builtin.call_index(),
VmHostFunction::ArgsStaticNonYielding(function),
);
return;
}
if let Some(&index) = self.host_function_symbols.get(&name)
&& let Some(slot) = self.host_functions.get_mut(index as usize)
{
*slot = VmHostFunction::ArgsStaticNonYielding(function);
self.resolved_calls_dirty = true;
return;
}

let index = self.register_static_non_yielding_args_function(function);
self.host_function_symbols.insert(name, index);
self.resolved_calls_dirty = true;
}

pub fn bind_builtin_override(
&mut self,
name: impl Into<String>,
Expand Down Expand Up @@ -1295,7 +1359,8 @@ impl Vm {
VmHostFunction::StackDynamic(_)
| VmHostFunction::StackStatic(_)
| VmHostFunction::ArgsDynamic(_)
| VmHostFunction::ArgsStatic(_) => unreachable!(),
| VmHostFunction::ArgsStatic(_)
| VmHostFunction::ArgsStaticNonYielding(_) => unreachable!(),
}
};
self.call_depth = self.call_depth.saturating_sub(1);
Expand Down Expand Up @@ -1350,7 +1415,9 @@ impl Vm {
.ok_or(VmError::InvalidCall(resolved_index))?;
Ok(matches!(
function,
VmHostFunction::ArgsDynamic(_) | VmHostFunction::ArgsStatic(_)
VmHostFunction::ArgsDynamic(_)
| VmHostFunction::ArgsStatic(_)
| VmHostFunction::ArgsStaticNonYielding(_)
))
}

Expand Down Expand Up @@ -1384,16 +1451,24 @@ impl Vm {
.get_mut(resolved_index as usize)
.ok_or(VmError::InvalidCall(resolved_index))?;
match function {
VmHostFunction::ArgsDynamic(function) => function.call(args),
VmHostFunction::ArgsStatic(function) => function(args),
VmHostFunction::ArgsDynamic(function) => (function.call(args), false),
VmHostFunction::ArgsStatic(function) => (function(args), false),
VmHostFunction::ArgsStaticNonYielding(function) => (function(args), true),
VmHostFunction::Dynamic(_)
| VmHostFunction::Static(_)
| VmHostFunction::StackDynamic(_)
| VmHostFunction::StackStatic(_) => unreachable!(),
}
};
self.call_depth = self.call_depth.saturating_sub(1);
let (outcome, non_yielding) = outcome;
let outcome = outcome?;
if non_yielding {
let value = require_non_yielding_host_value(outcome)?;
self.stack.truncate(arg_start);
self.stack.push(value);
return Ok(HostCallExecOutcome::Returned);
}

match outcome {
CallOutcome::Return(values) => {
Expand Down Expand Up @@ -1446,7 +1521,8 @@ impl Vm {
VmHostFunction::Dynamic(_)
| VmHostFunction::Static(_)
| VmHostFunction::ArgsDynamic(_)
| VmHostFunction::ArgsStatic(_) => unreachable!(),
| VmHostFunction::ArgsStatic(_)
| VmHostFunction::ArgsStaticNonYielding(_) => unreachable!(),
}
};
self.call_depth = self.call_depth.saturating_sub(1);
Expand Down Expand Up @@ -1588,6 +1664,22 @@ impl Vm {
Ok(())
}

pub(super) fn sync_jit_non_yielding_host_imports(&mut self) {
let imports = self
.resolved_calls
.iter()
.map(|&slot| {
matches!(
self.host_functions.get(usize::from(slot)),
Some(VmHostFunction::ArgsStaticNonYielding(_))
)
})
.collect();
if self.jit.set_non_yielding_host_imports(imports) {
self.native_traces.clear();
}
}

pub(super) fn resolve_call_target(&mut self, index: u16, argc: u8) -> VmResult<u16> {
if self.program.imports.is_empty() {
return Ok(index);
Expand Down
18 changes: 17 additions & 1 deletion src/vm/jit/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ pub(crate) enum SsaInstKind {
MapIterTakeValue {
slot: SsaValueId,
},
HostCall {
import: u16,
args: Vec<SsaValueId>,
},
IntNeg {
input: SsaValueId,
},
Expand Down Expand Up @@ -346,9 +350,10 @@ pub(crate) enum SsaInstKind {
}

impl SsaInstKind {
fn inputs(&self) -> Vec<SsaValueId> {
pub(crate) fn inputs(&self) -> Vec<SsaValueId> {
match self {
Self::Constant(_) => Vec::new(),
Self::HostCall { args, .. } => args.clone(),
Self::UnboxInt { input }
| Self::UnboxFloat { input }
| Self::UnboxBool { input }
Expand Down Expand Up @@ -730,6 +735,14 @@ impl SsaTraceBuilder {
self.trace.entry
}

pub(crate) fn defining_inst(&self, value: SsaValueId) -> Option<&SsaInst> {
self.trace
.blocks
.iter()
.flat_map(|block| &block.insts)
.find(|inst| inst.output.is_some_and(|output| output.id == value))
}

pub(crate) fn create_block(&mut self) -> SsaBlockId {
let id = SsaBlockId::new(self.trace.blocks.len() as u32);
self.trace.blocks.push(SsaBlock {
Expand Down Expand Up @@ -1011,6 +1024,9 @@ fn render_inst_kind(kind: &SsaInstKind) -> String {
SsaInstKind::MapIterNext { slot } => format!("map_iter_next {slot}"),
SsaInstKind::MapIterTakeKey { slot } => format!("map_iter_take_key {slot}"),
SsaInstKind::MapIterTakeValue { slot } => format!("map_iter_take_value {slot}"),
SsaInstKind::HostCall { import, args } => {
format!("host_call {import}({})", render_value_list(args))
}
SsaInstKind::IntNeg { input } => format!("ineg {input}"),
SsaInstKind::IntAdd { lhs, rhs } => format!("iadd {lhs}, {rhs}"),
SsaInstKind::IntAddImm { lhs, imm } => format!("iadd_imm {lhs}, {imm}"),
Expand Down
Loading
Loading