Repository navigation
Improve effect handling implementation - #263
Conversation
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
…ect raiser and handler Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
…rdination scripts Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
|
@ecioppettini I'd like to hear from you on two points:
|
Well, the proper way is to add the opcode in the quint spec, the template itself should not use a constant method hash and instead source that from the arguments, and then that needs to be added to the circuit too But I can do that myself after this. Or if you want I can stack a PR, if you think we don't want to merge the intermediate state (but I think it is fine?)
Well, tbh we could just remove this unless you think it is useful as a test for the runtime. The circuit technically has coverage for this anyway. |
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Alright, will leave it to you.
Done. |
| let stack = store.as_context_mut().data_mut().instance_stack().clone(); | ||
| assert!( | ||
| !frames.is_empty(), | ||
| !stack.is_empty(), | ||
| "`debug_exit_frames()` was empty, must use `Config::guest_debug(true)`" | ||
| ); | ||
|
|
||
| let core_name = format!("starstream:effect-handler/{abi_name}#{name}"); | ||
| // FrameHandle is invalidated as soon as guest execution resumes. | ||
| // Resolve every dispatcher to a stable Func before calling any | ||
| // handler so an unhandled inner frame can continue outward. | ||
| let mut handlers = Vec::new(); | ||
| for each in frames { | ||
| debug!(pc = ?each.wasm_function_index_and_pc(&mut store)); | ||
| if let Ok(instance) = each.instance(&mut store) | ||
| && let Some(handler) = instance.get_func(&mut store, &core_name) | ||
| debug!(abi = ?abi_name, effect = ?name, ?params, stack_len = ?stack.len(), "raise_effect"); | ||
|
|
||
| for (i, instance) in stack.into_iter().rev().enumerate() { | ||
| if let Some(interface) = instance.get_export_index(store.as_context_mut(), None, &format!("starstream:effect-handler/{abi_name}")) | ||
| && let Some(idx) = instance.get_export_index(store.as_context_mut(), Some(&interface), &name) | ||
| && let Some(handler) = instance.get_func(store.as_context_mut(), idx) | ||
| { | ||
| handlers.push(handler); | ||
| } | ||
| } | ||
| for handler in handlers { | ||
| debug!(handler = ?handler); | ||
| let mut results = [wasmtime::Val::I32(0)]; | ||
| handler | ||
| .call_async(&mut store, &[], &mut results[..]) | ||
| .await?; | ||
| debug!(results = ?results); | ||
| match results[0] { | ||
| wasmtime::Val::I32(0) => continue, | ||
| wasmtime::Val::I32(1) => { | ||
| break; | ||
| } | ||
| _ => { | ||
| return Err(wasmtime::Error::msg("invalid effect handler status code")); | ||
| debug!(frame = ?i, handler = ?handler); | ||
| let mut handler_result = Val::U8(0); | ||
| handler.call_async(&mut store, params, std::slice::from_mut(&mut handler_result)).await?; |
There was a problem hiding this comment.
I think this logic is wrong?
Here we are calling into the handler with call_async, but with the same instance stack. But what if the handler raises another effect? we would be searching from the same point, which could be a child
like, this allows something like:
A -> B -> C (raise E) -> B (handle E) -> B (raise F inside the handler) -> C (handle F) !
where C would be handling an effect for B, which is the parent
instead when B raises, the instance_stack should be narrowed to start searching from B
There was a problem hiding this comment.
Hmm, you might be right. I'll have to think about which way makes more sense...
There was a problem hiding this comment.
I don't think the current semantics really make sense under any model? If I'm reading this right
There are systems that have bidirectional effect handlers (https://dl.acm.org/doi/10.1145/3428207), where you could have a handler performing an effect that the child handles. Kind of like an effect handler that needs effects, that are defined on the point where the effect is performed/raised.
But that's a different type system feature generally, because the required effect is on the handler and not on the function that defines the handler.
Plus this doesn't even implement that I think, because if you have A -> B -> C -> D, then C may be the right handler for an effect in B, but this would still look in D.
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
|
Nested try/withs are now handled properly: |
|
Failing test is starstream-proving-runtime::runtime_e2e effect_handler_reentry_satisfies_the_circuit. |
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
|
|
||
| // Effect handling. | ||
| linker.func_wrap("push-effect-handler-frame", |mut store, (id,): (u32,)| { | ||
| let Some(&instance) = store.data_mut().starstream().instance_stack.last() else { |
There was a problem hiding this comment.
I think this may still be wrong, since a try inside an effect handler would get the wrong instance.
like
try {
foo()
}
with E() {
try {
// this push-effect-handler-frame may get foo's instance (or a child of it)
bar()
} with F() {
}
}
I feel like we should just keep track of the current instance. At least that's how I would implement this in the prover (although I wouldn't use wasmtime's instance ids in there, the prover just allocates contiguous ids in instantiation order)
There was a problem hiding this comment.
Ah, you're right. We need to push the handler's instance to the instance stack while we call the handler.
As far as I can tell, wasmtime is generally not interested in exposing the "current instance". wasmtime::Caller doesn't do it and even debug_exit_frames only returns the core Instance, not the component Instance.
Signed-off-by: Tad Hardesty <tad.hardesty@midnight.foundation>
Follow-up to #251.
push-effect-handler-frame,add-effect-handler, andpop-effect-handler-frame.withfunction.component::Instancerather than using the debug stack walk which only returns coreInstances.Remaining future work:
withclause.