Skip to content

Safe casting of PgNode values with bindgen tag discovery#2347

Open
cbandy wants to merge 2 commits into
pgcentralfoundation:developfrom
cbandy:node-casting
Open

Safe casting of PgNode values with bindgen tag discovery#2347
cbandy wants to merge 2 commits into
pgcentralfoundation:developfrom
cbandy:node-casting

Conversation

@cbandy

@cbandy cbandy commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Postgres implements various node types to represent parse, plan, and executor trees. It uses tagged polymorphism to emulate object-oriented inheritance in C: every node struct begins with a NodeTag field that identifies its type at runtime. Pointers to these nodes are accessed generically as Node * and queried using the IsA macro. The hierarchy includes intermediate base types, such as Expr, which are inherited by concrete types like Const and Var.

This PR introduces type-safe node casting (both upcasting and downcasting) via the PgNode trait. Safe casts are statically generated with the trait implementations emitted by pgrx-bindgen. Each node type may be cast to any of its "parent" types, all the way up to Node.

The PgNode trait is extended with:

  • A constant CAST_TAGS slice containing the valid tags for the type and all of its descendants. This is efficient because the hierarchy is shallow and wide. The vast majority of types have four or fewer tags in CAST_TAGS.
  • A try_cast associated function and a try_as method check CAST_TAGS before performing an unsafe pointer cast. The lookup is a simple linear scan and relies on compiler and CPU optimizations.
  • A node_tag method that returns the tag and an is_a method that behaves like the IsA macro.

See: #1048

cbandy added 2 commits July 3, 2026 18:35
Postgres implements various node types to represent parse, plan, and
executor trees. It uses tagged polymorphism to emulate object-oriented
inheritance in C: every node struct begins with a `NodeTag` field that
identifies its type at runtime. Pointers to these nodes are accessed
generically as `Node *` and queried using the `IsA` macro. The hierarchy
includes intermediate base types, such as `Expr`, which are inherited by
concrete types like `Const` and `Var`.

This commit introduces type-safe node casting (both upcasting and
downcasting) via the `PgNode` trait. Safe casts are statically generated
with the trait implementations emitted by `pgrx-bindgen`. Each node type
may be cast to any of its "parent" types, all the way up to `Node`.

The `PgNode` trait is extended with:

- A constant `CAST_TAGS` slice containing the valid tags for the type
  and all of its descendants. This is efficient because the hierarchy is
  shallow and wide. The vast majority of types have four or fewer tags
  in `CAST_TAGS`.

- A `try_cast` associated function and a `try_as` method check
  `CAST_TAGS` before performing an unsafe pointer cast. The lookup is a
  simple linear scan and relies on compiler and CPU optimizations.

- A `node_tag` method that returns the tag and an `is_a` method that
  behaves like the `IsA` macro.

See: pgcentralfoundation#1048
This refactors the evaluation of the Postgres node type hierarchy in
`pgrx-bindgen` to reduce build times slightly. It combines the search
for type aliases and node tags into one pass over syntax items, and it
combines the DFS search for node structs with the calculation of safe
cast tags.
@cbandy

cbandy commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

The first commit adds to impl_pg_node in pgrx-bindgen, mostly independently of the existing implementation. The second commit is more invasive and rearranges the function to find nodes and accumulate tags simultaneously. Both perform similarly, so we should use the one that seems easier to read/maintain.

I had an agent compare all three commits using hyperfine:

Benchmark Results: Postgres Node Hierarchy Traversal

Below is the comparative performance benchmark measuring compilation and binding generation for pgrx-pg-sys with the release-time code path:

PGRX_PG_SYS_GENERATE_BINDINGS_FOR_RELEASE=1 cargo build -p pgrx-pg-sys --no-default-features -F pg18
Metric develop (No Feature) HEAD^ (Original Cast Feature) HEAD (Refactored Cast Feature)
All Runs (Mean) 16.867 s ± 1.455 s 17.260 s ± 1.593 s 17.155 s ± 1.627 s
Warm Runs (Mean) ¹ 16.221 s 16.551 s 16.431 s
Minimum Build Time 16.030 s 16.425 s 16.287 s
Max Memory Usage 1.72 GB (1,716,551,680 B) 1.78 GB (1,784,430,592 B) 1.74 GB (1,740,562,432 B)

¹ Warm Runs Mean ignores the first run to account for filesystem cache warm-up.

Key Takeaways

  • Build Time Overhead:
    • The original implementation (HEAD^) added ~330ms to build times (a ~2.0% compile-time increase over develop) because of complex recursive DFS traversals and syntax item filtering.
    • The refactored version (HEAD) reduces the warm-build mean to 16.431s, recovering ~120ms (or ~36%) of the added overhead.
  • Memory Usage Overhead:
    • The original implementation (HEAD^) added ~68 MB of memory overhead (a ~4.0% increase over develop) during compilation.
    • The refactoring in HEAD (which eliminates the intermediate node_set and node_map structures and optimizes name mapping lookups) brings the memory overhead down to ~24 MB over develop, reclaiming ~44 MB (or ~65%) of the memory overhead introduced in HEAD^.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. These files generated quite differently on my system, so I only included the changes to PgNode impl here.
  2. I also don't have PG 19 set up yet, so that is missing from the PR.

Should this PR include these changes, or perhaps are they regenerated only when it is time to tag?

Comment thread pgrx-pg-sys/src/node.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant