Skip to content

feat(builtins): add get_debug_type - #1937

Open
JGamboa wants to merge 10 commits into
illegalstudio:mainfrom
JGamboa:feat/get-debug-type
Open

JGamboa wants to merge 10 commits into
illegalstudio:mainfrom
JGamboa:feat/get-debug-type

Conversation

@JGamboa

@JGamboa JGamboa commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Third family of the missing-builtins PR from the #1886 split (review item 19). Before this PR, get_debug_type() was undefined. It is now a real builtin:

  • it has a shared contract, so it gets generated docs, get_defined_functions()['internal'] lists it, and ReflectionFunction::isInternal() returns true;
  • it has a builtin! home (src/builtins/types/get_debug_type.rs);
  • it has a native runtime helper, __rt_debug_type_name, on AArch64 and x86_64;
  • it has a Magician eval implementation.

It follows PHP 8.5 for every kind of value:

  • Scalars and arrays use the zend type names: null, bool, int, float, string, array.
  • An object reports its class name. Closures and first-class callables report Closure, enums report the enum name, and generators report Generator. An incomplete unserialized object reports __PHP_Incomplete_Class.
  • A resource reports resource (stream), resource (stream filter) or resource (stream-context), and resource (closed) once it has been closed, whether the value is boxed or not.
  • An anonymous class gets php-src's prefix: its parent, else its first declared interface, else class, followed by @anonymous. That gives class@anonymous, App\Base@anonymous and App\Shape@anonymous. The implicit Stringable that a __toString() method adds does not count.

Anonymous-class names in the runtime metadata

elephc names anonymous classes class@anonymous#N (namespaced, e.g. App\class@anonymous#0). The flattened interface list cannot tell a declared Stringable apart from the implicit one. To keep the change small:

  • ClassInfo gets one field, anonymous_debug_name: Option<String>. The checker computes it from the declared extends/implements and leaves it None for named classes.
  • The runtime data gets one dense table, _class_debug_name_entries. It has the same row shape and bound (_class_name_count) as _class_name_entries. A named class reuses its existing _class_name_<id> bytes. Only an anonymous class adds its own literal. The cost is 16 bytes per emitted class id.

get_class(), var_dump() and Reflection are unchanged.

Eval

  • Inside eval, get_debug_type() names eval-declared anonymous classes from their EvalClass parent and interfaces.
  • For compiled objects, eval reads the new table through __elephc_eval_value_object_debug_class_name. This is a twin of the existing class-name wrapper; both are now emitted by one parametrized function per target.
  • Compiled code with a persistent eval context sends object and boxed operands through the existing __elephc_eval_object_class_name bridge with a new lookup kind (get_debug_type). Objects of eval-declared classes, which own no generated class id, are therefore named correctly.

Repro (no issue yet)

No issue lists get_debug_type() (#859 covers preg_quote, version_compare, uniqid and stristr).

<?php
echo get_debug_type(1), " ", get_debug_type(new class extends ArrayObject {}), "\n";
// PHP 8.5: int ArrayObject@anonymous
// main:    Undefined function: get_debug_type

Known differences

  • PHP spells the anonymous-class prefix as it is written in the source (new class extends arrayobject {} gives arrayobject@anonymous). elephc uses the declared spelling of the class (ArrayObject@anonymous), because name resolution canonicalizes names before the checker sees them.
  • These answers come from existing resource typing that this PR does not change, and get_resource_type() agrees with them:
    • stream_context_create() is typed as a stream resource, so it reports resource (stream);
    • a resource stored in an array literal next to stream_context_create() is boxed as an int.

Tests

On main 0842fca every new test fails with Undefined function (or, for Reflection, Function get_debug_type() does not exist). On this branch they pass:

  • codegen::casts_and_constants::get_debug_type::test_get_debug_type_names_scalars_arrays_and_objects
  • codegen::casts_and_constants::get_debug_type::test_get_debug_type_names_anonymous_classes_like_php
  • codegen::casts_and_constants::get_debug_type::test_get_debug_type_names_open_and_closed_resources
  • codegen::casts_and_constants::get_debug_type::test_get_debug_type_is_an_internal_function. It covers eval in both directions, string callables, call_user_func, array_map, first-class callables, case-insensitive and namespaced calls, get_defined_functions()['internal'] and isInternal().
  • error_tests::misc::string_and_type_builtins::test_error_get_debug_type_wrong_args

Every expected output was measured on PHP 8.5.10. Other changes:

  • The contract-count gates were updated (+1).
  • The builtin docs and registries were regenerated with the upstream scripts.
  • examples/core-introspection shows the builtin.
  • The x86_64 and Linux AArch64 runtime helpers and data were assembled with clang, but were only run on macOS AArch64. CI covers Linux execution.

This PR and the open #1924 / #1925 builtin PRs are independent; each is based on main. They bump the same contract counts (registry.rs/support.rs) and regenerate the same docs, so whichever lands later needs a rebase and a fresh docs regeneration. I will do it.

Refs #1886

AI assistance

This PR was prepared with substantial help from Claude Code (Anthropic). The behavior for each value kind was checked against PHP 8.5 before and after implementation.

🤖 Generated with Claude Code

@github-actions github-actions Bot added area:builtins Touches PHP builtin declarations or emitters. area:magician Touches eval, include execution, or elephc-magician. area:runtime Touches runtime helpers, GC, ownership, or bridge runtimes. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:xl Very large pull request that needs deliberate review planning. target:linux-x86_64 Contains behavior specific to the Linux x86_64 target. type:feature Introduces new user-visible behavior or capabilities. labels Oct 20, 2026
@greptile-apps

greptile-apps Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The change reaches PHP programs that call the new builtin in compiled code or `eval()`.

Fix runtime callback names for eval-created objects before merging.

Fix All in Claude CodeFindings

  1. P1 Callbacks lose eval class names ▶
Fix with agent prompt
### Issue 1
src/ir/runtime_fn.rs:2108
A runtime-selected `get_debug_type` callback reports `stdClass` instead of `class@anonymous` for an object returned by `eval('return new class {};')`. The newly enabled callback uses a generated wrapper without an eval context. It therefore reads the native backing class instead of the eval-declared class, giving logs and error messages the wrong type.

Make the wrapper consult the object's eval context and add a regression test with an eval-created object.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds get_debug_type() to compiled PHP and eval.

  • get_debug_type() returns PHP-style names for values, objects, and resources.
  • Anonymous classes get debug names from their parent or first declared interface.
  • eval() can name values with the same debug-type rules as compiled code.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["get_debug_type(value)"] --> B{"Call path"}
    B --> C["Direct call with eval context"]
    C --> D["Read eval class name"]
    B --> E["Runtime-selected callback"]
    E --> F["Generated wrapper without eval context"]
    F --> G["Read native backing class name"]
Loading

Reviews (4) · Last reviewed commit: "docs(builtins): regenerate builtin docs ..." · Reviewed by Greptile

Comment thread src/codegen_support/runtime/eval_bridge/x86_64_values_classes.rs
Comment thread src/codegen/lower_inst/builtins/scalar_metadata.rs Outdated
Comment thread src/ir/runtime_fn.rs Outdated
Comment thread crates/elephc-builtin-contract/src/catalog_data.rs
Comment thread src/codegen_support/runtime/strings/debug_type_name.rs
Comment thread crates/elephc-builtin-contract/src/catalog_data.rs Outdated
Add get_debug_type() as a registry builtin with a shared contract, a
builtin! home, the __rt_debug_type_name runtime helper on AArch64 and
x86_64, and a Magician eval implementation.

Anonymous classes are named the way php-src does (class@anonymous,
Parent@anonymous, Interface@anonymous from the first declared
interface). The checker records that spelling in
ClassInfo::anonymous_debug_name and the runtime data emits it in a new
_class_debug_name_entries table parallel to _class_name_entries. Eval
reads the table through __elephc_eval_value_object_debug_class_name,
and compiled code with an eval context asks the bridge through a new
get_debug_type class-lookup kind.
The C-ABI wrappers behind get_class() and get_debug_type() inside eval
rejected the reserved incomplete-class id (-2) with their unsigned bounds
check, so an unserialized object of an unknown class made the eval call
fail. Answer __PHP_Incomplete_Class on both targets, as the native
helpers already do.
… get_debug_type

An iterable operand is a type-erased pointer to an array or a Traversable
object, but get_debug_type() always answered "array" for it. Dispatch on
the heap kind at run time so a Generator or ArrayIterator reports its
class name.
The answer depends on a boxed cell's tag, an object's class id and a
resource payload that an explicit close flips, so the call is not
state-independent. Declare READS_HEAP like get_class().
@JGamboa
JGamboa force-pushed the feat/get-debug-type branch from 228426f to 854919b Compare October 21, 2026 06:04
Comment thread src/builtins/types/get_debug_type.rs
Without a php code fence the generated page joined the example lines into one Markdown paragraph. Regenerate the builtin docs and registry.
get_debug_type was missing from the generic runtime-callable list, so its
callable policy was static-only: a callback name known only at run time
(array_map($name, ...), $name(...), call_user_func($name, ...)) failed with
"does not name a supported callable". Like gettype it takes one mixed value
and answers a static type name, so the generic wrapper applies.
Comment thread src/ir/runtime_fn.rs
@@ -2099,6 +2105,7 @@ impl RuntimeFnId {
| RuntimeFnId::ArrayProduct
| RuntimeFnId::CloneWith
| RuntimeFnId::Count
| RuntimeFnId::GetDebugType

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Callbacks lose eval class names

A runtime-selected get_debug_type callback reports stdClass instead of class@anonymous for an object returned by eval('return new class {};'). The newly enabled callback uses a generated wrapper without an eval context. It therefore reads the native backing class instead of the eval-declared class, giving logs and error messages the wrong type.

Make the wrapper consult the object's eval context and add a regression test with an eval-created object.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ir/runtime_fn.rs
Line: 2108

Comment:
**Callbacks lose eval class names**

A runtime-selected `get_debug_type` callback reports `stdClass` instead of `class@anonymous` for an object returned by `eval('return new class {};')`. The newly enabled callback uses a generated wrapper without an eval context. It therefore reads the native backing class instead of the eval-declared class, giving logs and error messages the wrong type.

Make the wrapper consult the object's eval context and add a regression test with an eval-created object.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex Fix in Cursor

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:runtime Touches runtime helpers, GC, ownership, or bridge runtimes. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:xl Very large pull request that needs deliberate review planning. target:linux-x86_64 Contains behavior specific to the Linux x86_64 target. type:feature Introduces new user-visible behavior or capabilities.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant