feat/strict php flag#545
Merged
Merged
Conversation
nahime0
force-pushed
the
feat/strict-php-flag
branch
from
July 16, 2026 12:43
2eae74a to
139ed5f
Compare
Add the `extension` flag to BuiltinSpec/builtin! and tag the 24 elephc-only builtins (ptr_*, zval_*, class_attribute_*, buffers). Migrate buffer_len/buffer_free from checker-resident arms to registry homes under src/builtins/pointers/, preserving messages and lowering. Preludes now call internal __elephc_ptr_* aliases instead of the PHP-visible pointer builtins; parity gates pin the extension set and forbid preludes from calling PHP-visible extensions.
New src/strict_php module: thread-local mode state plus an exhaustive AST audit (no wildcard arms) that rejects elephc-only syntax — ifdef, packed class, extern, ptr_cast<T>, buffer_new<T>, typed locals, ptr/buffer<T> annotations, and reserved __elephc_* names — collecting every violation with spans. The audit runs on the main file after parsing and on each included/autoloaded user file at its parse site, so injected compiler preludes are never audited. --strict-php conflicts with --define (defines only feed the rejected ifdef extension).
Under strict mode, extension builtins behave exactly as if they did not exist, matching the PHP interpreter: the catalog stops resolving them (function_exists() reports false, callable tables skip them, user code may redeclare the names), check_builtin falls through to user-function resolution, and the undefined-function diagnostic names the disabled extension. Internal __elephc_* aliases stay resolvable so injected preludes keep compiling. Also drops buffer_len/buffer_free from the legacy catalog list now that the registry owns them; buffer_new remains the one catalog-name-only entry.
In-process error tests cover the audit rejections, hidden builtins with the disabled-extension hint, user redeclaration of extension names, and include-file auditing (plus the include-variant false-positive regression). CLI tests drive the real binary: identical output for plain PHP, user-shadowed ptr_get, function_exists probes, prelude programs, --check composition, and the --define conflict. Adds the strict-php example (also valid under the PHP interpreter); the examples corpus lowers it with strict mode enabled to match its documented invocation.
CLI reference gains a Strict PHP mode section (scope, exemptions, the --define conflict, and the static-subset caveat); every beyond-php page notes its rejection under the flag; README/ROADMAP/AGENTS/CONTRIBUTING updated for the extension flag, the buffer builtin migration, and the prelude alias rule. Builtin docs regenerated: buffer_len/buffer_free now come from the registry (their hand-curated generator entries removed), and the internal __elephc_ptr_* alias pages are included.
Magician derives its extension set (raw_memory area + class_attribute_* symbols; 21 names, zval_* stays static-only) and filters it out of the single lookup choke point when strict mode is on, so eval dispatch, function_exists, and is_callable inside eval'd code behave like the PHP interpreter — calls hit the standard unknown-function fatal while user functions shadowing those names stay callable through the native table. Strictness reaches the bridge via a new __elephc_eval_set_strict_php FFI setter (thread-local flag, defaulting off) that codegen emits while initializing the eval context, only in strict compilations. A new cross-registry parity gate pins magician's extension set against the compiler registry, and public metadata APIs expose both sets.
…ct-php The AOT literal-eval plan walker treated ifdef blocks and typed local declarations as loweable statement shapes. Under --strict-php they now classify as bridge-requiring, so no elephc-only syntax inside an eval fragment can be AOT-compiled and silently execute: the fragment reaches the magician bridge, whose parser rejects the syntax at runtime exactly like the PHP interpreter (eval failures stay execute-time, never compile-time). The remaining extension nodes (ptr_cast, buffer_new, packed/extern declarations) already classified as fallback, and the AOT-eligible builtin allowlists contain only genuine PHP builtins.
CLI tests drive strict binaries through both eval paths: literal and dynamic fragments calling extension builtins fail at runtime with the standard unknown-function fatal, introspection inside eval agrees with the AOT surface (and keeps the extensions without the flag), and a user function shadowing an extension name stays callable from eval'd code.
CLI reference explains how strict mode reaches eval() (runtime fatals, introspection, execute-time semantics preserved, shadowing); ROADMAP records the eval coverage; CONTRIBUTING documents magician's derived extension set and the cross-registry parity gate.
ROADMAP is the planning document: implementations only flag items already present, never add new entries; every implementation lands a user-facing bullet in CHANGELOG under [Unreleased] (creating the section and its compare link when missing). Drop the two strict-php bullets that were added to the roadmap; the changelog entry covers them.
The builtin JSON export now carries the registry's extension flag (with the eval-derived flag for appended records and a hand-table flag for the catalog-name-only buffer_new), the docs registry threads it through as is_extension, and every extension builtin's user page gains a 'Strict PHP mode: hidden' line in its Availability section — 25 pages: ptr_*, zval_*, buffer_*, class_attribute_*. The docs link audit now strips in-page anchors before checking filesystem targets.
Attribute args are ordinary expressions, but the audit ignored every attributes field (statement, parameter, property, method, constant, enum case), so #[Foo(buffer_new<int>(4))] compiled and ran in strict mode. Every attribute group now goes through the expression audit; regression tests cover function/param/property/method positions and reserved __elephc_* calls. Also documents why the reserved-prefix check applies only to free-function names (the compiler never synthesizes __elephc_* into class/method/property symbol spaces).
The original edit swallowed the start of the following mb_strlen() bullet, leaving its tail glued to the strict-php entry.
The memoized metadata snapshot read the strict-filtered catalog, so a first call on a strict-enabled thread would cache the filtered view for every later consumer. The catalog now exposes the unfiltered union (all_supported_builtin_function_names) for metadata consumers, with the strict filter applied on top for compilation surfaces; a unit test pins the unfiltered set under strict mode.
The main-file audit now runs after magic-constant substitution like the include/autoload audit sites (and still before conditional consumes ifdef nodes), matching the module contract. Strict-mode test fixtures hold an RAII guard (elephc and magician sides) so a panicking fixture restores the previous state during unwinding instead of leaking it to later fixtures on the same thread.
nahime0
force-pushed
the
feat/strict-php-flag
branch
from
July 17, 2026 09:51
5fcf4e8 to
1d6b7f4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
--strict-phpcompiler flag: the program is accepted only if it uses PHP-compatible constructs. Every elephc extension — syntax, builtins, and theireval()reachability — either becomes a compile error or behaves exactly as it would under the PHP interpreter (i.e., as if it did not exist).What it does
Extension syntax is rejected at compile time. A dedicated AST audit (
src/strict_php/audit.rs, exhaustive matches with no wildcard arms) reports every violation with its span:ifdef,packed class,externdeclarations,ptr_cast<T>,buffer_new<T>, typed local declarations (int $x = 5;),ptr/buffer<T>type annotations, and compiler-reserved__elephc_*names. The audit runs on the main file and on everyinclude/required and autoloaded user file at its parse site; compiler-injected preludes are exempt.Extension builtins behave as nonexistent.
ptr_*,zval_*,buffer_*,class_attribute_*(declared via a newextensionflag in thebuiltin!registry) are hidden from the catalog:function_exists()returnsfalse, calling one is an undefined function (the diagnostic names the disabled extension), and user code may declare its own functions with those names — matching PHP, where redeclaring them is legal.is_realis intentionally kept (treated as PHP).eval()is covered with PHP's execute-time semantics. Strict binaries mark the magician bridge via a new__elephc_eval_set_strict_phpFFI setter emitted at eval-context init; magician filters its extension set (raw-memory area + attribute introspection, pinned cross-registry) out of the single lookup choke point, so calls inside eval'd code hit the standard unknown-function fatal andfunction_exists/is_callableagree with the AOT surface. The AOT literal-eval classifiers route extension syntax to the bridge so nothing elephc-only is ever AOT-executed. Fragments are never rejected at compile time — PHP only fails eval'd code when it executes. User functions shadowing extension names stay callable from eval through the native table.--defineconflicts with the flag (defines only feed the rejectedifdefextension), and strict mode composes with--check.Enabling changes
buffer_len/buffer_freemigrated from checker-resident arms to registry homes undersrc/builtins/pointers/(messages and lowering preserved);buffer_newstays catalog-name-only since its call form is dedicated syntax.internal: true__elephc_ptr_*aliases instead of PHP-visible pointer builtins, so the catalog filter needs no notion of code origin.Guard rails
EXPECTED_EXTENSION_BUILTINSpins the AOT extension set;extension_builtin_sets_agree_across_registriespins magician's derived set against it (minus static-onlyzval_*);preludes_never_call_php_visible_extension_builtinsscans prelude sources.Docs
CLI reference gains a “Strict PHP mode” section (scope, eval semantics, the
--defineconflict, and the caveat that strict guarantees PHP-compatible constructs, not full static-subset semantic parity); everydocs/beyond-php/page notes its rejection under the flag; newexamples/strict-php/runs identically underelephc --strict-phpand the PHP interpreter; generated builtin docs regenerated.Test plan
--check/--definecomposition