Skip to content

Add Describable interface for human-readable source descriptions#53

Merged
jcmvrij merged 6 commits into
0.xfrom
claude/rebase-add-describable-interfaces-mHnkq
May 6, 2026
Merged

Add Describable interface for human-readable source descriptions#53
jcmvrij merged 6 commits into
0.xfrom
claude/rebase-add-describable-interfaces-mHnkq

Conversation

@robertvansteen
Copy link
Copy Markdown
Contributor

Description

This PR introduces a new Describable interface that enables human-readable descriptions of the source tree, suitable for textualizing AST structures for AI consumption. All major source and pattern classes now implement this interface with appropriate describe() methods.

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Code refactoring

Changes Made

  • Created new Describable interface in src/Describable.php with a describe(): string method
  • Implemented Describable on all source classes:
    • StaticSource - exports values using SebastianBergmann\Exporter (e.g., 'hello', 42, null, true)
    • SymbolSource - describes names with optional namespace (e.g., price, math.pi)
    • TypeDefinition - describes source with type annotation (e.g., price (as number))
    • InfixExpression - describes binary operations with parentheses for nested expressions (e.g., price * (1 - discount))
    • UnaryExpression - describes unary operations (e.g., !active, -5)
    • MemberAccessSource - describes property access chains (e.g., user.address.city)
    • MatchExpression - describes match statements (e.g., match status { 'on' => true, _ => false })
    • MatchArm - describes pattern-to-expression mappings (e.g., 1 => 'one')
  • Implemented Describable on all pattern classes:
    • LiteralPattern - exports literal values
    • WildcardPattern - describes as underscore (_)
    • ExpressionPattern - delegates to inner source description
  • All implementations gracefully fall back to class short names when encountering non-describable sources
  • Added comprehensive test suite (DescribableTest.php) with 40+ test cases covering all classes and edge cases

Testing

  • Added new tests for the changes (40+ test cases in DescribableTest.php)
  • All existing tests pass
  • Tests cover normal cases, nested expressions, and fallback behavior for non-describable sources

Code Quality Checklist

  • My code follows the project's coding standards
  • I have added PHPDoc comments where necessary
  • My changes generate no new warnings or errors
  • I have added tests that prove my feature works
  • New and existing unit tests pass locally with my changes

Documentation

  • I have updated the README.md (if applicable)
  • I have updated the CHANGELOG.md
  • I have added/updated code examples (if applicable)

Breaking Changes

N/A - This is a purely additive change. All new implementations are on existing classes but don't modify their public API or behavior.

Additional Notes

The implementation uses reflection as a fallback mechanism when sources don't implement Describable, ensuring robustness when dealing with custom or third-party source implementations. The SebastianBergmann\Exporter is used for consistent value formatting across StaticSource and LiteralPattern.


By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.

https://claude.ai/code/session_01KErFD38kaqTb4JGusuSirg

claude added 5 commits April 30, 2026 08:27
Introduces a new optional Describable interface with a describe(): string
method that produces human-readable descriptions of Source trees. All five
core Sources implement it: StaticSource, SymbolSource, TypeDefinition,
InfixExpression, and UnaryExpression. Composite sources recursively
describe their children, falling back to the class short name for
non-Describable sources.

https://claude.ai/code/session_011wv1MvWN6N2u8bYZZz8QA8
- Replace preg_replace with str_ends_with/substr in TypeDefinition to
  avoid PHPStan error from preg_replace's nullable return type
- Add UsesClass attributes for Type classes used in DescribableTest
- Add tests for non-Describable fallback branches in TypeDefinition,
  InfixExpression (both left and right), and UnaryExpression

https://claude.ai/code/session_011wv1MvWN6N2u8bYZZz8QA8
Rework all Describable implementations to produce human-readable natural
language descriptions instead of terse structured notation:

- StaticSource: "the value 42"
- SymbolSource: "the symbol 'price'"
- TypeDefinition: "the symbol 'price' as a number"
- InfixExpression: "the value 1 plus the value 2" with operator word
  mappings and parenthetical grouping for nested expressions
- UnaryExpression: "the negation of the symbol 'active'"

https://claude.ai/code/session_011wv1MvWN6N2u8bYZZz8QA8
Use raw values and operator symbols instead of verbose natural language:
- StaticSource: 42, 'hello', null
- SymbolSource: price, math.pi
- TypeDefinition: price (as number)
- InfixExpression: price * quantity, with parens for nesting
- UnaryExpression: !active, -5

Complex example: price (as number) * (1 - rates.discount (as number))

https://claude.ai/code/session_011wv1MvWN6N2u8bYZZz8QA8
Adds describe() to MemberAccessSource, MatchExpression, MatchArm,
LiteralPattern, WildcardPattern and ExpressionPattern so the full
source tree can be textualised for AI consumption.

https://claude.ai/code/session_01KErFD38kaqTb4JGusuSirg
@robertvansteen robertvansteen changed the base branch from add-describable-interface-0.4-api to 0.x April 30, 2026 08:41
@robertvansteen robertvansteen marked this pull request as ready for review April 30, 2026 08:42
@robertvansteen robertvansteen requested a review from jcmvrij April 30, 2026 08:42
Comment on lines +17 to 24
public function describe(): string
{
$operand = $this->operand instanceof Describable
? $this->operand->describe()
: (new \ReflectionClass($this->operand))->getShortName();

return sprintf('%s%s', $this->operator, $operand);
}
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.

This could get confusing with infix.
! ($a || $b) would show as ! $a || $b

Suggested change
public function describe(): string
{
$operand = $this->operand instanceof Describable
? $this->operand->describe()
: (new \ReflectionClass($this->operand))->getShortName();
return sprintf('%s%s', $this->operator, $operand);
}
public function describe(): string
{
$operand = $this->operand instanceof Describable
? $this->operand->describe()
: (new \ReflectionClass($this->operand))->getShortName();
if ($this->operand instanceof InfixExpression) {
$operand = sprintf('(%s)', $operand);
}
return sprintf('%s%s', $this->operator, $operand);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — applied the suggestion in bfce70d and added a regression test (unary_wraps_infix_operand_in_parentheses) asserting !(a || b).


Generated by Claude Code

}

#[Test]
public function type_definition_with_non_describable_source_falls_back_to_class_name(): void
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.

None of the falls_back_to_class_name tests actually test it falls back to class name. I could change the fallback logic and tests still pass.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right. Replaced the anonymous classes with named fixtures (UndescribableSource, UndescribablePattern) and switched the assertions to assertSame against the full expected output (e.g. 'UndescribableSource + 1', '!UndescribableSource'), so the class-name fallback is now actually verified. Done in bfce70d.


Generated by Claude Code

- Wrap UnaryExpression's operand in parens when it's an InfixExpression
  so !($a || $b) renders as !(a || b) instead of !a || b.
- Replace anonymous-class fallback tests with named fixtures
  (UndescribableSource, UndescribablePattern) and assert exact output
  so the class-name fallback is actually verified.

https://claude.ai/code/session_01KErFD38kaqTb4JGusuSirg
@robertvansteen robertvansteen requested a review from jcmvrij April 30, 2026 12:32
@jcmvrij jcmvrij merged commit e6506e6 into 0.x May 6, 2026
3 checks passed
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.

3 participants