Skip to main content

ext_php_rs/types/
zval.rs

1//! The base value in PHP. A Zval can contain any PHP type, and the type that it
2//! contains is determined by a property inside the struct. The content of the
3//! Zval is stored in a union.
4
5use std::{convert::TryInto, ffi::c_void, fmt::Debug, ptr};
6
7use cfg_if::cfg_if;
8
9use crate::types::ZendIterator;
10use crate::types::iterable::Iterable;
11use crate::{
12    binary::Pack,
13    binary_slice::PackSlice,
14    boxed::ZBox,
15    convert::{FromZval, FromZvalMut, IntoZval, IntoZvalDyn},
16    error::{Error, Result},
17    ffi::{
18        _zval_struct__bindgen_ty_1, _zval_struct__bindgen_ty_2, GC_IMMUTABLE,
19        ext_php_rs_zend_string_release, zend_array_dup, zend_is_callable, zend_is_identical,
20        zend_is_iterable, zend_is_true, zend_resource, zend_value, zval, zval_ptr_dtor,
21    },
22    flags::DataType,
23    flags::ZvalTypeFlags,
24    rc::PhpRc,
25    types::{ZendCallable, ZendHashTable, ZendLong, ZendObject, ZendStr},
26};
27
28/// A zend value. This is the primary storage container used throughout the Zend
29/// engine.
30///
31/// A zval can be thought of as a Rust enum, a type that can contain different
32/// values such as integers, strings, objects etc.
33pub type Zval = zval;
34
35// TODO(david): can we make zval send+sync? main problem is that refcounted
36// types do not have atomic refcounters, so technically two threads could
37// reference the same object and attempt to modify refcounter at the same time.
38// need to look into how ZTS works.
39
40// unsafe impl Send for Zval {}
41// unsafe impl Sync for Zval {}
42
43impl Zval {
44    /// Creates a new, empty zval.
45    #[must_use]
46    pub const fn new() -> Self {
47        Self {
48            value: zend_value {
49                ptr: ptr::null_mut(),
50            },
51            #[allow(clippy::used_underscore_items)]
52            u1: _zval_struct__bindgen_ty_1 {
53                type_info: DataType::Null.as_u32(),
54            },
55            #[allow(clippy::used_underscore_items)]
56            u2: _zval_struct__bindgen_ty_2 { next: 0 },
57        }
58    }
59
60    /// Creates a null zval
61    #[must_use]
62    pub fn null() -> Zval {
63        let mut zval = Zval::new();
64        zval.set_null();
65        zval
66    }
67
68    /// Creates a zval containing an empty array.
69    #[must_use]
70    pub fn new_array() -> Zval {
71        let mut zval = Zval::new();
72        zval.set_hashtable(ZendHashTable::new());
73        zval
74    }
75
76    /// Dereference the zval, if it is a reference or indirect.
77    #[must_use]
78    #[inline]
79    pub fn dereference(&self) -> &Self {
80        if self.is_reference() {
81            // SAFETY: `is_reference()` guarantees `value.ref_` is valid.
82            unsafe { &(*self.value.ref_).val }
83        } else if self.is_indirect() {
84            // SAFETY: `is_indirect()` guarantees `value.zv` is a valid Zval pointer.
85            unsafe { &*(self.value.zv.cast::<Zval>()) }
86        } else {
87            self
88        }
89    }
90
91    /// Dereference the zval mutable, if it is a reference.
92    #[must_use]
93    #[inline]
94    pub fn dereference_mut(&mut self) -> &mut Self {
95        if self.is_reference() {
96            // SAFETY: `is_reference()` guarantees `value.ref_` is valid.
97            let reference = unsafe { self.value.ref_.as_mut().unwrap_unchecked() };
98            return &mut reference.val;
99        }
100        if self.is_indirect() {
101            // SAFETY: `is_indirect()` guarantees `value.zv` is a valid Zval pointer.
102            return unsafe { &mut *self.value.zv.cast::<Zval>() };
103        }
104        self
105    }
106
107    /// Returns the value of the zval if it is a long.
108    ///
109    /// References are dereferenced transparently.
110    #[must_use]
111    pub fn long(&self) -> Option<ZendLong> {
112        if self.get_type() == DataType::Long {
113            return Some(unsafe { self.value.lval });
114        }
115        let zval = self.dereference();
116        if zval.get_type() == DataType::Long {
117            Some(unsafe { zval.value.lval })
118        } else {
119            None
120        }
121    }
122
123    /// Returns the value of the zval if it is a bool.
124    ///
125    /// References are dereferenced transparently.
126    #[must_use]
127    pub fn bool(&self) -> Option<bool> {
128        match self.get_type() {
129            DataType::True => return Some(true),
130            DataType::False => return Some(false),
131            _ => {}
132        }
133        let zval = self.dereference();
134        match zval.get_type() {
135            DataType::True => Some(true),
136            DataType::False => Some(false),
137            _ => None,
138        }
139    }
140
141    /// Returns the value of the zval if it is a double.
142    ///
143    /// References are dereferenced transparently.
144    #[must_use]
145    pub fn double(&self) -> Option<f64> {
146        if self.get_type() == DataType::Double {
147            return Some(unsafe { self.value.dval });
148        }
149        let zval = self.dereference();
150        if zval.get_type() == DataType::Double {
151            Some(unsafe { zval.value.dval })
152        } else {
153            None
154        }
155    }
156
157    /// Returns the value of the zval as a zend string, if it is a string.
158    ///
159    /// References are dereferenced transparently.
160    ///
161    /// Note that this functions output will not be the same as
162    /// [`string()`](#method.string), as this function does not attempt to
163    /// convert other types into a [`String`].
164    #[must_use]
165    pub fn zend_str(&self) -> Option<&ZendStr> {
166        if self.get_type() == DataType::String {
167            return unsafe { self.value.str_.as_ref() };
168        }
169        let zval = self.dereference();
170        if zval.get_type() == DataType::String {
171            unsafe { zval.value.str_.as_ref() }
172        } else {
173            None
174        }
175    }
176
177    /// Returns the value of the zval if it is a string.
178    ///
179    /// [`str()`]: #method.str
180    pub fn string(&self) -> Option<String> {
181        self.str().map(ToString::to_string)
182    }
183
184    /// Returns the value of the zval if it is a string.
185    ///
186    /// Note that this functions output will not be the same as
187    /// [`string()`](#method.string), as this function does not attempt to
188    /// convert other types into a [`String`], as it could not pass back a
189    /// [`&str`] in those cases.
190    #[inline]
191    #[must_use]
192    pub fn str(&self) -> Option<&str> {
193        self.zend_str().and_then(|zs| zs.as_str().ok())
194    }
195
196    /// Returns the value of the zval if it is a string and can be unpacked into
197    /// a vector of a given type. Similar to the [`unpack`] function in PHP,
198    /// except you can only unpack one type.
199    ///
200    /// # Safety
201    ///
202    /// There is no way to tell if the data stored in the string is actually of
203    /// the given type. The results of this function can also differ from
204    /// platform-to-platform due to the different representation of some
205    /// types on different platforms. Consult the [`pack`] function
206    /// documentation for more details.
207    ///
208    /// [`pack`]: https://www.php.net/manual/en/function.pack.php
209    /// [`unpack`]: https://www.php.net/manual/en/function.unpack.php
210    pub fn binary<T: Pack>(&self) -> Option<Vec<T>> {
211        self.zend_str().map(T::unpack_into)
212    }
213
214    /// Returns the value of the zval if it is a string and can be unpacked into
215    /// a slice of a given type. Similar to the [`unpack`] function in PHP,
216    /// except you can only unpack one type.
217    ///
218    /// This function is similar to [`Zval::binary`] except that a slice is
219    /// returned instead of a vector, meaning the contents of the string is
220    /// not copied.
221    ///
222    /// # Safety
223    ///
224    /// There is no way to tell if the data stored in the string is actually of
225    /// the given type. The results of this function can also differ from
226    /// platform-to-platform due to the different representation of some
227    /// types on different platforms. Consult the [`pack`] function
228    /// documentation for more details.
229    ///
230    /// [`pack`]: https://www.php.net/manual/en/function.pack.php
231    /// [`unpack`]: https://www.php.net/manual/en/function.unpack.php
232    pub fn binary_slice<T: PackSlice>(&self) -> Option<&[T]> {
233        self.zend_str().map(T::unpack_into)
234    }
235
236    /// Returns the value of the zval if it is a resource.
237    ///
238    /// References are dereferenced transparently.
239    #[must_use]
240    pub fn resource(&self) -> Option<*mut zend_resource> {
241        if self.get_type() == DataType::Resource {
242            return Some(unsafe { self.value.res });
243        }
244        let zval = self.dereference();
245        if zval.get_type() == DataType::Resource {
246            Some(unsafe { zval.value.res })
247        } else {
248            None
249        }
250    }
251
252    /// Returns an immutable reference to the underlying zval hashtable if the
253    /// zval contains an array.
254    ///
255    /// References are dereferenced transparently.
256    #[must_use]
257    pub fn array(&self) -> Option<&ZendHashTable> {
258        if self.get_type() == DataType::Array {
259            return unsafe { self.value.arr.as_ref() };
260        }
261        let zval = self.dereference();
262        if zval.get_type() == DataType::Array {
263            unsafe { zval.value.arr.as_ref() }
264        } else {
265            None
266        }
267    }
268
269    /// Returns a mutable reference to the underlying zval hashtable if the zval
270    /// contains an array.
271    ///
272    /// # Array Separation
273    ///
274    /// PHP arrays use copy-on-write (COW) semantics. Before returning a mutable
275    /// reference, this method checks if the array is shared (refcount > 1) and
276    /// if so, creates a private copy. This is equivalent to PHP's
277    /// `SEPARATE_ARRAY()` macro and prevents the "Assertion failed:
278    /// `zend_gc_refcount` == 1" error that occurs when modifying shared arrays.
279    pub fn array_mut(&mut self) -> Option<&mut ZendHashTable> {
280        let zval = if self.get_type() == DataType::Array {
281            self
282        } else {
283            self.dereference_mut()
284        };
285        if zval.get_type() == DataType::Array {
286            unsafe {
287                let arr = zval.value.arr;
288                let ht = &*arr;
289                if ht.is_immutable() {
290                    zval.value.arr = zend_array_dup(arr);
291                } else if (*arr).gc.refcount > 1 {
292                    (*arr).gc.refcount -= 1;
293                    zval.value.arr = zend_array_dup(arr);
294                }
295                zval.value.arr.as_mut()
296            }
297        } else {
298            None
299        }
300    }
301
302    /// Returns the value of the zval if it is an object.
303    ///
304    /// References are dereferenced transparently.
305    #[must_use]
306    pub fn object(&self) -> Option<&ZendObject> {
307        if matches!(self.get_type(), DataType::Object(_)) {
308            return unsafe { self.value.obj.as_ref() };
309        }
310        let zval = self.dereference();
311        if matches!(zval.get_type(), DataType::Object(_)) {
312            unsafe { zval.value.obj.as_ref() }
313        } else {
314            None
315        }
316    }
317
318    /// Returns a mutable reference to the object contained in the [`Zval`], if
319    /// any.
320    ///
321    /// References are dereferenced transparently.
322    pub fn object_mut(&mut self) -> Option<&mut ZendObject> {
323        if matches!(self.get_type(), DataType::Object(_)) {
324            return unsafe { self.value.obj.as_mut() };
325        }
326        let zval = self.dereference_mut();
327        if matches!(zval.get_type(), DataType::Object(_)) {
328            unsafe { zval.value.obj.as_mut() }
329        } else {
330            None
331        }
332    }
333
334    /// Attempts to call a method on the object contained in the zval.
335    ///
336    /// # Errors
337    ///
338    /// * Returns an error if the [`Zval`] is not an object.
339    // TODO: Measure this
340    #[allow(clippy::inline_always)]
341    #[inline(always)]
342    pub fn try_call_method(&self, name: &str, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
343        self.object()
344            .ok_or(Error::Object)?
345            .try_call_method(name, params)
346    }
347
348    /// Returns the value of the zval if it is an internal indirect reference.
349    #[must_use]
350    pub fn indirect(&self) -> Option<&Zval> {
351        if self.is_indirect() {
352            Some(unsafe { &*(self.value.zv.cast::<Zval>()) })
353        } else {
354            None
355        }
356    }
357
358    /// Returns a mutable reference to the zval if it is an internal indirect
359    /// reference.
360    // TODO: Verify if this is safe to use, as it allows mutating the
361    // hashtable while only having a reference to it. #461
362    #[allow(clippy::mut_from_ref)]
363    #[must_use]
364    pub fn indirect_mut(&self) -> Option<&mut Zval> {
365        if self.is_indirect() {
366            Some(unsafe { &mut *(self.value.zv.cast::<Zval>()) })
367        } else {
368            None
369        }
370    }
371
372    /// Returns the value of the zval if it is a reference.
373    #[inline]
374    #[must_use]
375    pub fn reference(&self) -> Option<&Zval> {
376        if self.is_reference() {
377            Some(&unsafe { self.value.ref_.as_ref() }?.val)
378        } else {
379            None
380        }
381    }
382
383    /// Returns a mutable reference to the underlying zval if it is a reference.
384    pub fn reference_mut(&mut self) -> Option<&mut Zval> {
385        if self.is_reference() {
386            Some(&mut unsafe { self.value.ref_.as_mut() }?.val)
387        } else {
388            None
389        }
390    }
391
392    /// Returns the value of the zval if it is callable.
393    #[must_use]
394    pub fn callable(&self) -> Option<ZendCallable<'_>> {
395        // The Zval is checked if it is callable in the `new` function.
396        ZendCallable::new(self).ok()
397    }
398
399    /// Returns an iterator over the zval if it is traversable.
400    #[must_use]
401    pub fn traversable(&self) -> Option<&mut ZendIterator> {
402        if self.is_traversable() {
403            self.object()?.get_class_entry().get_iterator(self, false)
404        } else {
405            None
406        }
407    }
408
409    /// Returns an iterable over the zval if it is an array or traversable. (is
410    /// iterable)
411    #[must_use]
412    pub fn iterable(&self) -> Option<Iterable<'_>> {
413        if self.is_iterable() {
414            Iterable::from_zval(self)
415        } else {
416            None
417        }
418    }
419
420    /// Returns the value of the zval if it is a pointer.
421    ///
422    /// # Safety
423    ///
424    /// The caller must ensure that the pointer contained in the zval is in fact
425    /// a pointer to an instance of `T`, as the zval has no way of defining
426    /// the type of pointer.
427    #[must_use]
428    pub unsafe fn ptr<T>(&self) -> Option<*mut T> {
429        if self.is_ptr() {
430            Some(unsafe { self.value.ptr.cast::<T>() })
431        } else {
432            None
433        }
434    }
435
436    /// Attempts to call the zval as a callable with a list of arguments to pass
437    /// to the function. Note that a thrown exception inside the callable is
438    /// not detectable, therefore you should check if the return value is
439    /// valid rather than unwrapping. Returns a result containing the return
440    /// value of the function, or an error.
441    ///
442    /// You should not call this function directly, rather through the
443    /// [`call_user_func`] macro.
444    ///
445    /// # Parameters
446    ///
447    /// * `params` - A list of parameters to call the function with.
448    ///
449    /// # Errors
450    ///
451    /// * Returns an error if the [`Zval`] is not callable.
452    // TODO: Measure this
453    #[allow(clippy::inline_always)]
454    #[inline(always)]
455    pub fn try_call(&self, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
456        self.callable().ok_or(Error::Callable)?.try_call(params)
457    }
458
459    /// Returns the type of the Zval.
460    #[must_use]
461    pub fn get_type(&self) -> DataType {
462        DataType::from(u32::from(unsafe { self.u1.v.type_ }))
463    }
464
465    /// Returns true if the zval is a long, false otherwise.
466    #[must_use]
467    pub fn is_long(&self) -> bool {
468        self.get_type() == DataType::Long
469    }
470
471    /// Returns true if the zval is null, false otherwise.
472    #[must_use]
473    pub fn is_null(&self) -> bool {
474        self.get_type() == DataType::Null
475    }
476
477    /// Returns true if the zval is true, false otherwise.
478    #[must_use]
479    pub fn is_true(&self) -> bool {
480        self.get_type() == DataType::True
481    }
482
483    /// Returns true if the zval is false, false otherwise.
484    #[must_use]
485    pub fn is_false(&self) -> bool {
486        self.get_type() == DataType::False
487    }
488
489    /// Returns true if the zval is a bool, false otherwise.
490    #[must_use]
491    pub fn is_bool(&self) -> bool {
492        self.is_true() || self.is_false()
493    }
494
495    /// Returns true if the zval is a double, false otherwise.
496    #[must_use]
497    pub fn is_double(&self) -> bool {
498        self.get_type() == DataType::Double
499    }
500
501    /// Returns true if the zval is a string, false otherwise.
502    #[must_use]
503    pub fn is_string(&self) -> bool {
504        self.get_type() == DataType::String
505    }
506
507    /// Returns true if the zval is a resource, false otherwise.
508    #[must_use]
509    pub fn is_resource(&self) -> bool {
510        self.get_type() == DataType::Resource
511    }
512
513    /// Returns true if the zval is an array, false otherwise.
514    #[must_use]
515    pub fn is_array(&self) -> bool {
516        self.get_type() == DataType::Array
517    }
518
519    /// Returns true if the zval is an object, false otherwise.
520    #[must_use]
521    pub fn is_object(&self) -> bool {
522        matches!(self.get_type(), DataType::Object(_))
523    }
524
525    /// Returns true if the zval is a reference, false otherwise.
526    #[inline]
527    #[must_use]
528    pub fn is_reference(&self) -> bool {
529        self.get_type() == DataType::Reference
530    }
531
532    /// Returns true if the zval is a reference, false otherwise.
533    #[must_use]
534    pub fn is_indirect(&self) -> bool {
535        self.get_type() == DataType::Indirect
536    }
537
538    /// Returns true if the zval is callable, false otherwise.
539    #[must_use]
540    pub fn is_callable(&self) -> bool {
541        let ptr: *const Self = self;
542        unsafe { zend_is_callable(ptr.cast_mut(), 0, std::ptr::null_mut()) }
543    }
544
545    /// Checks if the zval is identical to another one.
546    /// This works like `===` in php.
547    ///
548    /// # Parameters
549    ///
550    /// * `other` - The the zval to check identity against.
551    #[must_use]
552    pub fn is_identical(&self, other: &Self) -> bool {
553        let self_p: *const Self = self;
554        let other_p: *const Self = other;
555        unsafe { zend_is_identical(self_p.cast_mut(), other_p.cast_mut()) }
556    }
557
558    /// Returns true if the zval is traversable, false otherwise.
559    #[must_use]
560    pub fn is_traversable(&self) -> bool {
561        match self.object() {
562            None => false,
563            Some(obj) => obj.is_traversable(),
564        }
565    }
566
567    /// Returns true if the zval is iterable (array or traversable), false
568    /// otherwise.
569    #[must_use]
570    pub fn is_iterable(&self) -> bool {
571        let ptr: *const Self = self;
572        unsafe { zend_is_iterable(ptr.cast_mut()) }
573    }
574
575    /// Returns true if the zval contains a pointer, false otherwise.
576    #[must_use]
577    pub fn is_ptr(&self) -> bool {
578        self.get_type() == DataType::Ptr
579    }
580
581    /// Returns true if the zval is a scalar value (integer, float, string, or
582    /// bool), false otherwise.
583    ///
584    /// This is equivalent to PHP's `is_scalar()` function.
585    #[must_use]
586    pub fn is_scalar(&self) -> bool {
587        matches!(
588            self.get_type(),
589            DataType::Long | DataType::Double | DataType::String | DataType::True | DataType::False
590        )
591    }
592
593    // =========================================================================
594    // Type Coercion Methods
595    // =========================================================================
596    //
597    // These methods convert the zval's value to a different type following
598    // PHP's type coercion rules. Unlike the mutating `coerce_into_*` methods
599    // in some implementations, these are pure functions that return a new value.
600
601    /// Coerces the value to a boolean following PHP's type coercion rules.
602    ///
603    /// This uses PHP's internal `zend_is_true` function to determine the
604    /// boolean value, which handles all PHP types correctly:
605    /// - `null` → `false`
606    /// - `false` → `false`, `true` → `true`
607    /// - `0`, `0.0`, `""`, `"0"` → `false`
608    /// - Empty arrays → `false`
609    /// - Everything else → `true`
610    ///
611    /// # Example
612    ///
613    /// ```no_run
614    /// use ext_php_rs::types::Zval;
615    ///
616    /// let mut zv = Zval::new();
617    /// zv.set_long(0);
618    /// assert_eq!(zv.coerce_to_bool(), false);
619    ///
620    /// zv.set_long(42);
621    /// assert_eq!(zv.coerce_to_bool(), true);
622    /// ```
623    #[must_use]
624    pub fn coerce_to_bool(&self) -> bool {
625        cfg_if! {
626            if #[cfg(php84)] {
627                let ptr: *const Self = self;
628                unsafe { zend_is_true(ptr) }
629            } else {
630                // Pre-PHP 8.4: zend_is_true takes *mut and returns c_int
631                let ptr = self as *const Self as *mut Self;
632                unsafe { zend_is_true(ptr) != 0 }
633            }
634        }
635    }
636
637    /// Coerces the value to a string following PHP's type coercion rules.
638    ///
639    /// This is a Rust implementation that matches PHP's behavior. Returns `None`
640    /// for types that cannot be meaningfully converted to strings (arrays,
641    /// resources, objects without `__toString`).
642    ///
643    /// Conversion rules:
644    /// - Strings → returned as-is
645    /// - Integers → decimal string representation
646    /// - Floats → string representation (uses uppercase `E` for scientific
647    ///   notation when exponent >= 14 or <= -5, matching PHP)
648    /// - `true` → `"1"`, `false` → `""`
649    /// - `null` → `""`
650    /// - `INF` → `"INF"`, `-INF` → `"-INF"`, `NAN` → `"NAN"` (uppercase)
651    /// - Objects with `__toString()` → result of calling `__toString()`
652    /// - Arrays, resources, objects without `__toString()` → `None`
653    ///
654    /// # Example
655    ///
656    /// ```no_run
657    /// use ext_php_rs::types::Zval;
658    ///
659    /// let mut zv = Zval::new();
660    /// zv.set_long(42);
661    /// assert_eq!(zv.coerce_to_string(), Some("42".to_string()));
662    ///
663    /// zv.set_bool(true);
664    /// assert_eq!(zv.coerce_to_string(), Some("1".to_string()));
665    /// ```
666    #[must_use]
667    pub fn coerce_to_string(&self) -> Option<String> {
668        // Already a string
669        if let Some(s) = self.str() {
670            return Some(s.to_string());
671        }
672
673        // Boolean
674        if let Some(b) = self.bool() {
675            return Some(if b { "1".to_string() } else { String::new() });
676        }
677
678        // Null
679        if self.is_null() {
680            return Some(String::new());
681        }
682
683        // Integer
684        if let Some(l) = self.long() {
685            return Some(l.to_string());
686        }
687
688        // Float
689        if let Some(d) = self.double() {
690            // PHP uses uppercase for special float values
691            if d.is_nan() {
692                return Some("NAN".to_string());
693            }
694            if d.is_infinite() {
695                return Some(if d.is_sign_positive() {
696                    "INF".to_string()
697                } else {
698                    "-INF".to_string()
699                });
700            }
701            // PHP uses a format similar to printf's %G with ~14 digits precision
702            // and uppercase E for scientific notation
703            return Some(php_float_to_string(d));
704        }
705
706        // Object with __toString
707        if let Some(obj) = self.object()
708            && let Ok(result) = obj.try_call_method("__toString", vec![])
709        {
710            return result.str().map(ToString::to_string);
711        }
712
713        // Arrays, resources, and objects without __toString cannot be converted
714        None
715    }
716
717    /// Coerces the value to an integer following PHP's type coercion rules.
718    ///
719    /// This is a Rust implementation that matches PHP's behavior. Returns `None`
720    /// for types that cannot be meaningfully converted to integers (arrays,
721    /// resources, objects).
722    ///
723    /// Conversion rules:
724    /// - Integers → returned as-is
725    /// - Floats → truncated toward zero
726    /// - `true` → `1`, `false` → `0`
727    /// - `null` → `0`
728    /// - Strings → parsed as integer (leading numeric portion only; stops at
729    ///   first non-digit; returns 0 if non-numeric; saturates on overflow)
730    /// - Arrays, resources, objects → `None`
731    ///
732    /// # Example
733    ///
734    /// ```no_run
735    /// use ext_php_rs::types::Zval;
736    ///
737    /// let mut zv = Zval::new();
738    /// zv.set_string("42abc", false);
739    /// assert_eq!(zv.coerce_to_long(), Some(42));
740    ///
741    /// zv.set_double(3.7);
742    /// assert_eq!(zv.coerce_to_long(), Some(3));
743    /// ```
744    #[must_use]
745    pub fn coerce_to_long(&self) -> Option<ZendLong> {
746        // Already an integer
747        if let Some(l) = self.long() {
748            return Some(l);
749        }
750
751        // Boolean
752        if let Some(b) = self.bool() {
753            return Some(ZendLong::from(b));
754        }
755
756        // Null
757        if self.is_null() {
758            return Some(0);
759        }
760
761        // Float - truncate toward zero
762        if let Some(d) = self.double() {
763            #[allow(clippy::cast_possible_truncation)]
764            return Some(d as ZendLong);
765        }
766
767        // String - parse leading numeric portion
768        if let Some(s) = self.str() {
769            return Some(parse_long_from_str(s));
770        }
771
772        // Arrays, resources, objects cannot be converted
773        None
774    }
775
776    /// Coerces the value to a float following PHP's type coercion rules.
777    ///
778    /// This is a Rust implementation that matches PHP's behavior. Returns `None`
779    /// for types that cannot be meaningfully converted to floats (arrays,
780    /// resources, objects).
781    ///
782    /// Conversion rules:
783    /// - Floats → returned as-is
784    /// - Integers → converted to float
785    /// - `true` → `1.0`, `false` → `0.0`
786    /// - `null` → `0.0`
787    /// - Strings → parsed as float (leading numeric portion including decimal
788    ///   point and scientific notation; returns 0.0 if non-numeric)
789    /// - Arrays, resources, objects → `None`
790    ///
791    /// # Example
792    ///
793    /// ```no_run
794    /// use ext_php_rs::types::Zval;
795    ///
796    /// let mut zv = Zval::new();
797    /// zv.set_string("3.14abc", false);
798    /// assert_eq!(zv.coerce_to_double(), Some(3.14));
799    ///
800    /// zv.set_long(42);
801    /// assert_eq!(zv.coerce_to_double(), Some(42.0));
802    /// ```
803    #[must_use]
804    pub fn coerce_to_double(&self) -> Option<f64> {
805        // Already a float
806        if let Some(d) = self.double() {
807            return Some(d);
808        }
809
810        // Integer
811        if let Some(l) = self.long() {
812            #[allow(clippy::cast_precision_loss)]
813            return Some(l as f64);
814        }
815
816        // Boolean
817        if let Some(b) = self.bool() {
818            return Some(if b { 1.0 } else { 0.0 });
819        }
820
821        // Null
822        if self.is_null() {
823            return Some(0.0);
824        }
825
826        // String - parse leading numeric portion
827        if let Some(s) = self.str() {
828            return Some(parse_double_from_str(s));
829        }
830
831        // Arrays, resources, objects cannot be converted
832        None
833    }
834
835    /// Sets the value of the zval as a string. Returns nothing in a result when
836    /// successful.
837    ///
838    /// # Parameters
839    ///
840    /// * `val` - The value to set the zval as.
841    /// * `persistent` - Whether the string should persist between requests.
842    ///
843    /// # Persistent Strings
844    ///
845    /// When `persistent` is `true`, the string is allocated from PHP's
846    /// persistent heap (using `malloc`) rather than the request-bound heap.
847    /// This is typically used for strings that need to survive across multiple
848    /// PHP requests, such as class names, function names, or module-level data.
849    ///
850    /// **Important:** The string will still be freed when the Zval is dropped.
851    /// The `persistent` flag only affects which memory allocator is used. If
852    /// you need a string to outlive the Zval, consider using
853    /// [`std::mem::forget`] on the Zval or storing the string elsewhere.
854    ///
855    /// For most use cases (return values, function arguments, temporary
856    /// storage), you should use `persistent: false`.
857    ///
858    /// # Errors
859    ///
860    /// Never returns an error.
861    // TODO: Check if we can drop the result here.
862    pub fn set_string(&mut self, val: &str, persistent: bool) -> Result<()> {
863        self.set_zend_string(ZendStr::new(val, persistent));
864        Ok(())
865    }
866
867    /// Sets the value of the zval as a Zend string.
868    ///
869    /// The Zval takes ownership of the string. When the Zval is dropped,
870    /// the string will be released.
871    ///
872    /// # Parameters
873    ///
874    /// * `val` - String content.
875    pub fn set_zend_string(&mut self, val: ZBox<ZendStr>) {
876        let is_interned = unsafe { val.gc.u.type_info } & GC_IMMUTABLE != 0;
877        let flags = if is_interned {
878            ZvalTypeFlags::InternedStringEx
879        } else {
880            ZvalTypeFlags::StringEx
881        };
882        self.change_type(flags);
883        self.value.str_ = val.into_raw();
884    }
885
886    /// Sets the value of the zval as a binary string, which is represented in
887    /// Rust as a vector.
888    ///
889    /// # Parameters
890    ///
891    /// * `val` - The value to set the zval as.
892    pub fn set_binary<T: Pack>(&mut self, val: Vec<T>) {
893        self.change_type(ZvalTypeFlags::StringEx);
894        let ptr = T::pack_into(val);
895        self.value.str_ = ptr;
896    }
897
898    /// Sets the value of the zval as an interned string. Returns nothing in a
899    /// result when successful.
900    ///
901    /// Interned strings are stored once and are immutable. PHP stores them in
902    /// an internal hashtable. Unlike regular strings, interned strings are not
903    /// reference counted and should not be freed by `zval_ptr_dtor`.
904    ///
905    /// # Parameters
906    ///
907    /// * `val` - The value to set the zval as.
908    /// * `persistent` - Whether the string should persist between requests.
909    ///
910    /// # Errors
911    ///
912    /// Never returns an error.
913    // TODO: Check if we can drop the result here.
914    pub fn set_interned_string(&mut self, val: &str, persistent: bool) -> Result<()> {
915        // Use InternedStringEx (without RefCounted) because interned strings
916        // should not have their refcount modified by zval_ptr_dtor.
917        self.change_type(ZvalTypeFlags::InternedStringEx);
918        self.value.str_ = ZendStr::new_interned(val, persistent).into_raw();
919        Ok(())
920    }
921
922    /// Sets the value of the zval as a long.
923    ///
924    /// # Parameters
925    ///
926    /// * `val` - The value to set the zval as.
927    pub fn set_long<T: Into<ZendLong>>(&mut self, val: T) {
928        self.internal_set_long(val.into());
929    }
930
931    fn internal_set_long(&mut self, val: ZendLong) {
932        self.change_type(ZvalTypeFlags::Long);
933        self.value.lval = val;
934    }
935
936    /// Sets the value of the zval as a double.
937    ///
938    /// # Parameters
939    ///
940    /// * `val` - The value to set the zval as.
941    pub fn set_double<T: Into<f64>>(&mut self, val: T) {
942        self.internal_set_double(val.into());
943    }
944
945    fn internal_set_double(&mut self, val: f64) {
946        self.change_type(ZvalTypeFlags::Double);
947        self.value.dval = val;
948    }
949
950    /// Sets the value of the zval as a boolean.
951    ///
952    /// # Parameters
953    ///
954    /// * `val` - The value to set the zval as.
955    pub fn set_bool<T: Into<bool>>(&mut self, val: T) {
956        self.internal_set_bool(val.into());
957    }
958
959    fn internal_set_bool(&mut self, val: bool) {
960        self.change_type(if val {
961            ZvalTypeFlags::True
962        } else {
963            ZvalTypeFlags::False
964        });
965    }
966
967    /// Sets the value of the zval as null.
968    ///
969    /// This is the default of a zval.
970    pub fn set_null(&mut self) {
971        self.change_type(ZvalTypeFlags::Null);
972    }
973
974    /// Sets the value of the zval as a resource.
975    ///
976    /// # Parameters
977    ///
978    /// * `val` - The value to set the zval as.
979    pub fn set_resource(&mut self, val: *mut zend_resource) {
980        self.change_type(ZvalTypeFlags::ResourceEx);
981        self.value.res = val;
982    }
983
984    /// Sets the value of the zval as a reference to an object.
985    ///
986    /// # Parameters
987    ///
988    /// * `val` - The value to set the zval as.
989    pub fn set_object(&mut self, val: &mut ZendObject) {
990        self.change_type(ZvalTypeFlags::ObjectEx);
991        val.inc_count(); // TODO(david): not sure if this is needed :/
992        self.value.obj = ptr::from_ref(val).cast_mut();
993    }
994
995    /// Sets the value of the zval as an array. Returns nothing in a result on
996    /// success.
997    ///
998    /// # Parameters
999    ///
1000    /// * `val` - The value to set the zval as.
1001    ///
1002    /// # Errors
1003    ///
1004    /// * Returns an error if the conversion to a hashtable fails.
1005    pub fn set_array<T: TryInto<ZBox<ZendHashTable>, Error = Error>>(
1006        &mut self,
1007        val: T,
1008    ) -> Result<()> {
1009        self.set_hashtable(val.try_into()?);
1010        Ok(())
1011    }
1012
1013    /// Sets the value of the zval as an array. Returns nothing in a result on
1014    /// success.
1015    ///
1016    /// # Parameters
1017    ///
1018    /// * `val` - The value to set the zval as.
1019    pub fn set_hashtable(&mut self, val: ZBox<ZendHashTable>) {
1020        // Handle immutable shared arrays (e.g., the empty array) similar to
1021        // ZVAL_EMPTY_ARRAY. Immutable arrays should not be reference counted.
1022        let type_info = if val.is_immutable() {
1023            ZvalTypeFlags::Array
1024        } else {
1025            ZvalTypeFlags::ArrayEx
1026        };
1027        self.change_type(type_info);
1028        self.value.arr = val.into_raw();
1029    }
1030
1031    /// Sets the value of the zval as a pointer.
1032    ///
1033    /// # Parameters
1034    ///
1035    /// * `ptr` - The pointer to set the zval as.
1036    pub fn set_ptr<T>(&mut self, ptr: *mut T) {
1037        self.u1.type_info = ZvalTypeFlags::Ptr.bits();
1038        self.value.ptr = ptr.cast::<c_void>();
1039    }
1040
1041    /// Used to drop the Zval but keep the value of the zval intact.
1042    ///
1043    /// This is important when copying the value of the zval, as the actual
1044    /// value will not be copied, but the pointer to the value (string for
1045    /// example) will be copied.
1046    pub(crate) fn release(mut self) {
1047        // NOTE(david): don't use `change_type` here as we are wanting to keep the
1048        // contents intact.
1049        self.u1.type_info = ZvalTypeFlags::Null.bits();
1050    }
1051
1052    /// Changes the type of the zval, freeing the current contents when
1053    /// applicable.
1054    ///
1055    /// # Parameters
1056    ///
1057    /// * `ty` - The new type of the zval.
1058    fn change_type(&mut self, ty: ZvalTypeFlags) {
1059        // SAFETY: we have exclusive mutable access to this zval so can free the
1060        // contents.
1061        //
1062        // For strings, we use zend_string_release directly instead of zval_ptr_dtor
1063        // to correctly handle persistent strings. zend_string_release properly checks
1064        // the IS_STR_PERSISTENT flag and uses the correct deallocator (free vs efree).
1065        // This fixes heap corruption issues when dropping Zvals containing persistent
1066        // strings (see issue #424).
1067        //
1068        // For simple types (Long, Double, Bool, Null, Undef, True, False), we don't
1069        // need to call zval_ptr_dtor because they don't have heap-allocated data.
1070        // This is important for cargo-php stub generation where zval_ptr_dtor is
1071        // stubbed as a null pointer - calling it would crash.
1072        if self.is_string() {
1073            unsafe {
1074                if let Some(str_ptr) = self.value.str_.as_mut() {
1075                    ext_php_rs_zend_string_release(str_ptr);
1076                }
1077            }
1078        } else if self.is_array()
1079            || self.is_object()
1080            || self.is_resource()
1081            || self.is_reference()
1082            || self.get_type() == DataType::ConstantExpression
1083        {
1084            // Only call zval_ptr_dtor for reference-counted types
1085            unsafe { zval_ptr_dtor(self) };
1086        }
1087        // Simple types (Long, Double, Bool, Null, etc.) need no cleanup
1088        self.u1.type_info = ty.bits();
1089    }
1090
1091    /// Extracts some type from a `Zval`.
1092    ///
1093    /// This is a wrapper function around `TryFrom`.
1094    #[must_use]
1095    pub fn extract<'a, T>(&'a self) -> Option<T>
1096    where
1097        T: FromZval<'a>,
1098    {
1099        FromZval::from_zval(self)
1100    }
1101
1102    /// Creates a shallow clone of the [`Zval`].
1103    ///
1104    /// This copies the contents of the [`Zval`], and increments the reference
1105    /// counter of the underlying value (if it is reference counted).
1106    ///
1107    /// For example, if the zval contains a long, it will simply copy the value.
1108    /// However, if the zval contains an object, the new zval will point to the
1109    /// same object, and the objects reference counter will be incremented.
1110    ///
1111    /// # Returns
1112    ///
1113    /// The cloned zval.
1114    #[must_use]
1115    pub fn shallow_clone(&self) -> Zval {
1116        let mut new = Zval::new();
1117        new.u1 = self.u1;
1118        new.value = self.value;
1119
1120        // SAFETY: `u1` union is only used for easier bitmasking. It is valid to read
1121        // from either of the variants.
1122        //
1123        // SAFETY: If the value if refcounted (`self.u1.type_info & Z_TYPE_FLAGS_MASK`)
1124        // then it is valid to dereference `self.value.counted`.
1125        unsafe {
1126            let flags = ZvalTypeFlags::from_bits_retain(self.u1.type_info);
1127            if flags.contains(ZvalTypeFlags::RefCounted) {
1128                (*self.value.counted).gc.refcount += 1;
1129            }
1130        }
1131
1132        new
1133    }
1134}
1135
1136impl Debug for Zval {
1137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1138        let mut dbg = f.debug_struct("Zval");
1139        let ty = self.get_type();
1140        dbg.field("type", &ty);
1141
1142        macro_rules! field {
1143            ($value: expr) => {
1144                dbg.field("val", &$value)
1145            };
1146        }
1147
1148        match ty {
1149            DataType::Undef | DataType::Null | DataType::ConstantExpression | DataType::Void => {
1150                field!(Option::<()>::None)
1151            }
1152            DataType::False => field!(false),
1153            DataType::True => field!(true),
1154            DataType::Long => field!(self.long()),
1155            DataType::Double => field!(self.double()),
1156            DataType::String | DataType::Mixed | DataType::Callable => field!(self.string()),
1157            DataType::Array => field!(self.array()),
1158            DataType::Object(_) => field!(self.object()),
1159            DataType::Resource => field!(self.resource()),
1160            DataType::Reference => field!(self.reference()),
1161            DataType::Bool => field!(self.bool()),
1162            DataType::Indirect => field!(self.indirect()),
1163            DataType::Iterable => field!(self.iterable()),
1164            // SAFETY: We are not accessing the pointer.
1165            DataType::Ptr => field!(unsafe { self.ptr::<c_void>() }),
1166        };
1167
1168        dbg.finish()
1169    }
1170}
1171
1172impl Drop for Zval {
1173    fn drop(&mut self) {
1174        self.change_type(ZvalTypeFlags::Null);
1175    }
1176}
1177
1178impl Default for Zval {
1179    fn default() -> Self {
1180        Self::new()
1181    }
1182}
1183
1184impl IntoZval for Zval {
1185    const TYPE: DataType = DataType::Mixed;
1186    const NULLABLE: bool = true;
1187
1188    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
1189        *zv = self;
1190        Ok(())
1191    }
1192}
1193
1194impl<'a> FromZval<'a> for &'a Zval {
1195    const TYPE: DataType = DataType::Mixed;
1196
1197    fn from_zval(zval: &'a Zval) -> Option<Self> {
1198        Some(zval)
1199    }
1200}
1201
1202impl<'a> FromZvalMut<'a> for &'a mut Zval {
1203    const TYPE: DataType = DataType::Mixed;
1204
1205    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
1206        Some(zval)
1207    }
1208}
1209
1210/// Formats a float to a string following PHP's type coercion rules.
1211///
1212/// This is a Rust implementation that matches PHP's behavior. PHP uses a format
1213/// similar to printf's `%G` with 14 significant digits:
1214/// - Uses scientific notation when exponent >= 14 or <= -5
1215/// - Uses uppercase 'E' for the exponent (e.g., "1.0E+52")
1216/// - Removes trailing zeros after the decimal point
1217fn php_float_to_string(d: f64) -> String {
1218    // Use Rust's %G-like formatting: format with precision, uppercase E
1219    // PHP uses ~14 significant digits
1220    let formatted = format!("{d:.14E}");
1221
1222    // Parse the formatted string to clean it up
1223    if let Some(e_pos) = formatted.find('E') {
1224        let mantissa_part = &formatted[..e_pos];
1225        let exp_part = &formatted[e_pos..];
1226
1227        // Parse exponent to decide format
1228        let exp: i32 = exp_part[1..].parse().unwrap_or(0);
1229
1230        // PHP uses scientific notation when exponent >= 14 or <= -5
1231        if exp >= 14 || exp <= -5 {
1232            // Keep scientific notation, but clean up mantissa
1233            let mantissa = mantissa_part.trim_end_matches('0').trim_end_matches('.');
1234
1235            // Ensure mantissa has at least one decimal digit if there's a decimal point
1236            let mantissa = if mantissa.contains('.') && mantissa.ends_with('.') {
1237                format!("{mantissa}0")
1238            } else if !mantissa.contains('.') {
1239                format!("{mantissa}.0")
1240            } else {
1241                mantissa.to_string()
1242            };
1243
1244            // Format exponent with sign
1245            if exp >= 0 {
1246                format!("{mantissa}E+{exp}")
1247            } else {
1248                format!("{mantissa}E{exp}")
1249            }
1250        } else {
1251            // Use decimal notation
1252            let s = d.to_string();
1253            // Clean up trailing zeros
1254            if s.contains('.') {
1255                s.trim_end_matches('0').trim_end_matches('.').to_string()
1256            } else {
1257                s
1258            }
1259        }
1260    } else {
1261        formatted
1262    }
1263}
1264
1265/// Parses an integer from a string following PHP's type coercion rules.
1266///
1267/// This is a Rust implementation that matches PHP's behavior. It extracts the
1268/// leading numeric portion of a string:
1269/// - `"42"` → 42
1270/// - `"42abc"` → 42
1271/// - `"  42"` → 42 (leading whitespace is skipped)
1272/// - `"-42"` → -42
1273/// - `"abc"` → 0
1274/// - `""` → 0
1275/// - Hexadecimal (`"0xFF"`) → 0 (not interpreted, stops at 'x')
1276/// - Octal (`"010"`) → 10 (not interpreted as octal)
1277/// - Overflow saturates to `ZendLong::MAX` or `ZendLong::MIN`
1278fn parse_long_from_str(s: &str) -> ZendLong {
1279    let s = s.trim_start();
1280    let bytes = s.as_bytes();
1281    if bytes.is_empty() {
1282        return 0;
1283    }
1284
1285    let mut i = 0;
1286    let mut is_negative = false;
1287
1288    // Handle optional sign
1289    if bytes[i] == b'-' || bytes[i] == b'+' {
1290        is_negative = bytes[i] == b'-';
1291        i += 1;
1292    }
1293
1294    let digits_start = i;
1295
1296    // Collect digits
1297    while i < bytes.len() && bytes[i].is_ascii_digit() {
1298        i += 1;
1299    }
1300
1301    // No digits found
1302    if i == digits_start {
1303        return 0;
1304    }
1305
1306    // Parse the slice, saturating on overflow (matching PHP behavior)
1307    match s[..i].parse::<ZendLong>() {
1308        Ok(n) => n,
1309        Err(_) => {
1310            // Overflow: saturate to max or min depending on sign
1311            if is_negative {
1312                ZendLong::MIN
1313            } else {
1314                ZendLong::MAX
1315            }
1316        }
1317    }
1318}
1319
1320/// Parses a float from a string following PHP's type coercion rules.
1321///
1322/// This is a Rust implementation that matches PHP's behavior. It extracts the
1323/// leading numeric portion of a string:
1324/// - `"3.14"` → 3.14
1325/// - `"3.14abc"` → 3.14
1326/// - `"  3.14"` → 3.14 (leading whitespace is skipped)
1327/// - `"-3.14"` → -3.14
1328/// - `"1e10"` → 1e10 (scientific notation supported)
1329/// - `".5"` → 0.5 (leading decimal point)
1330/// - `"5."` → 5.0 (trailing decimal point)
1331/// - `"abc"` → 0.0
1332/// - `""` → 0.0
1333fn parse_double_from_str(s: &str) -> f64 {
1334    let s = s.trim_start();
1335    let bytes = s.as_bytes();
1336    if bytes.is_empty() {
1337        return 0.0;
1338    }
1339
1340    let mut i = 0;
1341    let mut has_decimal = false;
1342    let mut has_exponent = false;
1343    let mut has_digits = false;
1344
1345    // Handle optional sign
1346    if bytes[i] == b'-' || bytes[i] == b'+' {
1347        i += 1;
1348    }
1349
1350    // Collect digits, decimal point, and exponent
1351    while i < bytes.len() {
1352        let c = bytes[i];
1353        if c.is_ascii_digit() {
1354            has_digits = true;
1355            i += 1;
1356        } else if c == b'.' && !has_decimal && !has_exponent {
1357            has_decimal = true;
1358            i += 1;
1359        } else if (c == b'e' || c == b'E') && !has_exponent && has_digits {
1360            has_exponent = true;
1361            i += 1;
1362            // Handle optional sign after exponent
1363            if i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+') {
1364                i += 1;
1365            }
1366        } else {
1367            break;
1368        }
1369    }
1370
1371    // Parse the slice or return 0.0
1372    s[..i].parse().unwrap_or(0.0)
1373}
1374
1375#[cfg(test)]
1376#[cfg(feature = "embed")]
1377#[allow(clippy::unwrap_used, clippy::approx_constant)]
1378mod tests {
1379    use super::*;
1380    use crate::embed::Embed;
1381
1382    #[test]
1383    fn test_zval_null() {
1384        Embed::run(|| {
1385            let zval = Zval::null();
1386            assert!(zval.is_null());
1387        });
1388    }
1389
1390    #[test]
1391    fn test_is_scalar() {
1392        Embed::run(|| {
1393            // Test scalar types - should return true
1394            let mut zval_long = Zval::new();
1395            zval_long.set_long(42);
1396            assert!(zval_long.is_scalar());
1397
1398            let mut zval_double = Zval::new();
1399            zval_double.set_double(1.5);
1400            assert!(zval_double.is_scalar());
1401
1402            let mut zval_true = Zval::new();
1403            zval_true.set_bool(true);
1404            assert!(zval_true.is_scalar());
1405
1406            let mut zval_false = Zval::new();
1407            zval_false.set_bool(false);
1408            assert!(zval_false.is_scalar());
1409
1410            let mut zval_string = Zval::new();
1411            zval_string
1412                .set_string("hello", false)
1413                .expect("set_string should succeed");
1414            assert!(zval_string.is_scalar());
1415
1416            // Test non-scalar types - should return false
1417            let zval_null = Zval::null();
1418            assert!(!zval_null.is_scalar());
1419
1420            let zval_array = Zval::new_array();
1421            assert!(!zval_array.is_scalar());
1422        });
1423    }
1424
1425    #[test]
1426    fn test_coerce_to_bool() {
1427        Embed::run(|| {
1428            let mut zv = Zval::new();
1429
1430            // === Truthy values ===
1431            // Integers
1432            zv.set_long(42);
1433            assert!(zv.coerce_to_bool(), "(bool)42 should be true");
1434
1435            zv.set_long(1);
1436            assert!(zv.coerce_to_bool(), "(bool)1 should be true");
1437
1438            zv.set_long(-1);
1439            assert!(zv.coerce_to_bool(), "(bool)-1 should be true");
1440
1441            // Floats
1442            zv.set_double(0.1);
1443            assert!(zv.coerce_to_bool(), "(bool)0.1 should be true");
1444
1445            zv.set_double(-0.1);
1446            assert!(zv.coerce_to_bool(), "(bool)-0.1 should be true");
1447
1448            // Strings - non-empty and not "0"
1449            zv.set_string("hello", false).unwrap();
1450            assert!(zv.coerce_to_bool(), "(bool)'hello' should be true");
1451
1452            zv.set_string("1", false).unwrap();
1453            assert!(zv.coerce_to_bool(), "(bool)'1' should be true");
1454
1455            // PHP: (bool)"false" is true (non-empty string)
1456            zv.set_string("false", false).unwrap();
1457            assert!(zv.coerce_to_bool(), "(bool)'false' should be true");
1458
1459            // PHP: (bool)"true" is true
1460            zv.set_string("true", false).unwrap();
1461            assert!(zv.coerce_to_bool(), "(bool)'true' should be true");
1462
1463            // PHP: (bool)" " (space) is true
1464            zv.set_string(" ", false).unwrap();
1465            assert!(zv.coerce_to_bool(), "(bool)' ' should be true");
1466
1467            // PHP: (bool)"00" is true (only exactly "0" is false)
1468            zv.set_string("00", false).unwrap();
1469            assert!(zv.coerce_to_bool(), "(bool)'00' should be true");
1470
1471            zv.set_bool(true);
1472            assert!(zv.coerce_to_bool(), "(bool)true should be true");
1473
1474            // === Falsy values ===
1475            // Integer zero
1476            zv.set_long(0);
1477            assert!(!zv.coerce_to_bool(), "(bool)0 should be false");
1478
1479            // Float zero (both positive and negative)
1480            zv.set_double(0.0);
1481            assert!(!zv.coerce_to_bool(), "(bool)0.0 should be false");
1482
1483            zv.set_double(-0.0);
1484            assert!(!zv.coerce_to_bool(), "(bool)-0.0 should be false");
1485
1486            // Empty string
1487            zv.set_string("", false).unwrap();
1488            assert!(!zv.coerce_to_bool(), "(bool)'' should be false");
1489
1490            // String "0" - the only non-empty falsy string
1491            zv.set_string("0", false).unwrap();
1492            assert!(!zv.coerce_to_bool(), "(bool)'0' should be false");
1493
1494            zv.set_bool(false);
1495            assert!(!zv.coerce_to_bool(), "(bool)false should be false");
1496
1497            // Null
1498            let null_zv = Zval::null();
1499            assert!(!null_zv.coerce_to_bool(), "(bool)null should be false");
1500
1501            // Empty array
1502            let empty_array = Zval::new_array();
1503            assert!(!empty_array.coerce_to_bool(), "(bool)[] should be false");
1504        });
1505    }
1506
1507    #[test]
1508    fn test_coerce_to_string() {
1509        Embed::run(|| {
1510            let mut zv = Zval::new();
1511
1512            // === Integer to string ===
1513            zv.set_long(42);
1514            assert_eq!(zv.coerce_to_string(), Some("42".to_string()));
1515
1516            zv.set_long(-123);
1517            assert_eq!(zv.coerce_to_string(), Some("-123".to_string()));
1518
1519            zv.set_long(0);
1520            assert_eq!(zv.coerce_to_string(), Some("0".to_string()));
1521
1522            // === Float to string ===
1523            zv.set_double(3.14);
1524            assert_eq!(zv.coerce_to_string(), Some("3.14".to_string()));
1525
1526            zv.set_double(-3.14);
1527            assert_eq!(zv.coerce_to_string(), Some("-3.14".to_string()));
1528
1529            zv.set_double(0.0);
1530            assert_eq!(zv.coerce_to_string(), Some("0".to_string()));
1531
1532            zv.set_double(1.0);
1533            assert_eq!(zv.coerce_to_string(), Some("1".to_string()));
1534
1535            zv.set_double(1.5);
1536            assert_eq!(zv.coerce_to_string(), Some("1.5".to_string()));
1537
1538            zv.set_double(100.0);
1539            assert_eq!(zv.coerce_to_string(), Some("100".to_string()));
1540
1541            // Special float values (uppercase like PHP)
1542            zv.set_double(f64::INFINITY);
1543            assert_eq!(zv.coerce_to_string(), Some("INF".to_string()));
1544
1545            zv.set_double(f64::NEG_INFINITY);
1546            assert_eq!(zv.coerce_to_string(), Some("-INF".to_string()));
1547
1548            zv.set_double(f64::NAN);
1549            assert_eq!(zv.coerce_to_string(), Some("NAN".to_string()));
1550
1551            // Scientific notation thresholds (PHP uses E notation at exp >= 14 or <= -5)
1552            // PHP: (string)1e13 -> "10000000000000"
1553            zv.set_double(1e13);
1554            assert_eq!(zv.coerce_to_string(), Some("10000000000000".to_string()));
1555
1556            // PHP: (string)1e14 -> "1.0E+14"
1557            zv.set_double(1e14);
1558            assert_eq!(zv.coerce_to_string(), Some("1.0E+14".to_string()));
1559
1560            // PHP: (string)1e52 -> "1.0E+52"
1561            zv.set_double(1e52);
1562            assert_eq!(zv.coerce_to_string(), Some("1.0E+52".to_string()));
1563
1564            // PHP: (string)0.0001 -> "0.0001"
1565            zv.set_double(0.0001);
1566            assert_eq!(zv.coerce_to_string(), Some("0.0001".to_string()));
1567
1568            // PHP: (string)1e-5 -> "1.0E-5"
1569            zv.set_double(1e-5);
1570            assert_eq!(zv.coerce_to_string(), Some("1.0E-5".to_string()));
1571
1572            // Negative scientific notation
1573            // PHP: (string)(-1e52) -> "-1.0E+52"
1574            zv.set_double(-1e52);
1575            assert_eq!(zv.coerce_to_string(), Some("-1.0E+52".to_string()));
1576
1577            // PHP: (string)(-1e-10) -> "-1.0E-10"
1578            zv.set_double(-1e-10);
1579            assert_eq!(zv.coerce_to_string(), Some("-1.0E-10".to_string()));
1580
1581            // === Boolean to string ===
1582            // PHP: (string)true -> "1"
1583            zv.set_bool(true);
1584            assert_eq!(zv.coerce_to_string(), Some("1".to_string()));
1585
1586            // PHP: (string)false -> ""
1587            zv.set_bool(false);
1588            assert_eq!(zv.coerce_to_string(), Some(String::new()));
1589
1590            // === Null to string ===
1591            // PHP: (string)null -> ""
1592            let null_zv = Zval::null();
1593            assert_eq!(null_zv.coerce_to_string(), Some(String::new()));
1594
1595            // === String unchanged ===
1596            zv.set_string("hello", false).unwrap();
1597            assert_eq!(zv.coerce_to_string(), Some("hello".to_string()));
1598
1599            // === Array cannot be converted ===
1600            let arr_zv = Zval::new_array();
1601            assert_eq!(arr_zv.coerce_to_string(), None);
1602        });
1603    }
1604
1605    #[test]
1606    fn test_coerce_to_long() {
1607        Embed::run(|| {
1608            let mut zv = Zval::new();
1609
1610            // === Integer unchanged ===
1611            zv.set_long(42);
1612            assert_eq!(zv.coerce_to_long(), Some(42));
1613
1614            zv.set_long(-42);
1615            assert_eq!(zv.coerce_to_long(), Some(-42));
1616
1617            zv.set_long(0);
1618            assert_eq!(zv.coerce_to_long(), Some(0));
1619
1620            // === Float truncated (towards zero) ===
1621            // PHP: (int)3.14 -> 3
1622            zv.set_double(3.14);
1623            assert_eq!(zv.coerce_to_long(), Some(3));
1624
1625            // PHP: (int)3.99 -> 3
1626            zv.set_double(3.99);
1627            assert_eq!(zv.coerce_to_long(), Some(3));
1628
1629            // PHP: (int)-3.14 -> -3
1630            zv.set_double(-3.14);
1631            assert_eq!(zv.coerce_to_long(), Some(-3));
1632
1633            // PHP: (int)-3.99 -> -3
1634            zv.set_double(-3.99);
1635            assert_eq!(zv.coerce_to_long(), Some(-3));
1636
1637            // PHP: (int)0.0 -> 0
1638            zv.set_double(0.0);
1639            assert_eq!(zv.coerce_to_long(), Some(0));
1640
1641            // PHP: (int)-0.0 -> 0
1642            zv.set_double(-0.0);
1643            assert_eq!(zv.coerce_to_long(), Some(0));
1644
1645            // === Boolean to integer ===
1646            // PHP: (int)true -> 1
1647            zv.set_bool(true);
1648            assert_eq!(zv.coerce_to_long(), Some(1));
1649
1650            // PHP: (int)false -> 0
1651            zv.set_bool(false);
1652            assert_eq!(zv.coerce_to_long(), Some(0));
1653
1654            // === Null to integer ===
1655            // PHP: (int)null -> 0
1656            let null_zv = Zval::null();
1657            assert_eq!(null_zv.coerce_to_long(), Some(0));
1658
1659            // === String to integer (leading numeric portion) ===
1660            // PHP: (int)"123" -> 123
1661            zv.set_string("123", false).unwrap();
1662            assert_eq!(zv.coerce_to_long(), Some(123));
1663
1664            // PHP: (int)"  123" -> 123
1665            zv.set_string("  123", false).unwrap();
1666            assert_eq!(zv.coerce_to_long(), Some(123));
1667
1668            // PHP: (int)"123abc" -> 123
1669            zv.set_string("123abc", false).unwrap();
1670            assert_eq!(zv.coerce_to_long(), Some(123));
1671
1672            // PHP: (int)"abc123" -> 0
1673            zv.set_string("abc123", false).unwrap();
1674            assert_eq!(zv.coerce_to_long(), Some(0));
1675
1676            // PHP: (int)"" -> 0
1677            zv.set_string("", false).unwrap();
1678            assert_eq!(zv.coerce_to_long(), Some(0));
1679
1680            // PHP: (int)"0" -> 0
1681            zv.set_string("0", false).unwrap();
1682            assert_eq!(zv.coerce_to_long(), Some(0));
1683
1684            // PHP: (int)"-0" -> 0
1685            zv.set_string("-0", false).unwrap();
1686            assert_eq!(zv.coerce_to_long(), Some(0));
1687
1688            // PHP: (int)"+123" -> 123
1689            zv.set_string("+123", false).unwrap();
1690            assert_eq!(zv.coerce_to_long(), Some(123));
1691
1692            // PHP: (int)"   -456" -> -456
1693            zv.set_string("   -456", false).unwrap();
1694            assert_eq!(zv.coerce_to_long(), Some(-456));
1695
1696            // PHP: (int)"3.14" -> 3 (stops at decimal point)
1697            zv.set_string("3.14", false).unwrap();
1698            assert_eq!(zv.coerce_to_long(), Some(3));
1699
1700            // PHP: (int)"3.99" -> 3
1701            zv.set_string("3.99", false).unwrap();
1702            assert_eq!(zv.coerce_to_long(), Some(3));
1703
1704            // PHP: (int)"-3.99" -> -3
1705            zv.set_string("-3.99", false).unwrap();
1706            assert_eq!(zv.coerce_to_long(), Some(-3));
1707
1708            // PHP: (int)"abc" -> 0
1709            zv.set_string("abc", false).unwrap();
1710            assert_eq!(zv.coerce_to_long(), Some(0));
1711
1712            // === Array cannot be converted ===
1713            let arr_zv = Zval::new_array();
1714            assert_eq!(arr_zv.coerce_to_long(), None);
1715        });
1716    }
1717
1718    #[test]
1719    fn test_coerce_to_double() {
1720        Embed::run(|| {
1721            let mut zv = Zval::new();
1722
1723            // === Float unchanged ===
1724            zv.set_double(3.14);
1725            assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1726
1727            zv.set_double(-3.14);
1728            assert!((zv.coerce_to_double().unwrap() - (-3.14)).abs() < f64::EPSILON);
1729
1730            zv.set_double(0.0);
1731            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1732
1733            // === Integer to float ===
1734            // PHP: (float)42 -> 42.0
1735            zv.set_long(42);
1736            assert!((zv.coerce_to_double().unwrap() - 42.0).abs() < f64::EPSILON);
1737
1738            // PHP: (float)-42 -> -42.0
1739            zv.set_long(-42);
1740            assert!((zv.coerce_to_double().unwrap() - (-42.0)).abs() < f64::EPSILON);
1741
1742            // PHP: (float)0 -> 0.0
1743            zv.set_long(0);
1744            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1745
1746            // === Boolean to float ===
1747            // PHP: (float)true -> 1.0
1748            zv.set_bool(true);
1749            assert!((zv.coerce_to_double().unwrap() - 1.0).abs() < f64::EPSILON);
1750
1751            // PHP: (float)false -> 0.0
1752            zv.set_bool(false);
1753            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1754
1755            // === Null to float ===
1756            // PHP: (float)null -> 0.0
1757            let null_zv = Zval::null();
1758            assert!((null_zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1759
1760            // === String to float ===
1761            // PHP: (float)"3.14" -> 3.14
1762            zv.set_string("3.14", false).unwrap();
1763            assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1764
1765            // PHP: (float)"  3.14" -> 3.14
1766            zv.set_string("  3.14", false).unwrap();
1767            assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1768
1769            // PHP: (float)"3.14abc" -> 3.14
1770            zv.set_string("3.14abc", false).unwrap();
1771            assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1772
1773            // PHP: (float)"abc3.14" -> 0.0
1774            zv.set_string("abc3.14", false).unwrap();
1775            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1776
1777            // PHP: (float)"" -> 0.0
1778            zv.set_string("", false).unwrap();
1779            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1780
1781            // PHP: (float)"0" -> 0.0
1782            zv.set_string("0", false).unwrap();
1783            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1784
1785            // PHP: (float)"0.0" -> 0.0
1786            zv.set_string("0.0", false).unwrap();
1787            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1788
1789            // PHP: (float)"+3.14" -> 3.14
1790            zv.set_string("+3.14", false).unwrap();
1791            assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1792
1793            // PHP: (float)"   -3.14" -> -3.14
1794            zv.set_string("   -3.14", false).unwrap();
1795            assert!((zv.coerce_to_double().unwrap() - (-3.14)).abs() < f64::EPSILON);
1796
1797            // Scientific notation
1798            // PHP: (float)"1e10" -> 1e10
1799            zv.set_string("1e10", false).unwrap();
1800            assert!((zv.coerce_to_double().unwrap() - 1e10).abs() < 1.0);
1801
1802            // PHP: (float)"1E10" -> 1e10
1803            zv.set_string("1E10", false).unwrap();
1804            assert!((zv.coerce_to_double().unwrap() - 1e10).abs() < 1.0);
1805
1806            // PHP: (float)"1.5e-3" -> 0.0015
1807            zv.set_string("1.5e-3", false).unwrap();
1808            assert!((zv.coerce_to_double().unwrap() - 0.0015).abs() < f64::EPSILON);
1809
1810            // PHP: (float)".5" -> 0.5
1811            zv.set_string(".5", false).unwrap();
1812            assert!((zv.coerce_to_double().unwrap() - 0.5).abs() < f64::EPSILON);
1813
1814            // PHP: (float)"5." -> 5.0
1815            zv.set_string("5.", false).unwrap();
1816            assert!((zv.coerce_to_double().unwrap() - 5.0).abs() < f64::EPSILON);
1817
1818            // PHP: (float)"abc" -> 0.0
1819            zv.set_string("abc", false).unwrap();
1820            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1821
1822            // === Array cannot be converted ===
1823            let arr_zv = Zval::new_array();
1824            assert_eq!(arr_zv.coerce_to_double(), None);
1825        });
1826    }
1827
1828    #[test]
1829    fn test_parse_long_from_str() {
1830        use crate::ffi::zend_long as ZendLong;
1831
1832        // Basic cases
1833        assert_eq!(parse_long_from_str("42"), 42);
1834        assert_eq!(parse_long_from_str("0"), 0);
1835        assert_eq!(parse_long_from_str("-42"), -42);
1836        assert_eq!(parse_long_from_str("+42"), 42);
1837
1838        // Leading/trailing content
1839        assert_eq!(parse_long_from_str("42abc"), 42);
1840        assert_eq!(parse_long_from_str("  42"), 42);
1841        assert_eq!(parse_long_from_str("\t\n42"), 42);
1842        assert_eq!(parse_long_from_str("  -42"), -42);
1843        assert_eq!(parse_long_from_str("42.5"), 42); // stops at decimal point
1844
1845        // Non-numeric strings
1846        assert_eq!(parse_long_from_str("abc"), 0);
1847        assert_eq!(parse_long_from_str(""), 0);
1848        assert_eq!(parse_long_from_str("  "), 0);
1849        assert_eq!(parse_long_from_str("-"), 0);
1850        assert_eq!(parse_long_from_str("+"), 0);
1851        assert_eq!(parse_long_from_str("abc123"), 0);
1852
1853        // Edge cases matching PHP behavior:
1854        // - Hexadecimal: PHP's (int)"0xFF" returns 0 (stops at 'x')
1855        assert_eq!(parse_long_from_str("0xFF"), 0);
1856        assert_eq!(parse_long_from_str("0x10"), 0);
1857
1858        // - Octal notation is NOT interpreted, just reads leading digits
1859        assert_eq!(parse_long_from_str("010"), 10); // not 8
1860
1861        // - Binary notation is NOT interpreted
1862        assert_eq!(parse_long_from_str("0b101"), 0);
1863
1864        // Overflow tests - saturates to max/min
1865        assert_eq!(
1866            parse_long_from_str("99999999999999999999999999"),
1867            ZendLong::MAX
1868        );
1869        assert_eq!(
1870            parse_long_from_str("-99999999999999999999999999"),
1871            ZendLong::MIN
1872        );
1873
1874        // Large but valid numbers
1875        assert_eq!(parse_long_from_str("9223372036854775807"), ZendLong::MAX); // i64::MAX
1876        assert_eq!(parse_long_from_str("-9223372036854775808"), ZendLong::MIN); // i64::MIN
1877    }
1878
1879    #[test]
1880    fn test_parse_double_from_str() {
1881        // Basic cases
1882        assert!((parse_double_from_str("3.14") - 3.14).abs() < f64::EPSILON);
1883        assert!((parse_double_from_str("0.0") - 0.0).abs() < f64::EPSILON);
1884        assert!((parse_double_from_str("-3.14") - (-3.14)).abs() < f64::EPSILON);
1885        assert!((parse_double_from_str("+3.14") - 3.14).abs() < f64::EPSILON);
1886        assert!((parse_double_from_str(".5") - 0.5).abs() < f64::EPSILON);
1887        assert!((parse_double_from_str("5.") - 5.0).abs() < f64::EPSILON);
1888
1889        // Scientific notation
1890        assert!((parse_double_from_str("1e10") - 1e10).abs() < 1.0);
1891        assert!((parse_double_from_str("1E10") - 1e10).abs() < 1.0);
1892        assert!((parse_double_from_str("1.5e-3") - 1.5e-3).abs() < f64::EPSILON);
1893        assert!((parse_double_from_str("1.5E+3") - 1500.0).abs() < f64::EPSILON);
1894        assert!((parse_double_from_str("-1.5e2") - (-150.0)).abs() < f64::EPSILON);
1895
1896        // Leading/trailing content
1897        assert!((parse_double_from_str("3.14abc") - 3.14).abs() < f64::EPSILON);
1898        assert!((parse_double_from_str("  3.14") - 3.14).abs() < f64::EPSILON);
1899        assert!((parse_double_from_str("\t\n3.14") - 3.14).abs() < f64::EPSILON);
1900        assert!((parse_double_from_str("1e10xyz") - 1e10).abs() < 1.0);
1901
1902        // Non-numeric strings
1903        assert!((parse_double_from_str("abc") - 0.0).abs() < f64::EPSILON);
1904        assert!((parse_double_from_str("") - 0.0).abs() < f64::EPSILON);
1905        assert!((parse_double_from_str("  ") - 0.0).abs() < f64::EPSILON);
1906        assert!((parse_double_from_str("-") - 0.0).abs() < f64::EPSILON);
1907        assert!((parse_double_from_str("e10") - 0.0).abs() < f64::EPSILON); // no digits before e
1908
1909        // Integer values
1910        assert!((parse_double_from_str("42") - 42.0).abs() < f64::EPSILON);
1911        assert!((parse_double_from_str("-42") - (-42.0)).abs() < f64::EPSILON);
1912
1913        // Edge cases:
1914        // - Hexadecimal: not supported, returns 0
1915        assert!((parse_double_from_str("0xFF") - 0.0).abs() < f64::EPSILON);
1916
1917        // - Multiple decimal points: stops at second
1918        assert!((parse_double_from_str("1.2.3") - 1.2).abs() < f64::EPSILON);
1919
1920        // - Multiple exponents: stops at second e
1921        assert!((parse_double_from_str("1e2e3") - 100.0).abs() < f64::EPSILON);
1922    }
1923
1924    #[test]
1925    fn test_php_float_to_string() {
1926        // Basic decimal values
1927        assert_eq!(php_float_to_string(3.14), "3.14");
1928        assert_eq!(php_float_to_string(42.0), "42");
1929        assert_eq!(php_float_to_string(-3.14), "-3.14");
1930        assert_eq!(php_float_to_string(0.0), "0");
1931
1932        // Large numbers use scientific notation with uppercase E
1933        // PHP: (string)(float)'999...9' -> "1.0E+52"
1934        assert_eq!(php_float_to_string(1e52), "1.0E+52");
1935        assert_eq!(php_float_to_string(1e20), "1.0E+20");
1936        assert_eq!(php_float_to_string(1e14), "1.0E+14");
1937
1938        // Very small numbers also use scientific notation (at exp <= -5)
1939        assert_eq!(php_float_to_string(1e-10), "1.0E-10");
1940        assert_eq!(php_float_to_string(1e-5), "1.0E-5");
1941        assert_eq!(php_float_to_string(0.00001), "1.0E-5");
1942
1943        // Numbers that don't need scientific notation
1944        assert_eq!(php_float_to_string(1e13), "10000000000000");
1945        assert_eq!(php_float_to_string(0.001), "0.001");
1946        assert_eq!(php_float_to_string(0.0001), "0.0001"); // 1e-4 is decimal
1947    }
1948}