1use alef_core::config::TraitBridgeConfig;
7use alef_core::ir::{ApiSurface, MethodDef, TypeDef, TypeRef};
8use std::fmt::Write;
9
10pub fn gen_trait_bridge(
12 trait_type: &TypeDef,
13 bridge_cfg: &TraitBridgeConfig,
14 core_import: &str,
15 api: &ApiSurface,
16) -> String {
17 let mut out = String::with_capacity(8192);
18 let struct_name = format!("Php{}Bridge", bridge_cfg.trait_name);
19 let trait_path = trait_type.rust_path.replace('-', "_");
20
21 let type_paths: std::collections::HashMap<&str, &str> = api
23 .types
24 .iter()
25 .map(|t| (t.name.as_str(), t.rust_path.as_str()))
26 .chain(api.enums.iter().map(|e| (e.name.as_str(), e.rust_path.as_str())))
27 .collect();
28
29 let is_visitor_bridge = bridge_cfg.type_alias.is_some()
31 && bridge_cfg.register_fn.is_none()
32 && bridge_cfg.super_trait.is_none()
33 && trait_type.methods.iter().all(|m| m.has_default_impl);
34
35 if is_visitor_bridge {
36 gen_visitor_bridge(
37 &mut out,
38 trait_type,
39 bridge_cfg,
40 &struct_name,
41 &trait_path,
42 core_import,
43 &type_paths,
44 );
45 }
46
47 out
48}
49
50fn gen_visitor_bridge(
55 out: &mut String,
56 trait_type: &TypeDef,
57 _bridge_cfg: &TraitBridgeConfig,
58 struct_name: &str,
59 trait_path: &str,
60 core_crate: &str,
61 type_paths: &std::collections::HashMap<&str, &str>,
62) {
63 writeln!(out, "fn nodecontext_to_php_array(").unwrap();
65 writeln!(out, " ctx: &{core_crate}::visitor::NodeContext,").unwrap();
66 writeln!(out, ") -> ext_php_rs::boxed::ZBox<ext_php_rs::types::ZendHashTable> {{").unwrap();
67 writeln!(out, " let mut arr = ext_php_rs::types::ZendHashTable::new();").unwrap();
68 writeln!(
69 out,
70 " arr.insert(\"nodeType\", ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", ctx.node_type)).unwrap_or_default()).ok();"
71 )
72 .unwrap();
73 writeln!(
74 out,
75 " arr.insert(\"tagName\", ext_php_rs::types::Zval::try_from(ctx.tag_name.clone()).unwrap_or_default()).ok();"
76 )
77 .unwrap();
78 writeln!(
79 out,
80 " arr.insert(\"depth\", ext_php_rs::types::Zval::try_from(ctx.depth as i64).unwrap_or_default()).ok();"
81 )
82 .unwrap();
83 writeln!(
84 out,
85 " arr.insert(\"indexInParent\", ext_php_rs::types::Zval::try_from(ctx.index_in_parent as i64).unwrap_or_default()).ok();"
86 )
87 .unwrap();
88 writeln!(
89 out,
90 " arr.insert(\"isInline\", ext_php_rs::types::Zval::try_from(ctx.is_inline).unwrap_or_default()).ok();"
91 )
92 .unwrap();
93 writeln!(out, " if let Some(ref pt) = ctx.parent_tag {{").unwrap();
94 writeln!(
95 out,
96 " arr.insert(\"parentTag\", ext_php_rs::types::Zval::try_from(pt.clone()).unwrap_or_default()).ok();"
97 )
98 .unwrap();
99 writeln!(out, " }}").unwrap();
100 writeln!(out, " let mut attrs = ext_php_rs::types::ZendHashTable::new();").unwrap();
101 writeln!(out, " for (k, v) in &ctx.attributes {{").unwrap();
102 writeln!(
103 out,
104 " attrs.insert(k.as_str(), ext_php_rs::types::Zval::try_from(v.clone()).unwrap_or_default()).ok();"
105 )
106 .unwrap();
107 writeln!(out, " }}").unwrap();
108 writeln!(out, " let mut attrs_zval = ext_php_rs::types::Zval::new();").unwrap();
109 writeln!(out, " attrs_zval.set_hashtable(attrs);").unwrap();
110 writeln!(out, " arr.insert(\"attributes\", attrs_zval).ok();").unwrap();
111 writeln!(out, " arr").unwrap();
112 writeln!(out, "}}").unwrap();
113 writeln!(out).unwrap();
114
115 writeln!(out, "pub struct {struct_name} {{").unwrap();
119 writeln!(out, " php_obj: *mut ext_php_rs::types::ZendObject,").unwrap();
120 writeln!(out, "}}").unwrap();
121 writeln!(out).unwrap();
122
123 writeln!(out, "// SAFETY: PHP objects are single-threaded; the bridge is used").unwrap();
126 writeln!(out, "// only within a single PHP request, never across threads.").unwrap();
127 writeln!(out, "unsafe impl Send for {struct_name} {{}}").unwrap();
128 writeln!(out, "unsafe impl Sync for {struct_name} {{}}").unwrap();
129 writeln!(out).unwrap();
130
131 writeln!(out, "impl std::fmt::Debug for {struct_name} {{").unwrap();
133 writeln!(
134 out,
135 " fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{"
136 )
137 .unwrap();
138 writeln!(out, " write!(f, \"{struct_name}\")").unwrap();
139 writeln!(out, " }}").unwrap();
140 writeln!(out, "}}").unwrap();
141 writeln!(out).unwrap();
142
143 writeln!(out, "impl {struct_name} {{").unwrap();
147 writeln!(
148 out,
149 " pub fn new(php_obj: &mut ext_php_rs::types::ZendObject) -> Self {{"
150 )
151 .unwrap();
152 writeln!(out, " Self {{ php_obj: php_obj as *mut _ }}").unwrap();
153 writeln!(out, " }}").unwrap();
154 writeln!(out, "}}").unwrap();
155 writeln!(out).unwrap();
156
157 writeln!(out, "impl {trait_path} for {struct_name} {{").unwrap();
159 for method in &trait_type.methods {
160 if method.trait_source.is_some() {
161 continue;
162 }
163 gen_visitor_method_php(out, method, type_paths);
164 }
165 writeln!(out, "}}").unwrap();
166 writeln!(out).unwrap();
167}
168
169fn visitor_param_type(
171 ty: &TypeRef,
172 is_ref: bool,
173 optional: bool,
174 tp: &std::collections::HashMap<&str, &str>,
175) -> String {
176 if optional && matches!(ty, TypeRef::String) && is_ref {
177 return "Option<&str>".to_string();
178 }
179 if is_ref {
180 if let TypeRef::Vec(inner) = ty {
181 let inner_str = param_type(inner, "", false, tp);
182 return format!("&[{inner_str}]");
183 }
184 }
185 param_type(ty, "", is_ref, tp)
186}
187
188fn gen_visitor_method_php(out: &mut String, method: &MethodDef, type_paths: &std::collections::HashMap<&str, &str>) {
190 let name = &method.name;
191 let php_name = to_camel_case(name);
192
193 let mut sig_parts = vec!["&mut self".to_string()];
194 for p in &method.params {
195 let ty_str = visitor_param_type(&p.ty, p.is_ref, p.optional, type_paths);
196 sig_parts.push(format!("{}: {}", p.name, ty_str));
197 }
198 let sig = sig_parts.join(", ");
199
200 let ret_ty = match &method.return_type {
201 TypeRef::Named(n) => type_paths
202 .get(n.as_str())
203 .map(|p| p.replace('-', "_"))
204 .unwrap_or_else(|| n.clone()),
205 other => param_type(other, "", false, type_paths),
206 };
207
208 writeln!(out, " fn {name}({sig}) -> {ret_ty} {{").unwrap();
209
210 writeln!(
212 out,
213 " // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call."
214 )
215 .unwrap();
216 writeln!(out, " let php_obj_ref = unsafe {{ &*self.php_obj }};").unwrap();
217
218 let has_args = !method.params.is_empty();
220 if has_args {
221 writeln!(out, " let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();").unwrap();
222 for p in &method.params {
223 if let TypeRef::Named(n) = &p.ty {
224 if n == "NodeContext" {
225 writeln!(
226 out,
227 " let ctx_arr = nodecontext_to_php_array({}{});",
228 if p.is_ref { "" } else { "&" },
229 p.name
230 )
231 .unwrap();
232 writeln!(
233 out,
234 " args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());"
235 )
236 .unwrap();
237 continue;
238 }
239 }
240 if p.optional && matches!(&p.ty, TypeRef::String) && p.is_ref {
243 writeln!(
244 out,
245 " args.push(match {0} {{ Some(s) => ext_php_rs::types::Zval::try_from(s.to_string()).unwrap_or_default(), None => ext_php_rs::types::Zval::new() }});",
246 p.name
247 )
248 .unwrap();
249 continue;
250 }
251 if matches!(&p.ty, TypeRef::String) {
252 if p.is_ref {
253 writeln!(
254 out,
255 " args.push(ext_php_rs::types::Zval::try_from({}.to_string()).unwrap_or_default());",
256 p.name
257 )
258 .unwrap();
259 } else {
260 writeln!(
261 out,
262 " args.push(ext_php_rs::types::Zval::try_from({}.clone()).unwrap_or_default());",
263 p.name
264 )
265 .unwrap();
266 }
267 continue;
268 }
269 if matches!(&p.ty, TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool)) {
270 writeln!(
271 out,
272 " {{ let mut _zv = ext_php_rs::types::Zval::new(); _zv.set_bool({}); args.push(_zv); }}",
273 p.name
274 )
275 .unwrap();
276 continue;
277 }
278 writeln!(
280 out,
281 " args.push(ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default());",
282 p.name
283 )
284 .unwrap();
285 }
286 }
287
288 if has_args {
292 writeln!(
293 out,
294 " let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args.iter().map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn).collect();"
295 )
296 .unwrap();
297 }
298 let args_expr = if has_args { "dyn_args" } else { "vec![]" };
299 writeln!(
300 out,
301 " let result = php_obj_ref.try_call_method(\"{php_name}\", {args_expr});"
302 )
303 .unwrap();
304
305 writeln!(out, " match result {{").unwrap();
307 writeln!(out, " Err(_) => {ret_ty}::Continue,").unwrap();
308 writeln!(out, " Ok(val) => {{").unwrap();
309 writeln!(
310 out,
311 " let s = val.string().unwrap_or_default().to_lowercase();"
312 )
313 .unwrap();
314 writeln!(out, " match s.as_str() {{").unwrap();
315 writeln!(out, " \"continue\" => {ret_ty}::Continue,").unwrap();
316 writeln!(out, " \"skip\" => {ret_ty}::Skip,").unwrap();
317 writeln!(
318 out,
319 " \"preserve_html\" | \"preservehtml\" => {ret_ty}::PreserveHtml,"
320 )
321 .unwrap();
322 writeln!(out, " other => {ret_ty}::Custom(other.to_string()),").unwrap();
323 writeln!(out, " }}").unwrap();
324 writeln!(out, " }}").unwrap();
325 writeln!(out, " }}").unwrap();
326 writeln!(out, " }}").unwrap();
327 writeln!(out).unwrap();
328}
329
330fn to_camel_case(name: &str) -> String {
332 let mut result = String::with_capacity(name.len());
333 let mut capitalize_next = false;
334 for (i, c) in name.chars().enumerate() {
335 if c == '_' {
336 capitalize_next = true;
337 } else if capitalize_next {
338 result.extend(c.to_uppercase());
339 capitalize_next = false;
340 } else if i == 0 {
341 result.extend(c.to_lowercase());
342 } else {
343 result.push(c);
344 }
345 }
346 result
347}
348
349fn param_type(ty: &TypeRef, ci: &str, is_ref: bool, tp: &std::collections::HashMap<&str, &str>) -> String {
351 match ty {
352 TypeRef::Bytes if is_ref => "&[u8]".into(),
353 TypeRef::Bytes => "Vec<u8>".into(),
354 TypeRef::String if is_ref => "&str".into(),
355 TypeRef::String => "String".into(),
356 TypeRef::Path if is_ref => "&std::path::Path".into(),
357 TypeRef::Path => "std::path::PathBuf".into(),
358 TypeRef::Named(n) => {
359 let qualified = tp
360 .get(n.as_str())
361 .map(|p| p.replace('-', "_"))
362 .unwrap_or_else(|| format!("{ci}::{n}"));
363 if is_ref { format!("&{qualified}") } else { qualified }
364 }
365 TypeRef::Vec(inner) => format!("Vec<{}>", param_type(inner, ci, false, tp)),
366 TypeRef::Optional(inner) => format!("Option<{}>", param_type(inner, ci, false, tp)),
367 TypeRef::Primitive(p) => prim(p).into(),
368 TypeRef::Unit => "()".into(),
369 TypeRef::Char => "char".into(),
370 TypeRef::Map(k, v) => format!(
371 "std::collections::HashMap<{}, {}>",
372 param_type(k, ci, false, tp),
373 param_type(v, ci, false, tp)
374 ),
375 TypeRef::Json => "serde_json::Value".into(),
376 TypeRef::Duration => "std::time::Duration".into(),
377 }
378}
379
380fn prim(p: &alef_core::ir::PrimitiveType) -> &'static str {
381 use alef_core::ir::PrimitiveType::*;
382 match p {
383 Bool => "bool",
384 U8 => "u8",
385 U16 => "u16",
386 U32 => "u32",
387 U64 => "u64",
388 I8 => "i8",
389 I16 => "i16",
390 I32 => "i32",
391 I64 => "i64",
392 F32 => "f32",
393 F64 => "f64",
394 Usize => "usize",
395 Isize => "isize",
396 }
397}
398
399pub fn find_bridge_param<'a>(
404 func: &alef_core::ir::FunctionDef,
405 bridges: &'a [TraitBridgeConfig],
406) -> Option<(usize, &'a TraitBridgeConfig)> {
407 for (idx, param) in func.params.iter().enumerate() {
408 let named = match ¶m.ty {
409 TypeRef::Named(n) => Some(n.as_str()),
410 TypeRef::Optional(inner) => {
411 if let TypeRef::Named(n) = inner.as_ref() {
412 Some(n.as_str())
413 } else {
414 None
415 }
416 }
417 _ => None,
418 };
419 for bridge in bridges {
420 if let Some(type_name) = named {
421 if bridge.type_alias.as_deref() == Some(type_name) {
422 return Some((idx, bridge));
423 }
424 }
425 if bridge.param_name.as_deref() == Some(param.name.as_str()) {
426 return Some((idx, bridge));
427 }
428 }
429 }
430 None
431}
432
433#[allow(clippy::too_many_arguments)]
436pub fn gen_bridge_function(
437 func: &alef_core::ir::FunctionDef,
438 bridge_param_idx: usize,
439 bridge_cfg: &TraitBridgeConfig,
440 mapper: &dyn alef_codegen::type_mapper::TypeMapper,
441 opaque_types: &ahash::AHashSet<String>,
442 core_import: &str,
443) -> String {
444 use alef_core::ir::TypeRef;
445
446 let struct_name = format!("Php{}Bridge", bridge_cfg.trait_name);
447 let handle_path = format!("{core_import}::visitor::VisitorHandle");
448 let param_name = &func.params[bridge_param_idx].name;
449 let bridge_param = &func.params[bridge_param_idx];
450 let is_optional = bridge_param.optional || matches!(&bridge_param.ty, TypeRef::Optional(_));
451
452 let mut sig_parts = Vec::new();
454 for (idx, p) in func.params.iter().enumerate() {
455 if idx == bridge_param_idx {
456 let php_obj_ty = "&mut ext_php_rs::types::ZendObject";
460 if is_optional {
461 sig_parts.push(format!("{}: Option<{php_obj_ty}>", p.name));
462 } else {
463 sig_parts.push(format!("{}: {php_obj_ty}", p.name));
464 }
465 } else {
466 let promoted = idx > bridge_param_idx || func.params[..idx].iter().any(|pp| pp.optional);
467 let base = mapper.map_type(&p.ty);
468 let ty = match &p.ty {
471 TypeRef::Named(n) if !opaque_types.contains(n.as_str()) => {
472 if p.optional || promoted {
473 format!("Option<&mut {base}>")
474 } else {
475 format!("&mut {base}")
476 }
477 }
478 TypeRef::Optional(inner) => {
479 if let TypeRef::Named(n) = inner.as_ref() {
480 if !opaque_types.contains(n.as_str()) {
481 format!("Option<&mut {base}>")
482 } else if p.optional || promoted {
483 format!("Option<{base}>")
484 } else {
485 base
486 }
487 } else if p.optional || promoted {
488 format!("Option<{base}>")
489 } else {
490 base
491 }
492 }
493 _ => {
494 if p.optional || promoted {
495 format!("Option<{base}>")
496 } else {
497 base
498 }
499 }
500 };
501 sig_parts.push(format!("{}: {}", p.name, ty));
502 }
503 }
504
505 let params_str = sig_parts.join(", ");
506 let return_type = mapper.map_type(&func.return_type);
507 let ret = mapper.wrap_return(&return_type, func.error_type.is_some());
508
509 let err_conv = ".map_err(|e| ext_php_rs::exception::PhpException::default(e.to_string()))";
510
511 let bridge_wrap = if is_optional {
513 format!(
514 "let {param_name} = {param_name}.map(|v| {{\n \
515 let bridge = {struct_name}::new(v);\n \
516 std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n \
517 }});"
518 )
519 } else {
520 format!(
521 "let {param_name} = {{\n \
522 let bridge = {struct_name}::new({param_name});\n \
523 std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n \
524 }};"
525 )
526 };
527
528 let serde_bindings: String = func
530 .params
531 .iter()
532 .enumerate()
533 .filter(|(idx, p)| {
534 if *idx == bridge_param_idx {
535 return false;
536 }
537 let named = match &p.ty {
538 TypeRef::Named(n) => Some(n.as_str()),
539 TypeRef::Optional(inner) => {
540 if let TypeRef::Named(n) = inner.as_ref() {
541 Some(n.as_str())
542 } else {
543 None
544 }
545 }
546 _ => None,
547 };
548 named.is_some_and(|n| !opaque_types.contains(n))
549 })
550 .map(|(_, p)| {
551 let name = &p.name;
552 let core_path = format!(
553 "{core_import}::{}",
554 match &p.ty {
555 TypeRef::Named(n) => n.clone(),
556 TypeRef::Optional(inner) =>
557 if let TypeRef::Named(n) = inner.as_ref() {
558 n.clone()
559 } else {
560 String::new()
561 },
562 _ => String::new(),
563 }
564 );
565 if p.optional || matches!(&p.ty, TypeRef::Optional(_)) {
566 format!(
567 "let {name}_core: Option<{core_path}> = {name}.map(|v| {{\n \
568 let json = serde_json::to_string(&v){err_conv}?;\n \
569 serde_json::from_str(&json){err_conv}\n \
570 }}).transpose()?;\n "
571 )
572 } else {
573 format!(
574 "let {name}_json = serde_json::to_string(&{name}){err_conv}?;\n \
575 let {name}_core: {core_path} = serde_json::from_str(&{name}_json){err_conv}?;\n "
576 )
577 }
578 })
579 .collect();
580
581 let call_args: Vec<String> = func
583 .params
584 .iter()
585 .enumerate()
586 .map(|(idx, p)| {
587 if idx == bridge_param_idx {
588 return p.name.clone();
589 }
590 match &p.ty {
591 TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
592 if p.optional {
593 format!("{}.as_ref().map(|v| &v.inner)", p.name)
594 } else {
595 format!("&{}.inner", p.name)
596 }
597 }
598 TypeRef::Named(_) => format!("{}_core", p.name),
599 TypeRef::Optional(inner) => {
600 if let TypeRef::Named(n) = inner.as_ref() {
601 if opaque_types.contains(n.as_str()) {
602 format!("{}.as_ref().map(|v| &v.inner)", p.name)
603 } else {
604 format!("{}_core", p.name)
605 }
606 } else {
607 p.name.clone()
608 }
609 }
610 TypeRef::String | TypeRef::Char => {
611 if p.is_ref {
612 format!("&{}", p.name)
613 } else {
614 p.name.clone()
615 }
616 }
617 _ => p.name.clone(),
618 }
619 })
620 .collect();
621 let call_args_str = call_args.join(", ");
622
623 let core_fn_path = {
624 let path = func.rust_path.replace('-', "_");
625 if path.starts_with(core_import) {
626 path
627 } else {
628 format!("{core_import}::{}", func.name)
629 }
630 };
631 let core_call = format!("{core_fn_path}({call_args_str})");
632
633 let return_wrap = match &func.return_type {
634 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
635 format!("{name} {{ inner: std::sync::Arc::new(val) }}")
636 }
637 TypeRef::Named(_) => "val.into()".to_string(),
638 TypeRef::String | TypeRef::Bytes => "val.into()".to_string(),
639 _ => "val".to_string(),
640 };
641
642 let body = if func.error_type.is_some() {
643 if return_wrap == "val" {
644 format!("{bridge_wrap}\n {serde_bindings}{core_call}{err_conv}")
645 } else {
646 format!("{bridge_wrap}\n {serde_bindings}{core_call}.map(|val| {return_wrap}){err_conv}")
647 }
648 } else {
649 format!("{bridge_wrap}\n {serde_bindings}{core_call}")
650 };
651
652 let func_name = &func.name;
653 let mut out = String::with_capacity(1024);
654 if func.error_type.is_some() {
655 writeln!(out, "#[allow(clippy::missing_errors_doc)]").ok();
656 }
657 writeln!(out, "pub fn {func_name}({params_str}) -> {ret} {{").ok();
658 writeln!(out, " {body}").ok();
659 writeln!(out, "}}").ok();
660
661 out
662}