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

Add a debug feature to record the callsite of calls to construct new AST nodes #397

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
34 changes: 34 additions & 0 deletions experimental/ast/decl.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/bufbuild/protocompile/experimental/internal"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/internal/arena"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)

const (
Expand Down Expand Up @@ -159,6 +160,31 @@ func (d DeclAny) Span() report.Span {
)
}

// Trace returns a stack trace for the site at which d was constructed using
// a [Nodes].
//
// Returns "" if a trace was not recorded. See Nodes.EnableTracing.
func (d DeclAny) Trace() string {
switch d.Kind() {
case DeclKindBody:
return d.AsBody().Trace()
case DeclKindDef:
return d.AsDef().Trace()
case DeclKindEmpty:
return d.AsEmpty().Trace()
case DeclKindImport:
return d.AsImport().Trace()
case DeclKindPackage:
return d.AsPackage().Trace()
case DeclKindRange:
return d.AsRange().Trace()
case DeclKindSyntax:
return d.AsSyntax().Trace()
default:
return ""
}
}

// rawDecl is the actual data of a DeclAny.
type rawDecl struct {
ptr arena.Untyped
Expand Down Expand Up @@ -187,6 +213,14 @@ func (d declImpl[Raw]) AsAny() DeclAny {
return rawDecl{arena.Compress(d.raw).Untyped(), kind}.With(d.Context())
}

// Trace returns a stack trace for the site at which d was constructed using
// a [Nodes].
//
// Returns "" if a trace was not recorded. See Nodes.EnableTracing.
func (d declImpl[Raw]) Trace() string {
return d.Context().Nodes().traces[unsafex.Addr(d.raw)]
}

func wrapDecl[Raw any](ctx Context, ptr arena.Pointer[Raw]) declImpl[Raw] {
if ctx == nil || ptr.Nil() {
return declImpl[Raw]{}
Expand Down
34 changes: 34 additions & 0 deletions experimental/ast/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/arena"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)

const (
Expand Down Expand Up @@ -199,6 +200,31 @@ func (e ExprAny) Span() report.Span {
)
}

// Trace returns a stack trace for the site at which e was constructed using
// a [Nodes].
//
// Returns "" if a trace was not recorded. See Nodes.EnableTracing.
func (e ExprAny) Trace() string {
switch e.Kind() {
case ExprKindArray:
return e.AsArray().Trace()
case ExprKindDict:
return e.AsDict().Trace()
case ExprKindField:
return e.AsField().Trace()
case ExprKindPrefixed:
return e.AsPrefixed().Trace()
case ExprKindRange:
return e.AsRange().Trace()

case ExprKindLiteral, ExprKindPath:
// ExprLiteral and ExprPath do not currently record traces.
fallthrough
default:
return ""
}
}

// typeImpl is the common implementation of pointer-like Expr* types.
type exprImpl[Raw any] struct {
// NOTE: These fields are sorted by alignment.
Expand All @@ -221,6 +247,14 @@ func (e exprImpl[Raw]) AsAny() ExprAny {
)
}

// Trace returns a stack trace for the site at which e was constructed using
// a [Nodes].
//
// Returns "" if a trace was not recorded. See Nodes.EnableTracing.
func (e exprImpl[Raw]) Trace() string {
return e.Context().Nodes().traces[unsafex.Addr(e.raw)]
}

// exprs is storage for the various kinds of Exprs in a Context.
type exprs struct {
prefixes arena.Arena[rawExprPrefixed]
Expand Down
93 changes: 78 additions & 15 deletions experimental/ast/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ package ast

import (
"fmt"
"runtime"
"strings"

"github.com/bufbuild/protocompile/experimental/internal"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/arena"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)

// Nodes provides storage for the various AST node types, and can be used
Expand All @@ -28,10 +31,28 @@ type Nodes struct {
// The context for these nodes.
Context Context

// If set, this will cause any call that constructs a new AST node to log
// the stack trace of the caller. Those stack traces can later be recalled
// by calling the Trace method on an AST node.
//
// Tracing is best effort: some node types are currently unable to record
// a creation site.
//
// Enabling this feature will result in significant parser slowdown; it is
// intended for debugging only.
EnableTracing bool

decls decls
types types
exprs exprs
options arena.Arena[rawCompactOptions]

// Map of arena pointer addresses to recorded stack traces. We use a
// uintptr because all of the nodes associated with this context live
// have the same lifetime as this map: they are not freed (allowing their
// address to be reused) until traces is also freed.
traces map[uintptr]string
scratch []uintptr // Reusable scratch space for traceNode.
}

// Root returns the root AST node for this context.
Expand All @@ -47,7 +68,7 @@ func (n *Nodes) Root() File {
func (n *Nodes) NewDeclEmpty(semicolon token.Token) DeclEmpty {
n.panicIfNotOurs(semicolon)

decl := wrapDeclEmpty(n.Context, n.decls.empties.NewCompressed(rawDeclEmpty{
decl := wrapDeclEmpty(n.Context, newNode(n, &n.decls.empties, rawDeclEmpty{
semi: semicolon.ID(),
}))

Expand All @@ -58,7 +79,7 @@ func (n *Nodes) NewDeclEmpty(semicolon token.Token) DeclEmpty {
func (n *Nodes) NewDeclSyntax(args DeclSyntaxArgs) DeclSyntax {
n.panicIfNotOurs(args.Keyword, args.Equals, args.Value, args.Options, args.Semicolon)

return wrapDeclSyntax(n.Context, n.decls.syntaxes.NewCompressed(rawDeclSyntax{
return wrapDeclSyntax(n.Context, newNode(n, &n.decls.syntaxes, rawDeclSyntax{
keyword: args.Keyword.ID(),
equals: args.Equals.ID(),
value: args.Value.raw,
Expand All @@ -71,7 +92,7 @@ func (n *Nodes) NewDeclSyntax(args DeclSyntaxArgs) DeclSyntax {
func (n *Nodes) NewDeclPackage(args DeclPackageArgs) DeclPackage {
n.panicIfNotOurs(args.Keyword, args.Path, args.Options, args.Semicolon)

return wrapDeclPackage(n.Context, n.decls.packages.NewCompressed(rawDeclPackage{
return wrapDeclPackage(n.Context, newNode(n, &n.decls.packages, rawDeclPackage{
keyword: args.Keyword.ID(),
path: args.Path.raw,
options: n.options.Compress(args.Options.raw),
Expand All @@ -83,7 +104,7 @@ func (n *Nodes) NewDeclPackage(args DeclPackageArgs) DeclPackage {
func (n *Nodes) NewDeclImport(args DeclImportArgs) DeclImport {
n.panicIfNotOurs(args.Keyword, args.Modifier, args.ImportPath, args.Options, args.Semicolon)

return wrapDeclImport(n.Context, n.decls.imports.NewCompressed(rawDeclImport{
return wrapDeclImport(n.Context, newNode(n, &n.decls.imports, rawDeclImport{
keyword: args.Keyword.ID(),
modifier: args.Modifier.ID(),
importPath: args.ImportPath.raw,
Expand Down Expand Up @@ -118,7 +139,7 @@ func (n *Nodes) NewDeclDef(args DeclDefArgs) DeclDef {
}
}

return wrapDeclDef(n.Context, n.decls.defs.NewCompressed(raw))
return wrapDeclDef(n.Context, newNode(n, &n.decls.defs, raw))
}

// NewDeclBody creates a new DeclBody node.
Expand All @@ -127,7 +148,7 @@ func (n *Nodes) NewDeclDef(args DeclDefArgs) DeclDef {
func (n *Nodes) NewDeclBody(braces token.Token) DeclBody {
n.panicIfNotOurs(braces)

return wrapDeclBody(n.Context, n.decls.bodies.NewCompressed(rawDeclBody{
return wrapDeclBody(n.Context, newNode(n, &n.decls.bodies, rawDeclBody{
braces: braces.ID(),
}))
}
Expand All @@ -138,7 +159,7 @@ func (n *Nodes) NewDeclBody(braces token.Token) DeclBody {
func (n *Nodes) NewDeclRange(args DeclRangeArgs) DeclRange {
n.panicIfNotOurs(args.Keyword, args.Options, args.Semicolon)

return wrapDeclRange(n.Context, n.decls.ranges.NewCompressed(rawDeclRange{
return wrapDeclRange(n.Context, newNode(n, &n.decls.ranges, rawDeclRange{
keyword: args.Keyword.ID(),
options: n.options.Compress(args.Options.raw),
semi: args.Semicolon.ID(),
Expand All @@ -149,7 +170,7 @@ func (n *Nodes) NewDeclRange(args DeclRangeArgs) DeclRange {
func (n *Nodes) NewExprPrefixed(args ExprPrefixedArgs) ExprPrefixed {
n.panicIfNotOurs(args.Prefix, args.Expr)

ptr := n.exprs.prefixes.NewCompressed(rawExprPrefixed{
ptr := newNode(n, &n.exprs.prefixes, rawExprPrefixed{
prefix: args.Prefix.ID(),
expr: args.Expr.raw,
})
Expand All @@ -163,7 +184,7 @@ func (n *Nodes) NewExprPrefixed(args ExprPrefixedArgs) ExprPrefixed {
func (n *Nodes) NewExprRange(args ExprRangeArgs) ExprRange {
n.panicIfNotOurs(args.Start, args.To, args.End)

ptr := n.exprs.ranges.NewCompressed(rawExprRange{
ptr := newNode(n, &n.exprs.ranges, rawExprRange{
to: args.To.ID(),
start: args.Start.raw,
end: args.End.raw,
Expand All @@ -180,7 +201,7 @@ func (n *Nodes) NewExprRange(args ExprRangeArgs) ExprRange {
func (n *Nodes) NewExprArray(brackets token.Token) ExprArray {
n.panicIfNotOurs(brackets)

ptr := n.exprs.arrays.NewCompressed(rawExprArray{
ptr := newNode(n, &n.exprs.arrays, rawExprArray{
brackets: brackets.ID(),
})
return ExprArray{exprImpl[rawExprArray]{
Expand All @@ -195,7 +216,7 @@ func (n *Nodes) NewExprArray(brackets token.Token) ExprArray {
func (n *Nodes) NewExprDict(braces token.Token) ExprDict {
n.panicIfNotOurs(braces)

ptr := n.exprs.dicts.NewCompressed(rawExprDict{
ptr := newNode(n, &n.exprs.dicts, rawExprDict{
braces: braces.ID(),
})
return ExprDict{exprImpl[rawExprDict]{
Expand All @@ -208,7 +229,7 @@ func (n *Nodes) NewExprDict(braces token.Token) ExprDict {
func (n *Nodes) NewExprKV(args ExprFieldArgs) ExprField {
n.panicIfNotOurs(args.Key, args.Colon, args.Value)

ptr := n.exprs.fields.NewCompressed(rawExprField{
ptr := newNode(n, &n.exprs.fields, rawExprField{
key: args.Key.raw,
colon: args.Colon.ID(),
value: args.Value.raw,
Expand All @@ -223,7 +244,7 @@ func (n *Nodes) NewExprKV(args ExprFieldArgs) ExprField {
func (n *Nodes) NewTypePrefixed(args TypePrefixedArgs) TypePrefixed {
n.panicIfNotOurs(args.Prefix, args.Type)

ptr := n.types.prefixes.NewCompressed(rawTypePrefixed{
ptr := newNode(n, &n.types.prefixes, rawTypePrefixed{
prefix: args.Prefix.ID(),
ty: args.Type.raw,
})
Expand All @@ -239,7 +260,7 @@ func (n *Nodes) NewTypePrefixed(args TypePrefixedArgs) TypePrefixed {
func (n *Nodes) NewTypeGeneric(args TypeGenericArgs) TypeGeneric {
n.panicIfNotOurs(args.Path, args.AngleBrackets)

ptr := n.types.generics.NewCompressed(rawTypeGeneric{
ptr := newNode(n, &n.types.generics, rawTypeGeneric{
path: args.Path.raw,
args: rawTypeList{brackets: args.AngleBrackets.ID()},
})
Expand All @@ -253,7 +274,7 @@ func (n *Nodes) NewTypeGeneric(args TypeGenericArgs) TypeGeneric {
func (n *Nodes) NewCompactOptions(brackets token.Token) CompactOptions {
n.panicIfNotOurs(brackets)

return wrapOptions(n.Context, n.options.NewCompressed(rawCompactOptions{
return wrapOptions(n.Context, newNode(n, &n.options, rawCompactOptions{
brackets: brackets.ID(),
}))
}
Expand Down Expand Up @@ -290,3 +311,45 @@ func (n *Nodes) panicIfNotOurs(that ...any) {
))
}
}

// newNode creates a new node in the given arena, recording debugging information
// on n as it does so.
//
// This function wants to be a method of Nodes, but can't because it's generic.
func newNode[T any](n *Nodes, arena *arena.Arena[T], value T) arena.Pointer[T] {
p := arena.NewCompressed(value)
if n.EnableTracing {
traceNode(n, arena, p) // Outlined to promote inlining of newNode.
}
return p
}

// traceNode inserts a backtrace to the caller of newNode as the backtrace for
// the node at p.
func traceNode[T any](n *Nodes, arena *arena.Arena[T], p arena.Pointer[T]) {
if n.scratch == nil {
// NOTE: If spending four words on traces + scratch turns out to be
// wasteful, we can instead store this slice in traces itself, behind
// the uintptr value 1, which no pointer uses as its address.
n.scratch = make([]uintptr, 256)
}

var buf strings.Builder
// 0 means runtime.Callers, 1 means traceNode, and 2 means newNode. Thus,
// we want 3 for the caller of newNode.
trace := n.scratch[:runtime.Callers(3, n.scratch)]
frames := runtime.CallersFrames(trace)
for {
frame, more := frames.Next()
fmt.Fprintf(&buf, "at %s\n %s:%d\n", frame.Function, frame.File, frame.Line)
if !more {
break
}
}

if n.traces == nil {
n.traces = make(map[uintptr]string)
}

n.traces[unsafex.Addr(arena.Deref(p))] = buf.String()
}
9 changes: 9 additions & 0 deletions experimental/ast/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/token"
"github.com/bufbuild/protocompile/internal/arena"
"github.com/bufbuild/protocompile/internal/ext/unsafex"
)

// CompactOptions represents the collection of options attached to a field-like declaration,
Expand Down Expand Up @@ -120,6 +121,14 @@ func (o CompactOptions) Span() report.Span {
return report.Join(o.Brackets())
}

// Trace returns a stack trace for the site at which o was constructed using
// a [Nodes].
//
// Returns "" if a trace was not recorded. See Nodes.EnableTracing.
func (o CompactOptions) Trace() string {
return o.Context().Nodes().traces[unsafex.Addr(o.raw)]
}

func wrapOptions(c Context, ptr arena.Pointer[rawCompactOptions]) CompactOptions {
if ptr.Nil() {
return CompactOptions{}
Expand Down
Loading
Loading