Skip to content

feat/strict php flag#545

Merged
nahime0 merged 18 commits into
mainfrom
feat/strict-php-flag
Jul 17, 2026
Merged

feat/strict php flag#545
nahime0 merged 18 commits into
mainfrom
feat/strict-php-flag

Conversation

@nahime0

@nahime0 nahime0 commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

Adds a --strict-php compiler flag: the program is accepted only if it uses PHP-compatible constructs. Every elephc extension — syntax, builtins, and their eval() 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, extern declarations, 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 every include/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 new extension flag in the builtin! registry) are hidden from the catalog: function_exists() returns false, 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_real is 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_php FFI 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 and function_exists/is_callable agree 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.

--define conflicts with the flag (defines only feed the rejected ifdef extension), and strict mode composes with --check.

Enabling changes

  • buffer_len/buffer_free migrated from checker-resident arms to registry homes under src/builtins/pointers/ (messages and lowering preserved); buffer_new stays catalog-name-only since its call form is dedicated syntax.
  • The image/web preludes now call 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_BUILTINS pins the AOT extension set; extension_builtin_sets_agree_across_registries pins magician's derived set against it (minus static-only zval_*); preludes_never_call_php_visible_extension_builtins scans prelude sources.
  • All filters are gated on the strict flag: default-mode behavior is unchanged (verified by the existing eval-dispatch, buffer, image, web, and parity suites).

Docs

CLI reference gains a “Strict PHP mode” section (scope, eval semantics, the --define conflict, and the caveat that strict guarantees PHP-compatible constructs, not full static-subset semantic parity); every docs/beyond-php/ page notes its rejection under the flag; new examples/strict-php/ runs identically under elephc --strict-php and the PHP interpreter; generated builtin docs regenerated.

Test plan

  • 24 in-process error tests (audit rejections, hidden builtins + hint, shadowing, include auditing and the include-variant false-positive regression)
  • 14 CLI end-to-end tests against the real binary, including both eval paths, introspection coherence, prelude programs, and --check/--define composition
  • Magician unit tests (931 passing) and cross-registry/prelude parity gates
  • Focused regression suites: buffers (28), pointers (38), image (165), eval dispatch (133), eval parity (18), IR smoke (255), full error suite (1071), lib (684) — all green, zero-warning build

@github-actions github-actions Bot added area:builtins Touches PHP builtin declarations or emitters. area:magician Touches eval, include execution, or elephc-magician. area:web Touches --web mode, its prelude, or elephc-web. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:xl Very large pull request that needs deliberate review planning. type:feature Introduces new user-visible behavior or capabilities. labels Jul 15, 2026
@nahime0
nahime0 force-pushed the feat/strict-php-flag branch from 2eae74a to 139ed5f Compare July 16, 2026 12:43
nahime0 added 18 commits July 17, 2026 11:27
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
nahime0 force-pushed the feat/strict-php-flag branch from 5fcf4e8 to 1d6b7f4 Compare July 17, 2026 09:51
@nahime0
nahime0 merged commit 0b26415 into main Jul 17, 2026
113 checks passed
@nahime0
nahime0 deleted the feat/strict-php-flag branch July 17, 2026 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:builtins Touches PHP builtin declarations or emitters. area:magician Touches eval, include execution, or elephc-magician. area:web Touches --web mode, its prelude, or elephc-web. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:xl Very large pull request that needs deliberate review planning. type:feature Introduces new user-visible behavior or capabilities.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant