Skip to content

Commit afe63de

Browse files
authored
Merge pull request #3 from commandprompt/chore/post-vtable-cleanup
Post-vtable-refactor cleanup: docs + relocate plx_diag_prefix into the engine
2 parents 32ea464 + 94f7965 commit afe63de

10 files changed

Lines changed: 116 additions & 67 deletions

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@ All notable changes to plx are recorded here. The format follows
44
[Keep a Changelog](https://keepachangelog.com/), and plx uses the extension
55
version in `plx.control` (currently `1.0`).
66

7+
## [Unreleased]
8+
9+
### Changed
10+
11+
- Internal refactor of the transpiler behind a `PlxSurface.parse_body` vtable
12+
(#2). The single ~11k-line `src/plx_transpile.c` is split into a
13+
dialect-neutral engine (declared in the new `src/plx_engine.h`) plus per-dialect
14+
front ends in `src/plx_dialect_*.c` and the shared `src/plx_parse_brace.c`;
15+
the hardcoded `block_style` dispatch is replaced by a per-dialect `parse_body`
16+
function pointer. No functional change; generated plpgsql is byte-identical
17+
and all regression tests pass. Adding a dialect no longer touches shared code.
18+
- Follow-up to the above: relocated the dialect-neutral `plx_diag_prefix()`
19+
helper out of `src/plx_dialect_ruby.c` and back into the engine
20+
(`src/plx_transpile.c`), where its other callers (Python and the brace
21+
front end) already live. No functional change.
22+
- Tightened linkage and removed dead code left by the refactor: the six
23+
single-dialect `plx_*_parse_body` front ends are now `static` (only the
24+
shared `plx_brace_parse_body` keeps a prototype in `plx_engine.h`); dropped
25+
the write-only `Ctx.nt` field; and corrected a stale COBOL comment about a
26+
keyword table the surface does not carry. No functional change.
27+
728
## [1.3.1] - 2026-07-16
829

930
Code-only patch release (no catalog changes) carrying the memory-safety and

doc/ARCHITECTURE.md

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# plx Architecture
22

3-
plx lets a function body be written in a Ruby, PHP, JavaScript, or Python
4-
dialect and executed by the standard plpgsql interpreter. This document
5-
describes how that works in the extension as built.
3+
plx lets a function body be written in a Ruby, PHP, JavaScript, TypeScript,
4+
Python, Go, COBOL, Oracle PL/SQL, or Transact-SQL dialect and executed by the
5+
standard plpgsql interpreter. This document describes how that works in the
6+
extension as built.
67

78
## The core idea
89

@@ -85,36 +86,66 @@ It is dialect-pluggable through a `PlxSurface` (in `plx_int.h`) that each dialec
8586
supplies. The surface describes what varies between languages:
8687

8788
- the keyword table, mapping each dialect's spellings to canonical keywords;
88-
- the block style: keyword-delimited (`end`), brace-delimited (`{ }`), or
89-
indentation (INDENT/DEDENT);
89+
- the block style, used as a lexer hint (indentation-tokenized vs.
90+
newline-tokenized);
9091
- comment syntax, the variable sigil (for example `$`), the string-concatenation
9192
operator, and how string interpolation is written (`#{}`, `$var` and `{$e}`,
92-
`${}` template literals, or f-strings).
93-
94-
The shared code (`plx_transpile.c`) is dialect-neutral: the lexer, the three
95-
block parsers, the expression rewriter, DECLARE-hoisting and type inference, the
96-
statement lowering, and the intrinsics (`query`, `fetch_one`, `perform`,
97-
`execute`, `return_query`, cursors, and so on) are all driven by the surface.
93+
`${}` template literals, or f-strings);
94+
- `parse_body`, the front-end entry point (see below), and
95+
`self_contained_block`, a flag for dialects that emit their own
96+
`DECLARE`/`BEGIN`/`END` (PL/SQL).
97+
98+
### Engine and front ends
99+
100+
The transpiler is split into a dialect-neutral **engine** and per-dialect
101+
**front ends**, selected through the `parse_body` function pointer on the
102+
surface (a vtable method). `plx_transpile()` just calls
103+
`cx->surf->parse_body(cx)` and then runs one assemble tail; there is no
104+
per-dialect branching in the driver.
105+
106+
- The **engine** lives in `plx_transpile.c` and is declared to the front ends
107+
through `plx_engine.h`: the shared byte lexer (`plx_lex`), the expression
108+
rewriter (`plx_rewrite_expr`), the leaf-statement emitter and intrinsics
109+
(`query`, `fetch_one`, `perform`, `execute`, `return_query`, cursors, and so
110+
on), the symbol table, string/interpolation decoding, and the final
111+
DECLARE-hoisting + assemble. It contains no dialect-specific code.
112+
- Each **front end** owns its dialect's tokenizer, parser, and statement
113+
lowering, and implements `parse_body` by transforming `cx->body` into
114+
`cx->out`. The text-family dialects (`php`/`js`/`ts`) share a brace parser in
115+
`plx_parse_brace.c`; Ruby (keyword-`end`) and Python (indentation) parse on
116+
top of the shared lexer inside their own translation units; and the
117+
standalone dialects (COBOL, PL/SQL, T-SQL, Go) run their own tokenizer and
118+
emitter, calling back into the engine only for shared services.
98119

99120
## Files
100121

101122
```
102123
plx.control, plx--1.0.sql extension control and install SQL
103124
src/plx.h public ABI (PlxDialect, PlxFuncMeta)
104125
src/plx_int.h internal ABI (PlxSurface, canonical keywords)
126+
src/plx_engine.h engine interface: PlxCtx, tokens, symtab, and the
127+
plx_* entry points the front ends call
105128
src/plx_core.c PL handler binding, registry, generic validator
106129
and inline handler
107-
src/plx_transpile.c the shared transpiler
108-
src/plx_dialect_ruby.c the plxruby surface and trampolines
109-
src/plx_dialect_php.c the plxphp surface and trampolines
110-
src/plx_dialect_js.c the plxjs surface and trampolines
111-
src/plx_dialect_python.c the plxpython3 surface and trampolines
130+
src/plx_transpile.c the dialect-neutral engine + plx_transpile() driver
131+
src/plx_strbuild.c string-builder intrinsic helpers
132+
src/plx_parse_brace.c shared brace front end (php/js/ts) + TS preprocess
133+
src/plx_dialect_ruby.c the plxruby surface + Ruby front end
134+
src/plx_dialect_php.c the plxphp surface (parse_body -> brace front end)
135+
src/plx_dialect_js.c the plxjs surface (parse_body -> brace front end)
136+
src/plx_dialect_ts.c the plxts surface (parse_body -> brace front end)
137+
src/plx_dialect_python.c the plxpython3 surface + Python front end
138+
src/plx_dialect_go.c the plxgo surface + Go front end
139+
src/plx_dialect_cobol.c the plxcobol surface + COBOL front end
140+
src/plx_dialect_plsql.c the plxplsql surface + PL/SQL front end
141+
src/plx_dialect_tsql.c the plxtsql surface + T-SQL front end
112142
```
113143

114-
Everything links into a single `plx.so`. A dialect is a `PlxSurface` plus three
115-
small trampolines (validator, inline handler, and the shared call-handler
116-
binding), registered in `_PG_init`. Adding a dialect is a new surface and a few
117-
`CREATE LANGUAGE` lines; it does not touch the shared transpiler.
144+
Everything links into a single `plx.so`. A dialect is a `PlxSurface` (including
145+
its `parse_body` front end) plus small trampolines (validator, inline handler,
146+
and the shared call-handler binding), registered in `_PG_init`. Adding a dialect
147+
is a new `plx_dialect_X.c` (a surface with its `parse_body`, plus a few
148+
`CREATE LANGUAGE` lines), and does not touch the shared engine.
118149

119150
## Trust
120151

@@ -132,4 +163,6 @@ fuzzed (see `test/fuzz.py`).
132163
- [TRANSPILER.md](TRANSPILER.md): the original transpiler design specification.
133164
- [PARITY.md](PARITY.md): the plpgsql construct parity matrix.
134165
- The per-dialect chapters: [plxruby](plxruby.md), [plxphp](plxphp.md),
135-
[plxjs](plxjs.md), [plxpython3](plxpython3.md).
166+
[plxjs](plxjs.md), [plxts](plxts.md), [plxpython3](plxpython3.md),
167+
[plxgo](plxgo.md), [plxcobol](plxcobol.md), [plxplsql](plxplsql.md),
168+
[plxtsql](plxtsql.md).

src/plx_dialect_cobol.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
* it does not fit the shared byte lexer's expression model (hyphenated words,
66
* period sentence terminators, PICTURE clauses), so the COBOL front end (below)
77
* uses its own tokenizer and recursive-descent parser and emits plpgsql
8-
* directly; the dialect-neutral engine lives in plx_transpile.c. The keyword
9-
* table is unused by the COBOL path but kept for consistency.
8+
* directly; the dialect-neutral engine lives in plx_transpile.c. The COBOL
9+
* front end does not use the shared keyword table (its surface carries none).
1010
*/
1111
#include "postgres.h"
1212

@@ -2184,7 +2184,7 @@ cob_block(Cb *cb, int ind)
21842184
cb->cx->depth--;
21852185
}
21862186

2187-
void
2187+
static void
21882188
plx_cobol_parse_body(Ctx *cx)
21892189
{
21902190
Cb cb;

src/plx_dialect_go.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1913,7 +1913,7 @@ go_block(Go *g, int ind)
19131913
g->pos++;
19141914
}
19151915

1916-
void
1916+
static void
19171917
plx_go_parse_body(Ctx *cx)
19181918
{
19191919
Go g;

src/plx_dialect_plsql.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ pl_emit_raw(Pl *pl, int a, int b, StringInfo out)
270270
(int) (pl->t[b - 1].s + pl->t[b - 1].len - pl->t[a].s));
271271
}
272272

273-
void
273+
static void
274274
plx_plsql_parse_body(Ctx *cx)
275275
{
276276
Pl pl;

src/plx_dialect_python.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ parse_py_program(Ctx *cx)
566566

567567

568568

569-
void
569+
static void
570570
plx_python_parse_body(Ctx *cx)
571571
{
572572
plx_lex(cx);

src/plx_dialect_ruby.c

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -409,34 +409,6 @@ parse_iter(Ctx *cx, int a, int do_pos, int e, int ind)
409409
appendStringInfoString(o, "END LOOP;\n");
410410
}
411411

412-
/* GET STACKED DIAGNOSTICS lines for the fields used in a handler (see diag_mask) */
413-
char *
414-
plx_diag_prefix(int mask, int ind)
415-
{
416-
static const struct { int bit; const char *fld; const char *item; } t[] = {
417-
{PLX_DIAG_DETAIL, "detail", "PG_EXCEPTION_DETAIL"},
418-
{PLX_DIAG_HINT, "hint", "PG_EXCEPTION_HINT"},
419-
{PLX_DIAG_CONSTRAINT, "constraint", "CONSTRAINT_NAME"},
420-
{PLX_DIAG_COLUMN, "column", "COLUMN_NAME"},
421-
{PLX_DIAG_TABLE, "table", "TABLE_NAME"},
422-
{PLX_DIAG_SCHEMA, "schema", "SCHEMA_NAME"},
423-
{PLX_DIAG_DATATYPE, "datatype", "PG_DATATYPE_NAME"},
424-
};
425-
StringInfoData p;
426-
int i;
427-
428-
if (!mask)
429-
return pstrdup("");
430-
initStringInfo(&p);
431-
for (i = 0; i < (int) (sizeof(t) / sizeof(t[0])); i++)
432-
if (mask & t[i].bit)
433-
{
434-
appendStringInfoSpaces(&p, ind * 2);
435-
appendStringInfo(&p, "GET STACKED DIAGNOSTICS __plx_%s = %s;\n", t[i].fld, t[i].item);
436-
}
437-
return p.data;
438-
}
439-
440412
/* begin [body] [rescue [Class] [=> e] handler]* [ensure body] end */
441413
static void
442414
parse_begin(Ctx *cx, int ind)
@@ -763,7 +735,7 @@ parse_stmt_inner(Ctx *cx, int ind, bool toplevel)
763735
}
764736

765737

766-
void
738+
static void
767739
plx_ruby_parse_body(Ctx *cx)
768740
{
769741
plx_lex(cx);

src/plx_dialect_tsql.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1488,7 +1488,7 @@ tq_block(Tq *tq, int ind)
14881488
tq_stmt(tq, ind);
14891489
}
14901490

1491-
void
1491+
static void
14921492
plx_tsql_parse_body(Ctx *cx)
14931493
{
14941494
Tq tq;

src/plx_engine.h

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ typedef struct PlxCtx
8484
{
8585
const char *body;
8686
Tok *t;
87-
int nt, pos;
87+
int pos;
8888
const PlxFuncMeta *meta;
8989
const PlxSurface *surf;
9090
StringInfoData out; /* emitted BEGIN..END body */
@@ -137,14 +137,10 @@ PlxLocal2 *plx_local_find(Ctx *cx, const char *name, int len);
137137
bool plx_is_param(Ctx *cx, const char *name, int len);
138138

139139
/* ---------------------------------------------- dialect front ends (vtable) */
140-
/* Each is wired into a PlxSurface.parse_body in the owning plx_dialect_*.c /
141-
* plx_parse_brace.c. They transform cx->body into cx->out. */
142-
void plx_ruby_parse_body(Ctx *cx);
140+
/* Each dialect wires a parse_body into its PlxSurface (see plx_dialect_*.c);
141+
* the function transforms cx->body into cx->out. Only the brace front end is
142+
* shared across translation units (php / js / ts) and so needs a prototype
143+
* here; every other parse_body is static to its own dialect file. */
143144
void plx_brace_parse_body(Ctx *cx); /* php / js / ts (ts sets ts_types) */
144-
void plx_python_parse_body(Ctx *cx);
145-
void plx_cobol_parse_body(Ctx *cx);
146-
void plx_plsql_parse_body(Ctx *cx);
147-
void plx_tsql_parse_body(Ctx *cx);
148-
void plx_go_parse_body(Ctx *cx);
149145

150146
#endif /* PLX_ENGINE_H */

src/plx_transpile.c

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,6 @@ plx_lex(Ctx *cx)
507507
t[n].kind = T_EOF; t[n].line = line; t[n].s = tokstart; n++;
508508
}
509509
cx->t = t;
510-
cx->nt = n;
511510
cx->pos = 0;
512511
#undef PUSH
513512
}
@@ -1591,6 +1590,34 @@ plx_parse_args(Ctx *cx, int a, int *as, int *ae, int maxargs, int *after)
15911590
return n;
15921591
}
15931592

1593+
/* GET STACKED DIAGNOSTICS lines for the fields used in a handler (see diag_mask) */
1594+
char *
1595+
plx_diag_prefix(int mask, int ind)
1596+
{
1597+
static const struct { int bit; const char *fld; const char *item; } t[] = {
1598+
{PLX_DIAG_DETAIL, "detail", "PG_EXCEPTION_DETAIL"},
1599+
{PLX_DIAG_HINT, "hint", "PG_EXCEPTION_HINT"},
1600+
{PLX_DIAG_CONSTRAINT, "constraint", "CONSTRAINT_NAME"},
1601+
{PLX_DIAG_COLUMN, "column", "COLUMN_NAME"},
1602+
{PLX_DIAG_TABLE, "table", "TABLE_NAME"},
1603+
{PLX_DIAG_SCHEMA, "schema", "SCHEMA_NAME"},
1604+
{PLX_DIAG_DATATYPE, "datatype", "PG_DATATYPE_NAME"},
1605+
};
1606+
StringInfoData p;
1607+
int i;
1608+
1609+
if (!mask)
1610+
return pstrdup("");
1611+
initStringInfo(&p);
1612+
for (i = 0; i < (int) (sizeof(t) / sizeof(t[0])); i++)
1613+
if (mask & t[i].bit)
1614+
{
1615+
appendStringInfoSpaces(&p, ind * 2);
1616+
appendStringInfo(&p, "GET STACKED DIAGNOSTICS __plx_%s = %s;\n", t[i].fld, t[i].item);
1617+
}
1618+
return p.data;
1619+
}
1620+
15941621
/* Emit a Ruby string token as raw SQL text (interp #{e} -> rewritten expr,
15951622
* inline; no surrounding quotes). Used for static query/perform/fetch SQL. */
15961623
void

0 commit comments

Comments
 (0)