Skip to content

Refactor dialect front ends behind a PlxSurface.parse_body vtable#2

Merged
jdatcmd merged 1 commit into
commandprompt:masterfrom
jonahharris:refactor/dialect-vtable
Jul 17, 2026
Merged

Refactor dialect front ends behind a PlxSurface.parse_body vtable#2
jdatcmd merged 1 commit into
commandprompt:masterfrom
jonahharris:refactor/dialect-vtable

Conversation

@jonahharris

Copy link
Copy Markdown

What & why

The transpiler kept every dialect's lexer, parser, and emitter inside the single ~11k-line src/plx_transpile.c and dispatched them with a hardcoded block_style if-ladder — the "general" transpiler owned each language's implementation. This restructures it to mirror the precomp design: a dialect-neutral engine plus per-dialect front ends selected through a function pointer on the per-dialect descriptor.

This is a pure architectural change. The generated plpgsql is byte-identical.

Architecture

  • PlxSurface becomes the vtable. It already rides on cx->surf; it gains void (*parse_body)(struct PlxCtx *cx) and a self_contained_block flag. plx_transpile() now calls cx.surf->parse_body(&cx) and runs a single assemble tail (conditional on self_contained_block for PL/SQL, which emits its own DECLARE/BEGIN/END). The block_style if-ladder is gone; block_style survives only as a lexer hint (indent vs. newline tokenizing).
  • New src/plx_engine.h exposes the shared engine: the context (struct PlxCtx), Tok/PlxLocal2 types, PLX_MAX_DEPTH/PLX_DIAG_*, and the plx_*-prefixed entry points (plx_lex, plx_rewrite_expr, plx_emit_core/leaf, plx_span_text, plx_local_add/find, plx_skip_seps, plx_is_ident[_start], …). The engine — lexer, expression rewriter, leaf emitter, symbol table, interpolation, assemble — stays in plx_transpile.c, now ~2.6k lines.
  • Each dialect owns its front end. COBOL/PL-SQL/T-SQL/Go/Ruby/Python tokenizers+parsers move into their plx_dialect_*.c; the shared PHP/JS/TS brace parser (plus TypeScript preprocessing) moves into a new src/plx_parse_brace.c. php/js/ts become thin config stubs whose parse_body points at plx_brace_parse_body.

Result

before after
plx_transpile.c 11,135 2,655
per-dialect files ~70-line stubs 478–2,277 lines each
new files plx_engine.h, plx_parse_brace.c

Adding a dialect is now: new plx_dialect_X.c with a PlxSurface + parse_body, register it — no edit to the shared engine.

Verification

  • Byte-identical output: md5(pg_proc.prosrc) captured for all 209 functions the regression suite creates, and diffed after every relocation step — identical throughout.
  • All 13 installcheck tests pass at each step and at the end.
  • Zero compiler warnings on a full clean rebuild.
  • No transpiler logic was changed — only relocated.

🤖 Generated with Claude Code

The transpiler put every dialect's lexer, parser, and emitter inside the
single ~11k-line src/plx_transpile.c and dispatched them with a hardcoded
`block_style` if-ladder. This inverts SOLID responsibility: the "general"
transpiler owned each language. This commit restructures it to mirror the
precomp design — a dialect-neutral engine plus per-dialect front ends selected
through a function pointer on the per-dialect descriptor.

Architecture:
- PlxSurface (the per-dialect descriptor already threaded as cx->surf) gains a
  vtable method `void (*parse_body)(struct PlxCtx *cx)` and a
  `self_contained_block` flag. plx_transpile() now calls
  `cx.surf->parse_body(&cx)` and runs one assemble tail (conditional on
  self_contained_block for PL/SQL, which emits its own DECLARE/BEGIN/END) —
  the block_style if-ladder is gone. block_style survives only as a lexer hint.
- New src/plx_engine.h exposes the shared engine: the Ctx (now `struct PlxCtx`),
  Tok/PlxLocal2 types, PLX_MAX_DEPTH/PLX_DIAG_* macros, and the plx_*-prefixed
  entry points (plx_lex, plx_rewrite_expr, plx_emit_core/leaf, plx_span_text,
  plx_local_add/find, plx_skip_seps, plx_is_ident[_start], ...). The engine
  (lexer, expression rewriter, leaf emitter, symbol table, interpolation,
  assemble) stays in plx_transpile.c, now ~2.6k lines.
- Each dialect's tokenizer/parser/emitter moves into its own translation unit:
  COBOL/PL-SQL/T-SQL/Go/Ruby/Python into their plx_dialect_*.c, and the shared
  PHP/JS/TS brace parser (+ TypeScript preprocessing) into a new
  src/plx_parse_brace.c. php/js/ts become thin config stubs whose parse_body
  points at plx_brace_parse_body.

No functional change. The generated plpgsql (pg_proc.prosrc) is byte-identical
across all 209 regression functions before and after; the move was verified at
every step against that golden set. All 13 installcheck tests pass and the tree
builds with zero compiler warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved — verified locally

Reviewed as a pure architectural refactor and verified the claims empirically (build + full regression suite against unchanged goldens), not just by reading the diff.

Verification

  • Clean rebuild of all 14 translation units: zero warnings.
  • installcheck (13 tests, all dialects): 13/13 pass against the unchanged test/expected goldens — consistent with the "no functional change / byte-identical output" claim.
  • The one genuinely new logic surface — the plx_transpile() driver — checks out as a 1:1 behavioral equivalent of the old dispatch: the new self_contained_block path reproduces the old PLX_BLK_PLSQL branch exactly, and the else path reproduces the old DECLARE/BEGIN/END assemble exactly. The block_style if-ladder is correctly reduced to a lexer hint.
  • Diff touches only src/* + Makefile; no test/expected edits, as expected for a no-op refactor.

Assessment

  • Mirrors the existing precomp engine+front-end design, so it's consistent with the established architecture.
  • Real maintainability win: plx_transpile.c 11,135 → 2,655 lines; adding a dialect no longer touches shared code.
  • Low risk: mechanical relocation with a clean, verified dispatch equivalence.

LGTM. 🤖 verified with Claude Code.

@jdatcmd
jdatcmd merged commit 32ea464 into commandprompt:master Jul 17, 2026
6 checks passed
jdatcmd pushed a commit that referenced this pull request Jul 17, 2026
Follow-on cleanup from the post-merge audit of #2, in the same spirit as the
plx_diag_prefix relocation:

- Linkage (#2): the six single-dialect front ends (ruby/python/cobol/plsql/
  tsql/go plx_*_parse_body) are only ever referenced by their own surface's
  .parse_body in their own translation unit, so make them static and drop
  their prototypes from plx_engine.h. Only plx_brace_parse_body is genuinely
  shared (php/js/ts) and keeps its prototype.
- Dead code (#3): drop the write-only Ctx.nt field (set once in plx_lex,
  never read; the standalone tokenizers track their own token counts on their
  own structs).
- Comment (#4): COBOL's surface carries no keyword table (.kws = NULL); fix
  the header comment that claimed a table was "kept for consistency".

No functional change. Clean rebuild (zero warnings), 13/13 installcheck pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants