use crate::type_map::WasmMapper;
use ahash::AHashSet;
use alef_codegen::type_mapper::TypeMapper;
use alef_codegen::{generators, naming::to_node_name};
use alef_core::ir::{FunctionDef, TypeRef};
use std::collections::HashMap;
fn should_have_input_dto(type_name: &str) -> bool {
type_name.ends_with("Config")
}
fn to_camel_case(snake: &str) -> String {
let mut result = String::new();
let mut capitalize_next = false;
for ch in snake.chars() {
if ch == '_' {
capitalize_next = true;
} else if capitalize_next {
result.push(ch.to_uppercase().next().unwrap());
capitalize_next = false;
} else {
result.push(ch);
}
}
result
}
pub(super) fn gen_input_dto_for_type(
type_name: &str,
core_import: &str,
type_def: &alef_core::ir::TypeDef,
) -> (String, String) {
let input_name = format!("{}Input", type_name);
let core_path = format!("{}::{}", core_import, type_name);
let fields: Vec<_> = type_def
.fields
.iter()
.map(|f| {
let dto_ty = format!("Option<{}>", type_ref_to_dto_type(&f.ty, core_import));
let camel_case_name = to_camel_case(&f.name);
minijinja::context! {
name => &f.name,
ty => &dto_ty,
core_name => &f.name,
serde_rename => &camel_case_name,
conv => dto_field_conversion(&f.ty),
}
})
.collect::<Vec<_>>();
let code = if !fields.is_empty() {
crate::template_env::render(
"gen_input_dto",
minijinja::context! {
input_name => &input_name,
core_path => &core_path,
fields => &fields,
has_default => type_def.has_default,
},
)
} else {
String::new()
};
(code, input_name)
}
fn type_ref_to_dto_type(ty: &alef_core::ir::TypeRef, core_import: &str) -> String {
use alef_core::ir::TypeRef;
match ty {
TypeRef::String | TypeRef::Char => "String".to_string(),
TypeRef::Primitive(p) => match p {
alef_core::ir::PrimitiveType::Bool => "bool".to_string(),
alef_core::ir::PrimitiveType::U8 => "u8".to_string(),
alef_core::ir::PrimitiveType::U16 => "u16".to_string(),
alef_core::ir::PrimitiveType::U32 => "u32".to_string(),
alef_core::ir::PrimitiveType::U64 => "u64".to_string(),
alef_core::ir::PrimitiveType::I8 => "i8".to_string(),
alef_core::ir::PrimitiveType::I16 => "i16".to_string(),
alef_core::ir::PrimitiveType::I32 => "i32".to_string(),
alef_core::ir::PrimitiveType::I64 => "i64".to_string(),
alef_core::ir::PrimitiveType::F32 => "f32".to_string(),
alef_core::ir::PrimitiveType::F64 => "f64".to_string(),
alef_core::ir::PrimitiveType::Usize => "usize".to_string(),
alef_core::ir::PrimitiveType::Isize => "isize".to_string(),
},
TypeRef::Vec(inner) => format!("Vec<{}>", type_ref_to_dto_type(inner, core_import)),
TypeRef::Optional(inner) => format!("Option<{}>", type_ref_to_dto_type(inner, core_import)),
TypeRef::Map(k, v) => format!(
"std::collections::HashMap<{}, {}>",
type_ref_to_dto_type(k, core_import),
type_ref_to_dto_type(v, core_import)
),
TypeRef::Json => "serde_json::Value".to_string(),
TypeRef::Bytes => "Vec<u8>".to_string(),
TypeRef::Path => "String".to_string(),
TypeRef::Duration => "u64".to_string(),
TypeRef::Named(n) => format!("{core_import}::{n}"),
TypeRef::Unit => "()".to_string(),
}
}
fn dto_field_conversion(ty: &alef_core::ir::TypeRef) -> String {
use alef_core::ir::TypeRef;
match ty {
TypeRef::Duration => "Into::into(std::time::Duration::from_millis(v))".to_string(),
TypeRef::Path => "Into::into(std::path::PathBuf::from(v))".to_string(),
_ => "v.into()".to_string(),
}
}
pub(super) fn emit_rustdoc(doc: &str) -> String {
if doc.is_empty() {
return String::new();
}
let sanitized = alef_codegen::doc_emission::sanitize_rust_idioms(doc, alef_codegen::doc_emission::DocTarget::TsDoc);
crate::template_env::render(
"rustdoc",
minijinja::context! {
lines => sanitized.lines().collect::<Vec<_>>(),
},
)
}
pub(super) fn typeref_to_core_type_str(ty: &TypeRef) -> String {
use alef_core::ir::PrimitiveType;
match ty {
TypeRef::String | TypeRef::Char => "String".to_string(),
TypeRef::Primitive(p) => match p {
PrimitiveType::Bool => "bool".to_string(),
PrimitiveType::U8 => "u8".to_string(),
PrimitiveType::U16 => "u16".to_string(),
PrimitiveType::U32 => "u32".to_string(),
PrimitiveType::U64 => "u64".to_string(),
PrimitiveType::I8 => "i8".to_string(),
PrimitiveType::I16 => "i16".to_string(),
PrimitiveType::I32 => "i32".to_string(),
PrimitiveType::I64 => "i64".to_string(),
PrimitiveType::F32 => "f32".to_string(),
PrimitiveType::F64 => "f64".to_string(),
PrimitiveType::Usize => "usize".to_string(),
PrimitiveType::Isize => "isize".to_string(),
},
TypeRef::Vec(inner) => format!("Vec<{}>", typeref_to_core_type_str(inner)),
TypeRef::Optional(inner) => format!("Option<{}>", typeref_to_core_type_str(inner)),
TypeRef::Map(k, v) => format!(
"std::collections::HashMap<{}, {}>",
typeref_to_core_type_str(k),
typeref_to_core_type_str(v)
),
TypeRef::Json => "serde_json::Value".to_string(),
TypeRef::Bytes => "Vec<u8>".to_string(),
TypeRef::Path => "String".to_string(),
TypeRef::Duration => "u64".to_string(),
TypeRef::Named(n) => n.to_string(),
TypeRef::Unit => "()".to_string(),
}
}
pub(super) fn format_param_unused(name: &str, ty: &str, unused: bool) -> String {
let prefix = if unused { "_" } else { "" };
format!("{}{}: {}", prefix, name, ty)
}
pub(super) fn gen_function(
func: &FunctionDef,
mapper: &WasmMapper,
core_import: &str,
opaque_types: &AHashSet<String>,
prefix: &str,
mutex_types: &AHashSet<String>,
api: &alef_core::ir::ApiSurface,
) -> String {
let mut input_dtos = String::new();
let mut input_dto_names: HashMap<String, String> = HashMap::new();
for p in &func.params {
if let TypeRef::Named(name) = &p.ty {
if !opaque_types.contains(name.as_str()) && should_have_input_dto(name) {
if let Some(type_def) = api.types.iter().find(|t| t.name == *name) {
let (dto_code, dto_name) = gen_input_dto_for_type(name, core_import, type_def);
if !dto_code.is_empty() {
input_dtos.push_str(&dto_code);
input_dtos.push_str("\n\n");
input_dto_names.insert(name.clone(), dto_name);
}
}
}
}
}
let can_delegate = alef_codegen::shared::can_auto_delegate_function(func, opaque_types);
let params: Vec<String> = func
.params
.iter()
.map(|p| {
let ty = mapper.map_type(&p.ty);
let mapped_ty = if p.optional { format!("Option<{}>", ty) } else { ty };
format_param_unused(&p.name, &mapped_ty, !can_delegate && !func.is_async)
})
.collect();
let return_type = mapper.map_type(&func.return_type);
let return_annotation = mapper.wrap_return(&return_type, func.error_type.is_some());
let js_name = to_node_name(&func.name);
let js_name_attr = if js_name != func.name {
format!("(js_name = \"{}\")", js_name)
} else {
String::new()
};
let mut attrs = emit_rustdoc(&func.doc);
if func.params.len() > 7 {
attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
}
if func.error_type.is_some() {
attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
}
let core_fn_path = {
let path = func.rust_path.replace('-', "_");
if path.starts_with(core_import) {
path
} else {
format!("{core_import}::{}", func.name)
}
};
if func.is_async {
let has_named = alef_codegen::generators::has_named_params(&func.params, opaque_types);
let async_params: Vec<String> = if has_named {
func.params
.iter()
.map(|p| match &p.ty {
TypeRef::Named(name) if !opaque_types.contains(name.as_str()) => {
let mapped_ty = if p.optional {
"Option<JsValue>".to_string()
} else {
"JsValue".to_string()
};
format!("{}: {}", p.name, mapped_ty)
}
_ => {
let ty = mapper.map_type(&p.ty);
let mapped_ty = if p.optional { format!("Option<{}>", ty) } else { ty };
format!("{}: {}", p.name, mapped_ty)
}
})
.collect()
} else {
params.clone()
};
let mut serde_bindings = String::new();
if has_named {
for p in &func.params {
if let TypeRef::Named(name) = &p.ty {
if !opaque_types.contains(name.as_str()) {
let core_path = format!("{}::{}", core_import, name);
let err_conv = ".map_err(|e| JsValue::from_str(&e.to_string()))";
if p.optional {
serde_bindings.push_str(&crate::template_env::render(
"serde_named_optional",
minijinja::context! {
param_name => &p.name,
core_path => &core_path,
err_conv => &err_conv,
},
));
serde_bindings.push_str(" ");
} else {
serde_bindings.push_str(&crate::template_env::render(
"serde_named_required",
minijinja::context! {
param_name => &p.name,
core_path => &core_path,
err_conv => &err_conv,
},
));
serde_bindings.push_str(" ");
}
}
} else if let TypeRef::Vec(inner) = &p.ty
&& let TypeRef::Named(name) = inner.as_ref()
&& !opaque_types.contains(name.as_str())
{
let core_path = format!("{}::{}", core_import, name);
if p.optional {
serde_bindings.push_str(&format!(
"let {name}_core: Option<Vec<{core_path}>> = {name}.map(|values| values.into_iter().map(Into::into).collect());\n ",
name = p.name
));
} else {
serde_bindings.push_str(&format!(
"let {name}_core: Vec<{core_path}> = {name}.into_iter().map(Into::into).collect();\n ",
name = p.name
));
}
}
}
}
let let_bindings = serde_bindings;
let call_args = if let_bindings.is_empty() {
generators::gen_call_args(&func.params, opaque_types)
} else {
generators::gen_call_args_with_let_bindings(&func.params, opaque_types)
};
let core_call = format!("{core_fn_path}({call_args})");
let return_expr = match &func.return_type {
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
if mutex_types.contains(n.as_str()) {
format!(
"result.into_iter().map(|v| {} {{ inner: Arc::new(std::sync::Mutex::new(v)) }}).collect::<Vec<_>>()",
mapper.map_type(inner)
)
} else {
format!(
"result.into_iter().map(|v| {} {{ inner: Arc::new(v) }}).collect::<Vec<_>>()",
mapper.map_type(inner)
)
}
}
TypeRef::Named(_) => {
let inner_mapped = mapper.map_type(inner);
format!("result.into_iter().map({inner_mapped}::from).collect::<Vec<_>>()")
}
_ => "result".to_string(),
},
TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
let prefixed = mapper.map_type(&func.return_type);
if mutex_types.contains(n.as_str()) {
format!("{prefixed} {{ inner: Arc::new(std::sync::Mutex::new(result)) }}")
} else {
format!("{prefixed} {{ inner: Arc::new(result) }}")
}
}
TypeRef::Named(_) => {
format!("{return_type}::from(result)")
}
TypeRef::Unit => "result".to_string(),
_ => "result".to_string(),
};
let body = if func.error_type.is_some() {
format!(
"{let_bindings}let result = {core_call}.await\n \
.map_err(|e| JsValue::from_str(&e.to_string()))?;\n \
Ok({return_expr})"
)
} else {
format!(
"{let_bindings}let result = {core_call}.await;\n \
{return_expr}"
)
};
let fn_code = format!(
"{attrs}#[wasm_bindgen{js_name_attr}]\npub async fn {}({}) -> {} {{\n \
{body}\n}}",
func.name,
async_params.join(", "),
return_annotation
);
format!("{input_dtos}{fn_code}")
} else if can_delegate {
let mut let_bindings = if alef_codegen::generators::has_named_params(&func.params, opaque_types) {
alef_codegen::generators::gen_named_let_bindings_no_promote(&func.params, opaque_types, core_import)
} else {
String::new()
};
let needs_result_wrap = func
.params
.iter()
.any(|p| matches!(&p.ty, TypeRef::Vec(outer) if matches!(outer.as_ref(), TypeRef::Vec(_))))
&& func.error_type.is_none();
for p in &func.params {
if let TypeRef::Vec(outer_inner) = &p.ty
&& matches!(outer_inner.as_ref(), TypeRef::Vec(_))
{
let elem_ty = if let TypeRef::Vec(elem) = outer_inner.as_ref() {
typeref_to_core_type_str(elem.as_ref())
} else {
"String".to_string()
};
let core_ty = format!("Vec<Vec<{elem_ty}>>");
if p.optional {
let err_conv = format!(".expect(\"deserialize {}\")", p.name);
let_bindings.push_str(&crate::template_env::render(
"serde_vec_nested_optional",
minijinja::context! {
param_name => &p.name,
core_ty => &core_ty,
err_conv => &err_conv,
},
));
let_bindings.push_str(" ");
} else {
let err_conv = format!(".expect(\"deserialize {}\")", p.name);
let_bindings.push_str(&crate::template_env::render(
"serde_vec_nested_required",
minijinja::context! {
param_name => &p.name,
core_ty => &core_ty,
err_conv => &err_conv,
},
));
let_bindings.push_str(" ");
}
}
}
let _ = needs_result_wrap;
let call_args = if let_bindings.is_empty() {
generators::gen_call_args(&func.params, opaque_types)
} else {
generators::gen_call_args_with_let_bindings(&func.params, opaque_types)
};
let core_call = format!("{core_fn_path}({call_args})");
let body = if func.error_type.is_some() {
let wrap = wasm_wrap_return_fn(
"result",
&func.return_type,
opaque_types,
func.returns_ref,
func.returns_cow,
prefix,
mutex_types,
);
format!(
"{let_bindings}let result = {core_call}.map_err(|e| JsValue::from_str(&e.to_string()))?;\n Ok({wrap})"
)
} else {
format!(
"{let_bindings}{}",
wasm_wrap_return_fn(
&core_call,
&func.return_type,
opaque_types,
func.returns_ref,
func.returns_cow,
prefix,
mutex_types
)
)
};
let fn_code = format!(
"{attrs}#[wasm_bindgen{js_name_attr}]\npub fn {}({}) -> {} {{\n \
{body}\n}}",
func.name,
params.join(", "),
return_annotation
);
format!("{input_dtos}{fn_code}")
} else if func.error_type.is_some()
&& (func.sanitized || alef_codegen::generators::has_named_params(&func.params, opaque_types))
{
let serde_params: Vec<String> = func
.params
.iter()
.map(|p| match &p.ty {
TypeRef::Named(name) if !opaque_types.contains(name.as_str()) => {
let mapped_ty = if p.optional {
"Option<JsValue>".to_string()
} else {
"JsValue".to_string()
};
format!("{}: {}", p.name, mapped_ty)
}
TypeRef::Vec(inner) => {
if matches!(inner.as_ref(), TypeRef::Named(_)) {
if p.optional {
format!("{}: Option<Vec<String>>", p.name)
} else {
format!("{}: Vec<String>", p.name)
}
} else {
let ty = mapper.map_type(&p.ty);
let mapped_ty = if p.optional { format!("Option<{}>", ty) } else { ty };
format!("{}: {}", p.name, mapped_ty)
}
}
_ => {
let ty = mapper.map_type(&p.ty);
let mapped_ty = if p.optional { format!("Option<{}>", ty) } else { ty };
format!("{}: {}", p.name, mapped_ty)
}
})
.collect();
let mut serde_bindings = String::new();
for p in &func.params {
match &p.ty {
TypeRef::Named(name) if !opaque_types.contains(name.as_str()) => {
let core_path = format!("{}::{}", core_import, name);
let err_conv = ".map_err(|e| JsValue::from_str(&e.to_string()))";
if should_have_input_dto(name) {
let input_dto_type = input_dto_names
.get(name)
.cloned()
.unwrap_or_else(|| format!("{}Input", name));
if p.optional {
serde_bindings.push_str(&crate::template_env::render(
"serde_config_optional",
minijinja::context! {
param_name => &p.name,
core_path => &core_path,
err_conv => &err_conv,
input_dto_type => &input_dto_type,
},
));
serde_bindings.push_str(" ");
} else {
serde_bindings.push_str(&crate::template_env::render(
"serde_config_required",
minijinja::context! {
param_name => &p.name,
core_path => &core_path,
err_conv => &err_conv,
input_dto_type => &input_dto_type,
},
));
serde_bindings.push_str(" ");
}
} else {
if p.optional {
serde_bindings.push_str(&crate::template_env::render(
"serde_named_optional",
minijinja::context! {
param_name => &p.name,
core_path => &core_path,
err_conv => &err_conv,
},
));
serde_bindings.push_str(" ");
} else {
serde_bindings.push_str(&crate::template_env::render(
"serde_named_required",
minijinja::context! {
param_name => &p.name,
core_path => &core_path,
err_conv => &err_conv,
},
));
serde_bindings.push_str(" ");
}
}
}
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => {
let inner_name = match inner.as_ref() {
TypeRef::Named(n) => n,
_ => "UnknownTuple",
};
let core_path = format!("{}::{}", core_import, inner_name);
let err_conv = ".map_err(|e| JsValue::from_str(&e.to_string()))";
if p.optional {
serde_bindings.push_str(&crate::template_env::render(
"serde_vec_named_optional",
minijinja::context! {
param_name => &p.name,
core_path => &core_path,
err_conv => &err_conv,
},
));
serde_bindings.push_str(" ");
} else {
serde_bindings.push_str(&crate::template_env::render(
"serde_vec_named_required",
minijinja::context! {
param_name => &p.name,
core_path => &core_path,
err_conv => &err_conv,
},
));
serde_bindings.push_str(" ");
}
}
TypeRef::Vec(inner)
if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char)
&& p.sanitized
&& p.original_type.is_some() =>
{
let err_conv = ".map_err(|e| JsValue::from_str(&e.to_string()))";
if p.optional {
serde_bindings.push_str(&crate::template_env::render(
"serde_vec_tuple_optional",
minijinja::context! {
param_name => &p.name,
err_conv => &err_conv,
},
));
serde_bindings.push_str(" ");
} else {
serde_bindings.push_str(&crate::template_env::render(
"serde_vec_tuple_required",
minijinja::context! {
param_name => &p.name,
err_conv => &err_conv,
},
));
serde_bindings.push_str(" ");
}
}
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char) && p.is_ref => {
if p.optional {
serde_bindings.push_str(&crate::template_env::render(
"serde_vec_string_refs_optional",
minijinja::context! {
param_name => &p.name,
},
));
serde_bindings.push_str(" ");
} else {
serde_bindings.push_str(&crate::template_env::render(
"serde_vec_string_refs_required",
minijinja::context! {
param_name => &p.name,
},
));
serde_bindings.push_str(" ");
}
}
TypeRef::Vec(outer_inner) if matches!(outer_inner.as_ref(), TypeRef::Vec(_)) => {
let elem_ty = if let TypeRef::Vec(elem) = outer_inner.as_ref() {
typeref_to_core_type_str(elem.as_ref())
} else {
"String".to_string()
};
let core_ty = format!("Vec<Vec<{elem_ty}>>");
let err_conv = ".map_err(|e| JsValue::from_str(&e.to_string()))";
if p.optional {
serde_bindings.push_str(&crate::template_env::render(
"serde_vec_nested_optional",
minijinja::context! {
param_name => &p.name,
core_ty => &core_ty,
err_conv => &err_conv,
},
));
serde_bindings.push_str(" ");
} else {
serde_bindings.push_str(&crate::template_env::render(
"serde_vec_nested_required",
minijinja::context! {
param_name => &p.name,
core_ty => &core_ty,
err_conv => &err_conv,
},
));
serde_bindings.push_str(" ");
}
}
_ => {}
}
}
let call_args = generators::gen_call_args_with_let_bindings(&func.params, opaque_types);
let core_call = format!("{core_fn_path}({call_args})");
let wrap = wasm_wrap_return_fn(
"result",
&func.return_type,
opaque_types,
func.returns_ref,
func.returns_cow,
prefix,
mutex_types,
);
let body = if matches!(func.return_type, TypeRef::Unit) {
format!("{serde_bindings}{core_call}.map_err(|e| JsValue::from_str(&e.to_string()))?;\n Ok(())")
} else {
format!(
"{serde_bindings}let result = {core_call}.map_err(|e| JsValue::from_str(&e.to_string()))?;\n Ok({wrap})"
)
};
let fn_code = format!(
"{attrs}#[wasm_bindgen{js_name_attr}]\npub fn {}({}) -> {} {{\n \
{body}\n}}",
func.name,
serde_params.join(", "),
return_annotation
);
format!("{input_dtos}{fn_code}")
} else {
let body = gen_wasm_unimplemented_body(&func.return_type, &func.name, func.error_type.is_some());
let fn_code = format!(
"{attrs}#[wasm_bindgen{js_name_attr}]\npub fn {}({}) -> {} {{\n \
{body}\n}}",
func.name,
params.join(", "),
return_annotation
);
format!("{input_dtos}{fn_code}")
}
}
pub(super) fn gen_env_shims(shim_names: &[String]) -> String {
let mut out = String::from("// WASM environment shims for C scanner interop\n");
for name in shim_names {
let shim = match name.as_str() {
"iswspace" => concat!(
"#[unsafe(no_mangle)]\n",
"pub extern \"C\" fn iswspace(c: u32) -> i32 {\n",
" char::from_u32(c).map_or(0, |ch| ch.is_whitespace() as i32)\n",
"}\n",
),
"iswalnum" => concat!(
"#[unsafe(no_mangle)]\n",
"pub extern \"C\" fn iswalnum(c: u32) -> i32 {\n",
" char::from_u32(c).map_or(0, |ch| ch.is_alphanumeric() as i32)\n",
"}\n",
),
"towupper" => concat!(
"#[unsafe(no_mangle)]\n",
"pub extern \"C\" fn towupper(c: u32) -> u32 {\n",
" char::from_u32(c).map_or(c, |ch| ch.to_uppercase().next().unwrap_or(ch) as u32)\n",
"}\n",
),
"iswalpha" => concat!(
"#[unsafe(no_mangle)]\n",
"pub extern \"C\" fn iswalpha(c: u32) -> i32 {\n",
" char::from_u32(c).map_or(0, |ch| ch.is_alphabetic() as i32)\n",
"}\n",
),
"iswlower" => concat!(
"#[unsafe(no_mangle)]\n",
"pub extern \"C\" fn iswlower(c: u32) -> i32 {\n",
" char::from_u32(c).map_or(0, |ch| ch.is_lowercase() as i32)\n",
"}\n",
),
"iswupper" => concat!(
"#[unsafe(no_mangle)]\n",
"pub extern \"C\" fn iswupper(c: u32) -> i32 {\n",
" char::from_u32(c).map_or(0, |ch| ch.is_uppercase() as i32)\n",
"}\n",
),
"iswxdigit" => concat!(
"#[unsafe(no_mangle)]\n",
"pub extern \"C\" fn iswxdigit(c: u32) -> i32 {\n",
" char::from_u32(c).map_or(0, |ch| ch.is_ascii_hexdigit() as i32)\n",
"}\n",
),
"towlower" => concat!(
"#[unsafe(no_mangle)]\n",
"pub extern \"C\" fn towlower(c: u32) -> u32 {\n",
" char::from_u32(c).map_or(c, |ch| ch.to_lowercase().next().unwrap_or(ch) as u32)\n",
"}\n",
),
"memchr" => concat!(
"/// # Safety\n",
"/// Caller must ensure `s` points to a buffer of at least `n` bytes.\n",
"#[unsafe(no_mangle)]\n",
"pub unsafe extern \"C\" fn memchr(s: *const u8, c: i32, n: usize) -> *const u8 {\n",
" if s.is_null() { return core::ptr::null(); }\n",
" let needle = c as u8;\n",
" let slice = unsafe { core::slice::from_raw_parts(s, n) };\n",
" match slice.iter().position(|&b| b == needle) {\n",
" Some(idx) => unsafe { s.add(idx) },\n",
" None => core::ptr::null(),\n",
" }\n",
"}\n",
),
"strcmp" => concat!(
"/// # Safety\n",
"/// Caller must ensure both pointers are valid null-terminated C strings.\n",
"#[unsafe(no_mangle)]\n",
"pub unsafe extern \"C\" fn strcmp(a: *const u8, b: *const u8) -> i32 {\n",
" if a.is_null() || b.is_null() { return 0; }\n",
" let mut i = 0isize;\n",
" loop {\n",
" let ca = unsafe { *a.offset(i) };\n",
" let cb = unsafe { *b.offset(i) };\n",
" if ca != cb { return (ca as i32) - (cb as i32); }\n",
" if ca == 0 { return 0; }\n",
" i += 1;\n",
" }\n",
"}\n",
),
_ => continue,
};
out.push_str(shim);
}
out.trim_end_matches('\n').to_string()
}
pub(super) fn gen_wasm_unimplemented_body(return_type: &TypeRef, fn_name: &str, has_error: bool) -> String {
let err_msg = format!("Not implemented: {fn_name}");
if has_error {
format!("Err(JsValue::from_str(\"{err_msg}\"))")
} else {
match return_type {
TypeRef::Unit => "()".to_string(),
TypeRef::String | TypeRef::Char | TypeRef::Path => format!("String::from(\"[unimplemented: {fn_name}]\")"),
TypeRef::Bytes => "Vec::new()".to_string(),
TypeRef::Primitive(p) => match p {
alef_core::ir::PrimitiveType::Bool => "false".to_string(),
_ => "0".to_string(),
},
TypeRef::Optional(_) => "None".to_string(),
TypeRef::Vec(_) => "Vec::new()".to_string(),
TypeRef::Map(_, _) => "Default::default()".to_string(),
TypeRef::Duration => "0u64".to_string(),
TypeRef::Named(_) | TypeRef::Json => format!("panic!(\"alef: {fn_name} not auto-delegatable\")"),
}
}
}
fn wasm_expr_is_already_arc(expr: &str) -> bool {
let trimmed = expr.trim();
trimmed == "self.inner"
|| trimmed == "self.inner.clone()"
|| trimmed.starts_with("self.inner.as_ref()")
|| trimmed.starts_with("self.inner.clone()")
}
#[allow(clippy::too_many_arguments)]
pub(super) fn wasm_wrap_return(
expr: &str,
return_type: &TypeRef,
type_name: &str,
opaque_types: &AHashSet<String>,
self_is_opaque: bool,
returns_ref: bool,
returns_cow: bool,
prefix: &str,
mutex_types: &AHashSet<String>,
) -> String {
match return_type {
TypeRef::Named(n) if n == type_name && self_is_opaque => {
if wasm_expr_is_already_arc(expr) {
format!("Self {{ inner: {expr} }}")
} else if mutex_types.contains(type_name) {
generators::wrap_return_with_mutex(
expr,
return_type,
type_name,
opaque_types,
mutex_types,
true,
returns_ref,
returns_cow,
)
} else if returns_ref {
format!("Self {{ inner: Arc::new({expr}.clone()) }}")
} else {
format!("Self {{ inner: Arc::new({expr}) }}")
}
}
TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
if wasm_expr_is_already_arc(expr) {
format!("{prefix}{n} {{ inner: {expr} }}")
} else if mutex_types.contains(n.as_str()) {
let wrapped = generators::wrap_return_with_mutex(
expr,
return_type,
type_name,
opaque_types,
mutex_types,
true,
returns_ref,
returns_cow,
);
if wrapped.starts_with(&format!("{n} {{")) {
format!("{prefix}{}{}", n, &wrapped[n.len()..])
} else {
wrapped
}
} else if returns_ref {
format!("{prefix}{n} {{ inner: Arc::new({expr}.clone()) }}")
} else {
format!("{prefix}{n} {{ inner: Arc::new({expr}) }}")
}
}
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
if mutex_types.contains(name.as_str()) {
let wrap_inner = generators::wrap_return_with_mutex(
"v",
inner.as_ref(),
type_name,
opaque_types,
mutex_types,
true,
returns_ref,
returns_cow,
);
format!("{expr}.map(|v| {prefix}{name} {{ {wrap_inner} }})")
} else if returns_ref {
format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }})")
} else {
format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v) }})")
}
}
_ => generators::wrap_return(
expr,
return_type,
type_name,
opaque_types,
self_is_opaque,
returns_ref,
returns_cow,
),
},
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
if mutex_types.contains(name.as_str()) {
let wrap_inner = generators::wrap_return_with_mutex(
"v",
inner.as_ref(),
type_name,
opaque_types,
mutex_types,
true,
returns_ref,
returns_cow,
);
format!("{expr}.into_iter().map(|v| {prefix}{name} {{ {wrap_inner} }}).collect()")
} else if returns_ref {
format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }}).collect()")
} else {
format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v) }}).collect()")
}
}
_ => generators::wrap_return(
expr,
return_type,
type_name,
opaque_types,
self_is_opaque,
returns_ref,
returns_cow,
),
},
_ => generators::wrap_return(
expr,
return_type,
type_name,
opaque_types,
self_is_opaque,
returns_ref,
returns_cow,
),
}
}
pub(super) fn wasm_wrap_return_fn(
expr: &str,
return_type: &TypeRef,
opaque_types: &AHashSet<String>,
returns_ref: bool,
returns_cow: bool,
prefix: &str,
mutex_types: &AHashSet<String>,
) -> String {
match return_type {
TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
if wasm_expr_is_already_arc(expr) {
format!("{prefix}{n} {{ inner: {expr} }}")
} else if mutex_types.contains(n.as_str()) {
let wrapped = generators::wrap_return_with_mutex(
expr,
return_type,
"",
opaque_types,
mutex_types,
true,
returns_ref,
returns_cow,
);
if wrapped.starts_with(&format!("{n} {{")) {
format!("{prefix}{}{}", n, &wrapped[n.len()..])
} else {
wrapped
}
} else if returns_ref {
format!("{prefix}{n} {{ inner: Arc::new({expr}.clone()) }}")
} else {
format!("{prefix}{n} {{ inner: Arc::new({expr}) }}")
}
}
TypeRef::Named(_) => {
if returns_cow {
format!("{expr}.into_owned().into()")
} else if returns_ref {
format!("{expr}.clone().into()")
} else {
format!("{expr}.into()")
}
}
TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
if returns_cow && matches!(return_type, TypeRef::Bytes) {
format!("{expr}.into_owned()")
} else if returns_ref {
format!("{expr}.into()")
} else {
expr.to_string()
}
}
TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
TypeRef::Json => format!("{expr}.to_string()"),
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
if mutex_types.contains(name.as_str()) {
let wrap_inner = generators::wrap_return_with_mutex(
"v",
inner.as_ref(),
"",
opaque_types,
mutex_types,
true,
returns_ref,
returns_cow,
);
format!("{expr}.map(|v| {prefix}{name} {{ {wrap_inner} }})")
} else if returns_ref {
format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }})")
} else {
format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v) }})")
}
}
TypeRef::Named(_) => {
if returns_ref {
format!("{expr}.map(|v| v.clone().into())")
} else {
format!("{expr}.map(Into::into)")
}
}
TypeRef::Path => {
format!("{expr}.map(Into::into)")
}
TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
if returns_ref {
format!("{expr}.map(Into::into)")
} else {
expr.to_string()
}
}
_ => expr.to_string(),
},
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
if mutex_types.contains(name.as_str()) {
let wrap_inner = generators::wrap_return_with_mutex(
"v",
inner.as_ref(),
"",
opaque_types,
mutex_types,
true,
returns_ref,
returns_cow,
);
format!("{expr}.into_iter().map(|v| {prefix}{name} {{ {wrap_inner} }}).collect()")
} else if returns_ref {
format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }}).collect()")
} else {
format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v) }}).collect()")
}
}
TypeRef::Named(_) => {
if returns_ref {
format!("{expr}.iter().map(|v| v.clone().into()).collect()")
} else {
format!("{expr}.into_iter().map(Into::into).collect()")
}
}
TypeRef::Path => {
format!("{expr}.into_iter().map(Into::into).collect()")
}
TypeRef::String | TypeRef::Char => {
if returns_ref {
format!("{expr}.iter().map(|s| s.to_string()).collect()")
} else {
expr.to_string()
}
}
TypeRef::Bytes => {
if returns_ref {
format!("{expr}.iter().map(|b| b.to_vec()).collect()")
} else {
expr.to_string()
}
}
_ => expr.to_string(),
},
_ => expr.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use alef_core::ir::{ParamDef, TypeRef};
use std::collections::HashMap;
fn param(name: &str, ty: TypeRef) -> ParamDef {
ParamDef {
name: name.to_string(),
ty,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: false,
is_mut: false,
newtype_wrapper: None,
original_type: None,
}
}
fn async_function(params: Vec<ParamDef>) -> FunctionDef {
FunctionDef {
name: "interact".to_string(),
rust_path: "kreuzcrawl::interact".to_string(),
original_rust_path: String::new(),
params,
return_type: TypeRef::Unit,
is_async: true,
error_type: Some("CrawlError".to_string()),
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}
}
#[test]
fn gen_env_shims_emits_expected_signatures_for_all_supported_names() {
let names: Vec<String> = [
"iswspace",
"iswalnum",
"towupper",
"iswalpha",
"iswlower",
"iswupper",
"iswxdigit",
"towlower",
"memchr",
"strcmp",
]
.iter()
.map(|s| (*s).to_string())
.collect();
let out = gen_env_shims(&names);
assert_eq!(out.matches("#[unsafe(no_mangle)]").count(), names.len(), "{out}");
for name in ["iswspace", "iswalnum", "iswalpha", "iswlower", "iswupper", "iswxdigit"] {
let sig = format!("pub extern \"C\" fn {name}(c: u32) -> i32");
assert!(out.contains(&sig), "missing signature `{sig}` in:\n{out}");
}
for name in ["towupper", "towlower"] {
let sig = format!("pub extern \"C\" fn {name}(c: u32) -> u32");
assert!(out.contains(&sig), "missing signature `{sig}` in:\n{out}");
}
assert!(
out.contains("pub unsafe extern \"C\" fn memchr(s: *const u8, c: i32, n: usize) -> *const u8"),
"{out}"
);
assert!(
out.contains("pub unsafe extern \"C\" fn strcmp(a: *const u8, b: *const u8) -> i32"),
"{out}"
);
}
#[test]
fn gen_env_shims_ignores_unknown_names() {
let names = vec!["not_a_real_shim".to_string()];
let out = gen_env_shims(&names);
assert!(!out.contains("#[unsafe(no_mangle)]"), "{out}");
}
#[test]
fn async_vec_named_params_convert_to_core_vec() {
let mapper = WasmMapper::new(HashMap::new(), "Wasm".to_string());
let func = async_function(vec![param(
"actions",
TypeRef::Vec(Box::new(TypeRef::Named("PageAction".to_string()))),
)]);
let api = alef_core::ir::ApiSurface {
crate_name: "kreuzcrawl".to_string(),
version: "0.1.0".to_string(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: HashMap::new(),
excluded_trait_names: std::collections::HashSet::new(),
};
let out = gen_function(
&func,
&mapper,
"kreuzcrawl",
&AHashSet::new(),
"Wasm",
&AHashSet::new(),
&api,
);
assert!(out.contains("actions: Vec<WasmPageAction>"));
assert!(
out.contains(
"let actions_core: Vec<kreuzcrawl::PageAction> = actions.into_iter().map(Into::into).collect();"
),
"{out}"
);
assert!(out.contains("kreuzcrawl::interact(actions_core).await"), "{out}");
}
}