Skip to content
Merged
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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@ All notable changes to plx are recorded here. The format follows
[Keep a Changelog](https://keepachangelog.com/), and plx uses the extension
version in `plx.control` (currently `1.0`).

## [Unreleased]

### Changed

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

## [1.3.1] - 2026-07-16

Code-only patch release (no catalog changes) carrying the memory-safety and
Expand Down
75 changes: 54 additions & 21 deletions doc/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# plx Architecture

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

## The core idea

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

- the keyword table, mapping each dialect's spellings to canonical keywords;
- the block style: keyword-delimited (`end`), brace-delimited (`{ }`), or
indentation (INDENT/DEDENT);
- the block style, used as a lexer hint (indentation-tokenized vs.
newline-tokenized);
- comment syntax, the variable sigil (for example `$`), the string-concatenation
operator, and how string interpolation is written (`#{}`, `$var` and `{$e}`,
`${}` template literals, or f-strings).

The shared code (`plx_transpile.c`) is dialect-neutral: the lexer, the three
block parsers, the expression rewriter, DECLARE-hoisting and type inference, the
statement lowering, and the intrinsics (`query`, `fetch_one`, `perform`,
`execute`, `return_query`, cursors, and so on) are all driven by the surface.
`${}` template literals, or f-strings);
- `parse_body`, the front-end entry point (see below), and
`self_contained_block`, a flag for dialects that emit their own
`DECLARE`/`BEGIN`/`END` (PL/SQL).

### Engine and front ends

The transpiler is split into a dialect-neutral **engine** and per-dialect
**front ends**, selected through the `parse_body` function pointer on the
surface (a vtable method). `plx_transpile()` just calls
`cx->surf->parse_body(cx)` and then runs one assemble tail; there is no
per-dialect branching in the driver.

- The **engine** lives in `plx_transpile.c` and is declared to the front ends
through `plx_engine.h`: the shared byte lexer (`plx_lex`), the expression
rewriter (`plx_rewrite_expr`), the leaf-statement emitter and intrinsics
(`query`, `fetch_one`, `perform`, `execute`, `return_query`, cursors, and so
on), the symbol table, string/interpolation decoding, and the final
DECLARE-hoisting + assemble. It contains no dialect-specific code.
- Each **front end** owns its dialect's tokenizer, parser, and statement
lowering, and implements `parse_body` by transforming `cx->body` into
`cx->out`. The text-family dialects (`php`/`js`/`ts`) share a brace parser in
`plx_parse_brace.c`; Ruby (keyword-`end`) and Python (indentation) parse on
top of the shared lexer inside their own translation units; and the
standalone dialects (COBOL, PL/SQL, T-SQL, Go) run their own tokenizer and
emitter, calling back into the engine only for shared services.

## Files

```
plx.control, plx--1.0.sql extension control and install SQL
src/plx.h public ABI (PlxDialect, PlxFuncMeta)
src/plx_int.h internal ABI (PlxSurface, canonical keywords)
src/plx_engine.h engine interface: PlxCtx, tokens, symtab, and the
plx_* entry points the front ends call
src/plx_core.c PL handler binding, registry, generic validator
and inline handler
src/plx_transpile.c the shared transpiler
src/plx_dialect_ruby.c the plxruby surface and trampolines
src/plx_dialect_php.c the plxphp surface and trampolines
src/plx_dialect_js.c the plxjs surface and trampolines
src/plx_dialect_python.c the plxpython3 surface and trampolines
src/plx_transpile.c the dialect-neutral engine + plx_transpile() driver
src/plx_strbuild.c string-builder intrinsic helpers
src/plx_parse_brace.c shared brace front end (php/js/ts) + TS preprocess
src/plx_dialect_ruby.c the plxruby surface + Ruby front end
src/plx_dialect_php.c the plxphp surface (parse_body -> brace front end)
src/plx_dialect_js.c the plxjs surface (parse_body -> brace front end)
src/plx_dialect_ts.c the plxts surface (parse_body -> brace front end)
src/plx_dialect_python.c the plxpython3 surface + Python front end
src/plx_dialect_go.c the plxgo surface + Go front end
src/plx_dialect_cobol.c the plxcobol surface + COBOL front end
src/plx_dialect_plsql.c the plxplsql surface + PL/SQL front end
src/plx_dialect_tsql.c the plxtsql surface + T-SQL front end
```

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

## Trust

Expand All @@ -132,4 +163,6 @@ fuzzed (see `test/fuzz.py`).
- [TRANSPILER.md](TRANSPILER.md): the original transpiler design specification.
- [PARITY.md](PARITY.md): the plpgsql construct parity matrix.
- The per-dialect chapters: [plxruby](plxruby.md), [plxphp](plxphp.md),
[plxjs](plxjs.md), [plxpython3](plxpython3.md).
[plxjs](plxjs.md), [plxts](plxts.md), [plxpython3](plxpython3.md),
[plxgo](plxgo.md), [plxcobol](plxcobol.md), [plxplsql](plxplsql.md),
[plxtsql](plxtsql.md).
6 changes: 3 additions & 3 deletions src/plx_dialect_cobol.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* it does not fit the shared byte lexer's expression model (hyphenated words,
* period sentence terminators, PICTURE clauses), so the COBOL front end (below)
* uses its own tokenizer and recursive-descent parser and emits plpgsql
* directly; the dialect-neutral engine lives in plx_transpile.c. The keyword
* table is unused by the COBOL path but kept for consistency.
* directly; the dialect-neutral engine lives in plx_transpile.c. The COBOL
* front end does not use the shared keyword table (its surface carries none).
*/
#include "postgres.h"

Expand Down Expand Up @@ -2184,7 +2184,7 @@ cob_block(Cb *cb, int ind)
cb->cx->depth--;
}

void
static void
plx_cobol_parse_body(Ctx *cx)
{
Cb cb;
Expand Down
2 changes: 1 addition & 1 deletion src/plx_dialect_go.c
Original file line number Diff line number Diff line change
Expand Up @@ -1913,7 +1913,7 @@ go_block(Go *g, int ind)
g->pos++;
}

void
static void
plx_go_parse_body(Ctx *cx)
{
Go g;
Expand Down
2 changes: 1 addition & 1 deletion src/plx_dialect_plsql.c
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ pl_emit_raw(Pl *pl, int a, int b, StringInfo out)
(int) (pl->t[b - 1].s + pl->t[b - 1].len - pl->t[a].s));
}

void
static void
plx_plsql_parse_body(Ctx *cx)
{
Pl pl;
Expand Down
2 changes: 1 addition & 1 deletion src/plx_dialect_python.c
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ parse_py_program(Ctx *cx)



void
static void
plx_python_parse_body(Ctx *cx)
{
plx_lex(cx);
Expand Down
30 changes: 1 addition & 29 deletions src/plx_dialect_ruby.c
Original file line number Diff line number Diff line change
Expand Up @@ -409,34 +409,6 @@ parse_iter(Ctx *cx, int a, int do_pos, int e, int ind)
appendStringInfoString(o, "END LOOP;\n");
}

/* GET STACKED DIAGNOSTICS lines for the fields used in a handler (see diag_mask) */
char *
plx_diag_prefix(int mask, int ind)
{
static const struct { int bit; const char *fld; const char *item; } t[] = {
{PLX_DIAG_DETAIL, "detail", "PG_EXCEPTION_DETAIL"},
{PLX_DIAG_HINT, "hint", "PG_EXCEPTION_HINT"},
{PLX_DIAG_CONSTRAINT, "constraint", "CONSTRAINT_NAME"},
{PLX_DIAG_COLUMN, "column", "COLUMN_NAME"},
{PLX_DIAG_TABLE, "table", "TABLE_NAME"},
{PLX_DIAG_SCHEMA, "schema", "SCHEMA_NAME"},
{PLX_DIAG_DATATYPE, "datatype", "PG_DATATYPE_NAME"},
};
StringInfoData p;
int i;

if (!mask)
return pstrdup("");
initStringInfo(&p);
for (i = 0; i < (int) (sizeof(t) / sizeof(t[0])); i++)
if (mask & t[i].bit)
{
appendStringInfoSpaces(&p, ind * 2);
appendStringInfo(&p, "GET STACKED DIAGNOSTICS __plx_%s = %s;\n", t[i].fld, t[i].item);
}
return p.data;
}

/* begin [body] [rescue [Class] [=> e] handler]* [ensure body] end */
static void
parse_begin(Ctx *cx, int ind)
Expand Down Expand Up @@ -763,7 +735,7 @@ parse_stmt_inner(Ctx *cx, int ind, bool toplevel)
}


void
static void
plx_ruby_parse_body(Ctx *cx)
{
plx_lex(cx);
Expand Down
2 changes: 1 addition & 1 deletion src/plx_dialect_tsql.c
Original file line number Diff line number Diff line change
Expand Up @@ -1488,7 +1488,7 @@ tq_block(Tq *tq, int ind)
tq_stmt(tq, ind);
}

void
static void
plx_tsql_parse_body(Ctx *cx)
{
Tq tq;
Expand Down
14 changes: 5 additions & 9 deletions src/plx_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ typedef struct PlxCtx
{
const char *body;
Tok *t;
int nt, pos;
int pos;
const PlxFuncMeta *meta;
const PlxSurface *surf;
StringInfoData out; /* emitted BEGIN..END body */
Expand Down Expand Up @@ -137,14 +137,10 @@ PlxLocal2 *plx_local_find(Ctx *cx, const char *name, int len);
bool plx_is_param(Ctx *cx, const char *name, int len);

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

#endif /* PLX_ENGINE_H */
29 changes: 28 additions & 1 deletion src/plx_transpile.c
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,6 @@ plx_lex(Ctx *cx)
t[n].kind = T_EOF; t[n].line = line; t[n].s = tokstart; n++;
}
cx->t = t;
cx->nt = n;
cx->pos = 0;
#undef PUSH
}
Expand Down Expand Up @@ -1591,6 +1590,34 @@ plx_parse_args(Ctx *cx, int a, int *as, int *ae, int maxargs, int *after)
return n;
}

/* GET STACKED DIAGNOSTICS lines for the fields used in a handler (see diag_mask) */
char *
plx_diag_prefix(int mask, int ind)
{
static const struct { int bit; const char *fld; const char *item; } t[] = {
{PLX_DIAG_DETAIL, "detail", "PG_EXCEPTION_DETAIL"},
{PLX_DIAG_HINT, "hint", "PG_EXCEPTION_HINT"},
{PLX_DIAG_CONSTRAINT, "constraint", "CONSTRAINT_NAME"},
{PLX_DIAG_COLUMN, "column", "COLUMN_NAME"},
{PLX_DIAG_TABLE, "table", "TABLE_NAME"},
{PLX_DIAG_SCHEMA, "schema", "SCHEMA_NAME"},
{PLX_DIAG_DATATYPE, "datatype", "PG_DATATYPE_NAME"},
};
StringInfoData p;
int i;

if (!mask)
return pstrdup("");
initStringInfo(&p);
for (i = 0; i < (int) (sizeof(t) / sizeof(t[0])); i++)
if (mask & t[i].bit)
{
appendStringInfoSpaces(&p, ind * 2);
appendStringInfo(&p, "GET STACKED DIAGNOSTICS __plx_%s = %s;\n", t[i].fld, t[i].item);
}
return p.data;
}

/* Emit a Ruby string token as raw SQL text (interp #{e} -> rewritten expr,
* inline; no surrounding quotes). Used for static query/perform/fetch SQL. */
void
Expand Down
Loading