From f9a530d0e4f75ce48a776c225c39b74c92a04d92 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Tue, 17 Dec 2024 11:41:53 -0800 Subject: [PATCH 1/2] Nodes.EnableTracing --- experimental/ast/decl.go | 9 ++++ experimental/ast/expr.go | 9 ++++ experimental/ast/nodes.go | 90 +++++++++++++++++++++++++++------ experimental/ast/options.go | 9 ++++ experimental/ast/type.go | 9 ++++ internal/ext/unsafex/unsafex.go | 8 +++ 6 files changed, 119 insertions(+), 15 deletions(-) diff --git a/experimental/ast/decl.go b/experimental/ast/decl.go index 42a23c4c..be74f525 100644 --- a/experimental/ast/decl.go +++ b/experimental/ast/decl.go @@ -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 ( @@ -187,6 +188,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]{} diff --git a/experimental/ast/expr.go b/experimental/ast/expr.go index 99ae1400..f43e3b28 100644 --- a/experimental/ast/expr.go +++ b/experimental/ast/expr.go @@ -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 ( @@ -221,6 +222,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] diff --git a/experimental/ast/nodes.go b/experimental/ast/nodes.go index 96ecc7e1..82c0e2e2 100644 --- a/experimental/ast/nodes.go +++ b/experimental/ast/nodes.go @@ -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 @@ -28,10 +31,25 @@ 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. + // + // 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. @@ -47,7 +65,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(), })) @@ -58,7 +76,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, @@ -71,7 +89,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), @@ -83,7 +101,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, @@ -118,7 +136,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. @@ -127,7 +145,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(), })) } @@ -138,7 +156,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(), @@ -149,7 +167,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, }) @@ -163,7 +181,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, @@ -180,7 +198,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]{ @@ -195,7 +213,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]{ @@ -208,7 +226,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, @@ -223,7 +241,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, }) @@ -239,7 +257,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()}, }) @@ -253,7 +271,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(), })) } @@ -290,3 +308,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() +} diff --git a/experimental/ast/options.go b/experimental/ast/options.go index f3ad686e..67dac918 100644 --- a/experimental/ast/options.go +++ b/experimental/ast/options.go @@ -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, @@ -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{} diff --git a/experimental/ast/type.go b/experimental/ast/type.go index d020d53a..334533f9 100644 --- a/experimental/ast/type.go +++ b/experimental/ast/type.go @@ -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 ( @@ -143,6 +144,14 @@ func (t typeImpl[Raw]) AsAny() TypeAny { return newTypeAny(t.Context(), wrapPathLike(kind, arena.Compress(t.raw))) } +// Trace returns a stack trace for the site at which t was constructed using +// a [Nodes]. +// +// Returns "" if a trace was not recorded. See Nodes.EnableTracing. +func (t typeImpl[Raw]) Trace() string { + return t.Context().Nodes().traces[unsafex.Addr(t.raw)] +} + // types is storage for every kind of Type in a Context.raw. type types struct { prefixes arena.Arena[rawTypePrefixed] diff --git a/internal/ext/unsafex/unsafex.go b/internal/ext/unsafex/unsafex.go index 63dcbd53..c39c4961 100644 --- a/internal/ext/unsafex/unsafex.go +++ b/internal/ext/unsafex/unsafex.go @@ -37,6 +37,14 @@ func Size[T any]() int { return int(unsafe.Sizeof(v)) } +// Addr converts any pointer type into an address. +// +// This function is primarily intended for cases where the address will never +// be turned back into a pointer. +func Addr[P ~*E, E any](p P) uintptr { + return uintptr(unsafe.Pointer(p)) +} + // Add is like [unsafe.Add], but it operates on a typed pointer and scales the // offset by that type's size, similar to pointer arithmetic in Rust or C. // From e7859d46d6ccafd0342281cace2c6ab9fbae5d5d Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Tue, 17 Dec 2024 12:09:06 -0800 Subject: [PATCH 2/2] add Trace to the Any types --- experimental/ast/decl.go | 25 +++++++++++++++++++++++++ experimental/ast/expr.go | 25 +++++++++++++++++++++++++ experimental/ast/nodes.go | 3 +++ experimental/ast/type.go | 18 ++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/experimental/ast/decl.go b/experimental/ast/decl.go index be74f525..16365f75 100644 --- a/experimental/ast/decl.go +++ b/experimental/ast/decl.go @@ -160,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 diff --git a/experimental/ast/expr.go b/experimental/ast/expr.go index f43e3b28..334b95f5 100644 --- a/experimental/ast/expr.go +++ b/experimental/ast/expr.go @@ -200,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. diff --git a/experimental/ast/nodes.go b/experimental/ast/nodes.go index 82c0e2e2..99b4303d 100644 --- a/experimental/ast/nodes.go +++ b/experimental/ast/nodes.go @@ -35,6 +35,9 @@ type Nodes struct { // 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 diff --git a/experimental/ast/type.go b/experimental/ast/type.go index 334533f9..6e469e74 100644 --- a/experimental/ast/type.go +++ b/experimental/ast/type.go @@ -125,6 +125,24 @@ func (t TypeAny) Span() report.Span { ) } +// Trace returns a stack trace for the site at which t was constructed using +// a [Nodes]. +// +// Returns "" if a trace was not recorded. See Nodes.EnableTracing. +func (t TypeAny) Trace() string { + switch t.Kind() { + case TypeKindGeneric: + return t.AsGeneric().Trace() + case TypeKindPrefixed: + return t.AsPrefixed().Trace() + case TypeKindPath: + // TypeKindPath does not currently record traces. + fallthrough + default: + return "" + } +} + // typeImpl is the common implementation of pointer-like Type* types. type typeImpl[Raw any] struct { // NOTE: These fields are sorted by alignment.