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