From 8f6b91a97ed5c97ceeeffa3df00f5bc0fd1f6d2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:44:22 +0000 Subject: [PATCH 1/7] Initial plan From af3737764ff17316cddbb46410c3c788571abc9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:51:00 +0000 Subject: [PATCH 2/7] Implement Coroutine::getTrace() method with parameters Co-authored-by: EdmondDantes <1571649+EdmondDantes@users.noreply.github.com> --- coroutine.c | 38 +++++++++++++++++++++++++++++++++++--- coroutine.stub.php | 7 ++++++- coroutine_arginfo.h | 4 +++- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/coroutine.c b/coroutine.c index 56a7ede..ae61c83 100644 --- a/coroutine.c +++ b/coroutine.c @@ -32,6 +32,7 @@ #include "zend_exceptions.h" #include "zend_generators.h" #include "zend_ini.h" +#include "zend_builtin_functions.h" #define METHOD(name) PHP_METHOD(Async_Coroutine, name) #define THIS_COROUTINE ((async_coroutine_t *) ZEND_ASYNC_OBJECT_TO_EVENT(Z_OBJ_P(ZEND_THIS))) @@ -1333,9 +1334,40 @@ METHOD(getException) METHOD(getTrace) { - // TODO: Implement debug trace collection - // This would require fiber stack trace functionality - array_init(return_value); + zend_long options = DEBUG_BACKTRACE_PROVIDE_OBJECT; + zend_long limit = 0; + + ZEND_PARSE_PARAMETERS_START(0, 2) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(options) + Z_PARAM_LONG(limit) + ZEND_PARSE_PARAMETERS_END(); + + async_coroutine_t *coroutine = THIS_COROUTINE; + async_fiber_context_t *fiber_context = coroutine->fiber_context; + + // Return empty array if coroutine is not suspended or has no fiber context + if (fiber_context == NULL || + fiber_context->context.status != ZEND_FIBER_STATUS_SUSPENDED || + !fiber_context->execute_data) { + array_init(return_value); + return; + } + + // Switch to the coroutine's VM stack to generate the backtrace + zend_vm_stack orig_vm_stack = EG(vm_stack); + zend_execute_data *orig_execute_data = EG(current_execute_data); + + EG(vm_stack) = fiber_context->vm_stack; + EG(current_execute_data) = fiber_context->execute_data; + + // Generate the backtrace using Zend's built-in function + // skip_last = 0 (don't skip any frames) + zend_fetch_debug_backtrace(return_value, 0, (int)options, (int)limit); + + // Restore original VM stack and execute data + EG(vm_stack) = orig_vm_stack; + EG(current_execute_data) = orig_execute_data; } // Location Methods diff --git a/coroutine.stub.php b/coroutine.stub.php index 1893dfa..0b0fdef 100644 --- a/coroutine.stub.php +++ b/coroutine.stub.php @@ -42,8 +42,13 @@ public function getException(): mixed {} /** * Returns the Coroutine debug trace. + * If the coroutine is in the suspended state, returns a backtrace array. + * Otherwise, returns an empty array. + * + * @param int $options Options for the backtrace (DEBUG_BACKTRACE_PROVIDE_OBJECT, DEBUG_BACKTRACE_IGNORE_ARGS) + * @param int $limit Maximum number of stack frames to return (0 for no limit) */ - public function getTrace(): array {} + public function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): array {} /** * Return spawn file and line. diff --git a/coroutine_arginfo.h b/coroutine_arginfo.h index e741424..4654dda 100644 --- a/coroutine_arginfo.h +++ b/coroutine_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 866a851c4d87feafd6585532d622426dc453710f */ + * Stub hash: 35d332163bf92ed3cad35c83017aaa67d650aa9d */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_getId, 0, 0, IS_LONG, 0) ZEND_END_ARG_INFO() @@ -16,6 +16,8 @@ ZEND_END_ARG_INFO() #define arginfo_class_Async_Coroutine_getException arginfo_class_Async_Coroutine_getResult ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_getTrace, 0, 0, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_LONG, 0, "DEBUG_BACKTRACE_PROVIDE_OBJECT") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, limit, IS_LONG, 0, "0") ZEND_END_ARG_INFO() #define arginfo_class_Async_Coroutine_getSpawnFileAndLine arginfo_class_Async_Coroutine_getTrace From 5ede1d44eb50080725e11a629c98961a7bb9a95d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:54:37 +0000 Subject: [PATCH 3/7] Add comprehensive tests for Coroutine::getTrace() method Co-authored-by: EdmondDantes <1571649+EdmondDantes@users.noreply.github.com> --- tests/coroutine/009-coroutine_getTrace.phpt | 21 ++++++++- .../037-coroutine_getTrace_suspended.phpt | 44 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 tests/coroutine/037-coroutine_getTrace_suspended.phpt diff --git a/tests/coroutine/009-coroutine_getTrace.phpt b/tests/coroutine/009-coroutine_getTrace.phpt index 787f2a4..69c7bbc 100644 --- a/tests/coroutine/009-coroutine_getTrace.phpt +++ b/tests/coroutine/009-coroutine_getTrace.phpt @@ -1,20 +1,39 @@ --TEST-- -Coroutine: getTrace() - returns empty array (TODO implementation) +Coroutine: getTrace() - returns empty array for non-suspended coroutine --FILE-- getTrace(); var_dump(is_array($trace)); var_dump(count($trace)); +// Test with options parameter +$trace2 = $coroutine->getTrace(DEBUG_BACKTRACE_IGNORE_ARGS); +var_dump(is_array($trace2)); +var_dump(count($trace2)); + +// Test with limit parameter +$trace3 = $coroutine->getTrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 5); +var_dump(is_array($trace3)); +var_dump(count($trace3)); + ?> --EXPECT-- bool(true) +int(0) +bool(true) +int(0) +bool(true) int(0) \ No newline at end of file diff --git a/tests/coroutine/037-coroutine_getTrace_suspended.phpt b/tests/coroutine/037-coroutine_getTrace_suspended.phpt new file mode 100644 index 0000000..e41dab4 --- /dev/null +++ b/tests/coroutine/037-coroutine_getTrace_suspended.phpt @@ -0,0 +1,44 @@ +--TEST-- +Coroutine: getTrace() - returns backtrace for suspended coroutine +--FILE-- +getTrace(); + +var_dump(is_array($trace)); +echo "Trace has entries: " . (count($trace) > 0 ? "yes" : "no") . "\n"; + +// Test with DEBUG_BACKTRACE_IGNORE_ARGS +$traceNoArgs = $coroutine->getTrace(DEBUG_BACKTRACE_IGNORE_ARGS); +var_dump(is_array($traceNoArgs)); + +// Test with limit +$traceLimited = $coroutine->getTrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2); +var_dump(is_array($traceLimited)); + +// Wait for coroutine to complete +await($coroutine); + +// After completion, trace should be empty +$traceAfter = $coroutine->getTrace(); +var_dump(is_array($traceAfter)); +var_dump(count($traceAfter) === 0); + +?> +--EXPECTF-- +bool(true) +Trace has entries: %s +bool(true) +bool(true) +bool(true) +bool(true) From 892f3b004f3707a3d75150cafd077443b93afcc7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:55:29 +0000 Subject: [PATCH 4/7] Address code review feedback - improve comments and logical grouping Co-authored-by: EdmondDantes <1571649+EdmondDantes@users.noreply.github.com> --- coroutine.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/coroutine.c b/coroutine.c index ae61c83..e1d8746 100644 --- a/coroutine.c +++ b/coroutine.c @@ -1348,8 +1348,7 @@ METHOD(getTrace) // Return empty array if coroutine is not suspended or has no fiber context if (fiber_context == NULL || - fiber_context->context.status != ZEND_FIBER_STATUS_SUSPENDED || - !fiber_context->execute_data) { + (fiber_context->context.status != ZEND_FIBER_STATUS_SUSPENDED || !fiber_context->execute_data)) { array_init(return_value); return; } @@ -1362,7 +1361,7 @@ METHOD(getTrace) EG(current_execute_data) = fiber_context->execute_data; // Generate the backtrace using Zend's built-in function - // skip_last = 0 (don't skip any frames) + // skip_last = 0 (skip zero frames from the end of the trace) zend_fetch_debug_backtrace(return_value, 0, (int)options, (int)limit); // Restore original VM stack and execute data From 43eb4e76d16b959c531f3c6d9c1b230500dee259 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:56:05 +0000 Subject: [PATCH 5/7] Complete implementation of Coroutine::getTrace() method Co-authored-by: EdmondDantes <1571649+EdmondDantes@users.noreply.github.com> --- _codeql_detected_source_root | 1 + 1 file changed, 1 insertion(+) create mode 120000 _codeql_detected_source_root diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root new file mode 120000 index 0000000..945c9b4 --- /dev/null +++ b/_codeql_detected_source_root @@ -0,0 +1 @@ +. \ No newline at end of file From ea75a0b9a09c5bd60b6c4e600b425f2ff4fd6115 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:12:26 +0000 Subject: [PATCH 6/7] Fix getTrace() to use ZEND_COROUTINE_SUSPENDED macro, return null instead of empty array, and fix tests to use suspend() Co-authored-by: EdmondDantes <1571649+EdmondDantes@users.noreply.github.com> --- coroutine.c | 14 +-- coroutine.stub.php | 4 +- coroutine_arginfo.h | 4 +- tests/coroutine/009-coroutine_getTrace.phpt | 32 ++++--- .../037-coroutine_getTrace_suspended.phpt | 87 ++++++++++++------- 5 files changed, 82 insertions(+), 59 deletions(-) diff --git a/coroutine.c b/coroutine.c index e1d8746..9b51f8e 100644 --- a/coroutine.c +++ b/coroutine.c @@ -1344,13 +1344,17 @@ METHOD(getTrace) ZEND_PARSE_PARAMETERS_END(); async_coroutine_t *coroutine = THIS_COROUTINE; + + // Return null if coroutine is not suspended + if (!ZEND_COROUTINE_SUSPENDED(&coroutine->coroutine)) { + RETURN_NULL(); + } + async_fiber_context_t *fiber_context = coroutine->fiber_context; - // Return empty array if coroutine is not suspended or has no fiber context - if (fiber_context == NULL || - (fiber_context->context.status != ZEND_FIBER_STATUS_SUSPENDED || !fiber_context->execute_data)) { - array_init(return_value); - return; + // Additional safety check for fiber context + if (fiber_context == NULL || !fiber_context->execute_data) { + RETURN_NULL(); } // Switch to the coroutine's VM stack to generate the backtrace diff --git a/coroutine.stub.php b/coroutine.stub.php index 0b0fdef..bfe3c82 100644 --- a/coroutine.stub.php +++ b/coroutine.stub.php @@ -43,12 +43,12 @@ public function getException(): mixed {} /** * Returns the Coroutine debug trace. * If the coroutine is in the suspended state, returns a backtrace array. - * Otherwise, returns an empty array. + * Otherwise, returns null. * * @param int $options Options for the backtrace (DEBUG_BACKTRACE_PROVIDE_OBJECT, DEBUG_BACKTRACE_IGNORE_ARGS) * @param int $limit Maximum number of stack frames to return (0 for no limit) */ - public function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): array {} + public function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): ?array {} /** * Return spawn file and line. diff --git a/coroutine_arginfo.h b/coroutine_arginfo.h index 4654dda..92362d5 100644 --- a/coroutine_arginfo.h +++ b/coroutine_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 35d332163bf92ed3cad35c83017aaa67d650aa9d */ + * Stub hash: 2469ab708f75ecce11e559548c2462562fd39c2a */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_getId, 0, 0, IS_LONG, 0) ZEND_END_ARG_INFO() @@ -15,7 +15,7 @@ ZEND_END_ARG_INFO() #define arginfo_class_Async_Coroutine_getException arginfo_class_Async_Coroutine_getResult -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_getTrace, 0, 0, IS_ARRAY, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_getTrace, 0, 0, IS_ARRAY, 1) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_LONG, 0, "DEBUG_BACKTRACE_PROVIDE_OBJECT") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, limit, IS_LONG, 0, "0") ZEND_END_ARG_INFO() diff --git a/tests/coroutine/009-coroutine_getTrace.phpt b/tests/coroutine/009-coroutine_getTrace.phpt index 69c7bbc..103f579 100644 --- a/tests/coroutine/009-coroutine_getTrace.phpt +++ b/tests/coroutine/009-coroutine_getTrace.phpt @@ -1,5 +1,5 @@ --TEST-- -Coroutine: getTrace() - returns empty array for non-suspended coroutine +Coroutine: getTrace() - returns null for non-suspended coroutine --FILE-- getTrace(); +echo "Trace before start is null: " . ($traceBeforeStart === null ? "yes" : "no") . "\n"; + // Wait for coroutine to complete await($coroutine); -// After completion, trace should be empty +// After completion, trace should be null $trace = $coroutine->getTrace(); +echo "Trace after completion is null: " . ($trace === null ? "yes" : "no") . "\n"; -var_dump(is_array($trace)); -var_dump(count($trace)); - -// Test with options parameter +// Test with options parameter - should still return null $trace2 = $coroutine->getTrace(DEBUG_BACKTRACE_IGNORE_ARGS); -var_dump(is_array($trace2)); -var_dump(count($trace2)); +echo "Trace with IGNORE_ARGS is null: " . ($trace2 === null ? "yes" : "no") . "\n"; -// Test with limit parameter +// Test with limit parameter - should still return null $trace3 = $coroutine->getTrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 5); -var_dump(is_array($trace3)); -var_dump(count($trace3)); +echo "Trace with limit is null: " . ($trace3 === null ? "yes" : "no") . "\n"; ?> --EXPECT-- -bool(true) -int(0) -bool(true) -int(0) -bool(true) -int(0) \ No newline at end of file +Trace before start is null: yes +Trace after completion is null: yes +Trace with IGNORE_ARGS is null: yes +Trace with limit is null: yes \ No newline at end of file diff --git a/tests/coroutine/037-coroutine_getTrace_suspended.phpt b/tests/coroutine/037-coroutine_getTrace_suspended.phpt index e41dab4..8ca243d 100644 --- a/tests/coroutine/037-coroutine_getTrace_suspended.phpt +++ b/tests/coroutine/037-coroutine_getTrace_suspended.phpt @@ -4,41 +4,62 @@ Coroutine: getTrace() - returns backtrace for suspended coroutine getTrace(); - -var_dump(is_array($trace)); -echo "Trace has entries: " . (count($trace) > 0 ? "yes" : "no") . "\n"; - -// Test with DEBUG_BACKTRACE_IGNORE_ARGS -$traceNoArgs = $coroutine->getTrace(DEBUG_BACKTRACE_IGNORE_ARGS); -var_dump(is_array($traceNoArgs)); +$parentCoroutine = null; +$childCoroutine = null; -// Test with limit -$traceLimited = $coroutine->getTrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2); -var_dump(is_array($traceLimited)); - -// Wait for coroutine to complete -await($coroutine); - -// After completion, trace should be empty -$traceAfter = $coroutine->getTrace(); -var_dump(is_array($traceAfter)); -var_dump(count($traceAfter) === 0); +// Spawn a parent coroutine +$parentCoroutine = spawn(function() use (&$childCoroutine) { + echo "Parent: Starting\n"; + + // Spawn a child coroutine that will suspend + $childCoroutine = spawn(function() { + echo "Child: Before suspend\n"; + suspend(); + echo "Child: After suspend\n"; + }); + + // Suspend to let child start and suspend + suspend(); + + echo "Parent: Back from suspend\n"; + + // Now check child's trace while it's suspended + $trace = $childCoroutine->getTrace(); + + if ($trace !== null) { + echo "Child trace is array: " . (is_array($trace) ? "yes" : "no") . "\n"; + echo "Child trace has entries: " . (count($trace) > 0 ? "yes" : "no") . "\n"; + + // Test with DEBUG_BACKTRACE_IGNORE_ARGS + $traceNoArgs = $childCoroutine->getTrace(DEBUG_BACKTRACE_IGNORE_ARGS); + echo "Trace with IGNORE_ARGS is array: " . (is_array($traceNoArgs) ? "yes" : "no") . "\n"; + + // Test with limit + $traceLimited = $childCoroutine->getTrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2); + echo "Trace with limit is array: " . (is_array($traceLimited) ? "yes" : "no") . "\n"; + } else { + echo "Child trace is null\n"; + } + + // Resume to let child finish + suspend(); + + // After completion, trace should be null + $traceAfter = $childCoroutine->getTrace(); + echo "Child trace after completion is null: " . ($traceAfter === null ? "yes" : "no") . "\n"; +}); ?> ---EXPECTF-- -bool(true) -Trace has entries: %s -bool(true) -bool(true) -bool(true) -bool(true) +--EXPECT-- +Parent: Starting +Child: Before suspend +Parent: Back from suspend +Child trace is array: yes +Child trace has entries: yes +Trace with IGNORE_ARGS is array: yes +Trace with limit is array: yes +Child: After suspend +Child trace after completion is null: yes From bd74a3cc36fa3140a287db4dd660506fec398f9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:14:01 +0000 Subject: [PATCH 7/7] Clarify comment in test - suspend yields control to scheduler Co-authored-by: EdmondDantes <1571649+EdmondDantes@users.noreply.github.com> --- tests/coroutine/037-coroutine_getTrace_suspended.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/coroutine/037-coroutine_getTrace_suspended.phpt b/tests/coroutine/037-coroutine_getTrace_suspended.phpt index 8ca243d..9e85a49 100644 --- a/tests/coroutine/037-coroutine_getTrace_suspended.phpt +++ b/tests/coroutine/037-coroutine_getTrace_suspended.phpt @@ -44,7 +44,7 @@ $parentCoroutine = spawn(function() use (&$childCoroutine) { echo "Child trace is null\n"; } - // Resume to let child finish + // Yield control to allow scheduler to resume the child coroutine suspend(); // After completion, trace should be null