Skip to main content

submilli_engine/codegen/function_emitter/
virtual_call.rs

1//! Codegen for virtual-package calls: `@mcp/<server>.<tool>(...)` and
2//! `@tools.<tool>(...)`.
3//!
4//! There is no per-tool Wasm import. Every call lowers to the package's single
5//! host fn: `submilli:mcp.call(server, tool, argsJson) -> unknown` or
6//! `submilli:tools.call(tool, argsJson) -> unknown`. The names and the args
7//! (serialized via the object's `toJson` vtable slot) are pushed as
8//! `(ref $string)`s, and the host returns guest objects directly. The typechecker
9//! wraps known-return calls in a normal `Cast`, so this module never emits cast
10//! validation.
11
12use wasm_encoder::{BlockType, Instruction};
13
14use crate::codegen::CodegenCtx;
15use crate::codegen::function_emitter::FunctionEmitter;
16use crate::codegen::function_emitter::expr::{emit_expr, emit_vtable_dispatch_on_object_stack};
17use crate::codegen::function_emitter::json::emit_inline_const_raw_string;
18use crate::{ExprId, VirtualDispatch};
19
20pub(super) fn emit_virtual_call(
21    emitter: &mut FunctionEmitter,
22    ctx: &CodegenCtx,
23    target: &VirtualDispatch,
24    tool: &str,
25    args: &[ExprId],
26) -> Result<(), crate::compiler_error::CompilerFailure> {
27    if args.len() > 1 {
28        return Err(crate::codegen::internal_failure(
29            "virtual-package calls require a single arguments object",
30        ));
31    }
32    // Push the `(ref $string)` args: the server (MCP only), tool, argsJson. A
33    // zero-arg tool sends `{}`; otherwise the args object is serialized via its
34    // vtable `toJson` slot.
35    if let VirtualDispatch::Mcp { server } = target {
36        emit_inline_const_string(emitter, ctx, server)?;
37    }
38    emit_inline_const_string(emitter, ctx, tool)?;
39    if let Some(&arg) = args.first() {
40        emit_expr(emitter, ctx, arg)?;
41        let ty = &ctx
42            .ta
43            .try_expr(arg)
44            .map_err(crate::codegen::arena_failure)?
45            .ty;
46        let local = emitter.add_anonymous_local(ctx.symbols.value_type(ty)?)?;
47        emitter.instruction(Instruction::LocalTee(local));
48        super::expr::emit_is_undefined(emitter, ctx)?;
49        emitter.emit_if(BlockType::Result(
50            ctx.symbols.value_type(&crate::Type::String)?,
51        ));
52        emit_inline_const_string(emitter, ctx, "{}")?;
53        emitter.emit_else();
54        emitter.instruction(Instruction::LocalGet(local));
55        emit_vtable_dispatch_on_object_stack(emitter, ctx, 1);
56        emitter.emit_end();
57    } else {
58        emit_inline_const_string(emitter, ctx, "{}")?;
59    }
60
61    let call_idx = ctx
62        .symbols
63        .func_idx(&crate::mangle::host(target.host_module(), "call"))
64        .ok_or_else(|| {
65            crate::codegen::internal_failure("virtual-package call imported during codegen")
66        })?;
67    emitter.instruction(Instruction::Call(call_idx));
68
69    Ok(())
70}
71
72/// Push a real `(ref $string)` for an inline constant: the `string_vtable` over a
73/// freshly built `$rawString`.
74fn emit_inline_const_string(
75    emitter: &mut FunctionEmitter,
76    ctx: &CodegenCtx,
77    text: &str,
78) -> Result<(), crate::compiler_error::CompilerFailure> {
79    let intrinsics = ctx
80        .symbols
81        .intrinsic_type_indices()
82        .ok_or_else(|| crate::codegen::internal_failure("MCP intrinsic types are missing"))?;
83    let string_vtable_idx = ctx
84        .symbols
85        .prelude_global_idx("string_vtable")
86        .ok_or_else(|| crate::codegen::internal_failure("MCP string vtable is missing"))?;
87    emitter.instruction(Instruction::GlobalGet(string_vtable_idx));
88    emit_inline_const_raw_string(emitter, ctx, text)?;
89    emitter.instruction(Instruction::I64Const(0));
90    emitter.instruction(Instruction::StructNew(intrinsics.string));
91    Ok(())
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::TypedAst;
98    use crate::codegen::invariant_tests::{assert_internal, with_context};
99    use crate::codegen::{SymbolTable, tests::mock_symbols_with_intrinsics};
100
101    #[test]
102    fn mcp_import_failures_are_compiler_failures() {
103        with_context(&TypedAst::new(), &mock_symbols_with_intrinsics(), |ctx| {
104            let mut emitter = FunctionEmitter::new(ctx, &[]).unwrap();
105            let server = VirtualDispatch::Mcp {
106                server: "server".to_string(),
107            };
108            assert_internal(
109                emit_virtual_call(&mut emitter, ctx, &server, "tool", &[]).unwrap_err(),
110            );
111            assert_internal(
112                emit_virtual_call(&mut emitter, ctx, &VirtualDispatch::Tools, "tool", &[])
113                    .unwrap_err(),
114            );
115            assert_internal(emit_inline_const_string(&mut emitter, ctx, "server").unwrap_err());
116        });
117        with_context(&TypedAst::new(), &SymbolTable::default(), |ctx| {
118            assert_internal(
119                emit_inline_const_string(
120                    &mut FunctionEmitter::new(ctx, &[]).unwrap(),
121                    ctx,
122                    "server",
123                )
124                .unwrap_err(),
125            );
126            let invalid = crate::ExprId(u32::MAX);
127            assert_internal(
128                emit_virtual_call(
129                    &mut FunctionEmitter::new(ctx, &[]).unwrap(),
130                    ctx,
131                    &VirtualDispatch::Tools,
132                    "tool",
133                    &[invalid; 2],
134                )
135                .unwrap_err(),
136            );
137        });
138    }
139}