use wasm_encoder::{BlockType, Instruction};
use crate::ExprId;
use crate::codegen::CodegenCtx;
use crate::codegen::function_emitter::FunctionEmitter;
use crate::codegen::function_emitter::expr::{emit_expr, emit_vtable_dispatch_on_object_stack};
use crate::codegen::function_emitter::json::emit_inline_const_raw_string;
pub(super) fn emit_mcp_call(
emitter: &mut FunctionEmitter,
ctx: &CodegenCtx,
server: &str,
tool: &str,
args: &[ExprId],
) -> Result<(), crate::compiler_error::CompilerFailure> {
if args.len() > 1 {
return Err(crate::codegen::internal_failure(
"MCP calls require a single arguments object",
));
}
emit_inline_const_string(emitter, ctx, server)?;
emit_inline_const_string(emitter, ctx, tool)?;
if let Some(&arg) = args.first() {
emit_expr(emitter, ctx, arg)?;
let ty = &ctx
.ta
.try_expr(arg)
.map_err(crate::codegen::arena_failure)?
.ty;
let local = emitter.add_anonymous_local(ctx.symbols.value_type(ty)?)?;
emitter.instruction(Instruction::LocalTee(local));
super::expr::emit_is_undefined(emitter, ctx)?;
emitter.emit_if(BlockType::Result(
ctx.symbols.value_type(&crate::Type::String)?,
));
emit_inline_const_string(emitter, ctx, "{}")?;
emitter.emit_else();
emitter.instruction(Instruction::LocalGet(local));
emit_vtable_dispatch_on_object_stack(emitter, ctx, 1);
emitter.emit_end();
} else {
emit_inline_const_string(emitter, ctx, "{}")?;
}
let call_idx = ctx
.symbols
.func_idx(&crate::mangle::host(
crate::runtime::MCP_MODULE_NAME,
"call",
))
.ok_or_else(|| {
crate::codegen::internal_failure("submilli:mcp.call imported during codegen")
})?;
emitter.instruction(Instruction::Call(call_idx));
Ok(())
}
fn emit_inline_const_string(
emitter: &mut FunctionEmitter,
ctx: &CodegenCtx,
text: &str,
) -> Result<(), crate::compiler_error::CompilerFailure> {
let intrinsics = ctx
.symbols
.intrinsic_type_indices()
.ok_or_else(|| crate::codegen::internal_failure("MCP intrinsic types are missing"))?;
let string_vtable_idx = ctx
.symbols
.prelude_global_idx("string_vtable")
.ok_or_else(|| crate::codegen::internal_failure("MCP string vtable is missing"))?;
emitter.instruction(Instruction::GlobalGet(string_vtable_idx));
emit_inline_const_raw_string(emitter, ctx, text)?;
emitter.instruction(Instruction::I64Const(0));
emitter.instruction(Instruction::StructNew(intrinsics.string));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::TypedAst;
use crate::codegen::invariant_tests::{assert_internal, with_context};
use crate::codegen::{SymbolTable, tests::mock_symbols_with_intrinsics};
#[test]
fn mcp_import_failures_are_compiler_failures() {
with_context(&TypedAst::new(), &mock_symbols_with_intrinsics(), |ctx| {
let mut emitter = FunctionEmitter::new(ctx, &[]).unwrap();
assert_internal(emit_mcp_call(&mut emitter, ctx, "server", "tool", &[]).unwrap_err());
assert_internal(emit_inline_const_string(&mut emitter, ctx, "server").unwrap_err());
});
with_context(&TypedAst::new(), &SymbolTable::default(), |ctx| {
assert_internal(
emit_inline_const_string(
&mut FunctionEmitter::new(ctx, &[]).unwrap(),
ctx,
"server",
)
.unwrap_err(),
);
let invalid = crate::ExprId(u32::MAX);
assert_internal(
emit_mcp_call(
&mut FunctionEmitter::new(ctx, &[]).unwrap(),
ctx,
"server",
"tool",
&[invalid; 2],
)
.unwrap_err(),
);
});
}
}