Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[lldb] Proof of concept data formatter compiler for Python #113734

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions lldb/examples/formatter-bytecode/optional_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
def OptionalSummaryProvider(valobj, _):
failure = 2
storage = valobj.GetChildMemberWithName("Storage")
hasVal = storage.GetChildMemberWithName("hasVal").GetValueAsUnsigned(failure)
if hasVal == failure:
return "<could not read Optional>"

if hasVal == 0:
return "None"

underlying_type = storage.GetType().GetTemplateArgumentType(0)
value = storage.GetChildMemberWithName("value")
value = value.Cast(underlying_type)
return value.GetSummary()
145 changes: 145 additions & 0 deletions lldb/examples/formatter-bytecode/python_to_assembly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#!/usr/bin/python3

import ast
import io
import sys
from typing import Any

BUILTINS = {
"Cast": "@cast",
"GetChildMemberWithName": "@get_child_with_name",
"GetSummary": "@get_summary",
"GetTemplateArgumentType": "@get_template_argument_type",
"GetType": "@get_type",
"GetValueAsUnsigned": "@get_value_as_unsigned",
}

COMPS = {
ast.Eq: "=",
ast.NotEq: "!=",
ast.Lt: "<",
ast.LtE: "=<",
ast.Gt: ">",
ast.GtE: "=>",
}

class Compiler(ast.NodeVisitor):
# Track the stack index of locals variables.
#
# This is essentially an ordered dictionary, where the key is an index on
# the stack, and the value is the name of the variable whose value is at
# that index.
#
# Ex: `locals[0]` is the name of the first value pushed on the stack, etc.
locals: list[str]

buffer: io.StringIO
final_buffer: io.StringIO

def __init__(self) -> None:
self.locals = []
self.buffer = io.StringIO()
self.final_buffer = io.StringIO()

def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
# Initialize `locals` with the (positional) arguments.
self.locals = [arg.arg for arg in node.args.args]
self.generic_visit(node)
self.locals.clear()

def visit_Compare(self, node: ast.Compare) -> None:
self.visit(node.left)
# XXX: Does not handle multiple comparisons, ex: `0 < x < 10`
self.visit(node.comparators[0])
self._output(COMPS[type(node.ops[0])])

def visit_If(self, node: ast.If) -> None:
self.visit(node.test)

# Does the body `return`?
has_return = any(isinstance(x, ast.Return) for x in node.body)

self._output("{")
self._visit_each(node.body)
if not node.orelse and not has_return:
# No else, and no early exit: a simple `if`
self._output("} if")
return

self._output("}")
if node.orelse:
# Handle else.
self._output("{")
self._visit_each(node.orelse)
self._output("} ifelse")
elif has_return:
# Convert early exit into an `ifelse`.
self._output("{")
self._output("} ifelse", final=True)

def visit_Constant(self, node: ast.Constant) -> None:
if isinstance(node.value, str):
self._output(f'"{node.value}"')
elif isinstance(node.value, bool):
self._output(int(node.value))
else:
self._output(node.value)

def visit_Call(self, node: ast.Call) -> None:
if isinstance(node.func, ast.Attribute):
# The receiver is the left hande side of the dot.
receiver = node.func.value
method = node.func.attr
if selector := BUILTINS.get(method):
# Visit the method's receiver to have its value on the stack.
self.visit(receiver)
# Visit the args to position them on the stack.
self._visit_each(node.args)
self._output(f"{selector} call")
else:
# TODO: fail
print(f"error: unsupported method {node.func.attr}", file=sys.stderr)

def visit_Assign(self, node: ast.Assign) -> None:
# Visit RHS first, putting values on the stack.
self.visit(node.value)
# Determine the name(s). Either a single Name, or a Tuple of Names.
target = node.targets[0]
if isinstance(target, ast.Name):
names = [target.id]
elif isinstance(target, ast.Tuple):
# These tuple elements are Name nodes.
names = [x.id for x in target.elts]

# Forget any previous bindings of these names.
# Their values are orphaned on the stack.
for local in self.locals:
if local in names:
old_idx = self.locals.index(local)
self.locals[old_idx] = ""

self.locals.extend(names)

def visit_Name(self, node: ast.Name) -> None:
idx = self.locals.index(node.id)
self._output(f"{idx} pick # {node.id}")

def _visit_each(self, nodes: list[ast.AST]) -> None:
for child in nodes:
self.visit(child)

def _output(self, x: Any, final: bool = False) -> None:
dest = self.final_buffer if final else self.buffer
print(x, file=dest)

@property
def output(self) -> str:
return compiler.buffer.getvalue() + compiler.final_buffer.getvalue()


if __name__ == "__main__":
with open(sys.argv[1]) as f:
root = ast.parse(f.read())
compiler = Compiler()
compiler.visit(root)
print(compiler.output)
Loading