Skip to main content

php_ast/
ast.rs

1use std::borrow::Cow;
2
3use serde::Serialize;
4
5use crate::Span;
6
7fn is_false(b: &bool) -> bool {
8    !*b
9}
10
11/// Arena-allocated Vec. Thin newtype over bumpalo::collections::Vec that implements Serialize and Debug.
12pub struct ArenaVec<'arena, T>(bumpalo::collections::Vec<'arena, T>);
13
14impl<'arena, T> ArenaVec<'arena, T> {
15    #[inline]
16    pub fn new_in(arena: &'arena bumpalo::Bump) -> Self {
17        Self(bumpalo::collections::Vec::new_in(arena))
18    }
19    #[inline]
20    pub fn with_capacity_in(cap: usize, arena: &'arena bumpalo::Bump) -> Self {
21        Self(bumpalo::collections::Vec::with_capacity_in(cap, arena))
22    }
23    #[inline]
24    pub fn push(&mut self, val: T) {
25        self.0.push(val)
26    }
27    /// Kept as an explicit method so `"ArenaVec::is_empty"` works as a serde
28    /// `skip_serializing_if` path (deref-inherited methods don't resolve via UFCS).
29    #[inline]
30    pub fn is_empty(&self) -> bool {
31        self.0.is_empty()
32    }
33}
34
35impl<'arena, T> IntoIterator for ArenaVec<'arena, T> {
36    type Item = T;
37    type IntoIter = bumpalo::collections::vec::IntoIter<'arena, T>;
38    #[inline]
39    fn into_iter(self) -> Self::IntoIter {
40        self.0.into_iter()
41    }
42}
43
44impl<'arena, T> std::ops::Deref for ArenaVec<'arena, T> {
45    type Target = [T];
46    #[inline]
47    fn deref(&self) -> &[T] {
48        &self.0
49    }
50}
51
52impl<'arena, T> std::ops::DerefMut for ArenaVec<'arena, T> {
53    #[inline]
54    fn deref_mut(&mut self) -> &mut [T] {
55        &mut self.0
56    }
57}
58
59impl<'arena, T: serde::Serialize> serde::Serialize for ArenaVec<'arena, T> {
60    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
61        self.0.as_slice().serialize(s)
62    }
63}
64
65impl<'arena, T: std::fmt::Debug> std::fmt::Debug for ArenaVec<'arena, T> {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        self.0.as_slice().fmt(f)
68    }
69}
70
71/// A comment found in the source file.
72#[derive(Debug, Serialize)]
73pub struct Comment<'src> {
74    pub kind: CommentKind,
75    /// Raw text of the comment including its delimiters (e.g. `// foo`, `/* bar */`, `/** baz */`).
76    pub text: &'src str,
77    pub span: Span,
78}
79
80/// Distinguishes the four syntactic forms of PHP comment.
81#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq)]
82pub enum CommentKind {
83    /// `// …` — single-line slash comment
84    Line,
85    /// `# …` — single-line hash comment
86    Hash,
87    /// `/* … */` — block comment
88    Block,
89    /// `/** … */` — doc-block comment (first non-whitespace char after `/*` is `*`)
90    Doc,
91}
92
93// =============================================================================
94/// The root AST node representing a complete PHP file.
95#[derive(Debug, Serialize)]
96pub struct Program<'arena, 'src> {
97    pub stmts: ArenaVec<'arena, Stmt<'arena, 'src>>,
98    pub span: Span,
99}
100
101// =============================================================================
102// Names and Types
103// =============================================================================
104
105/// A PHP name (identifier, qualified name, fully-qualified name, or relative name).
106///
107/// The `Simple` variant is the fast path for the common case (~95%) of single
108/// unqualified identifiers like `strlen`, `Foo`, `MyClass`. It avoids allocating
109/// an `ArenaVec` entirely.
110///
111/// The `Complex` variant handles qualified (`Foo\Bar`), fully-qualified (`\Foo\Bar`),
112/// and relative (`namespace\Foo`) names.
113pub enum Name<'arena, 'src> {
114    /// Single unqualified identifier — no `ArenaVec` allocation.
115    /// `&'src str` instead of `Cow` since this is always a borrowed slice of the source.
116    Simple { value: &'src str, span: Span },
117    /// Multi-part or prefixed name (`Foo\Bar`, `\Foo`, `namespace\Foo`).
118    Complex {
119        parts: ArenaVec<'arena, &'src str>,
120        kind: NameKind,
121        span: Span,
122    },
123}
124
125impl<'arena, 'src> Name<'arena, 'src> {
126    #[inline]
127    pub fn span(&self) -> Span {
128        match self {
129            Self::Simple { span, .. } | Self::Complex { span, .. } => *span,
130        }
131    }
132
133    #[inline]
134    pub fn kind(&self) -> NameKind {
135        match self {
136            Self::Simple { .. } => NameKind::Unqualified,
137            Self::Complex { kind, .. } => *kind,
138        }
139    }
140
141    /// Returns the name as a borrowed slice of the source string.
142    ///
143    /// Unlike [`to_string_repr`], this never allocates: it uses the stored
144    /// span to slice directly into `src`.  The slice includes any leading `\`
145    /// for fully-qualified names, exactly as it appears in the source.
146    ///
147    /// Use this when you need a zero-copy `&'src str` and already have the
148    /// source buffer available (e.g. inside [`crate::visitor::ScopeWalker`]).
149    #[inline]
150    pub fn src_repr(&self, src: &'src str) -> &'src str {
151        match self {
152            Self::Simple { value, .. } => value,
153            Self::Complex { span, .. } => &src[span.start as usize..span.end as usize],
154        }
155    }
156
157    /// Joins all parts with `\` and prepends `\` if fully qualified.
158    /// Returns `Cow::Borrowed` for simple names (zero allocation).
159    #[inline]
160    pub fn to_string_repr(&self) -> Cow<'src, str> {
161        match self {
162            Self::Simple { value, .. } => Cow::Borrowed(value),
163            Self::Complex { parts, kind, .. } => {
164                let joined = parts.join("\\");
165                if *kind == NameKind::FullyQualified {
166                    Cow::Owned(format!("\\{}", joined))
167                } else {
168                    Cow::Owned(joined)
169                }
170            }
171        }
172    }
173
174    /// Joins all parts with `\` without any leading backslash.
175    /// Returns `Cow::Borrowed` for simple names (zero allocation).
176    #[inline]
177    pub fn join_parts(&self) -> Cow<'src, str> {
178        match self {
179            Self::Simple { value, .. } => Cow::Borrowed(value),
180            Self::Complex { parts, .. } => Cow::Owned(parts.join("\\")),
181        }
182    }
183
184    /// Returns the parts as a slice.
185    /// For `Simple`, returns a single-element slice of the value.
186    #[inline]
187    pub fn parts_slice(&self) -> &[&'src str] {
188        match self {
189            Self::Simple { value, .. } => std::slice::from_ref(value),
190            Self::Complex { parts, .. } => parts,
191        }
192    }
193}
194
195impl<'arena, 'src> std::fmt::Debug for Name<'arena, 'src> {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        match self {
198            Self::Simple { value, span } => f
199                .debug_struct("Name")
200                .field("parts", &std::slice::from_ref(value))
201                .field("kind", &NameKind::Unqualified)
202                .field("span", span)
203                .finish(),
204            Self::Complex { parts, kind, span } => f
205                .debug_struct("Name")
206                .field("parts", parts)
207                .field("kind", kind)
208                .field("span", span)
209                .finish(),
210        }
211    }
212}
213
214impl<'arena, 'src> serde::Serialize for Name<'arena, 'src> {
215    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
216        use serde::ser::SerializeStruct;
217        let mut st = s.serialize_struct("Name", 3)?;
218        match self {
219            Self::Simple { value, span } => {
220                st.serialize_field("parts", std::slice::from_ref(value))?;
221                st.serialize_field("kind", &NameKind::Unqualified)?;
222                st.serialize_field("span", span)?;
223            }
224            Self::Complex { parts, kind, span } => {
225                st.serialize_field("parts", parts)?;
226                st.serialize_field("kind", kind)?;
227                st.serialize_field("span", span)?;
228            }
229        }
230        st.end()
231    }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
235pub enum NameKind {
236    /// A bare identifier with no namespace separator: `Foo`, `strlen`.
237    Unqualified,
238    /// A name with at least one internal `\` but no leading backslash: `Foo\Bar`.
239    Qualified,
240    /// A name with a leading `\`: `\Foo\Bar`.
241    FullyQualified,
242    /// A name starting with the `namespace` keyword: `namespace\Foo`.
243    Relative,
244}
245
246/// PHP built-in type keyword — zero-cost alternative to `Name::Simple` for the
247/// 20 reserved type names. One byte instead of a `Cow<str>` + `Span` in the AST.
248#[repr(u8)]
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
250pub enum BuiltinType {
251    /// `int` — integer scalar type.
252    Int,
253    /// `integer` — alias for `int`, accepted in type casts.
254    Integer,
255    /// `float` — floating-point scalar type.
256    Float,
257    /// `double` — alias for `float`, accepted in type casts.
258    Double,
259    /// `string` — string scalar type.
260    String,
261    /// `bool` — boolean scalar type.
262    Bool,
263    /// `boolean` — alias for `bool`, accepted in type casts.
264    Boolean,
265    /// `void` — return-only type indicating no value is returned.
266    Void,
267    /// `never` — return-only type for functions that never return normally (PHP 8.1+).
268    Never,
269    /// `mixed` — top type; accepts any value.
270    Mixed,
271    /// `object` — any object instance.
272    Object,
273    /// `iterable` — `array` or `Traversable` (deprecated in PHP 8.2; use `array|Traversable`).
274    Iterable,
275    /// `callable` — any callable value.
276    Callable,
277    /// `array` — any PHP array.
278    Array,
279    /// `self` — refers to the class in which the type hint appears.
280    Self_,
281    /// `parent` — refers to the parent class of the class in which the type hint appears.
282    Parent_,
283    /// `static` — late-static-bound type; the class on which the method was called.
284    Static,
285    /// `null` — the null type; only valid in union types.
286    Null,
287    /// `true` — the literal boolean `true` (PHP 8.2+).
288    True,
289    /// `false` — the literal boolean `false`.
290    False,
291}
292
293impl BuiltinType {
294    /// Returns the canonical lowercase spelling used in PHP and in serialized output.
295    #[inline]
296    pub fn as_str(self) -> &'static str {
297        match self {
298            Self::Int => "int",
299            Self::Integer => "integer",
300            Self::Float => "float",
301            Self::Double => "double",
302            Self::String => "string",
303            Self::Bool => "bool",
304            Self::Boolean => "boolean",
305            Self::Void => "void",
306            Self::Never => "never",
307            Self::Mixed => "mixed",
308            Self::Object => "object",
309            Self::Iterable => "iterable",
310            Self::Callable => "callable",
311            Self::Array => "array",
312            Self::Self_ => "self",
313            Self::Parent_ => "parent",
314            Self::Static => "static",
315            Self::Null => "null",
316            Self::True => "true",
317            Self::False => "false",
318        }
319    }
320}
321
322#[derive(Debug, Serialize)]
323pub struct TypeHint<'arena, 'src> {
324    pub kind: TypeHintKind<'arena, 'src>,
325    pub span: Span,
326}
327
328/// A PHP type hint.
329///
330/// `Keyword` is the fast path for the 20 built-in type names (`int`, `string`,
331/// `bool`, `self`, `array`, etc.). It stores only a 1-byte discriminant and a
332/// `Span`, avoiding the `Cow<str>` that `Named(Name::Simple)` would require.
333///
334/// Serialises identically to `Named` so all existing snapshots remain unchanged.
335#[derive(Debug)]
336pub enum TypeHintKind<'arena, 'src> {
337    /// A user-defined or qualified class name: `Foo`, `\Ns\Bar`.
338    Named(Name<'arena, 'src>),
339    /// Built-in type keyword (`int`, `string`, `bool`, `self`, …) — serialises as `Named` for snapshot compatibility.
340    Keyword(BuiltinType, Span),
341    /// Nullable type: `?T` — equivalent to `T|null`.
342    Nullable(&'arena TypeHint<'arena, 'src>),
343    /// Union type: `A|B|C` (PHP 8.0+).
344    Union(ArenaVec<'arena, TypeHint<'arena, 'src>>),
345    /// Intersection type: `A&B` (PHP 8.1+).
346    Intersection(ArenaVec<'arena, TypeHint<'arena, 'src>>),
347}
348
349impl<'arena, 'src> serde::Serialize for TypeHintKind<'arena, 'src> {
350    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
351        match self {
352            // Standard variants — match what #[derive(Serialize)] would produce.
353            Self::Named(name) => s.serialize_newtype_variant("TypeHintKind", 0, "Named", name),
354            Self::Nullable(inner) => {
355                s.serialize_newtype_variant("TypeHintKind", 2, "Nullable", inner)
356            }
357            Self::Union(types) => s.serialize_newtype_variant("TypeHintKind", 3, "Union", types),
358            Self::Intersection(types) => {
359                s.serialize_newtype_variant("TypeHintKind", 4, "Intersection", types)
360            }
361            // Keyword — serialise as if it were Named(Name::Simple { value: kw.as_str(), span }).
362            // This preserves all existing snapshot output.
363            Self::Keyword(builtin, span) => {
364                struct BuiltinNameRepr<'a>(&'a BuiltinType, &'a Span);
365                impl serde::Serialize for BuiltinNameRepr<'_> {
366                    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
367                        use serde::ser::SerializeStruct;
368                        let mut st = s.serialize_struct("Name", 3)?;
369                        st.serialize_field("parts", &[self.0.as_str()])?;
370                        st.serialize_field("kind", &NameKind::Unqualified)?;
371                        st.serialize_field("span", self.1)?;
372                        st.end()
373                    }
374                }
375                s.serialize_newtype_variant(
376                    "TypeHintKind",
377                    0,
378                    "Named",
379                    &BuiltinNameRepr(builtin, span),
380                )
381            }
382        }
383    }
384}
385
386// =============================================================================
387// Arguments
388// =============================================================================
389
390#[derive(Debug, Serialize)]
391pub struct Arg<'arena, 'src> {
392    pub name: Option<Name<'arena, 'src>>,
393    pub value: Expr<'arena, 'src>,
394    pub unpack: bool,
395    pub by_ref: bool,
396    pub span: Span,
397}
398
399// =============================================================================
400// Attributes
401// =============================================================================
402
403#[derive(Debug, Serialize)]
404pub struct Attribute<'arena, 'src> {
405    pub name: Name<'arena, 'src>,
406    pub args: ArenaVec<'arena, Arg<'arena, 'src>>,
407    pub span: Span,
408}
409
410// =============================================================================
411// Statements
412// =============================================================================
413
414#[derive(Debug, Serialize)]
415pub struct Stmt<'arena, 'src> {
416    pub kind: StmtKind<'arena, 'src>,
417    pub span: Span,
418}
419
420#[derive(Debug, Serialize)]
421pub enum StmtKind<'arena, 'src> {
422    /// Expression statement (e.g. `foo();`)
423    Expression(&'arena Expr<'arena, 'src>),
424
425    /// Echo statement: `echo expr1, expr2;`
426    Echo(ArenaVec<'arena, Expr<'arena, 'src>>),
427
428    /// Return statement: `return expr;`
429    Return(Option<&'arena Expr<'arena, 'src>>),
430
431    /// Block statement: `{ stmts }`
432    Block(ArenaVec<'arena, Stmt<'arena, 'src>>),
433
434    /// If statement
435    If(&'arena IfStmt<'arena, 'src>),
436
437    /// While loop
438    While(&'arena WhileStmt<'arena, 'src>),
439
440    /// For loop
441    For(&'arena ForStmt<'arena, 'src>),
442
443    /// Foreach loop
444    Foreach(&'arena ForeachStmt<'arena, 'src>),
445
446    /// Do-while loop
447    DoWhile(&'arena DoWhileStmt<'arena, 'src>),
448
449    /// Function declaration
450    Function(&'arena FunctionDecl<'arena, 'src>),
451
452    /// Break statement
453    Break(Option<&'arena Expr<'arena, 'src>>),
454
455    /// Continue statement
456    Continue(Option<&'arena Expr<'arena, 'src>>),
457
458    /// Switch statement
459    Switch(&'arena SwitchStmt<'arena, 'src>),
460
461    /// Goto statement
462    Goto(&'src str),
463
464    /// Label statement
465    Label(&'arena str),
466
467    /// Declare statement
468    Declare(&'arena DeclareStmt<'arena, 'src>),
469
470    /// Unset statement
471    Unset(ArenaVec<'arena, Expr<'arena, 'src>>),
472
473    /// Throw statement (also can be expression in PHP 8)
474    Throw(&'arena Expr<'arena, 'src>),
475
476    /// Try/catch/finally
477    TryCatch(&'arena TryCatchStmt<'arena, 'src>),
478
479    /// Global declaration
480    Global(ArenaVec<'arena, Expr<'arena, 'src>>),
481
482    /// Class declaration
483    Class(&'arena ClassDecl<'arena, 'src>),
484
485    /// Interface declaration
486    Interface(&'arena InterfaceDecl<'arena, 'src>),
487
488    /// Trait declaration
489    Trait(&'arena TraitDecl<'arena, 'src>),
490
491    /// Enum declaration
492    Enum(&'arena EnumDecl<'arena, 'src>),
493
494    /// Namespace declaration
495    Namespace(&'arena NamespaceDecl<'arena, 'src>),
496
497    /// Use declaration
498    Use(&'arena UseDecl<'arena, 'src>),
499
500    /// Top-level constant: `const FOO = expr;`
501    Const(ArenaVec<'arena, ConstItem<'arena, 'src>>),
502
503    /// Static variable declaration: `static $x = 1;`
504    StaticVar(ArenaVec<'arena, StaticVar<'arena, 'src>>),
505
506    /// __halt_compiler(); with remaining data
507    HaltCompiler(&'src str),
508
509    /// Nop (empty statement `;`)
510    Nop,
511
512    /// Inline HTML
513    InlineHtml(&'src str),
514
515    /// Error placeholder — parser always produces a tree
516    Error,
517}
518
519#[derive(Debug, Serialize)]
520pub struct IfStmt<'arena, 'src> {
521    pub condition: Expr<'arena, 'src>,
522    pub then_branch: &'arena Stmt<'arena, 'src>,
523    pub elseif_branches: ArenaVec<'arena, ElseIfBranch<'arena, 'src>>,
524    pub else_branch: Option<&'arena Stmt<'arena, 'src>>,
525}
526
527#[derive(Debug, Serialize)]
528pub struct ElseIfBranch<'arena, 'src> {
529    pub condition: Expr<'arena, 'src>,
530    pub body: Stmt<'arena, 'src>,
531    pub span: Span,
532}
533
534#[derive(Debug, Serialize)]
535pub struct WhileStmt<'arena, 'src> {
536    pub condition: Expr<'arena, 'src>,
537    pub body: &'arena Stmt<'arena, 'src>,
538}
539
540#[derive(Debug, Serialize)]
541pub struct ForStmt<'arena, 'src> {
542    pub init: ArenaVec<'arena, Expr<'arena, 'src>>,
543    pub condition: ArenaVec<'arena, Expr<'arena, 'src>>,
544    pub update: ArenaVec<'arena, Expr<'arena, 'src>>,
545    pub body: &'arena Stmt<'arena, 'src>,
546}
547
548#[derive(Debug, Serialize)]
549pub struct ForeachStmt<'arena, 'src> {
550    pub expr: Expr<'arena, 'src>,
551    pub key: Option<Expr<'arena, 'src>>,
552    pub value: Expr<'arena, 'src>,
553    pub body: &'arena Stmt<'arena, 'src>,
554}
555
556#[derive(Debug, Serialize)]
557pub struct DoWhileStmt<'arena, 'src> {
558    pub body: &'arena Stmt<'arena, 'src>,
559    pub condition: Expr<'arena, 'src>,
560}
561
562#[derive(Debug, Serialize)]
563pub struct FunctionDecl<'arena, 'src> {
564    pub name: &'src str,
565    pub params: ArenaVec<'arena, Param<'arena, 'src>>,
566    pub body: ArenaVec<'arena, Stmt<'arena, 'src>>,
567    pub return_type: Option<TypeHint<'arena, 'src>>,
568    pub by_ref: bool,
569    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub doc_comment: Option<Comment<'src>>,
572}
573
574#[derive(Debug, Serialize)]
575pub struct Param<'arena, 'src> {
576    pub name: &'src str,
577    pub type_hint: Option<TypeHint<'arena, 'src>>,
578    pub default: Option<Expr<'arena, 'src>>,
579    pub by_ref: bool,
580    pub variadic: bool,
581    pub is_readonly: bool,
582    pub is_final: bool,
583    pub visibility: Option<Visibility>,
584    pub set_visibility: Option<Visibility>,
585    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
586    #[serde(skip_serializing_if = "ArenaVec::is_empty")]
587    pub hooks: ArenaVec<'arena, PropertyHook<'arena, 'src>>,
588    pub span: Span,
589}
590
591#[derive(Debug, Serialize)]
592pub struct SwitchStmt<'arena, 'src> {
593    pub expr: Expr<'arena, 'src>,
594    pub cases: ArenaVec<'arena, SwitchCase<'arena, 'src>>,
595}
596
597#[derive(Debug, Serialize)]
598pub struct SwitchCase<'arena, 'src> {
599    pub value: Option<Expr<'arena, 'src>>,
600    pub body: ArenaVec<'arena, Stmt<'arena, 'src>>,
601    pub span: Span,
602}
603
604#[derive(Debug, Serialize)]
605pub struct TryCatchStmt<'arena, 'src> {
606    pub body: ArenaVec<'arena, Stmt<'arena, 'src>>,
607    pub catches: ArenaVec<'arena, CatchClause<'arena, 'src>>,
608    pub finally: Option<ArenaVec<'arena, Stmt<'arena, 'src>>>,
609}
610
611#[derive(Debug, Serialize)]
612pub struct CatchClause<'arena, 'src> {
613    pub types: ArenaVec<'arena, Name<'arena, 'src>>,
614    pub var: Option<&'src str>,
615    pub body: ArenaVec<'arena, Stmt<'arena, 'src>>,
616    pub span: Span,
617}
618
619// =============================================================================
620// OOP Declarations
621// =============================================================================
622
623#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
624pub enum Visibility {
625    /// `public` — accessible from anywhere.
626    Public,
627    /// `protected` — accessible within the class and its subclasses.
628    Protected,
629    /// `private` — accessible only within the declaring class.
630    Private,
631}
632
633#[derive(Debug, Serialize)]
634pub struct ClassDecl<'arena, 'src> {
635    pub name: Option<&'src str>,
636    pub modifiers: ClassModifiers,
637    pub extends: Option<Name<'arena, 'src>>,
638    pub implements: ArenaVec<'arena, Name<'arena, 'src>>,
639    pub members: ArenaVec<'arena, ClassMember<'arena, 'src>>,
640    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
641    #[serde(skip_serializing_if = "Option::is_none")]
642    pub doc_comment: Option<Comment<'src>>,
643}
644
645#[derive(Debug, Clone, Serialize, Default)]
646pub struct ClassModifiers {
647    pub is_abstract: bool,
648    pub is_final: bool,
649    pub is_readonly: bool,
650}
651
652#[derive(Debug, Serialize)]
653pub struct ClassMember<'arena, 'src> {
654    pub kind: ClassMemberKind<'arena, 'src>,
655    pub span: Span,
656}
657
658#[derive(Debug, Serialize)]
659pub enum ClassMemberKind<'arena, 'src> {
660    Property(PropertyDecl<'arena, 'src>),
661    Method(MethodDecl<'arena, 'src>),
662    ClassConst(ClassConstDecl<'arena, 'src>),
663    TraitUse(TraitUseDecl<'arena, 'src>),
664}
665
666#[derive(Debug, Serialize)]
667pub struct PropertyDecl<'arena, 'src> {
668    pub name: &'src str,
669    pub visibility: Option<Visibility>,
670    pub set_visibility: Option<Visibility>,
671    pub is_static: bool,
672    pub is_readonly: bool,
673    pub type_hint: Option<TypeHint<'arena, 'src>>,
674    pub default: Option<Expr<'arena, 'src>>,
675    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
676    #[serde(skip_serializing_if = "ArenaVec::is_empty")]
677    pub hooks: ArenaVec<'arena, PropertyHook<'arena, 'src>>,
678    #[serde(skip_serializing_if = "Option::is_none")]
679    pub doc_comment: Option<Comment<'src>>,
680}
681
682#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
683pub enum PropertyHookKind {
684    /// `get` hook — called when the property is read.
685    Get,
686    /// `set` hook — called when the property is written; receives the incoming value as `$value`.
687    Set,
688}
689
690#[derive(Debug, Serialize)]
691pub enum PropertyHookBody<'arena, 'src> {
692    /// `{ stmts }` — a full statement block.
693    Block(ArenaVec<'arena, Stmt<'arena, 'src>>),
694    /// `=> expr` — short-form expression body.
695    Expression(Expr<'arena, 'src>),
696    /// No body — the hook is declared abstract (on an abstract class or interface).
697    Abstract,
698}
699
700#[derive(Debug, Serialize)]
701pub struct PropertyHook<'arena, 'src> {
702    pub kind: PropertyHookKind,
703    pub body: PropertyHookBody<'arena, 'src>,
704    pub is_final: bool,
705    pub by_ref: bool,
706    pub params: ArenaVec<'arena, Param<'arena, 'src>>,
707    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
708    pub span: Span,
709}
710
711#[derive(Debug, Serialize)]
712pub struct MethodDecl<'arena, 'src> {
713    pub name: &'src str,
714    pub visibility: Option<Visibility>,
715    pub is_static: bool,
716    pub is_abstract: bool,
717    pub is_final: bool,
718    pub by_ref: bool,
719    pub params: ArenaVec<'arena, Param<'arena, 'src>>,
720    pub return_type: Option<TypeHint<'arena, 'src>>,
721    pub body: Option<ArenaVec<'arena, Stmt<'arena, 'src>>>,
722    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
723    #[serde(skip_serializing_if = "Option::is_none")]
724    pub doc_comment: Option<Comment<'src>>,
725}
726
727#[derive(Debug, Serialize)]
728pub struct ClassConstDecl<'arena, 'src> {
729    pub name: &'src str,
730    pub visibility: Option<Visibility>,
731    #[serde(skip_serializing_if = "Option::is_none")]
732    pub type_hint: Option<&'arena TypeHint<'arena, 'src>>,
733    pub value: Expr<'arena, 'src>,
734    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
735    #[serde(skip_serializing_if = "Option::is_none")]
736    pub doc_comment: Option<Comment<'src>>,
737}
738
739#[derive(Debug, Serialize)]
740pub struct TraitUseDecl<'arena, 'src> {
741    pub traits: ArenaVec<'arena, Name<'arena, 'src>>,
742    pub adaptations: ArenaVec<'arena, TraitAdaptation<'arena, 'src>>,
743}
744
745#[derive(Debug, Serialize)]
746pub struct TraitAdaptation<'arena, 'src> {
747    pub kind: TraitAdaptationKind<'arena, 'src>,
748    pub span: Span,
749}
750
751#[derive(Debug, Serialize)]
752pub enum TraitAdaptationKind<'arena, 'src> {
753    /// `A::foo insteadof B, C;`
754    Precedence {
755        trait_name: Name<'arena, 'src>,
756        method: Name<'arena, 'src>,
757        insteadof: ArenaVec<'arena, Name<'arena, 'src>>,
758    },
759    /// `foo as bar;` or `A::foo as protected bar;` or `foo as protected;`
760    Alias {
761        trait_name: Option<Name<'arena, 'src>>,
762        method: Name<'arena, 'src>,
763        new_modifier: Option<Visibility>,
764        new_name: Option<Name<'arena, 'src>>,
765    },
766}
767
768#[derive(Debug, Serialize)]
769pub struct InterfaceDecl<'arena, 'src> {
770    pub name: &'src str,
771    pub extends: ArenaVec<'arena, Name<'arena, 'src>>,
772    pub members: ArenaVec<'arena, ClassMember<'arena, 'src>>,
773    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
774    #[serde(skip_serializing_if = "Option::is_none")]
775    pub doc_comment: Option<Comment<'src>>,
776}
777
778#[derive(Debug, Serialize)]
779pub struct TraitDecl<'arena, 'src> {
780    pub name: &'src str,
781    pub members: ArenaVec<'arena, ClassMember<'arena, 'src>>,
782    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
783    #[serde(skip_serializing_if = "Option::is_none")]
784    pub doc_comment: Option<Comment<'src>>,
785}
786
787#[derive(Debug, Serialize)]
788pub struct EnumDecl<'arena, 'src> {
789    pub name: &'src str,
790    pub scalar_type: Option<Name<'arena, 'src>>,
791    pub implements: ArenaVec<'arena, Name<'arena, 'src>>,
792    pub members: ArenaVec<'arena, EnumMember<'arena, 'src>>,
793    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
794    #[serde(skip_serializing_if = "Option::is_none")]
795    pub doc_comment: Option<Comment<'src>>,
796}
797
798#[derive(Debug, Serialize)]
799pub struct EnumMember<'arena, 'src> {
800    pub kind: EnumMemberKind<'arena, 'src>,
801    pub span: Span,
802}
803
804#[derive(Debug, Serialize)]
805pub enum EnumMemberKind<'arena, 'src> {
806    /// An enum case: `case Foo;` or `case Foo = 'foo';` (backed enum).
807    Case(EnumCase<'arena, 'src>),
808    /// A method defined inside the enum body.
809    Method(MethodDecl<'arena, 'src>),
810    /// A constant defined inside the enum body: `const X = 1;`.
811    ClassConst(ClassConstDecl<'arena, 'src>),
812    /// A trait use inside the enum body: `use SomeTrait;`.
813    TraitUse(TraitUseDecl<'arena, 'src>),
814}
815
816#[derive(Debug, Serialize)]
817pub struct EnumCase<'arena, 'src> {
818    pub name: &'src str,
819    pub value: Option<Expr<'arena, 'src>>,
820    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
821    #[serde(skip_serializing_if = "Option::is_none")]
822    pub doc_comment: Option<Comment<'src>>,
823}
824
825// =============================================================================
826// Namespace & Use
827// =============================================================================
828
829#[derive(Debug, Serialize)]
830pub struct NamespaceDecl<'arena, 'src> {
831    pub name: Option<Name<'arena, 'src>>,
832    pub body: NamespaceBody<'arena, 'src>,
833}
834
835#[derive(Debug, Serialize)]
836pub enum NamespaceBody<'arena, 'src> {
837    /// `namespace Foo { … }` — braced form; the statements are scoped to this namespace.
838    Braced(ArenaVec<'arena, Stmt<'arena, 'src>>),
839    /// `namespace Foo;` — simple form; all subsequent statements until the next `namespace` or EOF are in scope.
840    Simple,
841}
842
843#[derive(Debug, Serialize)]
844pub struct DeclareStmt<'arena, 'src> {
845    pub directives: ArenaVec<'arena, (&'src str, Expr<'arena, 'src>)>,
846    pub body: Option<&'arena Stmt<'arena, 'src>>,
847}
848
849#[derive(Debug, Serialize)]
850pub struct UseDecl<'arena, 'src> {
851    pub kind: UseKind,
852    pub uses: ArenaVec<'arena, UseItem<'arena, 'src>>,
853}
854
855#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
856pub enum UseKind {
857    /// `use Foo\Bar` — imports a class, interface, trait, or enum.
858    Normal,
859    /// `use function Foo\bar` — imports a function.
860    Function,
861    /// `use const Foo\BAR` — imports a constant.
862    Const,
863}
864
865#[derive(Debug, Serialize)]
866pub struct UseItem<'arena, 'src> {
867    pub name: Name<'arena, 'src>,
868    pub alias: Option<&'src str>,
869    #[serde(skip_serializing_if = "Option::is_none")]
870    pub kind: Option<UseKind>,
871    pub span: Span,
872}
873
874#[derive(Debug, Serialize)]
875pub struct ConstItem<'arena, 'src> {
876    pub name: &'src str,
877    pub value: Expr<'arena, 'src>,
878    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
879    pub span: Span,
880}
881
882#[derive(Debug, Serialize)]
883pub struct StaticVar<'arena, 'src> {
884    pub name: &'src str,
885    pub default: Option<Expr<'arena, 'src>>,
886    pub span: Span,
887}
888
889// =============================================================================
890// Expressions
891// =============================================================================
892
893/// A name string that originates either from the source buffer (`&'src str`) or was
894/// constructed in the arena (`&'arena str`).
895///
896/// Using this as the payload for both `ExprKind::Variable` and `ExprKind::Identifier`
897/// gives them the same binding type, so or-patterns compile natively:
898///
899/// ```
900/// # use php_ast::ast::{ExprKind, NameStr};
901/// # fn example<'a, 'b>(kind: &ExprKind<'a, 'b>) {
902/// if let ExprKind::Variable(name) | ExprKind::Identifier(name) = kind {
903///     let _s: &str = name.as_str();
904/// }
905/// # }
906/// ```
907#[derive(Clone, Copy, PartialEq, Eq, Hash)]
908pub enum NameStr<'arena, 'src> {
909    /// Borrowed directly from the source buffer.
910    Src(&'src str),
911    /// Allocated in the bump arena (e.g. a joined qualified name).
912    Arena(&'arena str),
913}
914
915impl<'arena, 'src> NameStr<'arena, 'src> {
916    #[inline]
917    pub fn as_str(&self) -> &str {
918        match self {
919            NameStr::Src(s) => s,
920            NameStr::Arena(s) => s,
921        }
922    }
923}
924
925impl<'arena, 'src> std::ops::Deref for NameStr<'arena, 'src> {
926    type Target = str;
927    #[inline]
928    fn deref(&self) -> &str {
929        self.as_str()
930    }
931}
932
933impl<'arena, 'src> std::fmt::Debug for NameStr<'arena, 'src> {
934    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
935        self.as_str().fmt(f)
936    }
937}
938
939impl<'arena, 'src> serde::Serialize for NameStr<'arena, 'src> {
940    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
941        self.as_str().serialize(serializer)
942    }
943}
944
945#[derive(Debug, Serialize)]
946pub struct Expr<'arena, 'src> {
947    pub kind: ExprKind<'arena, 'src>,
948    pub span: Span,
949}
950
951#[derive(Debug, Serialize)]
952pub enum ExprKind<'arena, 'src> {
953    /// Integer literal
954    Int(i64),
955
956    /// Float literal
957    Float(f64),
958
959    /// String literal
960    String(&'arena str),
961
962    /// Interpolated string: `"Hello $name, you are {$age} years old"`
963    InterpolatedString(ArenaVec<'arena, StringPart<'arena, 'src>>),
964
965    /// Heredoc: `<<<EOT ... EOT`
966    Heredoc {
967        label: &'src str,
968        parts: ArenaVec<'arena, StringPart<'arena, 'src>>,
969    },
970
971    /// Nowdoc: `<<<'EOT' ... EOT`
972    Nowdoc {
973        label: &'src str,
974        value: &'arena str,
975    },
976
977    /// Shell execution: `` `command $var` ``
978    ShellExec(ArenaVec<'arena, StringPart<'arena, 'src>>),
979
980    /// Boolean literal
981    Bool(bool),
982
983    /// Null literal
984    Null,
985
986    /// Variable: `$name`
987    Variable(NameStr<'arena, 'src>),
988
989    /// Variable variable: `$$var`, `$$$var`, `${expr}`
990    VariableVariable(&'arena Expr<'arena, 'src>),
991
992    /// Identifier (bare name, e.g. function name in a call)
993    Identifier(NameStr<'arena, 'src>),
994
995    /// Assignment: `$x = expr` or `$x += expr`
996    Assign(AssignExpr<'arena, 'src>),
997
998    /// Binary operation: `expr op expr`
999    Binary(BinaryExpr<'arena, 'src>),
1000
1001    /// Unary prefix: `-expr`, `!expr`, `~expr`, `++$x`, `--$x`
1002    UnaryPrefix(UnaryPrefixExpr<'arena, 'src>),
1003
1004    /// Unary postfix: `$x++`, `$x--`
1005    UnaryPostfix(UnaryPostfixExpr<'arena, 'src>),
1006
1007    /// Ternary: `cond ? then : else` or short `cond ?: else`
1008    Ternary(TernaryExpr<'arena, 'src>),
1009
1010    /// Null coalescing: `expr ?? fallback`
1011    NullCoalesce(NullCoalesceExpr<'arena, 'src>),
1012
1013    /// Function call: `name(args)`
1014    FunctionCall(FunctionCallExpr<'arena, 'src>),
1015
1016    /// Array literal: `[1, 2, 3]` or `['a' => 1]`
1017    Array(ArenaVec<'arena, ArrayElement<'arena, 'src>>),
1018
1019    /// Array access: `$arr[index]`
1020    ArrayAccess(ArrayAccessExpr<'arena, 'src>),
1021
1022    /// Print expression: `print expr`
1023    Print(&'arena Expr<'arena, 'src>),
1024
1025    /// Parenthesized expression: `(expr)`
1026    Parenthesized(&'arena Expr<'arena, 'src>),
1027
1028    /// Cast expression: `(int)$x`, `(string)$x`, etc.
1029    Cast(CastKind, &'arena Expr<'arena, 'src>),
1030
1031    /// Error suppression: `@expr`
1032    ErrorSuppress(&'arena Expr<'arena, 'src>),
1033
1034    /// Isset: `isset($a, $b)`
1035    Isset(ArenaVec<'arena, Expr<'arena, 'src>>),
1036
1037    /// Empty: `empty($a)`
1038    Empty(&'arena Expr<'arena, 'src>),
1039
1040    /// Include/require: `include 'file.php'`
1041    Include(IncludeKind, &'arena Expr<'arena, 'src>),
1042
1043    /// Eval: `eval('code')`
1044    Eval(&'arena Expr<'arena, 'src>),
1045
1046    /// Exit/die: `exit`, `exit(1)`, `die('msg')`
1047    Exit(Option<&'arena Expr<'arena, 'src>>),
1048
1049    /// Magic constant: `__LINE__`, `__FILE__`, etc.
1050    MagicConst(MagicConstKind),
1051
1052    /// Clone: `clone $obj`
1053    Clone(&'arena Expr<'arena, 'src>),
1054
1055    /// Clone with property overrides: `clone($obj, ['prop' => $val])` — PHP 8.5+
1056    CloneWith(&'arena Expr<'arena, 'src>, &'arena Expr<'arena, 'src>),
1057
1058    /// New: `new Class(args)`
1059    New(NewExpr<'arena, 'src>),
1060
1061    /// Property access: `$obj->prop`
1062    PropertyAccess(PropertyAccessExpr<'arena, 'src>),
1063
1064    /// Nullsafe property access: `$obj?->prop`
1065    NullsafePropertyAccess(PropertyAccessExpr<'arena, 'src>),
1066
1067    /// Method call: `$obj->method(args)`
1068    MethodCall(&'arena MethodCallExpr<'arena, 'src>),
1069
1070    /// Nullsafe method call: `$obj?->method(args)`
1071    NullsafeMethodCall(&'arena MethodCallExpr<'arena, 'src>),
1072
1073    /// Static property access: `Class::$prop`
1074    StaticPropertyAccess(StaticAccessExpr<'arena, 'src>),
1075
1076    /// Static method call: `Class::method(args)`
1077    StaticMethodCall(&'arena StaticMethodCallExpr<'arena, 'src>),
1078
1079    /// Dynamic static method call: `Class::$method(args)`
1080    StaticDynMethodCall(&'arena StaticDynMethodCallExpr<'arena, 'src>),
1081
1082    /// Class constant access: `Class::CONST`
1083    ClassConstAccess(StaticAccessExpr<'arena, 'src>),
1084
1085    /// Dynamic class constant access: `Foo::{expr}`
1086    ClassConstAccessDynamic {
1087        class: &'arena Expr<'arena, 'src>,
1088        member: &'arena Expr<'arena, 'src>,
1089    },
1090
1091    /// Dynamic static property access: `A::$$b`, `A::${'b'}`
1092    StaticPropertyAccessDynamic {
1093        class: &'arena Expr<'arena, 'src>,
1094        member: &'arena Expr<'arena, 'src>,
1095    },
1096
1097    /// Closure: `function($x) use($y) { }`
1098    Closure(&'arena ClosureExpr<'arena, 'src>),
1099
1100    /// Arrow function: `fn($x) => expr`
1101    ArrowFunction(&'arena ArrowFunctionExpr<'arena, 'src>),
1102
1103    /// Match: `match(expr) { ... }`
1104    Match(MatchExpr<'arena, 'src>),
1105
1106    /// Throw as expression (PHP 8)
1107    ThrowExpr(&'arena Expr<'arena, 'src>),
1108
1109    /// Yield: `yield` / `yield $val` / `yield $key => $val`
1110    Yield(YieldExpr<'arena, 'src>),
1111
1112    /// Anonymous class: `new class(args) extends Foo implements Bar { ... }`
1113    AnonymousClass(&'arena ClassDecl<'arena, 'src>),
1114
1115    /// First-class callable: `strlen(...)`, `$obj->method(...)`, `Foo::bar(...)`
1116    CallableCreate(CallableCreateExpr<'arena, 'src>),
1117
1118    /// Omitted element in destructuring: `[$a, , $c]` or `list($a, , $c)`
1119    Omit,
1120
1121    /// Error placeholder
1122    Error,
1123}
1124
1125impl<'arena, 'src> Expr<'arena, 'src> {
1126    /// Returns the name string for `Variable` and `Identifier` nodes, `None` for everything else.
1127    pub fn name_str(&self) -> Option<&str> {
1128        match &self.kind {
1129            ExprKind::Variable(s) | ExprKind::Identifier(s) => Some(s.as_str()),
1130            _ => None,
1131        }
1132    }
1133}
1134
1135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1136pub enum CastKind {
1137    /// `(int)` or `(integer)` cast.
1138    Int,
1139    /// `(float)`, `(double)`, or `(real)` cast.
1140    Float,
1141    /// `(string)` cast.
1142    String,
1143    /// `(bool)` or `(boolean)` cast.
1144    Bool,
1145    /// `(array)` cast.
1146    Array,
1147    /// `(object)` cast.
1148    Object,
1149    /// `(unset)` cast — deprecated; casts to `null`.
1150    Unset,
1151    /// `(void)` cast — non-standard; treated as discarding the value.
1152    Void,
1153}
1154
1155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1156pub enum IncludeKind {
1157    /// `include 'file.php'` — emits a warning if the file is not found.
1158    Include,
1159    /// `include_once 'file.php'` — like `include`, but skipped if the file has already been included.
1160    IncludeOnce,
1161    /// `require 'file.php'` — fatal error if the file is not found.
1162    Require,
1163    /// `require_once 'file.php'` — like `require`, but skipped if the file has already been included.
1164    RequireOnce,
1165}
1166
1167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1168pub enum MagicConstKind {
1169    /// `__CLASS__` — name of the current class, or empty string outside a class.
1170    Class,
1171    /// `__DIR__` — directory of the current file.
1172    Dir,
1173    /// `__FILE__` — absolute path of the current file.
1174    File,
1175    /// `__FUNCTION__` — name of the current function or closure.
1176    Function,
1177    /// `__LINE__` — current line number in the source file.
1178    Line,
1179    /// `__METHOD__` — name of the current method including its class: `ClassName::methodName`.
1180    Method,
1181    /// `__NAMESPACE__` — name of the current namespace, or empty string in the global namespace.
1182    Namespace,
1183    /// `__TRAIT__` — name of the current trait, or empty string outside a trait.
1184    Trait,
1185    /// `__PROPERTY__` — name of the current property inside a property hook (PHP 8.4+).
1186    Property,
1187}
1188
1189// --- Expression sub-types ---
1190
1191#[derive(Debug, Serialize)]
1192pub struct AssignExpr<'arena, 'src> {
1193    pub target: &'arena Expr<'arena, 'src>,
1194    pub op: AssignOp,
1195    pub value: &'arena Expr<'arena, 'src>,
1196    #[serde(skip_serializing_if = "is_false")]
1197    pub by_ref: bool,
1198}
1199
1200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1201pub enum AssignOp {
1202    /// `=`
1203    Assign,
1204    /// `+=`
1205    Plus,
1206    /// `-=`
1207    Minus,
1208    /// `*=`
1209    Mul,
1210    /// `/=`
1211    Div,
1212    /// `%=`
1213    Mod,
1214    /// `**=`
1215    Pow,
1216    /// `.=`
1217    Concat,
1218    /// `&=`
1219    BitwiseAnd,
1220    /// `|=`
1221    BitwiseOr,
1222    /// `^=`
1223    BitwiseXor,
1224    /// `<<=`
1225    ShiftLeft,
1226    /// `>>=`
1227    ShiftRight,
1228    /// `??=`
1229    Coalesce,
1230}
1231
1232#[derive(Debug, Serialize)]
1233pub struct BinaryExpr<'arena, 'src> {
1234    pub left: &'arena Expr<'arena, 'src>,
1235    pub op: BinaryOp,
1236    pub right: &'arena Expr<'arena, 'src>,
1237}
1238
1239#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1240pub enum BinaryOp {
1241    /// `+`
1242    Add,
1243    /// `-`
1244    Sub,
1245    /// `*`
1246    Mul,
1247    /// `/`
1248    Div,
1249    /// `%`
1250    Mod,
1251    /// `**`
1252    Pow,
1253    /// `.` — string concatenation.
1254    Concat,
1255    /// `==` — loose equality (type-coercing).
1256    Equal,
1257    /// `!=` or `<>` — loose inequality.
1258    NotEqual,
1259    /// `===` — strict equality (type and value).
1260    Identical,
1261    /// `!==` — strict inequality.
1262    NotIdentical,
1263    /// `<`
1264    Less,
1265    /// `>`
1266    Greater,
1267    /// `<=`
1268    LessOrEqual,
1269    /// `>=`
1270    GreaterOrEqual,
1271    /// `<=>` — spaceship / three-way comparison; returns -1, 0, or 1.
1272    Spaceship,
1273    /// `&&` — short-circuit boolean AND (higher precedence than `and`).
1274    BooleanAnd,
1275    /// `||` — short-circuit boolean OR (higher precedence than `or`).
1276    BooleanOr,
1277    /// `&` — bitwise AND.
1278    BitwiseAnd,
1279    /// `|` — bitwise OR.
1280    BitwiseOr,
1281    /// `^` — bitwise XOR.
1282    BitwiseXor,
1283    /// `<<` — left bit-shift.
1284    ShiftLeft,
1285    /// `>>` — right bit-shift.
1286    ShiftRight,
1287    /// `and` — boolean AND (lower precedence than `&&`).
1288    LogicalAnd,
1289    /// `or` — boolean OR (lower precedence than `||`).
1290    LogicalOr,
1291    /// `xor` — boolean XOR.
1292    LogicalXor,
1293    /// `instanceof` — type-check operator; `$x instanceof Foo`.
1294    Instanceof,
1295    /// `|>` — pipe operator (PHP 8.5+); passes the left operand as the first argument of the right callable.
1296    Pipe,
1297}
1298
1299#[derive(Debug, Serialize)]
1300pub struct UnaryPrefixExpr<'arena, 'src> {
1301    pub op: UnaryPrefixOp,
1302    pub operand: &'arena Expr<'arena, 'src>,
1303}
1304
1305#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1306pub enum UnaryPrefixOp {
1307    /// `-expr` — arithmetic negation.
1308    Negate,
1309    /// `+expr` — unary plus (no-op for numbers, promotes to numeric).
1310    Plus,
1311    /// `!expr` — boolean NOT.
1312    BooleanNot,
1313    /// `~expr` — bitwise NOT.
1314    BitwiseNot,
1315    /// `++$x` — pre-increment; increments then returns the new value.
1316    PreIncrement,
1317    /// `--$x` — pre-decrement; decrements then returns the new value.
1318    PreDecrement,
1319}
1320
1321#[derive(Debug, Serialize)]
1322pub struct UnaryPostfixExpr<'arena, 'src> {
1323    pub operand: &'arena Expr<'arena, 'src>,
1324    pub op: UnaryPostfixOp,
1325}
1326
1327#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1328pub enum UnaryPostfixOp {
1329    /// `$x++` — post-increment; returns the current value then increments.
1330    PostIncrement,
1331    /// `$x--` — post-decrement; returns the current value then decrements.
1332    PostDecrement,
1333}
1334
1335#[derive(Debug, Serialize)]
1336pub struct TernaryExpr<'arena, 'src> {
1337    pub condition: &'arena Expr<'arena, 'src>,
1338    /// None for short ternary `$x ?: $y`
1339    pub then_expr: Option<&'arena Expr<'arena, 'src>>,
1340    pub else_expr: &'arena Expr<'arena, 'src>,
1341}
1342
1343#[derive(Debug, Serialize)]
1344pub struct NullCoalesceExpr<'arena, 'src> {
1345    pub left: &'arena Expr<'arena, 'src>,
1346    pub right: &'arena Expr<'arena, 'src>,
1347}
1348
1349#[derive(Debug, Serialize)]
1350pub struct FunctionCallExpr<'arena, 'src> {
1351    pub name: &'arena Expr<'arena, 'src>,
1352    pub args: ArenaVec<'arena, Arg<'arena, 'src>>,
1353}
1354
1355#[derive(Debug, Serialize)]
1356pub struct ArrayElement<'arena, 'src> {
1357    pub key: Option<Expr<'arena, 'src>>,
1358    pub value: Expr<'arena, 'src>,
1359    pub unpack: bool,
1360    #[serde(skip_serializing_if = "is_false")]
1361    pub by_ref: bool,
1362    pub span: Span,
1363}
1364
1365#[derive(Debug, Serialize)]
1366pub struct ArrayAccessExpr<'arena, 'src> {
1367    pub array: &'arena Expr<'arena, 'src>,
1368    pub index: Option<&'arena Expr<'arena, 'src>>,
1369}
1370
1371// --- OOP Expression sub-types ---
1372
1373#[derive(Debug, Serialize)]
1374pub struct NewExpr<'arena, 'src> {
1375    pub class: &'arena Expr<'arena, 'src>,
1376    pub args: ArenaVec<'arena, Arg<'arena, 'src>>,
1377}
1378
1379#[derive(Debug, Serialize)]
1380pub struct PropertyAccessExpr<'arena, 'src> {
1381    pub object: &'arena Expr<'arena, 'src>,
1382    pub property: &'arena Expr<'arena, 'src>,
1383}
1384
1385#[derive(Debug, Serialize)]
1386pub struct MethodCallExpr<'arena, 'src> {
1387    pub object: &'arena Expr<'arena, 'src>,
1388    pub method: &'arena Expr<'arena, 'src>,
1389    pub args: ArenaVec<'arena, Arg<'arena, 'src>>,
1390}
1391
1392#[derive(Debug, Serialize)]
1393pub struct StaticAccessExpr<'arena, 'src> {
1394    pub class: &'arena Expr<'arena, 'src>,
1395    pub member: &'arena Expr<'arena, 'src>,
1396}
1397
1398#[derive(Debug, Serialize)]
1399pub struct StaticMethodCallExpr<'arena, 'src> {
1400    pub class: &'arena Expr<'arena, 'src>,
1401    pub method: &'arena Expr<'arena, 'src>,
1402    pub args: ArenaVec<'arena, Arg<'arena, 'src>>,
1403}
1404
1405#[derive(Debug, Serialize)]
1406pub struct StaticDynMethodCallExpr<'arena, 'src> {
1407    pub class: &'arena Expr<'arena, 'src>,
1408    pub method: &'arena Expr<'arena, 'src>,
1409    pub args: ArenaVec<'arena, Arg<'arena, 'src>>,
1410}
1411
1412#[derive(Debug, Serialize)]
1413pub struct ClosureExpr<'arena, 'src> {
1414    pub is_static: bool,
1415    pub by_ref: bool,
1416    pub params: ArenaVec<'arena, Param<'arena, 'src>>,
1417    pub use_vars: ArenaVec<'arena, ClosureUseVar<'src>>,
1418    pub return_type: Option<TypeHint<'arena, 'src>>,
1419    pub body: ArenaVec<'arena, Stmt<'arena, 'src>>,
1420    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
1421}
1422
1423#[derive(Debug, Clone, Serialize)]
1424pub struct ClosureUseVar<'src> {
1425    pub name: &'src str,
1426    pub by_ref: bool,
1427    pub span: Span,
1428}
1429
1430#[derive(Debug, Serialize)]
1431pub struct ArrowFunctionExpr<'arena, 'src> {
1432    pub is_static: bool,
1433    pub by_ref: bool,
1434    pub params: ArenaVec<'arena, Param<'arena, 'src>>,
1435    pub return_type: Option<TypeHint<'arena, 'src>>,
1436    pub body: &'arena Expr<'arena, 'src>,
1437    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
1438}
1439
1440#[derive(Debug, Serialize)]
1441pub struct MatchExpr<'arena, 'src> {
1442    pub subject: &'arena Expr<'arena, 'src>,
1443    pub arms: ArenaVec<'arena, MatchArm<'arena, 'src>>,
1444}
1445
1446#[derive(Debug, Serialize)]
1447pub struct MatchArm<'arena, 'src> {
1448    /// None for `default`
1449    pub conditions: Option<ArenaVec<'arena, Expr<'arena, 'src>>>,
1450    pub body: Expr<'arena, 'src>,
1451    pub span: Span,
1452}
1453
1454#[derive(Debug, Serialize)]
1455pub struct YieldExpr<'arena, 'src> {
1456    pub key: Option<&'arena Expr<'arena, 'src>>,
1457    pub value: Option<&'arena Expr<'arena, 'src>>,
1458    /// `true` for `yield from expr` (generator delegation), `false` for plain `yield`
1459    pub is_from: bool,
1460}
1461
1462// --- First-class callable ---
1463
1464#[derive(Debug, Serialize)]
1465pub struct CallableCreateExpr<'arena, 'src> {
1466    pub kind: CallableCreateKind<'arena, 'src>,
1467}
1468
1469#[derive(Debug, Serialize)]
1470pub enum CallableCreateKind<'arena, 'src> {
1471    /// `foo(...)`, `$var(...)`, `\Ns\func(...)`
1472    Function(&'arena Expr<'arena, 'src>),
1473    /// `$obj->method(...)`
1474    Method {
1475        object: &'arena Expr<'arena, 'src>,
1476        method: &'arena Expr<'arena, 'src>,
1477    },
1478    /// `$obj?->method(...)`
1479    NullsafeMethod {
1480        object: &'arena Expr<'arena, 'src>,
1481        method: &'arena Expr<'arena, 'src>,
1482    },
1483    /// `Foo::bar(...)`
1484    StaticMethod {
1485        class: &'arena Expr<'arena, 'src>,
1486        method: &'arena Expr<'arena, 'src>,
1487    },
1488}
1489
1490// --- String interpolation ---
1491
1492#[derive(Debug, Serialize)]
1493pub enum StringPart<'arena, 'src> {
1494    /// A plain text segment of an interpolated string or heredoc.
1495    Literal(&'arena str),
1496    /// An embedded expression: `$var`, `{$expr}`, or `${var}`.
1497    Expr(Expr<'arena, 'src>),
1498}