diff --git a/README.md b/README.md index 0f12ff4..5c50bde 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,14 @@ Opt-in resolution tracing for [gosuperscript/axiom](https://github.com/gosupersc composer require gosuperscript/axiom-tracing ``` +Requires `gosuperscript/axiom ^0.4.0`. + ## Usage ### Conditional tracing with callback ```php +use Superscript\Axiom\Context; use Superscript\Axiom\Resolvers\DelegatingResolver; use Superscript\Axiom\Tracing\Tracing; use Superscript\Axiom\Tracing\TracedResult; @@ -30,18 +33,28 @@ if ($resolver instanceof TracingResolver) { }); } -// Calling code is unchanged -$result = $resolver->resolve($source); +// Calling code is unchanged — resolve takes a Source and a Context. +$result = $resolver->resolve($source, new Context()); +``` + +### Tracing an Expression + +```php +use Superscript\Axiom\Expression; + +$expression = new Expression($source, $resolver); +$result = $expression(['radius' => 5]); // fires the trace callback ``` ### Explicit trace retrieval ```php +use Superscript\Axiom\Context; use Superscript\Axiom\Tracing\TracingResolver; $tracer = new TracingResolver($delegatingResolver); -$traced = $tracer->traced($source); +$traced = $tracer->traced($source); // optionally pass a Context with bindings/definitions $traced->result; // Result> $traced->trace; // ResolutionTrace tree $traced->dump(); // formatted string @@ -61,17 +74,17 @@ foreach ($traced->trace->children() as $child) { ### Production use (flat inspector, no tracing) -For extracting resolver metadata without full tracing overhead: +For extracting resolver metadata without full tracing overhead, pass a flat +inspector via the `Context`: ```php +use Superscript\Axiom\Context; use Superscript\Axiom\Tracing\ResolutionContext; -use Superscript\Axiom\ResolutionInspector; -$context = new ResolutionContext(); -$resolver->instance(ResolutionInspector::class, $context); +$inspector = new ResolutionContext(); -$result = $resolver->resolve($source); -$httpResponse = $context->get('http_response'); +$result = $resolver->resolve($source, new Context(inspector: $inspector)); +$httpResponse = $inspector->get('http_response'); ``` ## Components diff --git a/composer.json b/composer.json index 32e48e5..673da86 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "require": { "php": "^8.4", - "gosuperscript/axiom": "^0.3.3", + "gosuperscript/axiom": "^0.4.0", "gosuperscript/monads": "^1.0" }, "require-dev": { diff --git a/src/TracingResolver.php b/src/TracingResolver.php index b33547b..6e1282b 100644 --- a/src/TracingResolver.php +++ b/src/TracingResolver.php @@ -4,7 +4,7 @@ namespace Superscript\Axiom\Tracing; -use Superscript\Axiom\ResolutionInspector; +use Superscript\Axiom\Context; use Superscript\Axiom\Resolvers\BindableResolver; use Superscript\Axiom\Resolvers\Resolver; use Superscript\Axiom\Source; @@ -29,17 +29,13 @@ public function __construct( // // DelegatingResolver::resolve() does NOT go through the IoC // Resolver binding for its own dispatch — it calls - // $this->container->make($resolverClass)->resolve($source) + // $this->container->make($resolverClass)->resolve($source, $context) // directly. The IoC Resolver binding is only injected into // child resolvers (InfixResolver, ValueResolver, etc.) when // they are constructed via the container. Those resolvers then // call $this->resolver->resolve() for recursive resolution, // which hits this TracingResolver. No infinite recursion. $this->inner->instance(Resolver::class, $this); - - // Replace any existing ResolutionInspector with our tree-aware - // version. Resolver annotations now land on trace nodes. - $this->inner->instance(ResolutionInspector::class, $this->inspector); } /** @param class-string $key */ @@ -70,12 +66,23 @@ public function onTrace(callable $callback): void $this->onTrace = $callback; } - public function resolve(Source $source): Result + public function resolve(Source $source, Context $context): Result { $isRoot = $this->depth === 0; if ($isRoot) { $this->root = null; + + // At the root, swap in our tree-aware inspector so resolver + // annotations land on trace nodes. Recursive calls will + // propagate this context down unchanged. + if ($context->inspector !== $this->inspector) { + $context = new Context( + bindings: $context->bindings, + definitions: $context->definitions, + inspector: $this->inspector, + ); + } } $this->depth++; @@ -99,13 +106,13 @@ public function resolve(Source $source): Result $this->inspector->setCurrent($node); // Delegate to the real resolver chain. - // $this->inner->resolve($source) is a direct method call on - // the DelegatingResolver instance — it does NOT go through - // the IoC binding (which points back to us). The IoC binding - // is only used when child resolvers internally call + // $this->inner->resolve($source, $context) is a direct method call + // on the DelegatingResolver instance — it does NOT go through the + // IoC binding (which points back to us). The IoC binding is only + // used when child resolvers internally call // $this->resolver->resolve() for recursive resolution. $start = microtime(true); - $result = $this->inner->resolve($source); + $result = $this->inner->resolve($source, $context); $duration = (microtime(true) - $start) * 1000; // Generic metadata — captured for every node automatically @@ -149,8 +156,10 @@ public function resolve(Source $source): Result /** * Explicit entry point for when you want the trace returned directly. */ - public function traced(Source $source): TracedResult + public function traced(Source $source, ?Context $context = null): TracedResult { + $context ??= new Context(); + // Temporarily capture the trace via onTrace $captured = null; $previousCallback = $this->onTrace; @@ -159,7 +168,7 @@ public function traced(Source $source): TracedResult $captured = $traced; }; - $result = $this->resolve($source); + $result = $this->resolve($source, $context); $this->onTrace = $previousCallback; diff --git a/tests/TracingResolverTest.php b/tests/TracingResolverTest.php index deedb55..4be38f5 100644 --- a/tests/TracingResolverTest.php +++ b/tests/TracingResolverTest.php @@ -6,6 +6,8 @@ use PHPUnit\Framework\TestCase; use RuntimeException; +use Superscript\Axiom\Context; +use Superscript\Axiom\Definitions; use Superscript\Axiom\Operators\DefaultOverloader; use Superscript\Axiom\Operators\OperatorOverloader; use Superscript\Axiom\Resolvers\DelegatingResolver; @@ -19,7 +21,6 @@ use Superscript\Axiom\Sources\StaticSource; use Superscript\Axiom\Sources\SymbolSource; use Superscript\Axiom\Sources\TypeDefinition; -use Superscript\Axiom\SymbolRegistry; use Superscript\Axiom\Tracing\TracedResult; use Superscript\Axiom\Tracing\TracingResolver; use Superscript\Axiom\Tracing\ResolutionTrace; @@ -28,7 +29,7 @@ final class TracingResolverTest extends TestCase { - private function createResolver(array $symbols = []): TracingResolver + private function createResolver(): TracingResolver { $delegating = new DelegatingResolver([ StaticSource::class => StaticResolver::class, @@ -39,13 +40,17 @@ private function createResolver(array $symbols = []): TracingResolver $delegating->instance(OperatorOverloader::class, new DefaultOverloader()); - if ($symbols !== []) { - $delegating->instance(SymbolRegistry::class, new SymbolRegistry($symbols)); - } - return new TracingResolver($delegating); } + /** + * @param array $definitions + */ + private function context(array $definitions = []): Context + { + return new Context(definitions: new Definitions($definitions)); + } + // --------------------------------------------------------------- // Tree structure tests // --------------------------------------------------------------- @@ -121,11 +126,12 @@ public function test_mixed_nesting_produces_correct_structure(): void public function test_symbol_resolution_produces_correct_tree(): void { - $tracer = $this->createResolver([ - 'rate' => new StaticSource(100), - ]); + $tracer = $this->createResolver(); - $traced = $tracer->traced(new SymbolSource('rate')); + $traced = $tracer->traced( + new SymbolSource('rate'), + $this->context(['rate' => new StaticSource(100)]), + ); $this->assertSame(SymbolSource::class, $traced->trace->sourceType); $this->assertCount(1, $traced->trace->children()); @@ -336,11 +342,12 @@ public function test_value_resolver_annotates_label(): void public function test_symbol_resolver_annotates_label(): void { - $tracer = $this->createResolver([ - 'base_rate' => new StaticSource(100), - ]); + $tracer = $this->createResolver(); - $traced = $tracer->traced(new SymbolSource('base_rate')); + $traced = $tracer->traced( + new SymbolSource('base_rate'), + $this->context(['base_rate' => new StaticSource(100)]), + ); $this->assertSame('base_rate', $traced->trace->label()); } @@ -373,8 +380,8 @@ public function test_on_trace_fires_once_per_top_level_resolve(): void $traces[] = $traced; }); - $tracer->resolve(new StaticSource(1)); - $tracer->resolve(new StaticSource(2)); + $tracer->resolve(new StaticSource(1), new Context()); + $tracer->resolve(new StaticSource(2), new Context()); $this->assertCount(2, $traces); } @@ -395,7 +402,8 @@ public function test_on_trace_does_not_fire_for_inner_resolutions(): void new StaticSource(1), '+', new StaticSource(2), - ) + ), + new Context(), ); $this->assertSame(1, $callCount); @@ -415,7 +423,8 @@ public function test_on_trace_receives_complete_trace_tree(): void new StaticSource(10), '+', new StaticSource(20), - ) + ), + new Context(), ); $this->assertNotNull($captured); @@ -449,7 +458,7 @@ public function test_traced_does_not_interfere_with_existing_on_trace(): void $tracer->traced(new StaticSource(1)); // The original callback should be restored - $tracer->resolve(new StaticSource(2)); + $tracer->resolve(new StaticSource(2), new Context()); $this->assertTrue($callbackFired); } @@ -461,7 +470,7 @@ public function test_resolve_returns_correct_result_for_static(): void { $tracer = $this->createResolver(); - $result = $tracer->resolve(new StaticSource(42)); + $result = $tracer->resolve(new StaticSource(42), new Context()); $this->assertTrue($result->isOk()); $this->assertSame(42, $result->unwrap()->unwrap()); @@ -476,7 +485,8 @@ public function test_resolve_returns_correct_result_for_infix(): void new StaticSource(10), '+', new StaticSource(20), - ) + ), + new Context(), ); $this->assertTrue($result->isOk()); @@ -488,7 +498,8 @@ public function test_resolve_returns_correct_result_for_type_definition(): void $tracer = $this->createResolver(); $result = $tracer->resolve( - new TypeDefinition(new NumberType(), new StaticSource(42)) + new TypeDefinition(new NumberType(), new StaticSource(42)), + new Context(), ); $this->assertTrue($result->isOk()); @@ -497,11 +508,12 @@ public function test_resolve_returns_correct_result_for_type_definition(): void public function test_resolve_returns_correct_result_for_symbol(): void { - $tracer = $this->createResolver([ - 'price' => new StaticSource(99), - ]); + $tracer = $this->createResolver(); - $result = $tracer->resolve(new SymbolSource('price')); + $result = $tracer->resolve( + new SymbolSource('price'), + $this->context(['price' => new StaticSource(99)]), + ); $this->assertTrue($result->isOk()); $this->assertSame(99, $result->unwrap()->unwrap()); @@ -521,7 +533,7 @@ public function test_resolution_that_throws(): void // Resolving a source type with no registered resolver should throw $this->expectException(RuntimeException::class); - $tracer->resolve(new SymbolSource('unknown')); + $tracer->resolve(new SymbolSource('unknown'), new Context()); } public function test_single_node_tree(): void diff --git a/tests/TracingTest.php b/tests/TracingTest.php index c718d3c..d4e5f6c 100644 --- a/tests/TracingTest.php +++ b/tests/TracingTest.php @@ -5,6 +5,7 @@ namespace Superscript\Axiom\Tracing\Tests; use PHPUnit\Framework\TestCase; +use Superscript\Axiom\Context; use Superscript\Axiom\Resolvers\DelegatingResolver; use Superscript\Axiom\Resolvers\StaticResolver; use Superscript\Axiom\Sources\StaticSource; @@ -56,7 +57,7 @@ public function test_zero_overhead_when_disabled(): void $resolver = Tracing::wrap($delegating, enabled: false); // Should work normally — no tracing overhead - $result = $resolver->resolve(new StaticSource(42)); + $result = $resolver->resolve(new StaticSource(42), new Context()); $this->assertTrue($result->isOk()); $this->assertSame(42, $result->unwrap()->unwrap());