Skip to content

[BISECT — do not merge] code-bisect point: a6cf1953d (1st shim, pre-sysroot) - #3

Closed
Guikingone wants to merge 238 commits into
mainfrom
bisect/code-a6cf1953d
Closed

[BISECT — do not merge] code-bisect point: a6cf1953d (1st shim, pre-sysroot)#3
Guikingone wants to merge 238 commits into
mainfrom
bisect/code-a6cf1953d

Conversation

@Guikingone

Copy link
Copy Markdown
Owner

Bisect probe — not for merge. Second step of the Windows gate regression bisection.

Context

PR illegalstudio#446 HEAD (a09222532) gate is RED with 34 allow-listed regressions. Image-vs-code probe (c60d60523, run 28931972142) came back GREEN → the image is stable; the regression is in the post-c60d60523 code (rebase + 6 shims + CI fixes).

The 34 split into two independent problems (classified from run 28929685427 junit):

  • 15 semantic — 12 CRLF/separator (PHP_EOL→CRLF, DIRECTORY_SEPARATOR→\\) + 3 winsock/access. Cause identified: this commit a6cf1953d.
  • 19 crashes (exit≠0, empty stderr) — json inf/nan (8), in_array string (3), assoc_in_array (2), recursive closures (3), argv (1), streams unresolvable/bad-address (2). Cause unknown.

What this run tests

a6cf1953d is the first shim, immediately before 9224b9c85 (MinGW cross-built sysroot). Its parity job is apt-based (MinGW + wine, no cmake sysroot) so it builds on the current image without the CI fixes. The 34 regressed tests need no zlib/pcre/bz2, so they link and the gate is comparable.

Decision table:

  • RED with the 19 crashes ⇒ a6cf1953d caused the crashes (main-wrapper WSAStartup / transform routing / a shared-codegen change).
  • RED with only the 15 semantic, not the 19 crashes ⇒ the crashes come from 9224b9c85+ (sysroot/link change).
  • It will be at least RED on the 12 CRLF tests (it carries PHP_EOL→CRLF).

Cleanup

PR and bisect/code-a6cf1953d deleted once the gate concludes.

Guikingone and others added 30 commits June 24, 2026 17:58
A `foreach($src as $k=>$v) $dst[$k]=$v` rebuild into a statically
`Array(Mixed)` destination collapsed every string key onto int 0, dropping
all but the last entry. EIR foreach keys are always boxed `Mixed` cells
(`Op::IterCurrentKey`), and `lower_array_assign` coerced the Mixed index
to int for indexed destinations.

Add `Op::ArraySetMixedKey` + the `__rt_array_set_mixed_key` runtime helper
(dual-arch), which tag-dispatches the Mixed key at runtime: integer/bool/
float keys stay on indexed storage (preserving indexed consumers like
`implode`), and string keys promote the destination to a hash. The promote
path runs `__rt_hash_to_mixed` so scalar slots copied from the indexed
source are boxed as Mixed cells before the new entry is inserted.

Two coupled runtime fixes make the promote path read back correctly:
- `__rt_array_push_int` first-write shape-specialization now clears the
  `Never` placeholder value_type (8) stamped by `[]`, not just the string
  fallback tag (1), so a prior int push copies with the correct int tag.
- `__rt_array_hash_union` copies scalar slots using the source value_type
  tag, so `__rt_hash_to_mixed` must box them or a seeded int reads back empty.

Route `Array(Mixed)`/`Array(Union)` iterator sources through
`DynamicIterable` so a runtime-promoted hash iterates correctly, while
concrete-element indexed arrays keep the fast static path.

Add checker foreach-key tracking (`foreach_key_locals`, per-function
lifetime mirroring the lowering's `foreach_int_key_locals`) so
`$dst[$k]=$v` under a foreach key defers to `ArraySetMixedKey`
(`Array(Mixed)`) while a non-foreach string-typed key (e.g. `"k".$i`)
still promotes to `AssocArray` for direct string-key reads. This restores
the three `test_assoc_array_dynamic_string_key_*` tests that the prior
Mixed-key routing had regressed.

Five regression tests cover string-key rebuild, entry count, int-key
stays-indexed (implode), mixed int/string keys, and string-after-int-seed.
3-target green: macOS-aarch64 full codegen suite 4138/0; linux-x86_64 and
linux-arm64 foreach_mixed 6/0, assoc 3/0, implode 5/0, runtime_gc 95/0,
foreach 84/0.
Auto-free native stream fds (kind 1 → close) and unfinalized HashContext
handles (kind 2 → elephc_crypto_free) when their Mixed box is released at
scope exit. The resource-kind subtype lives in the high payload word of the
Mixed cell; kind 0 resources (synthetic user-wrapper handles) are skipped.

Wiring:
- __rt_mixed_free_deep: tag-9 dispatch with kind 0/1/2 on AArch64 + x86_64
- __rt_hash_ctx_free: new runtime helper calling elephc_crypto_free
- _elephc_crypto_free_fn slot + publish entry
- hash_init/hash_copy box with kind=2, fopen boxes with kind=1

Closes the fd leak (unclosed fopen) and HashContext leak (unfinalized
hash_init) documented in ROADMAP v0.26.x.
Emit each __rt_* runtime helper into its own text section on Linux
(.section .text.<name>) so that --gc-sections can eliminate unreachable
helpers at link time. Add -Wl,--gc-sections to the Linux linker for
executables (cdylibs are unaffected).

macOS dead stripping is deferred: .subsections_via_symbols breaks
conditional branches to local labels in the runtime, and per-symbol
Mach-O sections require underscore-aware symbol handling that is not
trivial to retrofit. The runtime .o remains monolithic on macOS for
now; Linux binaries benefit immediately.

The runtime cache key (FNV-1a hash of generated assembly) automatically
invalidates when the section directives change, so no cache migration
is needed.

7 new tests verify that programs using regex, hash, classes, fopen,
arrays, and exceptions still run correctly after dead stripping.
Implements array_is_list, array_key_first/last, array_replace and
array_replace_recursive, array_diff_assoc and array_intersect_assoc,
array_merge_recursive, array_walk_recursive, array_find/any/all (PHP 8.4),
array_udiff/uintersect, and array_multisort through EIR lowering, reusing the
shared __rt_* runtime helpers (the legacy direct-AST emitters are not touched).

Hash-based set operations accept associative arrays and scalar-element indexed
arrays (converted to integer-keyed hashes via __rt_array_to_hash; result
keys/values widen to mixed for heterogeneous inputs). The predicate/comparator
builtins use the EIR descriptor-callback machinery (string, function, and
non-capturing closure callbacks). All are target-aware (ARM64 + x86_64) with
codegen and error tests, an examples/array-parity demo, and docs/php/arrays.md,
ROADMAP, and CHANGELOG updates.
Add the public serialize()/unserialize() builtins covering scalars,
nested arrays, and objects — including the __serialize/__unserialize/
__sleep/__wakeup magic methods and r:/R: object back-references
(repeated objects rebuild as one shared instance) — byte-for-byte
compatible with PHP's wire format.

Persist Phar/PharData state into the archive across all three formats
(native PHAR, tar, zip), round-tripping across objects, processes, and
the PHP interpreter:
- global metadata and stub via setMetadata()/getMetadata()/hasMetadata()/
  delMetadata() and setStub()/getStub()
- PharFileInfo per-file metadata on $phar["entry"]
- whole-archive tar compression for PharData::compress()/decompress()
  (sibling .tar.gz/.tar.bz2)
- signatures for native PHAR, tar, and zip, including Phar::OPENSSL
  RSA-SHA1 signing with a PEM private key (verifiable by PHP), alongside
  MD5/SHA1/SHA256/SHA512; tar/zip use a .phar/signature.bin control entry

Complete the ZIP phar surface: read entries written with a streaming
data descriptor, read and write ZIP64 archives, and read/write
traditional-PKWARE (ZipCrypto) encrypted entries via the setZipPassword()
compiler extension (entries incl. the stub encrypted, signature.bin in
the clear; cipher kept only for legacy compatibility).

EIR/checker correctness fixes surfaced along the way: dispatch synthetic
SPL methods reached through a mixed receiver or an object-iterator
foreach value, decompress compress.zlib:// and compress.bzip2:// fopen
wrappers on the EIR backend, infer mixed-receiver method calls as the
union of candidate return types instead of int, preserve callee-saved
r12-r15 in the x86 __rt_array_grow runtime helper, and route in-dir
codegen fixtures through the EIR backend.
… key reuse

Follow-up correctness fixes to the foreach Mixed-key array-write path:

- __rt_array_set_mixed_key promotes the destination to a hash when an integer
  key is negative or past the logical end, instead of the packed indexed path
  dropping negative writes or zero-filling gaps. Sparse and negative integer
  keys from a rebuild now survive like PHP. Dual-arch (arm64 + x86_64).

- Reset foreach_key_locals per function in with_local_storage_context so a
  foreach key name no longer leaks its boxed-Mixed-key classification into
  other functions that reuse the name as a genuine string key (which
  previously miscompiled the later direct string-key read).

- Drop the foreach-key marker on a direct reassignment ($k = ...) so a later
  $dst[$k] is routed by $k's real type, matching the lowering and fixing a
  spurious "AssocArray -> Array(Mixed)" backend error.

Adds 4 regression tests (sparse int, negative int, cross-function name reuse,
key reassigned to string). Verified on macOS-aarch64, linux-x86_64, linux-arm64.
…h-key-write

fix(eir): keep foreach string keys through Mixed-key array writes
An enum used as a class property type or a promoted-constructor-param type
(`private Tag $tag`) failed with "Unknown type: Tag", even though the same
enum resolves fine as a value and as a function-parameter type.

Class member types are resolved during the class schema pass, which runs
before the enum-processing phase populates `enums`. Class and interface
names are pre-declared into `declared_classes` before that pass, but enum
names were not, so `resolve_type_expr` fell through to "Unknown type".

Pre-declare enum names into `declared_classes` after the final assignment
(the earlier one is overwritten by the builtin-injection block), mirroring
the insert already done later in `schema::enums`.

Adds a codegen regression test for an enum as a promoted-constructor-param type.
Parameterize __rt_json_ftoa with the exponent marker char (w0/dil). serialize()
now passes 'E' so exponential floats render as d:1.0E+20; like PHP, while
json_encode keeps the lowercase 'e' JSON layout. Add regression tests for both.
…ta-persist

feat(streams|zip): Phar metadata/stub + per-file metadata persistence, serialize()/unserialize(), and full ZIP phar support
…ndir, fd reuse)

- elephc_crypto_final finalizes a clone, leaving the HashContext owned by its
  Mixed box so the kind-2 destructor frees it exactly once (no double-free / UAF
  on explicit-final-then-scope-exit or double-final)
- popen pipes (kind 3 -> __rt_pclose, reaps child) and opendir streams
  (kind 4 -> __rt_closedir) get proper scope-cleanup destructors
- explicit fclose/pclose/closedir stamp a -1 sentinel into the Mixed box so an
  already-released descriptor (whose fd may be reused) is never closed twice
- refresh crypto/runtime docstrings, memory-model/runtime docs, ROADMAP
- add regression tests for the above
Run scripts/docs/extract_builtins.py --render --force so the generated internals
pages track the io.rs line shifts from the new scope-cleanup helpers. Only
codegen_line link refs change.
…ope-cleanup

fix(core): resource scope-cleanup for Mixed-boxed tag-9 resources
Prepare runtime helpers for macOS per-symbol dead stripping, where
.subsections_via_symbols makes the Mach-O assembler reject any conditional
branch whose target lies in another atom (another helper).

- Rewrite cross-helper conditional branches (feof, fread, fwrite, fd_write,
  json_encode_array_int, buffer_len) as an inverted conditional skip over an
  unconditional branch, which may cross atoms.
- Make the generator done/epilogue labels local instead of global so each
  generator helper stays one atom; this also fixes a latent cross-atom
  fall-through from __rt_gen_send_done into __rt_gen_send_epilogue.

No behavior change: the generated code is equivalent on current builds.
Emit the macOS executable runtime object with .alt_entry internal labels and a
.subsections_via_symbols footer so each __rt_* helper is a single collectable
atom, then link with -dead_strip. Unreferenced runtime helpers are dropped from
the binary, the macOS analogue of the Linux per-section --gc-sections path.
cdylibs (pic) keep the full runtime.

The emitter only marks named identifier labels .alt_entry; numeric (1:/2:) and
L-prefixed labels are already assembler-local on Mach-O.

Add a guard test that assembles the full all-features runtime under dead
stripping so any new cross-atom conditional branch fails at build time.
Add a linking-page section describing the automatic, per-target runtime dead
stripping (Linux --gc-sections, macOS -dead_strip) and a CHANGELOG entry for
the completed feature.
Conflict resolution:
- generators/mod.rs: took main's stackful-coroutine generators (issue illegalstudio#329),
  which moved the helpers into the `coro` submodule and made my old-generator
  label changes obsolete. The dead-strip assemble guard confirms the new
  coroutine helpers are already single-atom safe (no cross-atom conditional
  branches), so no re-port was needed.
- CHANGELOG.md: kept both [Unreleased] sets — the dead-stripping entry plus
  main's new entries.

Re-validated on the merged tree: full all-features runtime assembles under
dead stripping, and dead_strip/generators/io/buffer/json tests pass.
The prior macOS approach marked every internal runtime label `.alt_entry`.
Older `as` (the CI Xcode) rejects conditional branches to `.alt_entry` labels
as "external", failing all macOS jobs. Rename internal labels to Mach-O
assembler-local `L`-locals instead: valid conditional-branch targets on every
toolchain that still do not start an atom under `.subsections_via_symbols`.

The few helpers reached by an unconditional `b`/`bl` from another atom
(`__rt_mixed_numeric_common`, `__rt_json_validate_string`/`_number`,
`__rt_date_entry`) must stay real symbols so `-dead_strip` keeps their atom
alive; emit those via the new `label_shared` (`.alt_entry`), which only
unconditional branches ever target. Add a guard test asserting no internal
`L__rt_*` label is referenced across atoms, the failure that segfaulted
`foreach` over an associative array.
…d-stripping

feat(core): runtime dead stripping via per-symbol sections and linker GC
# Conflicts:
#	docs/internals/builtins/_internal/__elephc_phar_list_entries.md
#	docs/internals/builtins/_internal/__elephc_phar_set_compression.md
#	docs/internals/builtins/array/count.md
#	docs/internals/builtins/class/function_exists.md
#	docs/internals/builtins/math/pi.md
#	docs/internals/builtins/misc/define.md
#	docs/internals/builtins/misc/defined.md
#	docs/internals/builtins/misc/empty.md
#	docs/internals/builtins/misc/phpversion.md
#	docs/internals/builtins/misc/unset.md
#	docs/internals/builtins/misc/var_dump.md
#	docs/internals/builtins/pointer/ptr.md
#	docs/internals/builtins/pointer/ptr_get.md
#	docs/internals/builtins/pointer/ptr_is_null.md
#	docs/internals/builtins/pointer/ptr_null.md
#	docs/internals/builtins/pointer/ptr_offset.md
#	docs/internals/builtins/pointer/ptr_read16.md
#	docs/internals/builtins/pointer/ptr_read32.md
#	docs/internals/builtins/pointer/ptr_read8.md
#	docs/internals/builtins/pointer/ptr_read_string.md
#	docs/internals/builtins/pointer/ptr_set.md
#	docs/internals/builtins/pointer/ptr_sizeof.md
#	docs/internals/builtins/pointer/ptr_write16.md
#	docs/internals/builtins/pointer/ptr_write32.md
#	docs/internals/builtins/pointer/ptr_write8.md
#	docs/internals/builtins/pointer/ptr_write_string.md
#	docs/internals/builtins/process/die.md
#	docs/internals/builtins/process/exec.md
#	docs/internals/builtins/process/exit.md
#	docs/internals/builtins/process/passthru.md
#	docs/internals/builtins/process/pclose.md
#	docs/internals/builtins/process/popen.md
#	docs/internals/builtins/process/readline.md
#	docs/internals/builtins/process/shell_exec.md
#	docs/internals/builtins/process/sleep.md
#	docs/internals/builtins/process/system.md
#	docs/internals/builtins/process/usleep.md
#	docs/internals/builtins/regex/preg_match.md
#	docs/internals/builtins/regex/preg_match_all.md
#	docs/internals/builtins/regex/preg_replace.md
#	docs/internals/builtins/regex/preg_replace_callback.md
#	docs/internals/builtins/regex/preg_split.md
#	docs/internals/builtins/spl/iterator_apply.md
#	docs/internals/builtins/spl/iterator_count.md
#	docs/internals/builtins/spl/iterator_to_array.md
#	docs/internals/builtins/spl/spl_autoload.md
#	docs/internals/builtins/spl/spl_autoload_call.md
#	docs/internals/builtins/spl/spl_autoload_extensions.md
#	docs/internals/builtins/spl/spl_autoload_functions.md
#	docs/internals/builtins/spl/spl_autoload_register.md
#	docs/internals/builtins/spl/spl_autoload_unregister.md
#	docs/internals/builtins/spl/spl_classes.md
#	docs/internals/builtins/spl/spl_object_hash.md
#	docs/internals/builtins/spl/spl_object_id.md
#	docs/internals/builtins/streams/fsockopen.md
#	docs/internals/builtins/streams/pfsockopen.md
#	docs/internals/builtins/streams/stream_bucket_append.md
#	docs/internals/builtins/streams/stream_bucket_prepend.md
#	docs/internals/builtins/streams/stream_filter_append.md
#	docs/internals/builtins/streams/stream_filter_prepend.md
#	docs/internals/builtins/string/addslashes.md
#	docs/internals/builtins/string/base64_decode.md
#	docs/internals/builtins/string/base64_encode.md
#	docs/internals/builtins/string/bin2hex.md
#	docs/internals/builtins/string/chop.md
#	docs/internals/builtins/string/chr.md
#	docs/internals/builtins/string/crc32.md
#	docs/internals/builtins/string/explode.md
#	docs/internals/builtins/string/grapheme_strrev.md
#	docs/internals/builtins/string/gzcompress.md
#	docs/internals/builtins/string/gzdeflate.md
#	docs/internals/builtins/string/gzinflate.md
#	docs/internals/builtins/string/gzuncompress.md
#	docs/internals/builtins/string/hash.md
#	docs/internals/builtins/string/hash_algos.md
#	docs/internals/builtins/string/hash_copy.md
#	docs/internals/builtins/string/hash_equals.md
#	docs/internals/builtins/string/hash_final.md
#	docs/internals/builtins/string/hash_hmac.md
#	docs/internals/builtins/string/hash_init.md
#	docs/internals/builtins/string/hash_update.md
#	docs/internals/builtins/string/hex2bin.md
#	docs/internals/builtins/string/html_entity_decode.md
#	docs/internals/builtins/string/htmlentities.md
#	docs/internals/builtins/string/htmlspecialchars.md
#	docs/internals/builtins/string/implode.md
#	docs/internals/builtins/string/inet_ntop.md
#	docs/internals/builtins/string/inet_pton.md
#	docs/internals/builtins/string/ip2long.md
#	docs/internals/builtins/string/lcfirst.md
#	docs/internals/builtins/string/long2ip.md
#	docs/internals/builtins/string/ltrim.md
#	docs/internals/builtins/string/md5.md
#	docs/internals/builtins/string/nl2br.md
#	docs/internals/builtins/string/number_format.md
#	docs/internals/builtins/string/ord.md
#	docs/internals/builtins/string/printf.md
#	docs/internals/builtins/string/rawurldecode.md
#	docs/internals/builtins/string/rawurlencode.md
#	docs/internals/builtins/string/rtrim.md
#	docs/internals/builtins/string/sha1.md
#	docs/internals/builtins/string/sprintf.md
#	docs/internals/builtins/string/sscanf.md
#	docs/internals/builtins/string/str_contains.md
#	docs/internals/builtins/string/str_ends_with.md
#	docs/internals/builtins/string/str_ireplace.md
#	docs/internals/builtins/string/str_pad.md
#	docs/internals/builtins/string/str_repeat.md
#	docs/internals/builtins/string/str_replace.md
#	docs/internals/builtins/string/str_split.md
#	docs/internals/builtins/string/str_starts_with.md
#	docs/internals/builtins/string/strcasecmp.md
#	docs/internals/builtins/string/strcmp.md
#	docs/internals/builtins/string/stripslashes.md
#	docs/internals/builtins/string/strlen.md
#	docs/internals/builtins/string/strpos.md
#	docs/internals/builtins/string/strrev.md
#	docs/internals/builtins/string/strrpos.md
#	docs/internals/builtins/string/strstr.md
#	docs/internals/builtins/string/strtolower.md
#	docs/internals/builtins/string/strtoupper.md
#	docs/internals/builtins/string/substr.md
#	docs/internals/builtins/string/substr_replace.md
#	docs/internals/builtins/string/trim.md
#	docs/internals/builtins/string/ucfirst.md
#	docs/internals/builtins/string/ucwords.md
#	docs/internals/builtins/string/urldecode.md
#	docs/internals/builtins/string/urlencode.md
#	docs/internals/builtins/string/vprintf.md
#	docs/internals/builtins/string/vsprintf.md
#	docs/internals/builtins/string/wordwrap.md
#	docs/internals/builtins/type/boolval.md
#	docs/internals/builtins/type/ctype_alnum.md
#	docs/internals/builtins/type/ctype_alpha.md
#	docs/internals/builtins/type/ctype_digit.md
#	docs/internals/builtins/type/ctype_space.md
#	docs/internals/builtins/type/floatval.md
#	docs/internals/builtins/type/get_resource_id.md
#	docs/internals/builtins/type/get_resource_type.md
#	docs/internals/builtins/type/gettype.md
#	docs/internals/builtins/type/intval.md
#	docs/internals/builtins/type/is_array.md
#	docs/internals/builtins/type/is_bool.md
#	docs/internals/builtins/type/is_callable.md
#	docs/internals/builtins/type/is_float.md
#	docs/internals/builtins/type/is_int.md
#	docs/internals/builtins/type/is_iterable.md
#	docs/internals/builtins/type/is_null.md
#	docs/internals/builtins/type/is_numeric.md
#	docs/internals/builtins/type/is_object.md
#	docs/internals/builtins/type/is_resource.md
#	docs/internals/builtins/type/is_scalar.md
#	docs/internals/builtins/type/is_string.md
#	docs/internals/builtins/type/settype.md
#	docs/php/builtins/misc/unset.md
#	docs/php/builtins/misc/var_dump.md
#	docs/php/builtins/pointer/ptr.md
#	docs/php/builtins/pointer/ptr_get.md
#	docs/php/builtins/pointer/ptr_is_null.md
#	docs/php/builtins/pointer/ptr_null.md
#	docs/php/builtins/pointer/ptr_offset.md
#	docs/php/builtins/pointer/ptr_read16.md
#	docs/php/builtins/pointer/ptr_read32.md
#	docs/php/builtins/pointer/ptr_read8.md
#	docs/php/builtins/pointer/ptr_read_string.md
#	docs/php/builtins/pointer/ptr_set.md
#	docs/php/builtins/pointer/ptr_sizeof.md
#	docs/php/builtins/pointer/ptr_write16.md
#	docs/php/builtins/pointer/ptr_write32.md
#	docs/php/builtins/pointer/ptr_write8.md
#	docs/php/builtins/pointer/ptr_write_string.md
#	docs/php/builtins/process/die.md
#	docs/php/builtins/process/exec.md
#	docs/php/builtins/process/exit.md
#	docs/php/builtins/process/passthru.md
#	docs/php/builtins/process/pclose.md
#	docs/php/builtins/process/popen.md
#	docs/php/builtins/process/readline.md
#	docs/php/builtins/process/shell_exec.md
#	docs/php/builtins/process/sleep.md
#	docs/php/builtins/process/system.md
#	docs/php/builtins/process/usleep.md
#	docs/php/builtins/regex/preg_match.md
#	docs/php/builtins/regex/preg_match_all.md
#	docs/php/builtins/regex/preg_replace.md
#	docs/php/builtins/regex/preg_replace_callback.md
#	docs/php/builtins/regex/preg_split.md
#	docs/php/builtins/spl/iterator_apply.md
#	docs/php/builtins/spl/iterator_count.md
#	docs/php/builtins/spl/iterator_to_array.md
#	docs/php/builtins/spl/spl_autoload.md
#	docs/php/builtins/spl/spl_autoload_call.md
#	docs/php/builtins/spl/spl_autoload_extensions.md
#	docs/php/builtins/spl/spl_autoload_functions.md
#	docs/php/builtins/spl/spl_autoload_register.md
#	docs/php/builtins/spl/spl_autoload_unregister.md
#	docs/php/builtins/spl/spl_classes.md
#	docs/php/builtins/spl/spl_object_hash.md
#	docs/php/builtins/spl/spl_object_id.md
#	docs/php/builtins/streams/fsockopen.md
#	docs/php/builtins/streams/pfsockopen.md
#	docs/php/builtins/streams/stream_bucket_append.md
#	docs/php/builtins/streams/stream_bucket_prepend.md
#	docs/php/builtins/streams/stream_filter_append.md
#	docs/php/builtins/streams/stream_filter_prepend.md
#	docs/php/builtins/string/addslashes.md
#	docs/php/builtins/string/base64_decode.md
#	docs/php/builtins/string/base64_encode.md
#	docs/php/builtins/string/bin2hex.md
#	docs/php/builtins/string/chop.md
#	docs/php/builtins/string/chr.md
#	docs/php/builtins/string/crc32.md
#	docs/php/builtins/string/explode.md
#	docs/php/builtins/string/grapheme_strrev.md
#	docs/php/builtins/string/gzcompress.md
#	docs/php/builtins/string/gzdeflate.md
#	docs/php/builtins/string/gzinflate.md
#	docs/php/builtins/string/gzuncompress.md
#	docs/php/builtins/string/hash.md
#	docs/php/builtins/string/hash_algos.md
#	docs/php/builtins/string/hash_copy.md
#	docs/php/builtins/string/hash_equals.md
#	docs/php/builtins/string/hash_final.md
#	docs/php/builtins/string/hash_hmac.md
#	docs/php/builtins/string/hash_init.md
#	docs/php/builtins/string/hash_update.md
#	docs/php/builtins/string/hex2bin.md
#	docs/php/builtins/string/html_entity_decode.md
#	docs/php/builtins/string/htmlentities.md
#	docs/php/builtins/string/htmlspecialchars.md
#	docs/php/builtins/string/implode.md
#	docs/php/builtins/string/inet_ntop.md
#	docs/php/builtins/string/inet_pton.md
#	docs/php/builtins/string/ip2long.md
#	docs/php/builtins/string/lcfirst.md
#	docs/php/builtins/string/long2ip.md
#	docs/php/builtins/string/ltrim.md
#	docs/php/builtins/string/md5.md
#	docs/php/builtins/string/nl2br.md
#	docs/php/builtins/string/number_format.md
#	docs/php/builtins/string/ord.md
#	docs/php/builtins/string/printf.md
#	docs/php/builtins/string/rawurldecode.md
#	docs/php/builtins/string/rawurlencode.md
#	docs/php/builtins/string/rtrim.md
#	docs/php/builtins/string/sha1.md
#	docs/php/builtins/string/sprintf.md
#	docs/php/builtins/string/sscanf.md
#	docs/php/builtins/string/str_contains.md
#	docs/php/builtins/string/str_ends_with.md
#	docs/php/builtins/string/str_ireplace.md
#	docs/php/builtins/string/str_pad.md
#	docs/php/builtins/string/str_repeat.md
#	docs/php/builtins/string/str_replace.md
#	docs/php/builtins/string/str_split.md
#	docs/php/builtins/string/str_starts_with.md
#	docs/php/builtins/string/strcasecmp.md
#	docs/php/builtins/string/strcmp.md
#	docs/php/builtins/string/stripslashes.md
#	docs/php/builtins/string/strlen.md
#	docs/php/builtins/string/strpos.md
#	docs/php/builtins/string/strrev.md
#	docs/php/builtins/string/strrpos.md
#	docs/php/builtins/string/strstr.md
#	docs/php/builtins/string/strtolower.md
#	docs/php/builtins/string/strtoupper.md
#	docs/php/builtins/string/substr.md
#	docs/php/builtins/string/substr_replace.md
#	docs/php/builtins/string/trim.md
#	docs/php/builtins/string/ucfirst.md
#	docs/php/builtins/string/ucwords.md
#	docs/php/builtins/string/urldecode.md
#	docs/php/builtins/string/urlencode.md
#	docs/php/builtins/string/vprintf.md
#	docs/php/builtins/string/vsprintf.md
#	docs/php/builtins/string/wordwrap.md
#	docs/php/builtins/type/boolval.md
#	docs/php/builtins/type/ctype_alnum.md
#	docs/php/builtins/type/ctype_alpha.md
#	docs/php/builtins/type/ctype_digit.md
#	docs/php/builtins/type/ctype_space.md
#	docs/php/builtins/type/floatval.md
#	docs/php/builtins/type/get_resource_id.md
#	docs/php/builtins/type/get_resource_type.md
#	docs/php/builtins/type/gettype.md
#	docs/php/builtins/type/intval.md
#	docs/php/builtins/type/is_array.md
#	docs/php/builtins/type/is_bool.md
#	docs/php/builtins/type/is_callable.md
#	docs/php/builtins/type/is_float.md
#	docs/php/builtins/type/is_int.md
#	docs/php/builtins/type/is_iterable.md
#	docs/php/builtins/type/is_null.md
#	docs/php/builtins/type/is_numeric.md
#	docs/php/builtins/type/is_object.md
#	docs/php/builtins/type/is_resource.md
#	docs/php/builtins/type/is_scalar.md
#	docs/php/builtins/type/is_string.md
#	docs/php/builtins/type/settype.md
#	scripts/docs/builtin_registry.json
#	tests/codegen/arrays/mod.rs
The hash-descent path stashed the iterator value low word at [rbp-24],
which aliases the pushed callee-saved r14 (callback environment). Walking
an associative array therefore corrupted the env register, crashing the
generated binary on Linux x86_64. Move the scratch to the hash-path-unused
length slot [rbp-40]. ARM64 was unaffected (keeps the value in a register).
nahime0 and others added 21 commits July 6, 2026 21:24
…ackend-removal

feat: remove legacy backend
…rray-strict-arg

feat: in_array() accepts the optional strict (3rd) argument
…[skip ci]

- Document AI-assisted contributions, planning larger work (.plans),
  and PR expectations (self-contained, reference the issue)
- Move the assembly comment policy into CONTRIBUTING.md (column-81
  alignment) and add scripts/check_asm_comments.py to verify it
- Point AGENTS.md to CONTRIBUTING.md once up front instead of
  repeating the reference per section
Drop the 11 finished plans (EIR overview/spec/skeleton/lowering/backend/
switchover, peephole, CSE-LICM-inlining, legacy cleanup, hot-path data type,
null-sentinel collision). Keep the unfinished ones: eir-06 register allocator,
DESIGN_eir_codegen_bugs, and phpstorm-plugin.
…d-arg-resolution

fix: resolve names inside named-argument values (missing NamedArg resolver arm)
…es builtin

Reconstruct PR illegalstudio#446's feature set on top of current main.

Windows x86_64 PE target:
- Platform::Windows with the MSx64 ABI (rcx/rdx/r8/r9, 32-byte shadow, 16-byte
  alignment) and ~40 Win32 shim wrappers converting SysV->MSx64 (WriteFile/ReadFile,
  sockets, file/dir ops, VirtualAlloc/Heap, BCryptGenRandom, ...).
- Syscall->shim transform wired into the pipeline + runtime cache (Windows-only) so
  PE binaries contain no raw Linux syscalls; unmapped syscalls route to
  __rt_unsupported_syscall instead of a silent trap.
- Entry wrapper (MSx64->SysV), MinGW-w64 assembler/linker, kernel32/msvcrt/winmm/
  ws2_32/bcrypt/shlwapi link set. --target windows-x86_64 (alias
  x86_64-pc-windows-gnu); .exe / .dll outputs. CI windows-pe-cross-compile job
  (MinGW + Wine) wired into the gate.

Fix the implicit end-of-main exit code on Windows: emit_exit and
emit_exit_with_result_reg now load the code into edi and call the __rt_sys_exit
(ExitProcess) shim directly, identical to an explicit exit($n), instead of returning
through the MinGW CRT. The CRT's exit() reaches the same rdi-consuming shim, so the
old return path left rdi holding leftover data and exited with a garbage nonzero code.

random_bytes(int $length): string:
- Cryptographically secure random byte string on every supported target
  (arc4random_buf / getrandom / BCryptGenRandom), fatal on entropy failure or a
  constant length below 1.
- Expressed as a single-source registry home file (src/builtins/math/random_bytes.rs)
  instead of the legacy hand-maintained tables; excluded from the legacy
  runtime-callable wrapper as an EIR-only builtin. Builtin docs + registry JSON
  regenerated.

Callable-invoker ABI on Windows x86_64:
- The descriptor-based runtime callable invoker read its descriptor from a
  hardcoded rdi, but on Windows the caller passes it in the MSx64 first arg
  (rcx); read it from int_arg_reg_name(target, 0) instead.
- The invoker clone helpers passed arguments to the __rt_array_clone_shallow /
  __rt_array_to_mixed / __rt_hash_clone_shallow / __rt_hash_to_mixed /
  __rt_array_new runtime helpers via int_arg_reg_name, which returns the target
  user-call ABI (rcx/rdx on Windows); those helpers use the System V AMD64 ABI on
  every x86_64 target and read rdi/rsi. Add runtime_int_arg_reg_name (SysV on all
  x86_64, platform ABI on AArch64) and route the four helper call sites through
  it. No-op on Linux and AArch64.

# Conflicts:
#	CHANGELOG.md
#	CLAUDE.md
#	docs/internals/builtins/math/mt_rand.md
#	docs/internals/builtins/math/rand.md
#	docs/internals/builtins/math/random_int.md
#	src/codegen/abi/mod.rs
#	src/codegen/builtins/arrays/call_user_func_array.rs
#	src/codegen/builtins/system/exit.rs
#	src/codegen/callable_dispatch.rs
#	src/codegen/prescan.rs
#	src/codegen/runtime_callable_invoker.rs
#	src/codegen_support/abi/bootstrap.rs
#	src/codegen_support/platform/windows_transform.rs
#	src/codegen_support/runtime/arrays/random_bytes.rs
#	src/codegen_support/runtime/emitters.rs
#	src/pipeline.rs
Run the codegen test suite under ELEPHC_TEST_TARGET=windows-x86_64 via
MinGW + Wine in a 16-shard CI matrix, emit a per-shard JUnit report
(profile.ci.junit), and compare each shard's failing-test set against a
curated allow-list of known-good Windows tests (3137 tests) plus a known
-failures list. The gate fails only when a previously-passing (allow-listed)
test regresses or a known-failure unexpectedly passes, so the full-suite
parity percentage stays informational. Adds scripts/gen_windows_codegen_
allowlist.py to regenerate the lists from a full run, and drops 2 wine-flaky
include_paths tests from the allow-list with retry tolerance.
…e path constants

Windows x86_64 P0 runtime bundle (stacked on the PE32+ base):

* `__rt_winsock_init` (WSAStartup MAKEWORD(2,2)) hooked into the main
  wrapper before `__elephc_main`, and `__rt_winsock_cleanup` (WSACleanup)
  into `__rt_sys_exit` before ExitProcess — socket shims previously called
  msvcrt socket funcs with no Winsock initialization. argc/argv are
  spilled across the init call (rcx/rdx are volatile on MSx64).
* `__rt_sys_access` via GetFileAttributesA (INVALID_FILE_ATTRIBUTES -> -1)
  plus a C-symbol `access` stub.
* `__rt_sys_ftruncate` via SetFilePointerEx + SetEndOfFile (fd spilled
  across the seek) plus a C-symbol `ftruncate` stub so the existing
  `call ftruncate` lowering resolves on Windows with no lowering change.
* C-symbol `umask` no-op stub (mirrors php-src Windows behavior).
* Transform routing for syscall 21 (access), 77 (ftruncate), and 82
  (rename) — the `__rt_sys_rename` shim was previously orphaned.

Target-aware path constants via the existing `PHP_OS` ConstRef precedent:
`PHP_EOL`, `DIRECTORY_SEPARATOR`, `PATH_SEPARATOR` now resolve in
`prescan::collect_constants` using new `Platform::php_eol()` /
`directory_separator()` / `path_separator()` helpers (Windows: CRLF, "\\",
";"; elsewhere: LF, "/", ":"). macOS/Linux behavior is unchanged.

`emit_shim_exit` prologue corrected to `sub rsp, 48` (N % 16 == 0) after
`and rsp, -16` so WSACleanup and ExitProcess stay 16-byte aligned; a
regression test locks the alignment invariant.
@Guikingone Guikingone closed this Jul 8, 2026
@Guikingone
Guikingone deleted the bisect/code-a6cf1953d branch July 8, 2026 12:33
Guikingone added a commit that referenced this pull request Aug 14, 2026
…ir searched as an empty haystack

Inside eval(), every array-taking builtin read whatever it was handed. A `false`
from a failed `scandir()` flowed into `in_array()`, `array_merge()`, `array_values()`
and the rest as if it were an empty array, and the caller's `catch (TypeError)`
never ran:

    eval('$d = @scandir("/nope");
          try { in_array("x", $d); echo "no-throw"; }
          catch (TypeError $e) { echo $e->getMessage(); }');

    php    in_array(): Argument #2 ($haystack) must be of type array, false given
    elephc no-throw

The COMPILED side already threw — `ARRAY_OR_FALSE_ARG_SITES` in
src/ir_lower/expr/array_builtin_args.rs wraps every `array|false` argument slot —
so the two backends disagreed on the same source. The by-reference receivers were
worse than silent: `sort($d)` on a `false` was a hard `RuntimeFatal`, an UNCATCHABLE
failure where php throws.

MEASURED against `php -n` 8.5.6. php's wording is not uniform, and that is the whole
reason this needs a table rather than one rule:

  count(false)               count(): Argument #1 ($value) must be of type Countable|array, false given
  in_array("x", false)       in_array(): Argument #2 ($haystack) must be of type array, false given
  array_search("x", false)   array_search(): Argument #2 ($haystack) must be of type array, false given
  sort($false)               sort(): Argument #1 ($array) must be of type array, false given
  array_merge(false, [])     array_merge(): Argument #1 must be of type array, false given
  array_merge([], false)     array_merge(): Argument #2 must be of type array, false given
  array_diff(false, [])      array_diff(): Argument #1 ($array) must be of type array, false given
  array_diff([], false)      array_diff(): Argument #2 must be of type array, false given
  array_map("strlen", false) array_map(): Argument #2 ($array) must be of type array, false given
  array_map(null, [], false) array_map(): Argument #3 must be of type array, false given
  array_values(false)        array_values(): Argument #1 ($array) must be of type array, false given

`array_merge()` is FULLY variadic, so even argument #1 carries no `($name)` segment,
where `array_diff`, `array_intersect`, their `_key` twins and `array_map` DO name
their first array and leave only the tail unnamed. `count()` alone expects
`Countable|array`, and a `Countable` object is still accepted.

The VALUE names itself, and not the way `gettype()` spells it:

  false -> false    true -> true      null -> null       int -> int
  float -> float    string -> string  resource -> resource
  object -> its own CLASS name (stdClass, C, ...), never "object"

Design. One shared module, builtins/array/array_arg_check.rs, carries a table that
mirrors the compiled backend's entry for entry, plus the type namer and three entry
points (`eval_check_array_args`, `eval_expect_countable_arg`,
`eval_expect_sort_array_arg`). All of them throw through eval's normal Throwable
channel, `eval_throw_type_error`, so the result is catchable.

Eval reaches these builtins by two roads and both had to be closed. The
evaluated-argument road has every operand in hand, so ONE table-driven sweep sits in
the area dispatcher `values_dispatch.rs`. The direct road evaluates inside each leaf
builtin, so the check sits there, driven by the same table.

Placement is measured, not assumed: php evaluates EVERY argument before the type
check throws — `array_slice(false, side())` still runs `side()` — so each site waits
for its whole operand list instead of checking the moment the array is in hand.

The by-reference family shares one real choke point,
`eval_array_mutation_lvalue_arg`, which had been answering `RuntimeFatal`. Giving it
the builtin's name turns that fatal into php's TypeError for the whole family at
once — sort, rsort, asort, arsort, ksort, krsort, natsort, natcasesort, shuffle,
usort, uasort, uksort, array_push, array_pop, array_shift, array_unshift,
array_splice, array_walk, end, next, prev and reset — every one of which php words
identically as `Argument #1 ($array)`, measured.

A class DECLARED INSIDE the eval has no runtime class cell to read: naming it from
the cell alone reported the backing `stdClass`, so the namer asks the eval context
first, exactly as `::class` already does.

Tests. Six magician unit tests in interpreter/tests/builtins_arrays_type_errors.rs
cover count (including the Countable object it must still accept), in_array across
all six scalar types, the variadic naming split, the by-reference receivers, the
eval-declared class name, and the twelve single-array projections. Every asserted
string was produced by running that same fragment under `php -n` first, so the tests
pin php rather than the implementation. One end-to-end codegen test,
test_array_builtin_type_errors_match_between_compiled_and_eval, runs the scandir
case through both backends in one program and asserts the two halves agree.
Guikingone added a commit that referenced this pull request Aug 14, 2026
…rol arguments

Three of the CSV surface's own arguments were read wrong, and each is silent — the
call succeeds and writes or parses something php would not.

    $h = fopen("php://memory", "r+");
    fputcsv($h, ['a\"b']);

    php    "a\"b"    (7 bytes)
    elephc "a\""b"   (8 bytes)

MEASURED against `php -n` 8.5.6, byte for byte, before any code was written:

  fputcsv($h, ['a\"b'])                    22615c2262220a    ret=7
  fputcsv($h, ['a\"b'], ",", '"', "\\")    22615c2262220a    ret=7
  fputcsv($h, ['a\"b'], ",", '"', "")      22615c222262220a  ret=8
  fputcsv($h, ["a","b"], ...,      "")     612c62            ret=3
  fputcsv($h, ["a","b"], ... eol absent)   612c620a          ret=4
  fputcsv($h, ["a","b"], ...,  "\r\n")     612c620d0a        ret=5
  str_getcsv('"a\"b",c')                   ["a\"b","c"]
  str_getcsv('"a\"b",c', ",", '"', "")     ["a\b\"","c"]

1. The DEFAULT $escape.

`fputcsv()` pushed a zero escape byte when the argument was absent, and the helper
reads zero as RFC 4180 doubling — php 9.0's default, not today's. `str_getcsv()`
pushed zero for every absent control and let the runtime choose, which is right for
the separator and the enclosure and wrong for the escape for the same reason. So the
same row came out differently depending on whether the caller spelled the argument,
and `fgetcsv()` — which did default correctly — no longer read back what its own
sibling wrote. All three now name their default byte outright.

2. The EMPTY $eol.

php writes no terminator at all for `fputcsv(..., eol: "")` and answers 3 for "a,b";
elephc substituted "\n" and answered 4. The helper decided on `eol_len == 0`, which
an absent argument and an empty string share, and the pointer cannot break the tie
either: a materialized empty string leaves it undefined. The absent case now travels
as a NEGATIVE length, which no real string has, and the helper reads the sign.

3. The SIX control arguments.

php validates each one before it touches a record: a separator or enclosure must be
exactly one character, an escape must be empty or one character, and each function
names its OWN argument position — `fgetcsv()` counts a `$length` first, so its
separator is #3 where `str_getcsv()`'s is #2. elephc took the first byte and dropped
the rest in silence, so `fgetcsv($h, 0, "::")` parsed on `:`, and an EMPTY separator
or enclosure quietly selected the default. Eleven messages, verbatim from `php -n`:

  str_getcsv(): Argument #2 ($separator) must be a single character
  str_getcsv(): Argument #3 ($enclosure) must be a single character
  str_getcsv(): Argument #4 ($escape) must be empty or a single character
  fgetcsv(): Argument #3/#4/#5, fputcsv(): Argument #3/#4/#5, likewise

The lowering guards the LENGTH register the string ABI already holds, so no operand
is re-materialized, and the throw goes through `emit_value_error_unless` — catchable,
as `catch (ValueError $e)` in the test proves. The eval magician raises the same
messages through its pending-throw state; the three lowerings now share one control
table instead of three copies of the same byte-extraction loop.

Verified on linux-x86_64 BY EXECUTION (qemu, `elephc-x86-asm`), not by reading the
emitted assembly: the escape matrix, the eol trio and all eleven ValueErrors are
byte-identical to the aarch64 run and to php.
Guikingone added a commit that referenced this pull request Aug 14, 2026
…rol ignored setCsvControl

Two divergences in the same three lines of prelude, both silent.

    $w = new SplFileObject("out.csv", "w");
    $w->fputcsv(["c", "d"], ",", '"', "\\", "");

    php    3   and the file holds `c,d`
    elephc 4   and the file holds `c,d\n`

MEASURED against `php -n` 8.5.6 before any code was written:

  $w->fputcsv(["a","b"])                        ret=4
  $w->fputcsv(["c","d"], ",", '"', "\\", "")    ret=3
  $w->fputcsv(["e","f"], ",", '"', "\\", "|EOL|")  ret=8
  file bytes                                    612c620a632c64652c667c454f4c7c

  $f->setCsvControl(";"); $f->fgetcsv()         ["a","b","c"]
  $g->fgetcsv(";", '"', "\\")                   ["a","b","c"]
  $h->fgetcsv(",", '"', "\\")                   ["a;b;c"]

1. `$eol` was declared and then not passed.

The method's signature carried it; the body called `fputcsv()` with five arguments. So the
sixth never left the prelude: every row ended in "\n" whatever the caller asked for, and the
returned count reported a byte that was not written the way it claimed.

2. An omitted control read a LITERAL, not the object.

php resolves `$separator`, `$enclosure` and `$escape` against the object's `setCsvControl()`
state when the call leaves them out — that is the whole point of `setCsvControl()`, and what
the 8.4 deprecation text means by "either explicitly or via SplFileObject::setCsvControl()".
elephc spelled `","` / `'"'` / `"\\"` as the parameter defaults, so the state was ignored: a
file configured for semicolons came back as one field per line. The three now default to null
— the only way to tell an omitted control from a spelled one — and the body falls back on the
property.

3. The `$escape` deprecation is now VERSION-GATED.

PHP 8.4 introduced it; 8.2 and 8.3 print nothing. elephc emitted it at every `--php-version`,
which makes a program built for 8.3 noisier than the interpreter it imitates. The gate could
not be TESTED before either: the codegen fixture harness never recorded the profile the
`--php-version` helper was handed, so lowering read the default no matter what the test asked
for. It records it now, exactly as `pipeline::compile` does, and the notice count is checked on
stderr at 8.2, 8.3 and 8.4 — stdout carries none of it, so a stdout-only assertion reads the
same for a gate that works and a gate that does not exist.

STILL OPEN, and now precisely: the SPL methods raise the FUNCTION's `ValueError` wording
(`fgetcsv(): Argument #3 ($separator) ...`) where php raises the METHOD's
(`SplFileObject::fgetcsv(): Argument #1 ($separator) ...`), and the SPL deprecation variant —
`SplFileObject::fgetcsv(): the $escape parameter must be provided, as its default value will
change, either explicitly or via SplFileObject::setCsvControl()` — is still not emitted at all.
Both need a prelude-visible diagnostic entry point that does not exist yet.
Guikingone added a commit that referenced this pull request Aug 15, 2026
…uses, and never deprecated

php's `stream_context_set_option()` has a stub no arity check can stand in for: the second
parameter is `array|string`, and the fourth carries NO default at all — it is `UNKNOWN`, not
`null`. What php accepts therefore depends on the ARGUMENT'S TYPE, not just on how many were
passed, and it deprecated the two-argument form in 8.3:

    $c = stream_context_create();
    try { stream_context_set_option($c, 'http', 'header'); echo "no-throw"; }
    catch (ValueError $e) { echo $e->getMessage(); }

    php    stream_context_set_option(): Argument #4 ($value) must be provided when argument #2 ($wrapper_or_options) is a string
    elephc no-throw   (and the option was never stored)

MEASURED against `php -n` 8.5.6, one fragment per row, before any code was written:

  ($c, ['http' => [...]])          E_DEPRECATED, then bool(true)
  ($c, ['http' => [...]], null)    bool(true), and NO deprecation
  ($c, ['http' => [...]], 'x')     ValueError: Argument #3 ($option_name) must be null when argument #2 ($wrapper_or_options) is an array
  ($c, ['http' => [...]], null, 5) ValueError: Argument #4 ($value) cannot be provided when argument #2 ($wrapper_or_options) is an array
  ($c, 'http')                     E_DEPRECATED, then ValueError: Argument #3 ($option_name) cannot be null when argument #2 ($wrapper_or_options) is a string
  ($c, 'http', 'header')           ValueError: Argument #4 ($value) must be provided when argument #2 ($wrapper_or_options) is a string
  ($c, 'http', null)               ValueError: Argument #3 ($option_name) cannot be null when argument #2 ($wrapper_or_options) is a string
  ($c, 'http', null, 'v')          ValueError: Argument #3 ($option_name) cannot be null when argument #2 ($wrapper_or_options) is a string

Two of those rows are the whole reason the arity is not the rule:

- the DEPRECATION counts arguments and fires BEFORE the shape is judged, so `($c, 'http')`
  prints the notice and THEN throws, while `($c, [...], null)` — the same options array, one
  argument further along — stays quiet;
- the three-argument form is legal for an array and a refusal for a string, with a different
  message each way.

Before / after, compiled and under `eval()`:

  fragment                          compiled before  eval before        after (both)
  ($c, ['http' => [...]])           bool(true)       bool(true)         E_DEPRECATED + bool(true)
  ($c, ['http' => [...]], 'x')      bool(true)       uncatchable fatal  ValueError #3
  ($c, ['http' => [...]], null, 5)  COMPILE FAILURE  uncatchable fatal  ValueError #4
  ($c, 'http')                      bool(true)       bool(true)         E_DEPRECATED + ValueError #3
  ($c, 'http', 'header')            bool(true)       uncatchable fatal  ValueError #4
  ($c, 'http', null, 'v')           bool(true)       uncatchable fatal  ValueError #3

The array-with-a-fourth-argument row is worth naming: it did not answer wrongly, it refused to
COMPILE — `unsupported EIR backend feature: stream_context_set_option wrapper for PHP type
AssocArray`. php has a message for that shape, so the refusal belongs at run time where a
`catch` can see it.

The two backends decide the same rules from different places, and that difference is load
bearing. eval reads the wrapper's RUNTIME tag, so it applies php's table exactly. Codegen only
has the DECLARED type, so it stays conservative where the declaration is `Mixed`: a non-null
`$option_name` is a ValueError for BOTH families and only the wording is a guess there, while a
null one leaves the permissive array path alone rather than refusing what is most likely a
legal options array behind a variable. A `Mixed` operand is almost never a wrapper string.

The notice is version-gated at 8.3 like the rest of the diagnostic surface, so
`--php-version 8.2` stays silent — verified by compiling the same fragment under both.
Guikingone pushed a commit that referenced this pull request Aug 24, 2026
…ences

Bridge ABI (elephc-pdo): charset-aware elephc_pdo_real_escape_string (closes the
GBK/Big5 trailing-byte breakout), elephc_pdo_mysql_thread_id and
elephc_pdo_mysql_param_count from the handshake/prepared statement (no round
trips), and elephc_pdo_sql_has_multiple_statements as the one authoritative
multi-statement scanner. scan_my_comment no longer skips /*! ... */ executable
comments (they are live SQL), closing the comment-hidden separator bypass in
both the bridge and the mysqli guard.

mysqli prelude:
- #1 transaction $name is a SQL comment (COMMIT /*name*/), not a savepoint;
  $flags composed into START TRANSACTION / COMMIT / ROLLBACK; savepoint() /
  release_savepoint() + procedural aliases added; MYSQLI_TRANS_COR_* declared.
  commit(0,"tx") now actually commits (was RELEASE SAVEPOINT -> silent data loss).
- #2 query() rejects /*!-hidden multi-statements via the bridge scanner.
- #3 real_escape_string routes through the charset-aware bridge escape.
- #5 stmt execute refreshes the connection's affected_rows/insert_id/warning_count;
  transaction control resets them (affected_rows after commit is 0, like php).
- #6 unconnected-object ops raise php 8's 'not fully initialized' Error.
- #11 thread_id/charset from the handshake, zero connect-time queries.
- illegalstudio#12 param_count from the bridge (kills the divergent PHP ?-scanner).
- illegalstudio#13 stmt close/__destruct clear hasPending; get_result/store_result guard on close.
- illegalstudio#15 begin_transaction empty-$name ValueError raised before any SQL.
Guikingone added a commit that referenced this pull request Aug 29, 2026
…s, and typed its argument wrong

Two auditors named `stream_context_set_options()`'s missing refusal independently, as the wrong
thing to have left. Chasing it found worse than a missing message.

## Two crashes

    stream_context_set_options($c, json_decode("1"));         SIGBUS
    stream_context_set_options($c, json_decode("[1]", true)); Fatal error: heap memory exhausted

php raises a `TypeError` for the first and its options-FORM `ValueError` for the second. The merge
this form performs walks its argument as a HASH and nothing checked it first, so a boxed integer
was dereferenced as a container, and a packed array had its header read as a bucket count and
allocated until the heap was gone. BOTH spellings crashed.

The one-argument publisher had already been taught to judge type and shape before publishing. This
form went straight to the merge.

## The two spellings refuse DIFFERENTLY

    stream_context_set_options($c, 1)   TypeError: Argument #2 ($options) must be of type array, int given
    stream_context_set_option($c, 1)    ValueError: Argument #3 ($option_name) cannot be null when
                                        argument #2 ($wrapper_or_options) is a string
    stream_context_set_option($c, $obj) TypeError: Argument #2 ($wrapper_or_options) must be of type
                                        array|string, stdClass given
    stream_context_set_option($c, null) Deprecated: Passing null to parameter #2 ($wrapper_or_options)
                                        of type array|string is deprecated
                                        ...then the ValueError above

The singular declares `array|string`, so a SCALAR has a string form and is COERCED into a wrapper
name — the complaint then moves to the argument that was not supplied. Only a value with no string
form is a type error there, and it names `array|string`. A null earns php 8.1's non-nullable notice
first and is coerced to `""`.

So the refusal ladder gained a STYLE rather than a second copy, and the whole family — `create`,
`set_options`, `set_option`, `set_default`, `get_default` — is now identical to `php -n` 8.5.6 over
30 measured shapes.

## An array literal of BUILTIN calls typed every element `string`

    foreach ([json_decode("1"), json_decode('"x"'), json_decode("[1]", true)] as $v) {
        echo gettype($v);
    }
    php:    integer string array
    elephc: string  string string

Found while isolating the crash above, and much wider than it: `json_decode()` returns `mixed` and
the EIR recorded each element as `mixed`, but the LITERAL came out `array<string>`, so the loop
variable was a declared string and `gettype()` answered from the declaration rather than the value.
Nine wrong answers out of nine, in silence.

The element walk consults user functions and extern functions and then guesses SYNTACTICALLY, which
has no way to know a builtin. The checker had already decided every builtin call's type and keyed
it by span — the lowering context carries that map for exactly this reason. Both walks ask it now:
the indexed one, and the string-keyed one, which has its own copy of the same ladder and which the
two tests written for the crash caught a commit later.

## `php://filter/...` over a COMPUTED path

    fopen("php://filter/read=string.toupper/resource=" . $path, "r")
    php:    wrapper_type=PHP        elephc: plainfile

The other omission both auditors named. The `uri` and `stream_type` were already right; only the
identity was not, because the LITERAL form is re-stamped by a parser a computed path never reaches.

The fix goes where the identity is RECORDED rather than at a second open site: that helper already
overrides the caller's fallback for userspace and Phar backends, and the URI it is handed is the
authority php itself uses — a scheme lookup. A recorded URI beginning `php://` is the `php` wrapper
whatever opened it.

The first version fell through into the userspace arm emitted just below it and reported
`user-space`, which the probe caught immediately.

Streams surface sweep 46/46. The context family, the flock family, the wrapper metadata and the
user-wrapper position programs are all identical to php.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants