[llvm] [Dexter] Add basic structured script parsing (PR #193710)

Orlando Cazalet-Hyams via llvm-commits llvm-commits at lists.llvm.org
Thu May 7 08:24:51 PDT 2026


================
@@ -0,0 +1,238 @@
+# DExTer : Debugging Experience Tester
+# ~~~~~~   ~         ~~         ~   ~~
+#
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+"""This file defines the DexterScript class. Using Nodes as building blocks, the DexterScript defines a complete Dexter
+test, a structured definition of locations, values, and actions used to drive a debugging session and evaluate the
+results.
+"""
+
+from pathlib import PurePath
+import os
+from typing import Any, Callable, Optional, Set
+import yaml
+
+from dex.test_script.Nodes import (
+    Expect,
+    Where,
+    setup_yaml_parser,
+)
+
+from dex.utils.Exceptions import Error
+from dex.utils.Timer import Timer
+
+
+class DexterScriptError(Error):
+    pass
+
+
+class Scope:
+    """Helper class used to simplify queries about the context of a Node in the Dexter Script. The context for a given
+    Node consists of some base context information in the root of the script, and then all Where nodes in the parent
+    chain of the current Node. Therefore each Script has a root Scope object, and each Node's context is given by a
+    Scope chain built from the root Scope and every Where between the root and the given Node.
+    """
+
+    def __init__(
+        self,
+        file: Optional[str] = None,
+        where: Optional[Where] = None,
+        parent_scope: "Optional[Scope]" = None,
+    ):
+        """Can be initialized with either a file for the default Scope, or with the properties of a Where
+        for any script-nested Scope.
+        """
+        if where is not None:
+            assert (
+                parent_scope is not None
+            ), "Scope for a Where node must have a parent scope!"
+            assert (
+                file is None
+            ), "Scope for a Where node cannot have a separately-defined file!"
+            self.file = None
+            self.where = where
+            self.parent_scope = parent_scope
+        else:
+            assert (
+                parent_scope is None
+            ), "Scope for a Root node cannot have a parent scope!"
+            self.file = file
+            self.where = None
+            self.parent_scope = None
+
+    def add_where(self, where: Where):
+        """Adds `where` to this Scope's chain."""
+        return Scope(where=where, parent_scope=self)
+
+
+class DexterScript:
+    def __init__(
+        self,
+        context,
+        script_obj,
+        scope: Scope,
+    ):
+        self.context = context
+        self.script_obj = script_obj
+        self.root_scope = scope
+        # `visit_script` will validate the structure of the script, as it traverses the full script and raises an
+        # exception if it sees anything unexpected.
+        self.visit_script()
+
+    # If a truthy value is returned, abort further visiting and return that value..
+    def _visit_script(
+        self, script, scope: Scope, visit_where=None, visit_expect=None, visit_then=None
+    ) -> Any:
+        def do(visitor, *args):
+            if visitor:
+                return visitor(*args)
+            return None
+
+        if not isinstance(script, dict):
+            raise DexterScriptError(f"Found unexpected node: {script}")
+        for key, value in script.items():
+            if isinstance(key, Where):
+                if result := do(visit_where, key, scope):
+                    return result
+                new_scope = scope.add_where(key)
+                if result := self._visit_script(
+                    value, new_scope, visit_where, visit_expect, visit_then
+                ):
+                    return result
+            elif isinstance(key, Expect):
+                if result := do(visit_expect, key, value, scope):
+                    return result
+            else:
+                raise DexterScriptError(f"Found unexpected node: {key}")
+
+    # Any visitor function provided may return a truthy value to abort the visit and return that value.
+    def visit_script(
+        self,
+        visit_where: Optional[Callable[[Where, Scope], Any]] = None,
+        visit_expect: Optional[Callable[[Expect, Any, Scope], Any]] = None,
+    ) -> Any:
+        """Visits all nodes in the script in pre-order traversal, calling any non-none provided visitor functions for
+        each respective node type. Note that we do not visit expected values independently of their associated expect;
+        instead, visit_expect accepts the Expect node and it's expected value as an argument.
+
+        If any visit function returns a truth-y value, traversal will early-exit and this function returns that value;
----------------
OCHyams wrote:

nit: you spelt it truthy elsewhere

https://github.com/llvm/llvm-project/pull/193710


More information about the llvm-commits mailing list