From fb85684e11b83953df488d8189b5a9a1d48d7474 Mon Sep 17 00:00:00 2001 From: xFrednet Date: Wed, 19 Mar 2025 20:11:16 +0100 Subject: [PATCH] Add pyrona aware mermaid output --- .gitignore | 1 + Include/internal/pycore_regions.h | 2 + Lib/test/test_inspect.py | 2 +- Objects/regions.c | 266 +++++++++++++++++++++++++++++- Python/bltinmodule.c | 17 ++ 5 files changed, 284 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index bc1c977041a863..1463f6c32c022d 100644 --- a/.gitignore +++ b/.gitignore @@ -145,6 +145,7 @@ Tools/msi/obj Tools/ssl/amd64 Tools/ssl/win32 Tools/freeze/test/outdir +/mermaid.md # This is where my build ends up. debug/ diff --git a/Include/internal/pycore_regions.h b/Include/internal/pycore_regions.h index 6a2c808ec2987a..0ca152914ff2c0 100644 --- a/Include/internal/pycore_regions.h +++ b/Include/internal/pycore_regions.h @@ -44,6 +44,8 @@ static inline void _Py_SET_REGION(PyObject *ob, Py_region_ptr_t region) { PyObject* _Py_MakeImmutable(PyObject* obj); #define Py_MakeImmutable(op) _Py_MakeImmutable(_PyObject_CAST(op)) +PyObject* _Py_Mermaid(PyObject *args, PyObject *kwargs); + PyObject* _Py_InvariantSrcFailure(void); #define Py_InvariantSrcFailure() _Py_InvariantSrcFailure() diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index d89953ab60f022..529a1ef4b0e4d7 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -4521,7 +4521,7 @@ def test_builtins_have_signatures(self): needs_semantic_update = {"round"} no_signature |= needs_semantic_update # These need *args support in Argument Clinic - needs_varargs = {"breakpoint", "min", "max", "print", + needs_varargs = {"breakpoint", "min", "max", "print", "mermaid", "__build_class__"} no_signature |= needs_varargs # These simply weren't covered in the initial AC conversion diff --git a/Objects/regions.c b/Objects/regions.c index ab7e602e211663..8be9ae0d0357fc 100644 --- a/Objects/regions.c +++ b/Objects/regions.c @@ -558,6 +558,15 @@ static PyObject* stack_pop(stack* s){ return object; } +// Returns a pointer to the top object without poping it. +static PyObject* stack_peek(stack* s){ + if(s->head == NULL){ + return NULL; + } + + return s->head->object; +} + static void stack_free(stack* s){ while(s->head != NULL){ PyObject* op = stack_pop(s); @@ -571,12 +580,16 @@ static bool stack_empty(stack* s){ return s->head == NULL; } -__attribute__((unused)) -static void stack_print(stack* s){ +static bool stack_contains(stack* s, PyObject* object){ node* n = s->head; while(n != NULL){ + if (n->object == object) { + return true; + } n = n->next; } + + return false; } static bool is_c_wrapper(PyObject* obj){ @@ -746,7 +759,7 @@ static void invariant_reset_captured_list(void) { int _Py_CheckRegionInvariant(PyThreadState *tstate) { // Check if we should perform the region invariant check - if(!invariant_do_region_check){ + if(!invariant_do_region_check || true){ return 0; } @@ -1640,6 +1653,253 @@ static int try_close(PyRegionObject *root_bridge) { return -1; } + +typedef struct drawmermaidvisitinfo { + // Nodes that have been seen before + stack* seen; + + // The number max depth of subregions that should be drawn from + // this point on. + int reg_budget; + // The number max depth of immutable objects that should be drawn from + // this point on. + int imm_budget; + int obj_budget; + // The source object of the reference. + PyObject* src; + + FILE *out; +} drawmermaidvisitinfo; + +static int _draw_mermaid_visit(PyObject* target, void* info_void) { + drawmermaidvisitinfo *info = _Py_CAST(drawmermaidvisitinfo *, info_void); + + // Self references don't look good in mermaid + if (target == info->src) { + return 0; + } + + fprintf(info->out, "%p --> %p\n", info->src, target); + + // Check if the target should be traversed + if (stack_contains(info->seen, target)) { + return 0; + } + + // Mark the object as seen + if (stack_push(info->seen, target)) { + PyErr_NoMemory(); + return -1; + } + + info->obj_budget -= 1; + if (info->obj_budget <= 0) { + fprintf(info->out, "%p:::maxdepth\n", target); + return 0; + } + + // c functions can't be traversed + if (is_c_wrapper(target) || PyFunction_Check(target)) { + fprintf(info->out, "%p:::maxdepth\n", target); + return 0; + } + + // Handle immutable objects + if (Py_IsImmutable(target)) { + if (info->imm_budget == 0) { + fprintf(info->out, "%p:::maxdepth\n", target); + return 0; + } + + info->imm_budget -= 1; + PyObject *old_src = info->src; + info->src = target; + int result = !visit_object(target, (visitproc)_draw_mermaid_visit, info); + info->src = old_src; + info->imm_budget += 1; + return result; + } + + // Don't traverse cowns + if (Py_IsCown(target)) { + return 0; + } + + int same_region = Py_REGION_DATA(target) == Py_REGION_DATA(info->src); + if (!same_region) { + if (info->reg_budget == 0) { + fprintf(info->out, "%p:::maxdepth\n", target); + return 0; + } + info->reg_budget -= 1; + } + + PyObject *old_src = info->src; + info->src = target; + int result = !visit_object(target, (visitproc)_draw_mermaid_visit, info); + info->src = old_src; + + if (!same_region) { + info->reg_budget += 1; + } + + return result; +} + +static PyObject *draw_mermaid(PyObject *obj, int reg_depth, int imm_depth, int draw_limit) { + // This is definitly not optimized for speed + PyObject *result = NULL; + stack *pending = NULL; + FILE *out = fopen("mermaid.md", "w"); + fprintf(out,"Note that only reachable objects are draw!\n"); + fprintf(out,"
\n\n"); + fprintf(out,"```mermaid\n"); + fprintf( + out, + "%%%%{init: {'theme': 'neutral', 'themeVariables': { 'fontSize': '16px' }}}%%%%\n" + ); + fprintf(out,"graph TD\n"); + + drawmermaidvisitinfo info = { + .seen = stack_new(), + .reg_budget = reg_depth, + .imm_budget = imm_depth - 1, + .obj_budget = draw_limit, + .src = obj, + .out = out, + }; + if (info.seen == NULL) { + PyErr_NoMemory(); + goto cleanup; + } + + // Mark the object as seen + if (stack_push(info.seen, obj)) { + PyErr_NoMemory(); + goto cleanup; + } + + // Traverse objects + if (!visit_object(obj, (visitproc)_draw_mermaid_visit, &info)) { + goto cleanup; + } + + // Draw regions and add text to objects + pending = stack_new(); + if (pending == NULL) { + PyErr_NoMemory(); + goto cleanup; + } + while (!stack_empty(info.seen)) { + Py_region_ptr_t current_region = Py_REGION(stack_peek(info.seen)); + + if (HAS_METADATA(current_region)) { + const regionmetadata *md = REGION_DATA_CAST(current_region); + fprintf(out, "style %p fill:#fcfbdd\n", md); + if (md->name) { + fprintf( + out, + "subgraph %p['%s']\n", + md, + get_region_name(stack_peek(info.seen)) + ); + } else { + fprintf(out, "subgraph %p['%p']\n", md, md->bridge); + } + } + + while (!stack_empty(info.seen)) { + PyObject *item = stack_pop(info.seen); + + // Skip objects from other regions + if (Py_REGION(item) != current_region) { + if (stack_push(pending, item)) { + PyErr_NoMemory(); + goto cleanup; + } + continue; + } + + const char *info_text = ""; + + if (is_c_wrapper(item)) { + info_text = "
#91;C-Wrapper#93;"; + } else if (PyFunction_Check(item)) { + info_text = "
#91;PyFunction#93;"; + } else if (Py_IsCown(item)) { + info_text = "
#91;Cown#93;"; + } else if (PyType_Check(item)) { + info_text = "
#91;Type#93;"; + } else if (PyTuple_Check(item)) { + info_text = "
#91;Tuple#93;"; + } else if (PyDict_Check(item)) { + info_text = "
#91;Dictionary#93;"; + } + + // Set text + if (Py_IsNone(item)) { + fprintf(out, " %p((None
%p
#40;immortal#41;))", item, item); + } else if (_Py_IsImmortal(item)) { + fprintf(out, " %p[%p
#40;immortal#41;%s]", item, item, info_text); + } else if (Py_is_bridge_object(item)) { + fprintf(out, " %p[\\%p
rc=%ld%s/]", item, item, Py_REFCNT(item), info_text); + } else { + fprintf(out, " %p[%p
rc=%ld%s]", item, item, Py_REFCNT(item), info_text); + } + + // Add color + if (IS_LOCAL_REGION(current_region)) { + fprintf(out, ":::local"); + } else if (IS_IMMUTABLE_REGION(current_region)) { + fprintf(out, ":::immutable"); + } + fprintf(out, "\n"); + } + + if (HAS_METADATA(current_region)) { + fprintf(out, "end\n"); + } + + stack *tmp = info.seen; + info.seen = pending; + pending = tmp; + } + + result = Py_None; +cleanup: + fprintf(out, "classDef local fill:#eefcdd\n"); + fprintf(out, "classDef immutable fill:#94f7ff\n"); + fprintf(out, "classDef maxdepth stroke-width:4px,stroke:#c00,stroke-dasharray: 10 5\n"); + fprintf(out, "```\n"); + fprintf(out, "
\n"); + + if (info.obj_budget <= 0) { + fprintf(out, "Stopped drawing after traversing %d objects\n", draw_limit); + } + fclose(out); + if (pending) { + stack_free(pending); + } + if (info.seen) { + stack_free(info.seen); + } + return result; +} + +PyObject* _Py_Mermaid(PyObject *args, PyObject *kwargs) { + PyObject *obj = Py_None; + int reg_depth = 3; // The maximum region depth to draw + int imm_depth = 1; // The maximum depth of immutable objects to draw + int draw_limit = 50; // The maximum number of objects to traverse + static char *kwlist[] = {"object", "reg_depth", "imm_depth", "draw_limit", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|iii", kwlist, &obj, ®_depth, &imm_depth, &draw_limit)) { + return NULL; + } + + return draw_mermaid(obj, reg_depth, imm_depth, draw_limit); +} + static void PyRegion_dealloc(PyRegionObject *self) { // Name is immutable and not in our region. diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 89d51a9d7d3bab..d2adf90e02d310 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -1891,6 +1891,22 @@ default keyword-only argument specifies an object to return if\n\ the provided iterable is empty.\n\ With two or more arguments, return the largest argument."); +/* AC: cannot convert yet, waiting for *args support */ +static PyObject * +builtin_mermaid(PyObject *module, PyObject *args, PyObject *kwds) +{ + return _Py_Mermaid(args, kwds); +} + +PyDoc_STRVAR(mermaid_docs, +"mermaid(obj, *[, reg_depth=1, imm_depth=1, draw_limit=50]) -> value\n\ +\n\ +Generates a Mermaid diagram of all reachable objects. The output is\n\ +written to a new `mermaid.md` file in the current working directory.\n\ +Optional Arguments_\n\ +* reg_depth: The depth of regions to show\n\ +* imm_depth: The depth of immutable objects to show\n\ +* draw_limit: The number of objects to traverse"); /*[clinic input] oct as builtin_oct @@ -3118,6 +3134,7 @@ static PyMethodDef builtin_methods[] = { BUILTIN_MAKEIMMUTABLE_METHODDEF BUILTIN_INVARIANTSRCFAILURE_METHODDEF BUILTIN_INVARIANTTGTFAILURE_METHODDEF + {"mermaid", _PyCFunction_CAST(builtin_mermaid), METH_VARARGS | METH_KEYWORDS, mermaid_docs}, BUILTIN_ITER_METHODDEF BUILTIN_AITER_METHODDEF BUILTIN_LEN_METHODDEF